--- id: mutation-applied name: Mutation Applied version: 0.1.0 summary: The approved mutation executed against its vendor system; the applier's result (e.g. owners: - visionary-engineering badges: - content: Approval lifecycle backgroundColor: purple textColor: purple icon: ArrowPathIcon - content: 'Implicit today — a status transition, not a published message' backgroundColor: gray textColor: gray icon: InformationCircleIcon --- ## Overview The approved mutation executed against its vendor system; the applier's result (e.g. the created QBO Bill) is recorded on the proposal. Terminal status `applied`. > **Implementation note:** today this "event" is a **state transition on the proposal row** in the [[container|proposal-store]] (status enum `pending → approved/rejected → applied/failed`), observed by hosts via tool results and the card refresh — there is no broker. If a real consumer appears (webhooks, portal inbox, audit sink), emitting these as first-class events is the natural seam — and their payloads are directly derivable from the `Proposal` TypeScript model (an Atlas/code-generation candidate). ## Architecture diagram --- id: mutation-failed name: Mutation Failed version: 0.1.0 summary: 'The applier threw (vendor down, validation, auth) — recorded as status `failed` with the error, surfaced to the card as a structured error.' owners: - visionary-engineering badges: - content: Approval lifecycle backgroundColor: purple textColor: purple icon: ArrowPathIcon - content: 'Implicit today — a status transition, not a published message' backgroundColor: gray textColor: gray icon: InformationCircleIcon --- ## Overview The applier threw (vendor down, validation, auth) — recorded as status `failed` with the error, surfaced to the card as a structured error. Terminal; a human re-proposes. > **Implementation note:** today this "event" is a **state transition on the proposal row** in the [[container|proposal-store]] (status enum `pending → approved/rejected → applied/failed`), observed by hosts via tool results and the card refresh — there is no broker. If a real consumer appears (webhooks, portal inbox, audit sink), emitting these as first-class events is the natural seam — and their payloads are directly derivable from the `Proposal` TypeScript model (an Atlas/code-generation candidate). ## Architecture diagram --- id: proposal-approved name: Proposal Approved version: 0.1.0 summary: 'A cleared human approved in the card (app-only commit path, RBAC-checked against the authenticated session).' owners: - visionary-engineering badges: - content: Approval lifecycle backgroundColor: purple textColor: purple icon: ArrowPathIcon - content: 'Implicit today — a status transition, not a published message' backgroundColor: gray textColor: gray icon: InformationCircleIcon --- ## Overview A cleared human approved in the card (app-only commit path, RBAC-checked against the authenticated session). The pending→approved transition is claimed atomically — a lost race means the write never runs twice. Dispatched by `kind` to the matching applier service. > **Implementation note:** today this "event" is a **state transition on the proposal row** in the [[container|proposal-store]] (status enum `pending → approved/rejected → applied/failed`), observed by hosts via tool results and the card refresh — there is no broker. If a real consumer appears (webhooks, portal inbox, audit sink), emitting these as first-class events is the natural seam — and their payloads are directly derivable from the `Proposal` TypeScript model (an Atlas/code-generation candidate). ## Architecture diagram --- id: proposal-created name: Proposal Created version: 0.1.0 summary: 'A propose_* command queued a mutation: a Proposal now exists in status `pending` with a freshly-minted per-card widget token.' owners: - visionary-engineering badges: - content: Approval lifecycle backgroundColor: purple textColor: purple icon: ArrowPathIcon - content: 'Implicit today — a status transition, not a published message' backgroundColor: gray textColor: gray icon: InformationCircleIcon --- ## Overview A propose_* command queued a mutation: a Proposal now exists in status `pending` with a freshly-minted per-card widget token. The approval card renders from this. > **Implementation note:** today this "event" is a **state transition on the proposal row** in the [[container|proposal-store]] (status enum `pending → approved/rejected → applied/failed`), observed by hosts via tool results and the card refresh — there is no broker. If a real consumer appears (webhooks, portal inbox, audit sink), emitting these as first-class events is the natural seam — and their payloads are directly derivable from the `Proposal` TypeScript model (an Atlas/code-generation candidate). ## Architecture diagram --- id: proposal-rejected name: Proposal Rejected version: 0.1.0 summary: A cleared human rejected the proposal. owners: - visionary-engineering badges: - content: Approval lifecycle backgroundColor: purple textColor: purple icon: ArrowPathIcon - content: 'Implicit today — a status transition, not a published message' backgroundColor: gray textColor: gray icon: InformationCircleIcon --- ## Overview A cleared human rejected the proposal. Terminal — nothing was written to any vendor. Rejections stay allowed even under WRITE_MODE=disabled (settling writes nothing). > **Implementation note:** today this "event" is a **state transition on the proposal row** in the [[container|proposal-store]] (status enum `pending → approved/rejected → applied/failed`), observed by hosts via tool results and the card refresh — there is no broker. If a real consumer appears (webhooks, portal inbox, audit sink), emitting these as first-class events is the natural seam — and their payloads are directly derivable from the `Proposal` TypeScript model (an Atlas/code-generation candidate). ## Architecture diagram --- id: commit-approval name: Commit Approval version: 0.1.0 summary: The ONLY execution path. A human click in the approval card approves (→ the write runs) or rejects a pending proposal. App-only — a compliant host never exposes it to the model. owners: - visionary-engineering schemaPath: schema.json sidebar: badge: App-only badges: - content: Human-only — app-only tool backgroundColor: red textColor: red icon: HandRaisedIcon - content: Executes the vendor write backgroundColor: red textColor: red icon: BoltIcon --- ## Overview MCP tool `commit_approval` (`_meta.ui.visibility:["app"]`, `destructiveHint`). Requires the per-card **widget token** (constant-time compared; dispensed only via [[query|get-card-data]]) and resolves the approver **role from the authenticated session** — never from a tool argument, so a proposer can never self-approve (§14.1). Sequence on approve: RBAC check (`canApprove(role, kind)`) → atomic `pending→approved` claim (lost race = 409, no double-apply) → the kind's applier service executes → [[event|mutation-applied]] or [[event|mutation-failed]]. On reject → [[event|proposal-rejected]]. Under `WRITE_MODE=disabled` (beta posture) an approve is refused at this choke point even for proposals already durable in the store; rejections stay allowed. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "CommitApproval", "type": "object", "properties": { "proposalId": { "type": "string", "minLength": 1 }, "decision": { "type": "string", "enum": [ "approve", "reject" ] }, "widgetToken": { "type": "string", "minLength": 1, "description": "The per-card token from get_card_data" }, "note": { "type": "string" }, "edits": { "type": "object", "description": "AR-collections only: subject/body edited at send", "properties": { "subject": { "type": "string" }, "body": { "type": "string" } } } }, "required": [ "proposalId", "decision", "widgetToken" ] } --- id: entra-authorize name: Entra Authorize version: 0.1.0 summary: 'The relayed authorization request Entra actually sees — a normal auth-code + PKCE flow, minus the resource param it would reject.' owners: - visionary-engineering sidebar: badge: OAuth badges: - content: Upstream identity backgroundColor: blue textColor: blue icon: KeyIcon --- ## Overview The relayed authorization request Entra actually sees — a normal auth-code + PKCE flow, minus the resource param it would reject. ## Architecture diagram --- id: entra-token name: Entra Token version: 0.1.0 summary: 'The relayed token exchange. Audience is driven by scope (api://…/mcp-access) since resource is stripped; the Entra app runs accessTokenAcceptedVersion=2.' owners: - visionary-engineering sidebar: badge: OAuth badges: - content: Upstream identity backgroundColor: blue textColor: blue icon: KeyIcon --- ## Overview The relayed token exchange. Audience is driven by scope (api://…/mcp-access) since resource is stripped; the Entra app runs accessTokenAcceptedVersion=2. ## Architecture diagram --- id: fishbowl-import-bill name: Fishbowl Import Bill version: 0.1.0 summary: 'Enters a PO-linked vendor bill in Fishbowl (dated with the warehouse receipt date), which auto-pushes to QBO.' owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor write — runs only after human approval backgroundColor: red textColor: red icon: ShieldCheckIcon --- ## Overview Enters a PO-linked vendor bill in Fishbowl (dated with the warehouse receipt date), which auto-pushes to QBO. Import name/columns still need live confirmation (no Fishbowl sandbox exists). NOT LIVE yet. Reached **only** from an approved proposal (the applier layer) — there is no agent-callable path to this operation. ## Architecture diagram --- id: fishbowl-import-sales-order name: Fishbowl Import Sales Order version: 0.1.0 summary: Creates a draft SO (status Estimate) via Fishbowl's /api/import CSV path — Fishbowl's only write surface. owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor write — runs only after human approval backgroundColor: red textColor: red icon: ShieldCheckIcon --- ## Overview Creates a draft SO (status Estimate) via Fishbowl's /api/import CSV path — Fishbowl's only write surface. NOT LIVE yet. Reached **only** from an approved proposal (the applier layer) — there is no agent-callable path to this operation. ## Architecture diagram --- id: graph-send-mail name: Graph Send Mail version: 0.1.0 summary: Sends the approved AR collections email via Microsoft Graph sendMail (app-only client credentials). owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor write — runs only after human approval backgroundColor: red textColor: red icon: ShieldCheckIcon --- ## Overview Sends the approved AR collections email via Microsoft Graph sendMail (app-only client credentials). Requires Mail.Send admin consent before live; idempotency = mark-sent-atomically + read-recheck (U6). Reached **only** from an approved proposal (the applier layer) — there is no agent-callable path to this operation. ## Architecture diagram --- id: oauth-authorize name: OAuth Authorize (proxied) version: 0.1.0 summary: claude.ai starts the user OAuth flow against the mesh's advertised authorization server; the gateway 302s the browser on to Entra with the RFC 8707 resource param stripped. owners: - visionary-engineering sidebar: badge: OAuth badges: - content: Auth handshake — unauthenticated by design backgroundColor: blue textColor: blue icon: KeyIcon --- ## Overview Path B (VIS-61): the mesh advertises itself as the OAuth authorization server (RFC 9728 PRM → RFC 8414 metadata) and relays `/oauth/authorize` to [[service|entra-id]] via [[command|entra-authorize]] with `resource` **dropped** — the AADSTS9010010 fix. PKCE, state, redirect_uri all pass through; the redirect target is fixed from config (not an open redirect). ## Architecture diagram --- id: oauth-token name: OAuth Token (proxied) version: 0.1.0 summary: 'claude.ai exchanges the auth code / refreshes tokens at the mesh''s token endpoint; the gateway relays the POST to Entra with resource stripped, holding no client secret itself.' owners: - visionary-engineering sidebar: badge: OAuth badges: - content: Auth handshake — stateless relay backgroundColor: blue textColor: blue icon: KeyIcon --- ## Overview The token relay ([[command|entra-token]]): claude.ai's client credentials ride through untouched (client_secret_post/basic), the body is size-bounded, upstream status/body are returned verbatim, and neither tokens nor credentials are logged. Entra unreachable → 502, fail closed. ## Architecture diagram --- id: propose-ar-collection-email name: Propose AR Collections Email version: 0.1.0 summary: 'Workflow #4 — a relationship-aware dunning email queued for approval. The approval IS the send gate; the human can edit subject/body at send.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Command — approval-gated backgroundColor: yellow textColor: yellow icon: ShieldCheckIcon - content: 'kind: ar-collections-email' backgroundColor: gray textColor: gray icon: CodeBracketIcon - content: 'approvers: admin · controller · ar-clerk' backgroundColor: blue textColor: blue icon: UserGroupIcon --- ## Overview MCP tool `propose_ar_collection_email` — agent-callable, but it **only queues**: it creates a `pending` proposal ([[event|proposal-created]]) on the [[service|approval-bus]] and returns the approval card. No vendor write happens here. **Watch the full lifecycle:** [[flow|ar-collections-flow]]. On approval [[service|ar-collections-applier]] sends via [[command|graph-send-mail]]. The **edit-at-send** path (unique to this kind) snapshots the edited payload before apply, so the audit trail records what the human actually approved. Outward-facing: a live send reaches a customer — needs `Mail.Send` admin consent before live. **Response shape note (CQRS):** the tool returns the *public* pending proposal (id, kind, summary, status) so the host can render the approval card — the command's real "return" is the proposal id; the mutation result only ever exists on the proposal record after a human approves. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposeArCollectionEmail", "type": "object", "properties": { "fromMailbox": { "type": "string", "minLength": 1 }, "toAddress": { "type": "string", "format": "email" }, "toName": { "type": "string" }, "ccAddresses": { "type": "array", "items": { "type": "string", "format": "email" } }, "subject": { "type": "string", "minLength": 1 }, "body": { "type": "string", "minLength": 1 }, "bodyType": { "type": "string", "enum": [ "Text", "HTML" ] }, "customerName": { "type": "string" }, "amountDue": { "type": "number", "exclusiveMinimum": 0 }, "invoiceRefs": { "type": "array", "items": { "type": "string" } } }, "required": [ "fromMailbox", "toAddress", "subject", "body" ] } --- id: propose-bill-date-correction name: Propose Bill-Date Correction version: 0.1.0 summary: 'Workflow #2 — the next-day QBO follow-up: correct a bill''s TxnDate/DueDate to the vendor invoice date. Queued for approval; applied as a QBO sparse update.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Command — approval-gated backgroundColor: yellow textColor: yellow icon: ShieldCheckIcon - content: 'kind: bill-date-correction' backgroundColor: gray textColor: gray icon: CodeBracketIcon - content: 'approvers: admin · controller · ap-clerk' backgroundColor: blue textColor: blue icon: UserGroupIcon --- ## Overview MCP tool `propose_bill_date_correction` — agent-callable, but it **only queues**: it creates a `pending` proposal ([[event|proposal-created]]) on the [[service|approval-bus]] and returns the approval card. No vendor write happens here. **Watch the full lifecycle:** [[flow|bill-date-correction-flow]]. On approval [[service|bill-date-correction-applier]] re-fetches the bill for a fresh SyncToken, then sparse-updates the one date field via [[command|qbo-update-bill]] — the optimistic-lock 409 path is tested. Lowest-blast-radius mutation in the set: single reversible field on an existing record, no external party sees it. **Response shape note (CQRS):** the tool returns the *public* pending proposal (id, kind, summary, status) so the host can render the approval card — the command's real "return" is the proposal id; the mutation result only ever exists on the proposal record after a human approves. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposeBillDateCorrection", "type": "object", "properties": { "billId": { "type": "string", "minLength": 1 }, "correctDate": { "type": "string", "minLength": 1, "description": "YYYY-MM-DD" }, "field": { "type": "string", "enum": [ "TxnDate", "DueDate" ], "description": "Default TxnDate" }, "reason": { "type": "string" } }, "required": [ "billId", "correctDate" ] } --- id: propose-invoice name: Propose Customer Invoice version: 0.1.0 summary: 'Workflow #3 — draft a customer invoice (AR) for approval; created in QBO only once a cleared human approves.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Command — approval-gated backgroundColor: yellow textColor: yellow icon: ShieldCheckIcon - content: 'kind: invoice-generation' backgroundColor: gray textColor: gray icon: CodeBracketIcon - content: 'approvers: admin · controller · ar-clerk' backgroundColor: blue textColor: blue icon: UserGroupIcon --- ## Overview MCP tool `propose_invoice` — agent-callable, but it **only queues**: it creates a `pending` proposal ([[event|proposal-created]]) on the [[service|approval-bus]] and returns the approval card. No vendor write happens here. **Watch the full lifecycle:** [[flow|invoice-flow]]. On approval [[service|invoice-applier]] creates the invoice via [[command|qbo-create-invoice]] (one SalesItemLineDetail per line). **Response shape note (CQRS):** the tool returns the *public* pending proposal (id, kind, summary, status) so the host can render the approval card — the command's real "return" is the proposal id; the mutation result only ever exists on the proposal record after a human approves. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposeInvoice", "type": "object", "properties": { "customerId": { "type": "string", "minLength": 1 }, "customerName": { "type": "string" }, "lines": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "itemId": { "type": "string", "minLength": 1 }, "description": { "type": "string" }, "amount": { "type": "number", "exclusiveMinimum": 0 }, "qty": { "type": "number" }, "unitPrice": { "type": "number" } }, "required": [ "itemId", "amount" ] } }, "txnDate": { "type": "string" }, "dueDate": { "type": "string" }, "docNumber": { "type": "string" }, "memo": { "type": "string" } }, "required": [ "customerId", "lines" ] } --- id: propose-order-entry name: Propose Sales Order version: 0.1.0 summary: 'Workflow #8 — draft a Fishbowl sales order (status Estimate) from an approved customer PO; created only once sales approves.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Command — approval-gated backgroundColor: yellow textColor: yellow icon: ShieldCheckIcon - content: 'kind: order-entry' backgroundColor: gray textColor: gray icon: CodeBracketIcon - content: 'approvers: admin · sales' backgroundColor: blue textColor: blue icon: UserGroupIcon --- ## Overview MCP tool `propose_order_entry` — agent-callable, but it **only queues**: it creates a `pending` proposal ([[event|proposal-created]]) on the [[service|approval-bus]] and returns the approval card. No vendor write happens here. **Watch the full lifecycle:** [[flow|order-entry-flow]]. On approval [[service|order-entry-applier]] imports the draft SO via [[command|fishbowl-import-sales-order]] (Fishbowl's CSV import path — its only write surface). **Fishbowl is not live yet** — at apply time an outage degrades to a structured `failed` proposal, never a crash. **Response shape note (CQRS):** the tool returns the *public* pending proposal (id, kind, summary, status) so the host can render the approval card — the command's real "return" is the proposal id; the mutation result only ever exists on the proposal record after a human approves. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposeOrderEntry", "type": "object", "properties": { "customerName": { "type": "string", "minLength": 1 }, "customerPo": { "type": "string" }, "soNum": { "type": "string" }, "opportunityId": { "type": "string" }, "note": { "type": "string" }, "lines": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "partNumber": { "type": "string", "minLength": 1 }, "quantity": { "type": "number", "exclusiveMinimum": 0 }, "unitPrice": { "type": "number" }, "description": { "type": "string" } }, "required": [ "partNumber", "quantity" ] } } }, "required": [ "customerName", "lines" ] } --- id: propose-vendor-bill name: Propose Vendor Bill version: 0.1.0 summary: 'Workflow #1 — AP intake. Queues a vendor bill as a PENDING proposal; nothing is written until a cleared human approves in the card.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Command — approval-gated backgroundColor: yellow textColor: yellow icon: ShieldCheckIcon - content: 'kind: vendor-bill-intake' backgroundColor: gray textColor: gray icon: CodeBracketIcon - content: 'approvers: admin · controller · ap-clerk' backgroundColor: blue textColor: blue icon: UserGroupIcon --- ## Overview MCP tool `propose_vendor_bill` — agent-callable, but it **only queues**: it creates a `pending` proposal ([[event|proposal-created]]) on the [[service|approval-bus]] and returns the approval card. No vendor write happens here. **Watch the full lifecycle:** [[flow|vendor-bill-flow]]. On approval [[service|vendor-bill-applier]] routes by PO linkage (the two-path rule from vendor discovery): **PO-linked** bills import into Fishbowl ([[command|fishbowl-import-bill]], auto-pushes to QBO); **non-PO** bills post directly to QBO ([[command|qbo-create-bill]]). **Response shape note (CQRS):** the tool returns the *public* pending proposal (id, kind, summary, status) so the host can render the approval card — the command's real "return" is the proposal id; the mutation result only ever exists on the proposal record after a human approves. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposeVendorBill", "type": "object", "properties": { "vendorId": { "type": "string", "minLength": 1 }, "vendorName": { "type": "string" }, "amount": { "type": "number", "exclusiveMinimum": 0 }, "accountId": { "type": "string", "minLength": 1, "description": "QBO expense AccountRef" }, "txnDate": { "type": "string" }, "dueDate": { "type": "string" }, "docNumber": { "type": "string" }, "memo": { "type": "string" } }, "required": [ "vendorId", "amount", "accountId" ] } --- id: qbo-create-bill name: QBO Create Bill version: 0.1.0 summary: Creates a vendor bill in QuickBooks (the non-PO path of vendor-bill intake). owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor write — runs only after human approval backgroundColor: red textColor: red icon: ShieldCheckIcon --- ## Overview Creates a vendor bill in QuickBooks (the non-PO path of vendor-bill intake). Idempotency will key QBO's native RequestId to the proposal id (U6 — not yet shipped; WRITE_MODE=gated refuses to boot against live connectors until it is). Reached **only** from an approved proposal (the applier layer) — there is no agent-callable path to this operation. ## Architecture diagram --- id: qbo-create-invoice name: QBO Create Invoice version: 0.1.0 summary: Creates a customer invoice from approved proposal lines. owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor write — runs only after human approval backgroundColor: red textColor: red icon: ShieldCheckIcon --- ## Overview Creates a customer invoice from approved proposal lines. Same U6 idempotency dependency as create-bill. Reached **only** from an approved proposal (the applier layer) — there is no agent-callable path to this operation. ## Architecture diagram --- id: qbo-update-bill name: QBO Update Bill (sparse) version: 0.1.0 summary: Sparse-updates one field on an existing bill using a fresh SyncToken (bill-date correction). owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor write — runs only after human approval backgroundColor: red textColor: red icon: ShieldCheckIcon --- ## Overview Sparse-updates one field on an existing bill using a fresh SyncToken (bill-date correction). Naturally idempotent — re-applying the same date is a no-op. Reached **only** from an approved proposal (the applier layer) — there is no agent-callable path to this operation. ## Architecture diagram --- id: smartsheet-append-scorecard-row name: Smartsheet Append Scorecard Row version: 0.1.0 summary: 'Appends a dated KPI snapshot row to the scorecard sheet, preserving formula columns. The one write that does NOT route through the approval bus.' owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Direct bounded write — outside the approval bus backgroundColor: yellow textColor: yellow icon: ExclamationTriangleIcon - content: WRITE_MODE-throttled backgroundColor: blue textColor: blue icon: ShieldCheckIcon --- ## Overview Triggered by [[query|kpi-scorecard]] with `publish: true` — sent by [[service|scorecard-service]], not by an applier. Bounded (appends one row, never edits formulas) and throttled by the same `WRITE_MODE` switch as everything else, but it bypasses propose→approve→apply. The pre-showtime audit flags this as the one CQRS-purity exception to either route through the bus or explicitly except in the rule engine (ADR §5's auto-approve list is the natural home). ## Architecture diagram --- id: accounting-read name: Accounting Read (customers · reports · aging) version: 0.1.0 summary: 'QBO customers, cash-flow/P&L reports and AR/AP aging (OAuth2 with rotated-token write-back).' owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor read backgroundColor: green textColor: green icon: BoltIcon --- ## Overview QBO customers, cash-flow/P&L reports and AR/AP aging (OAuth2 with rotated-token write-back). Feeds the account-join, cash-flow tracker, scorecard — and the bill re-fetch the date-correction applier needs for a fresh SyncToken. ## Architecture diagram --- id: cash-flow-tracker name: Cash Flow Tracker version: 0.1.0 summary: 'Workflow #5 — the unified cash view: QBO cash position + AR/AP aging, Fishbowl open SO/PO, ClaritySoft weighted pipeline, with divergence and runway alerts.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: cash_flow_tracker' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `cash_flow_tracker` — agent-callable, read-only. **Handled by [[service|cash-flow-service]]** — its page shows this workflow's exact vendor fan-out. **Watch the full lifecycle:** [[flow|cash-flow-tracker-flow]]. Returns `CashFlowView`: `cashPosition` (ending cash, burn, runway), `receivables`/`payables` aging buckets, `pipeline` weighted rollup, `openOrders` SO/PO totals, and `alerts` (`so-po-divergence`, `cash-runway-pipeline`). While Fishbowl is absent, `openOrders` is omitted and `unavailableSources: ['fishbowl']` marks the gap (the divergence alert is suppressed, never guessed). > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "CashFlowTracker", "type": "object", "properties": { "runwayThresholdMonths": { "type": "number", "exclusiveMinimum": 0, "description": "Runway alert threshold (months, default 3)" }, "divergenceThreshold": { "type": "number", "exclusiveMinimum": 0, "description": "SO/PO divergence alert ratio (default 1.25)" } } } --- id: crm-read name: CRM Read (accounts · contacts · opportunities) version: 0.1.0 summary: 'Paginated reads of ClaritySoft accounts, contacts and opportunities (api-key body-envelope auth).' owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor read backgroundColor: green textColor: green icon: BoltIcon --- ## Overview Paginated reads of ClaritySoft accounts, contacts and opportunities (api-key body-envelope auth). Feeds the account-join, daily pipeline, cash-flow pipeline section, scorecard and Project Hunter cross-ref. ## Architecture diagram --- id: daily-pipeline-view name: Daily Pipeline View version: 0.1.0 summary: 'Workflow #7 — ClaritySoft opportunities rolled up by stage (count + value): the company funnel, plus a per-owner slice when requested.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: daily_pipeline_view' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `daily_pipeline_view` — agent-callable, read-only. **Handled by [[service|daily-pipeline-service]]** — its page shows this workflow's exact vendor fan-out. **Watch the full lifecycle:** [[flow|daily-pipeline-flow]]. Returns `DailyPipelineView` — `company: PipelineFunnel` and optional `owner: PipelineFunnel` (stages sorted by value, totals). **Single-source**: only ClaritySoft — unaffected by a Fishbowl or QBO outage. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "DailyPipelineView", "type": "object", "properties": { "ownerId": { "type": "string", "description": "Optional CRM OwnerId for a per-owner slice" } } } --- id: find-customer-account name: Find Customer Account version: 0.1.0 summary: Resolve one joined customer account by company name (case/space-insensitive). owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: find_customer_account' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `find_customer_account` — agent-callable, read-only. **Handled by [[service|customer-account-service]]** — its page shows this workflow's exact vendor fan-out. **Watch the full lifecycle:** [[flow|customer-account-flow]]. Same join as [[query|list-customer-accounts]], filtered to one company. Returns the joined `CustomerAccount` or `{ found: false }` — plus the degraded-run caveat when a source is down. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "FindCustomerAccount", "type": "object", "properties": { "name": { "type": "string", "description": "Company name to look up" } }, "required": [ "name" ] } --- id: get-card-data name: Get Card Data version: 0.1.0 summary: 'Approval-card bootstrap: the proposal''s summary, payload and the per-card widget token the Accept/Send button needs. App-only — the assistant cannot call it.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: App-only badges: - content: App-only — card bootstrap backgroundColor: red textColor: red icon: HandRaisedIcon --- ## Overview MCP tool `get_card_data` (`_meta.ui.visibility:["app"]`, read-only). The **only** place the per-proposal `widgetToken` is dispensed — it goes to the rendered card, never to model-visible output (every agent-facing serialization strips it via `publicProposal`). ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GetCardData", "type": "object", "properties": { "proposalId": { "type": "string", "minLength": 1 } }, "required": [ "proposalId" ] } --- id: get-customer name: Get Customer (QBO) version: 0.1.0 summary: 'Fetch one QuickBooks customer by id, including its SyncToken (the optimistic-lock key QBO sparse updates require).' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: get_customer' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `get_customer` — agent-callable, read-only. **Handled by [[service|entity-read-service]]** — its page shows the exact vendor fan-out. Returns the clean `Customer` domain model. Input is validated as a numeric QBO id (id-injection guard). > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GetCustomer", "type": "object", "properties": { "id": { "type": "string", "pattern": "^\\d+$", "description": "QBO customer Id (numeric)" } }, "required": [ "id" ] } --- id: inventory-read name: Inventory Read (customers · open SO/PO) version: 0.1.0 summary: Fishbowl customers and open sales/purchase orders via data queries (session-login → bearer re-auth). owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor read backgroundColor: green textColor: green icon: BoltIcon --- ## Overview Fishbowl customers and open sales/purchase orders via data queries (session-login → bearer re-auth). Feeds the account-join, cash-flow tracker and scorecard. NOT LIVE yet — every consumer degrades gracefully (sections omitted + marked unavailable, never a failed tool call). ## Architecture diagram --- id: kpi-scorecard name: KPI Scorecard version: 0.1.0 summary: 'Workflow #9 — the L10 cross-system scorecard: 7 KPIs (cash, AR, AP, pipeline, open SO/PO, NWC) with target/status. Optional bounded publish to Smartsheet.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: kpi_scorecard' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `kpi_scorecard` — agent-callable, read-only. **Handled by [[service|scorecard-service]]** — its page shows this workflow's exact vendor fan-out. **Watch the full lifecycle:** [[flow|kpi-scorecard-flow]]. Read-only by default; returns `ScorecardView` (`Kpi` entries with `source` and green/yellow/red `status`). While Fishbowl is absent the two order-book KPIs are **omitted** (never zero-filled) and `unavailableSources` marks the gap. With `publish: true` it appends a dated snapshot row to Smartsheet via [[command|smartsheet-append-scorecard-row]] — a **direct bounded write** throttled by `WRITE_MODE` but *not* routed through the approval bus (flagged in the pre-showtime audit as the one CQRS seam to close or declare). > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "KpiScorecard", "type": "object", "properties": { "publish": { "type": "boolean", "description": "Append a snapshot row to Smartsheet (write; WRITE_MODE-gated)" }, "sheetId": { "type": "integer", "exclusiveMinimum": 0, "description": "Target sheet (required when publish=true)" }, "asOf": { "type": "string", "description": "ISO date; defaults to today" } } } --- id: lead-source-read name: Lead-Source Read (project candidates) version: 0.1.0 summary: Public-source project candidates for Project Hunter (mock feed + stubbed live ETL).. owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor read backgroundColor: green textColor: green icon: BoltIcon --- ## Overview Public-source project candidates for Project Hunter (mock feed + stubbed live ETL). ## Architecture diagram --- id: list-contacts name: List Contacts (CRM) version: 0.1.0 summary: 'All ClaritySoft contacts as clean domain models, vendor pagination hidden behind an async iterator.' owners: - visionary-engineering sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: list_contacts' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `list_contacts` — agent-callable, read-only. **Handled by [[service|entity-read-service]]** — its page shows the exact vendor fan-out. Returns `Contact[]` — id, name, email, accountId, lastModified. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Architecture diagram --- id: list-customer-accounts name: List Customer Accounts version: 0.1.0 summary: 'The account-join primitive: every customer joined across ClaritySoft (CRM), QuickBooks (accounting) and Fishbowl (inventory), with presence and linkage flags.' owners: - visionary-engineering sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: list_customer_accounts' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `list_customer_accounts` — agent-callable, read-only. **Handled by [[service|customer-account-service]]** — its page shows this workflow's exact vendor fan-out. **Watch the full lifecycle:** [[flow|customer-account-flow]]. Returns `CustomerAccount[]`: `key`, `displayName`, per-system slices (`crm`, `accounting`, `inventory`), `presentIn` and `linked` (true only when the account exists in all three systems). Degraded runs additionally return `unavailableSources` + an explicit caveat note. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Architecture diagram --- id: list-pending-approvals name: List Pending Approvals version: 0.1.0 summary: 'The approval inbox: every proposed mutation still awaiting a human decision.' owners: - visionary-engineering sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: list_pending_approvals' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `list_pending_approvals` — agent-callable, read-only. **Handled by [[service|approval-bus]]** — its page shows the exact vendor fan-out. Returns the **public** view of each pending proposal — the per-proposal `widgetToken` (the human-only execute secret) is stripped before anything reaches model-visible output. This is the **query side of the approval bus**: it reads the same [[container|proposal-store]] the command side writes — Jesh's shared-database CQRS, not a separate read store. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Architecture diagram --- id: project-hunter name: Project Hunter version: 0.1.0 summary: 'Workflow #6 — scores public-source project candidates against the QMax domain, cross-references ClaritySoft (dedup only, never creates a lead), returns a ranked report + digest.' owners: - visionary-engineering schemaPath: schema.json sidebar: badge: MCP tool badges: - content: Query — direct request/response backgroundColor: green textColor: green icon: BoltIcon - content: 'tool: project_hunter' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview MCP tool `project_hunter` — agent-callable, read-only. **Handled by [[service|project-hunter-service]]** — its page shows this workflow's exact vendor fan-out. **Watch the full lifecycle:** [[flow|project-hunter-flow]]. Returns `ProjectHunterReport` — ranked `HuntedProject[]` with score, signals, rationale and a CRM match flag, plus a markdown digest. **Capture-only**: no lead is written. > **Reading this page:** a message page renders its contract and its one hop — who sends it, who > receives it (that is EventCatalog's grain: message = contract). The **service page** shows this > workflow's full fan-out (its `sends`); the **flow** shows the lifecycle step by step. ## Input schema ## Architecture diagram ## Raw Schema:schema.json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProjectHunter", "type": "object", "properties": { "minScore": { "type": "number", "minimum": 0, "maximum": 100, "description": "Qualification threshold (default 50)" }, "limit": { "type": "integer", "exclusiveMinimum": 0, "description": "Max projects returned (default 20)" } } } --- id: sheets-read name: Sheets Read (scorecard sheet + columns) version: 0.1.0 summary: Smartsheet sheet + column reads (static PAT) used to locate the scorecard sheet and protect formula columns on publish.. owners: - visionary-engineering sidebar: badge: Vendor API badges: - content: Vendor read backgroundColor: green textColor: green icon: BoltIcon --- ## Overview Smartsheet sheet + column reads (static PAT) used to locate the scorecard sheet and protect formula columns on publish. ## Architecture diagram --- id: approval-bus name: Approval Bus (command service — owns the queue) version: 0.1.0 summary: 'The command side: owns the proposal queue. propose_* only queue; a cleared human approves in the card; only then does the kind''s applier execute the vendor write. Every transition is recorded on the shared store.' owners: - visionary-engineering receives: - id: propose-vendor-bill version: 0.1.0 - id: propose-bill-date-correction version: 0.1.0 - id: propose-invoice version: 0.1.0 - id: propose-order-entry version: 0.1.0 - id: propose-ar-collection-email version: 0.1.0 - id: commit-approval version: 0.1.0 - id: get-card-data version: 0.1.0 - id: list-pending-approvals version: 0.1.0 sends: - id: proposal-created version: 0.1.0 - id: proposal-approved version: 0.1.0 - id: proposal-rejected version: 0.1.0 - id: mutation-applied version: 0.1.0 - id: mutation-failed version: 0.1.0 writesTo: - id: proposal-store readsFrom: - id: proposal-store repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: Command side — human-gated backgroundColor: yellow textColor: yellow icon: ShieldCheckIcon --- ## Overview `src/approvals/` (engine, store, RBAC, write policy). The core invariant (ADR §5): **no executable action is ever exposed as an agent-callable tool.** The agent-callable `propose_*` tools only queue and render; execution lives exclusively behind the human-only [[command|commit-approval]] (app-only visibility + per-card widget token + session-bound RBAC). **Lifecycle:** `pending → approved → applied` / `failed`, or `pending → rejected`. The pending→decided transition is claimed **atomically** (row transition; lost race = 409) so a non-idempotent vendor write runs at most once per proposal. **Dispatch:** on approve, the proposal's `kind` selects its applier service — [[service|vendor-bill-applier]] · [[service|bill-date-correction-applier]] · [[service|invoice-applier]] · [[service|order-entry-applier]] · [[service|ar-collections-applier]] — which performs the one vendor write; the result/error is recorded back on the proposal ([[event|mutation-applied]] / [[event|mutation-failed]]). **Both CQRS sides of the store meet here:** commands write it; the read side ([[query|list-pending-approvals]]) reads the same rows — Jesh's shared-database CQRS, deliberately not two datastores. **Write policy:** `WRITE_MODE=disabled` (beta) refuses approves at the choke point; `gated` refuses to **boot** against live write connectors until applier idempotency (U6) ships. **RBAC:** static role→kind grants (admin/controller/ap-clerk/ar-clerk/sales/viewer) keyed off the verified Entra subject (`MCP_SUBJECT_ROLE_MAP`); the M4 admin portal replaces the map behind the same `canApprove` seam. ## Architecture diagram --- id: ar-collections-applier name: 'ArCollectionsApplier (workflow #4)' version: 0.1.0 summary: 'Applies an approved AR collections email: sends via Microsoft Graph sendMail. The approval IS the send gate; the human can edit subject/body at send.' owners: - visionary-engineering receives: - id: proposal-approved version: 0.1.0 sends: - id: graph-send-mail version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: Applier — runs only on human approval backgroundColor: red textColor: red icon: ShieldCheckIcon - content: 'kind: ar-collections-email' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview `src/workflows/ar-collections/ar-collections.applier.ts`. Receives [[event|proposal-approved]] **dispatched by kind** (`ar-collections-email`) from the [[service|approval-bus]] — in code this is a synchronous by-kind dispatch inside the commit path, not a broker subscription (see the event's implementation note). Single vendor write: [[command|graph-send-mail]] (app-only client credentials). The most outward-facing write in the system — a customer receives it — which is why the **edit-at-send** path (unique to this kind) snapshots the human's edited subject/body onto the proposal before apply, so the audit trail records exactly what was approved. Blocked on `Mail.Send` admin consent before live. **Watch the full lifecycle:** [[flow|ar-collections-flow]] (from [[command|propose-ar-collection-email]] to the vendor write). ## Architecture diagram --- id: bill-date-correction-applier name: 'BillDateCorrectionApplier (workflow #2)' version: 0.1.0 summary: 'Applies an approved bill-date correction: re-fetches the bill for a fresh SyncToken, then sparse-updates the one date field in QBO.' owners: - visionary-engineering receives: - id: proposal-approved version: 0.1.0 sends: - id: accounting-read version: 0.1.0 - id: qbo-update-bill version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: Applier — runs only on human approval backgroundColor: red textColor: red icon: ShieldCheckIcon - content: 'kind: bill-date-correction' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview `src/workflows/bill-date-correction/bill-date-correction.applier.ts`. Receives [[event|proposal-approved]] **dispatched by kind** (`bill-date-correction`) from the [[service|approval-bus]] — in code this is a synchronous by-kind dispatch inside the commit path, not a broker subscription (see the event's implementation note). Two vendor operations per apply: a QBO **read** ([[query|accounting-read]] — re-fetch the bill for a fresh `SyncToken`), then the sparse **write** ([[command|qbo-update-bill]] — one field, `TxnDate` or `DueDate`). Naturally idempotent (re-applying the same date is a no-op) and the optimistic-lock 409 path is tested — which is why this is the recommended **first live command**. **Watch the full lifecycle:** [[flow|bill-date-correction-flow]] (from [[command|propose-bill-date-correction]] to the vendor write). ## Architecture diagram --- id: cash-flow-service name: CashFlowService — cash-flow read workflow version: 0.1.0 summary: 'Workflow #5. Fans into QBO (cash position + AR/AP aging), Fishbowl (open SO/PO) and ClaritySoft (weighted pipeline) in parallel, assembles the unified cash view with divergence + runway alerts.' owners: - visionary-engineering receives: - id: cash-flow-tracker version: 0.1.0 sends: - id: accounting-read version: 0.1.0 - id: inventory-read version: 0.1.0 - id: crm-read version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: 'Read workflow — direct request/response, no events' backgroundColor: green textColor: green icon: ArrowsRightLeftIcon --- ## Overview `src/workflows/cash-flow/cash-flow.service.ts`. Three parallel vendor reads, one assembled `CashFlowView`: - [[query|accounting-read]] → QBO cash-flow report + receivables/payables aging buckets - [[query|inventory-read]] → Fishbowl open sales/purchase orders (the SO/PO rollup) - [[query|crm-read]] → ClaritySoft opportunities (the stage-weighted pipeline rollup) Then the derived layer: cash position (ending cash, burn, runway months), the `so-po-divergence` alert and the `cash-runway-pipeline` alert. **Degradation (Fishbowl is not live):** the inventory read is allowed to be ABSENT — `openOrders` is omitted, `unavailableSources: ['fishbowl']` is set, the divergence alert is suppressed (never guessed), and the MCP layer attaches an explicit "do not report absence as fact" note. A Fishbowl outage degrades the view; it never fails the tool call. **Layering:** MCP tool → this workflow service (orchestration, cross-system joins) → foundational services (clean domain models, vendor envelope stripped) → clients (raw vendor APIs) — each behind DI, every connector swappable to fixtures via `*_USE_MOCK`. > **CQRS note:** reads are the "typical reactions" side — normal request/response, **no event lifecycle**. Only commands flow through the [[service|approval-bus]]. ## Architecture diagram --- id: claritysoft-crm name: ClaritySoft CRM version: 0.1.0 summary: 'CRM of record: accounts, contacts, opportunities/pipeline. Read-only in the beta — the first connector going live.' owners: - visionary-engineering receives: - id: crm-read version: 0.1.0 badges: - content: Connector — going live read-only backgroundColor: green textColor: green icon: CheckCircleIcon --- ## Overview Auth: api-key in the request body envelope (redaction-flagged for logs). Rate-limit + webhook surface confirmed with the vendor. **Going live read-only now** — the first real data in the mesh. Feeds four of the six read services: [[service|daily-pipeline-service]] (sole source), [[service|cash-flow-service]], [[service|scorecard-service]], [[service|customer-account-service]], [[service|project-hunter-service]] and [[service|entity-read-service]]. ## Architecture diagram --- id: claude-ai name: claude.ai (Max + approval card) version: 0.1.0 summary: 'The host: QMax staff drive the mesh from claude.ai. The Max agent calls queries and propose commands; the human-only approval card calls the app-only execute pair. Connected via remote MCP with Entra OAuth.' owners: - visionary-engineering receives: - id: proposal-created version: 0.1.0 - id: proposal-approved version: 0.1.0 - id: proposal-rejected version: 0.1.0 - id: mutation-applied version: 0.1.0 - id: mutation-failed version: 0.1.0 sends: - id: list-customer-accounts version: 0.1.0 - id: find-customer-account version: 0.1.0 - id: daily-pipeline-view version: 0.1.0 - id: cash-flow-tracker version: 0.1.0 - id: kpi-scorecard version: 0.1.0 - id: project-hunter version: 0.1.0 - id: get-customer version: 0.1.0 - id: list-contacts version: 0.1.0 - id: list-pending-approvals version: 0.1.0 - id: propose-vendor-bill version: 0.1.0 - id: propose-bill-date-correction version: 0.1.0 - id: propose-invoice version: 0.1.0 - id: propose-order-entry version: 0.1.0 - id: propose-ar-collection-email version: 0.1.0 - id: get-card-data version: 0.1.0 - id: commit-approval version: 0.1.0 - id: oauth-authorize version: 0.1.0 - id: oauth-token version: 0.1.0 badges: - content: Host — external backgroundColor: blue textColor: blue icon: GlobeAltIcon --- ## Overview Two principals share this surface: the **model** (agent-callable tools + the 9 invocable workflow prompts — the front door) and the **human** (the rendered approval card, whose Accept/Send button drives the app-only pair). The write gate rests on the host enforcing `_meta.ui.visibility:["app"]` — verified live as part of the go-live gate. ## Architecture diagram --- id: customer-account-service name: CustomerAccountService — account-join workflow version: 0.1.0 summary: 'The account-join primitive: every customer folded across ClaritySoft (CRM), QBO (accounting) and Fishbowl (inventory), with presence + linkage flags. Serves both the list and find tools.' owners: - visionary-engineering receives: - id: list-customer-accounts version: 0.1.0 - id: find-customer-account version: 0.1.0 sends: - id: crm-read version: 0.1.0 - id: accounting-read version: 0.1.0 - id: inventory-read version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: 'Read workflow — direct request/response, no events' backgroundColor: green textColor: green icon: ArrowsRightLeftIcon --- ## Overview `src/workflows/customer-account/customer-account.service.ts`. Three parallel vendor reads, folded by normalized company name: - [[query|crm-read]] → ClaritySoft accounts - [[query|accounting-read]] → QBO customers - [[query|inventory-read]] → Fishbowl customers Each joined `CustomerAccount` carries per-system slices, `presentIn`, and `linked` (true only when the account exists in all three systems). This join is the foundation the cross-system workflows build on. **Degradation (Fishbowl is not live):** the join still folds the reachable systems; `unavailableSources` marks the gap and the MCP layer attaches the "absence from a down system is not a fact" note, so `presentIn`/`linked` are never misread. **Layering:** MCP tool → this workflow service (orchestration, cross-system joins) → foundational services (clean domain models, vendor envelope stripped) → clients (raw vendor APIs) — each behind DI, every connector swappable to fixtures via `*_USE_MOCK`. > **CQRS note:** reads are the "typical reactions" side — normal request/response, **no event lifecycle**. Only commands flow through the [[service|approval-bus]]. ## Architecture diagram --- id: daily-pipeline-service name: DailyPipelineService — pipeline read workflow version: 0.1.0 summary: 'Workflow #7. Single-source: ClaritySoft opportunities rolled up by stage (count + value) into the company funnel, plus an optional per-owner slice.' owners: - visionary-engineering receives: - id: daily-pipeline-view version: 0.1.0 sends: - id: crm-read version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: 'Read workflow — direct request/response, no events' backgroundColor: green textColor: green icon: ArrowsRightLeftIcon --- ## Overview `src/workflows/daily-pipeline/daily-pipeline.service.ts`. The simplest read workflow — one vendor, no joins: [[query|crm-read]] → stage rollup (stages sorted by value, totals) for the company, and again filtered by `OwnerId` when a per-owner slice is requested. **Single-source means resilient:** unaffected by a Fishbowl or QBO outage — this is the demo-safe workflow. **Layering:** MCP tool → this workflow service (orchestration, cross-system joins) → foundational services (clean domain models, vendor envelope stripped) → clients (raw vendor APIs) — each behind DI, every connector swappable to fixtures via `*_USE_MOCK`. > **CQRS note:** reads are the "typical reactions" side — normal request/response, **no event lifecycle**. Only commands flow through the [[service|approval-bus]]. ## Architecture diagram --- id: entity-read-service name: Entity reads — direct foundational-service tools version: 0.1.0 summary: 'The two thin entity-read tools: get_customer (QBO, with SyncToken) and list_contacts (ClaritySoft). Direct foundational-service passthrough — no workflow orchestration.' owners: - visionary-engineering receives: - id: get-customer version: 0.1.0 - id: list-contacts version: 0.1.0 sends: - id: accounting-read version: 0.1.0 - id: crm-read version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: 'Read workflow — direct request/response, no events' backgroundColor: green textColor: green icon: ArrowsRightLeftIcon --- ## Overview `src/services/customers + src/services/contacts (called directly from the MCP layer)`. In code these are **direct calls from the MCP layer to the foundational services** (`CustomersService`, `ContactsService`) — there is no workflow class. They are grouped as one small catalog service so the graph stays honest about their fan-out without inflating the real workflow nodes: [[query|get-customer]] → [[query|accounting-read]] (QBO, returns the `SyncToken` optimistic-lock key sparse updates need) and [[query|list-contacts]] → [[query|crm-read]]. **Layering:** MCP tool → this workflow service (orchestration, cross-system joins) → foundational services (clean domain models, vendor envelope stripped) → clients (raw vendor APIs) — each behind DI, every connector swappable to fixtures via `*_USE_MOCK`. > **CQRS note:** reads are the "typical reactions" side — normal request/response, **no event lifecycle**. Only commands flow through the [[service|approval-bus]]. ## Architecture diagram --- id: entra-id name: Microsoft Entra ID (QMax tenant) version: 0.1.0 summary: 'The identity provider: org-scoped OAuth for QMax staff. Issues the v2 access tokens (audience + scope mcp-access) the gateway verifies on every request.' owners: - visionary-engineering receives: - id: entra-authorize version: 0.1.0 - id: entra-token version: 0.1.0 badges: - content: Identity — external backgroundColor: blue textColor: blue icon: KeyIcon --- ## Overview The mesh trusts Entra for **authentication** only; **authorization** (role → mutation-kind grants) stays in-house behind the `canApprove` seam. App registration: access-token v2, scope `mcp-access`, claude.ai redirect. ## Architecture diagram --- id: fishbowl-inventory name: Fishbowl Inventory version: 0.1.0 summary: 'Inventory/ERP: customers, parts, open SO/PO; the write path is CSV import (draft sales orders, PO-linked bills). NOT LIVE — the long pole; on-prem behind a Cloudflare Tunnel, no sandbox exists.' owners: - visionary-engineering receives: - id: inventory-read version: 0.1.0 - id: fishbowl-import-sales-order version: 0.1.0 - id: fishbowl-import-bill version: 0.1.0 badges: - content: Connector — NOT LIVE (long pole) backgroundColor: red textColor: red icon: ExclamationTriangleIcon --- ## Overview Auth: session-login → bearer re-auth. **Not deployed and won't be ready for the beta** — every Fishbowl-fed read ([[service|cash-flow-service]] · [[service|scorecard-service]] · [[service|customer-account-service]]) degrades to a partial result with the inventory data omitted and marked unavailable (never a failed tool call), and Fishbowl-bound applies ([[service|order-entry-applier]] · [[service|vendor-bill-applier]] PO path) degrade to a structured `failed` proposal. The read-only live smoke test is sequenced early once the tunnel is up (no sandbox = the riskiest connector). ## Architecture diagram --- id: invoice-applier name: 'InvoiceGenerationApplier (workflow #3)' version: 0.1.0 summary: 'Applies an approved customer invoice: creates it in QBO with one SalesItemLineDetail per approved line.' owners: - visionary-engineering receives: - id: proposal-approved version: 0.1.0 sends: - id: qbo-create-invoice version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: Applier — runs only on human approval backgroundColor: red textColor: red icon: ShieldCheckIcon - content: 'kind: invoice-generation' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview `src/workflows/invoice-generation/invoice-generation.applier.ts`. Receives [[event|proposal-approved]] **dispatched by kind** (`invoice-generation`) from the [[service|approval-bus]] — in code this is a synchronous by-kind dispatch inside the commit path, not a broker subscription (see the event's implementation note). Single vendor write: [[command|qbo-create-invoice]]. Shares the U6 idempotency dependency (QBO native `RequestId` keyed to the proposal id) with create-bill — `WRITE_MODE=gated` refuses to boot against live write connectors until U6 ships. **Watch the full lifecycle:** [[flow|invoice-flow]] (from [[command|propose-invoice]] to the vendor write). ## Architecture diagram --- id: lead-source-web name: Lead Source (Web) version: 0.1.0 summary: Public-source project-candidate feed for Project Hunter. Mock feed + stubbed live ETL. owners: - visionary-engineering receives: - id: lead-source-read version: 0.1.0 badges: - content: Feed — mock + stubbed ETL backgroundColor: gray textColor: gray icon: RssIcon --- ## Overview Capture-only input: candidates are scored and cross-referenced against the CRM by [[service|project-hunter-service]]; no lead is ever created automatically. ## Architecture diagram --- id: mcp-gateway name: MCP Gateway (transport + auth) version: 0.1.0 summary: 'The single front door: the streamable-HTTP MCP endpoint, per-request Entra JWT verification (issuer + audience + scope), bounded sessions, and the OAuth authorization-server proxy.' owners: - visionary-engineering receives: - id: oauth-authorize version: 0.1.0 - id: oauth-token version: 0.1.0 sends: - id: entra-authorize version: 0.1.0 - id: entra-token version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' --- ## Overview `src/mcp/http-transport.ts` + `src/mcp/auth/*`. **Every** `/mcp` request (POST, SSE, DELETE) is bearer-verified against Entra's JWKS *per request* — RS256-pinned, issuer + audience + required scope `mcp-access` enforced, fail-closed on any key-resolution failure, uniform 401s so callers can't probe. The verified subject is re-bound per request (a session id can never launder another subject's role). All 16 tool messages transit this gateway; they are drawn at their **handling workflow service** (the per-workflow services in this domain) to keep the graph honest about which workflow touches what. **Unauthenticated by design:** `/healthz`, the RFC 9728 PRM, and the OAuth proxy paths (they ARE the handshake a 401'd client runs). **Sessions:** capped map (default 128) + 30-min idle eviction — a hostile client can't grow memory unbounded. ### Beta vs production The OAuth **proxy** (Path B) is the live beta config — it exists solely to strip RFC 8707 `resource` (AADSTS9010010). Production Path A: connector on a QMax-verified domain + App ID URI, proxy off. ## Architecture diagram --- id: microsoft-365 name: Microsoft 365 (Graph) version: 0.1.0 summary: 'Mail: the AR collections send path (Graph sendMail, app-only client credentials). Blocked on admin consent for Mail.Send — deferred with the write phase.' owners: - visionary-engineering receives: - id: graph-send-mail version: 0.1.0 badges: - content: Connector — pending admin consent backgroundColor: yellow textColor: yellow icon: ClockIcon --- ## Overview The send is the most outward-facing write in the system (a customer receives it), which is why the approval **is** the send gate and the human can edit at send ([[service|ar-collections-applier]]). RBAC will scope the sending app to the AR mailbox. ## Architecture diagram --- id: order-entry-applier name: 'OrderEntryApplier (workflow #8)' version: 0.1.0 summary: 'Applies an approved sales order: imports a draft SO (status Estimate) via Fishbowl''s CSV import path.' owners: - visionary-engineering receives: - id: proposal-approved version: 0.1.0 sends: - id: fishbowl-import-sales-order version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: Applier — runs only on human approval backgroundColor: red textColor: red icon: ShieldCheckIcon - content: 'kind: order-entry' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview `src/workflows/order-entry/order-entry.applier.ts`. Receives [[event|proposal-approved]] **dispatched by kind** (`order-entry`) from the [[service|approval-bus]] — in code this is a synchronous by-kind dispatch inside the commit path, not a broker subscription (see the event's implementation note). Single vendor write: [[command|fishbowl-import-sales-order]] — Fishbowl's `/api/import` CSV path, its only write surface. **Fishbowl is not live**: an apply while it is absent degrades to a structured `failed` proposal (surfaced on the card), never a crash. Gated behind the VIS-41 RBAC decision before it joins the live cut. **Watch the full lifecycle:** [[flow|order-entry-flow]] (from [[command|propose-order-entry]] to the vendor write). ## Architecture diagram --- id: project-hunter-service name: ProjectHunterService — project-hunter workflow version: 0.1.0 summary: 'Workflow #6. Scores public-source project candidates against the QMax domain rubric, cross-references ClaritySoft for dedup (never creates a lead), returns a ranked report + digest.' owners: - visionary-engineering receives: - id: project-hunter version: 0.1.0 sends: - id: lead-source-read version: 0.1.0 - id: crm-read version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: 'Read workflow — direct request/response, no events' backgroundColor: green textColor: green icon: ArrowsRightLeftIcon --- ## Overview `src/workflows/project-hunter/project-hunter.service.ts`. Two sources: [[query|lead-source-read]] (the public project-candidate feed) and [[query|crm-read]] (ClaritySoft accounts, **dedup cross-reference only**). Candidates are scored against the domain rubric (sulphur/asphalt/petrochem signals, unit counts, geography), filtered by `minScore`, ranked, and returned with rationale + a markdown digest. **Capture-only by design:** no lead is ever written to the CRM — a human decides what enters the pipeline. (Tom's custom-GPT scoring rubric is the model for the portable-rubric direction.) **Layering:** MCP tool → this workflow service (orchestration, cross-system joins) → foundational services (clean domain models, vendor envelope stripped) → clients (raw vendor APIs) — each behind DI, every connector swappable to fixtures via `*_USE_MOCK`. > **CQRS note:** reads are the "typical reactions" side — normal request/response, **no event lifecycle**. Only commands flow through the [[service|approval-bus]]. ## Architecture diagram --- id: quickbooks-online name: QuickBooks Online version: 0.1.0 summary: 'Accounting of record: customers, bills, invoices, reports, AR/AP aging. Reads feed cash-flow + scorecard + the account join; writes (bills/invoices) are approval-gated.' owners: - visionary-engineering receives: - id: accounting-read version: 0.1.0 - id: qbo-create-bill version: 0.1.0 - id: qbo-update-bill version: 0.1.0 - id: qbo-create-invoice version: 0.1.0 badges: - content: Connector — mock (prod pending Intuit review) backgroundColor: yellow textColor: yellow icon: ClockIcon --- ## Overview Auth: OAuth2 with rotated-token write-back. Sandbox verified; production app pending Intuit review. Writes use SyncToken optimistic locking; idempotency will key native RequestId to the proposal id (U6). Written to by three appliers: [[service|vendor-bill-applier]] (non-PO path) · [[service|bill-date-correction-applier]] · [[service|invoice-applier]]. ## Architecture diagram --- id: scorecard-service name: ScorecardService — KPI scorecard workflow version: 0.1.0 summary: 'Workflow #9 — the L10 scorecard: 7 KPIs assembled from QBO, Fishbowl and ClaritySoft, with an optional bounded publish (dated snapshot row) to Smartsheet.' owners: - visionary-engineering receives: - id: kpi-scorecard version: 0.1.0 sends: - id: accounting-read version: 0.1.0 - id: inventory-read version: 0.1.0 - id: crm-read version: 0.1.0 - id: sheets-read version: 0.1.0 - id: smartsheet-append-scorecard-row version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: 'Read workflow — direct request/response, no events' backgroundColor: green textColor: green icon: ArrowsRightLeftIcon --- ## Overview `src/workflows/scorecard/scorecard.service.ts`. Four sources, seven KPIs (cash, AR, AP, weighted pipeline, open SO, open PO, NWC), each tagged with its `source` and a green/yellow/red `status`: - [[query|accounting-read]] → cash + AR/AP (QBO) - [[query|inventory-read]] → open SO/PO values (Fishbowl) - [[query|crm-read]] → open weighted pipeline (ClaritySoft) - [[query|sheets-read]] → locate the scorecard sheet + protect formula columns (Smartsheet, publish only) **Degradation (Fishbowl is not live):** the two Fishbowl KPIs are **omitted** — never zero-filled (a zero SO value reads as a fact) — and `unavailableSources: ['fishbowl']` is set. **The CQRS exception, on purpose and flagged:** with `publish: true` this service performs [[command|smartsheet-append-scorecard-row]] — a **direct bounded write** (one dated snapshot row, formula columns preserved) throttled by `WRITE_MODE` but *not* routed through the approval bus. The pre-showtime audit tracks it as the one seam to either route through the bus or declare as an auto-approve exception in the rule engine (ADR §5). **Layering:** MCP tool → this workflow service (orchestration, cross-system joins) → foundational services (clean domain models, vendor envelope stripped) → clients (raw vendor APIs) — each behind DI, every connector swappable to fixtures via `*_USE_MOCK`. > **CQRS note:** reads are the "typical reactions" side — normal request/response, **no event lifecycle**. Only commands flow through the [[service|approval-bus]]. ## Architecture diagram --- id: smartsheet name: Smartsheet version: 0.1.0 summary: 'Ops sheets: the KPI scorecard target. Read + the one bounded direct write (dated snapshot row append, formula columns preserved).' owners: - visionary-engineering receives: - id: sheets-read version: 0.1.0 - id: smartsheet-append-scorecard-row version: 0.1.0 badges: - content: Connector — prod token in hand backgroundColor: green textColor: green icon: CheckCircleIcon --- ## Overview Auth: static bearer PAT with service-account verify. Production token in hand — ready to flip live. Both messages come from [[service|scorecard-service]] — the append is the flagged direct bounded write outside the approval bus. ## Architecture diagram --- id: vendor-bill-applier name: 'VendorBillApplier — AP intake (workflow #1)' version: 0.1.0 summary: 'Applies an approved vendor bill by PO linkage: PO-linked bills import into Fishbowl (auto-pushes to QBO); non-PO bills post directly to QBO.' owners: - visionary-engineering receives: - id: proposal-approved version: 0.1.0 sends: - id: qbo-create-bill version: 0.1.0 - id: fishbowl-import-bill version: 0.1.0 repository: language: TypeScript (NestJS) url: 'https://github.com/visionarytechnology/qmax-service-mesh' badges: - content: Applier — runs only on human approval backgroundColor: red textColor: red icon: ShieldCheckIcon - content: 'kind: vendor-bill-intake' backgroundColor: gray textColor: gray icon: CodeBracketIcon --- ## Overview `src/workflows/vendor-bill/vendor-bill.applier.ts`. Receives [[event|proposal-approved]] **dispatched by kind** (`vendor-bill-intake`) from the [[service|approval-bus]] — in code this is a synchronous by-kind dispatch inside the commit path, not a broker subscription (see the event's implementation note). The two-path rule from vendor discovery, routed at apply time by `routeVendorBill`: **PO-linked** → [[command|fishbowl-import-bill]] (dated with the warehouse receipt date; Fishbowl auto-pushes to QBO); **non-PO** → [[command|qbo-create-bill]]. The Fishbowl path degrades to a structured `failed` proposal while the connector is absent — never a crash. **Watch the full lifecycle:** [[flow|vendor-bill-flow]] (from [[command|propose-vendor-bill]] to the vendor write). ## Architecture diagram --- id: external-systems name: External Systems version: 0.1.0 summary: 'Everything outside the mesh: the claude.ai host, the Entra identity provider, and the five vendor platforms behind the connector layer (all fixture-mocked until each goes live).' owners: - visionary-engineering services: - id: claude-ai version: 0.1.0 - id: entra-id version: 0.1.0 - id: claritysoft-crm version: 0.1.0 - id: quickbooks-online version: 0.1.0 - id: fishbowl-inventory version: 0.1.0 - id: smartsheet version: 0.1.0 - id: microsoft-365 version: 0.1.0 - id: lead-source-web version: 0.1.0 --- ## Overview The mesh talks to the world through **five ratified connectors** (ADR §8) — ClaritySoft, QuickBooks Online, Fishbowl, Smartsheet, Microsoft 365 — plus the lead-source feed, the claude.ai host and the Entra tenant. Every connector sits behind a `*_USE_MOCK` flag with fixture mocks, so the build was never blocked on live credentials, and connectors flip live **one at a time, read-only first**. **Live status (beta):** ClaritySoft going live read-only now · Smartsheet token in hand · QBO pending Intuit review · M365 pending admin consent · **Fishbowl not live (the long pole)** — its absence degrades reads to partial results, never failures. --- id: qmax-operations name: QMax Operations (Service Mesh) version: 0.1.0 summary: 'The QMax service mesh: a layered NestJS build exposing the QMax platform as one remote MCP server, split CQRS-style into per-workflow read services and a human-gated approval bus + appliers over a shared proposal store.' owners: - visionary-engineering services: - id: mcp-gateway version: 0.1.0 - id: cash-flow-service version: 0.1.0 - id: daily-pipeline-service version: 0.1.0 - id: scorecard-service version: 0.1.0 - id: project-hunter-service version: 0.1.0 - id: customer-account-service version: 0.1.0 - id: entity-read-service version: 0.1.0 - id: approval-bus version: 0.1.0 - id: vendor-bill-applier version: 0.1.0 - id: bill-date-correction-applier version: 0.1.0 - id: invoice-applier version: 0.1.0 - id: order-entry-applier version: 0.1.0 - id: ar-collections-applier version: 0.1.0 flows: - id: cash-flow-tracker-flow version: 0.1.0 - id: daily-pipeline-flow version: 0.1.0 - id: kpi-scorecard-flow version: 0.1.0 - id: project-hunter-flow version: 0.1.0 - id: customer-account-flow version: 0.1.0 - id: vendor-bill-flow version: 0.1.0 - id: bill-date-correction-flow version: 0.1.0 - id: invoice-flow version: 0.1.0 - id: order-entry-flow version: 0.1.0 - id: ar-collections-flow version: 0.1.0 - id: gated-write version: 0.1.0 badges: - content: 'CQRS — queries direct, commands via the approval bus' backgroundColor: blue textColor: blue icon: ArrowsRightLeftIcon - content: 'Beta: read-only, WRITE_MODE=disabled' backgroundColor: yellow textColor: yellow icon: ShieldCheckIcon --- ## Overview The mesh is **CQRS, not CRUD** — the ratified mental model for the whole system: - **Queries** are normal request/response: MCP tool → workflow service → foundational service → vendor client. No side effects, no bus, **no events** — a read's "inner workings" are exactly *which vendor reads it fans into*, and that fan-out is on each service page. - **Commands** **never write directly**: `propose_*` queues a proposal on the [[container|proposal-store]] and comes back `pending`; a **cleared human** approves in the card; only then does the kind's applier execute the one vendor write. Data flows back only via the proposal record. - **One shared database** — the [[service|approval-bus]] owns the queue, the read side reads the same store. Deliberately not event-sourced, not two datastores. ## How to read this catalog (the grain) - A **message page** (query/command/event) is the *contract* — it renders one hop: who sends, who receives. That's by design, not a gap. - A **service page** is the *behavior* — one service per workflow, mirroring `src/workflows/*`, so its `sends` list IS that workflow's exact vendor fan-out. - A **flow page** is the *lifecycle* — one flow per tool, so you can watch a single tool call travel the system end to end. Read flows end at the vendor services (no events); command flows run the full propose → event → approve → apply chain. The 9 Phase-2 production workflows map onto this split: 4 reads ([[flow|daily-pipeline-flow]] · [[flow|cash-flow-tracker-flow]] · [[flow|kpi-scorecard-flow]] · [[flow|project-hunter-flow]]) + the account join ([[flow|customer-account-flow]]) + 5 gated writes ([[flow|vendor-bill-flow]] · [[flow|bill-date-correction-flow]] · [[flow|invoice-flow]] · [[flow|order-entry-flow]] · [[flow|ar-collections-flow]]). Each is an MCP tool **and** an invocable MCP prompt (the front door). ## Where this catalog could be generated from code (Atlas direction) | Catalog artifact | Code source of truth | |---|---| | Query/command input schemas | the zod `inputSchema` on each `registerTool` call | | Response shapes | the TypeScript model files (`*.model.ts`) | | Event payloads + lifecycle | the `Proposal` model + `ProposalStatus` enum | | Proposal-store ERD | the Drizzle schema (`src/approvals/postgres/schema.ts`) | | Per-workflow service send/receive maps | the Nest DI graph (each workflow service's constructor injections) | | **The per-tool flows** | tool → workflow service → injected services → clients, walked from `qmax-mcp.server.ts` | Hand-maintained today (via `gen-catalog.mjs` in this directory — the tables at the top of that script are the editable source); each row is mechanically derivable — that's the evaluation this catalog is running. --- id: visionary-engineering name: Visionary Engineering summary: Builds and operates the QMax service mesh (the QMax × Visionary Phase-2 engagement). members: - george - jesh --- ## Overview The Visionary Technology engineering team on the QMax engagement. Owns the mesh end to end: connectors, workflows, the approval bus, the MCP surface and the deploy. --- id: george name: George role: Engineer / operator summary: Builds and operates the QMax service mesh end to end. avatarUrl: 'https://avatars.githubusercontent.com/u/9919?s=200&v=4' --- Builds and operates the QMax service mesh: connectors, workflows, the approval engine, the MCP surface and the DigitalOcean deploy. --- id: jesh name: Jesh role: Lead engineer summary: Lead engineer — owns the architecture and the CQRS mandate. avatarUrl: 'https://avatars.githubusercontent.com/u/9919?s=200&v=4' --- Lead engineer. Owns the architecture: the ADR, the CQRS mandate (queries direct; commands via the approval bus over one shared database), and the hosting keystone. --- id: proposal-store name: Proposal Store version: 0.1.0 summary: 'The shared CQRS database: every proposal, its payload, status transitions, decisions, results and per-card widget token. Postgres (Drizzle) in the real topology; in-memory in the beta.' container_type: database technology: PostgreSQL (Drizzle ORM) / in-memory authoritative: true access_mode: readWrite classification: internal owners: - visionary-engineering --- ### What is this? One store, both sides — Jesh's shared-database CQRS. The [[service|approval-bus]] owns writes (state transitions are row-atomic: `transition(id, to, patch)` claims `pending→decided` exactly once) and serves the read side ([[query|list-pending-approvals]]) from the same rows. Deliberately **not** event-sourced and **not** split into read/write stores — "it's not gonna be this complicated." Swappable behind the `ApprovalStore` interface: `PROPOSAL_STORE=postgres` (durable, the real topology — DO Managed Postgres) or `memory` (tests/demo; the live beta runs memory because nothing writes yet). ### Schema `src/approvals/postgres/schema.ts` (Drizzle) — proposals table: id, kind, summary, payload (jsonb), requestedBy, status, createdAt, decidedBy/At/Note, result (jsonb), error, widgetToken. Migrations via drizzle-kit. *(An ERD here is directly derivable from the Drizzle schema — an Atlas/code-generation candidate.)* --- id: ar-collections-flow name: Propose AR Collections Email — command lifecycle version: 0.1.0 summary: 'One propose_ar_collection_email call end to end: queue → human reviews AND CAN EDIT subject/body → approve is the send → Graph sendMail → MutationApplied.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: propose label: MCP tool call - id: propose title: Propose AR Collections Email message: id: propose-ar-collection-email version: 0.1.0 next_step: id: bus-queue label: queue only — no vendor write - id: bus-queue title: Approval Bus queues the proposal service: id: approval-bus version: 0.1.0 next_step: id: evt-created label: pending + widget token minted - id: evt-created title: Proposal Created message: id: proposal-created version: 0.1.0 next_step: id: human label: approval card renders - id: human title: Cleared human reviews the card actor: name: Approver — can edit subject/body at send (snapshot recorded) next_step: id: commit label: human click — approve - id: commit title: Commit Approval (app-only) message: id: commit-approval version: 0.1.0 next_step: id: bus-apply label: RBAC + atomic pending→approved claim - id: bus-apply title: Approval Bus claims + dispatches service: id: approval-bus version: 0.1.0 next_step: id: evt-approved label: exactly-once claim - id: evt-approved title: Proposal Approved message: id: proposal-approved version: 0.1.0 next_step: id: applier label: 'dispatch by kind: ar-collections-email' - id: applier title: ArCollectionsApplier — send exactly what was approved service: id: ar-collections-applier version: 0.1.0 next_step: id: w-graph-send-mail label: the approved (possibly edited) email - id: w-graph-send-mail title: Graph Send Mail message: id: graph-send-mail version: 0.1.0 next_step: id: s-microsoft-365 label: the one vendor write - id: s-microsoft-365 title: Microsoft 365 (Graph) service: id: microsoft-365 version: 0.1.0 next_step: id: evt-applied label: result recorded on the proposal - id: evt-applied title: Mutation Applied message: id: mutation-applied version: 0.1.0 next_step: id: end label: card refreshes - id: end title: 'Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)' --- ## Overview The most outward-facing write in the system — a customer receives it — so the approval **is** the send gate, and the **edit-at-send** path (unique to this kind) snapshots the human's edited subject/body onto the proposal before apply: the audit trail records exactly what was approved, not what was proposed. Blocked on `Mail.Send` admin consent before live. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Propose AR Collections Email** → *queue only — no vendor write* 1. **Approval Bus queues the proposal** → *pending + widget token minted* 1. **Proposal Created** → *approval card renders* 1. **Cleared human reviews the card** → *human click — approve* 1. **Commit Approval (app-only)** → *RBAC + atomic pending→approved claim* 1. **Approval Bus claims + dispatches** → *exactly-once claim* 1. **Proposal Approved** → *dispatch by kind: ar-collections-email* 1. **ArCollectionsApplier — send exactly what was approved** → *the approved (possibly edited) email* 1. **Graph Send Mail** → *the one vendor write* 1. **Microsoft 365 (Graph)** → *result recorded on the proposal* 1. **Mutation Applied** → *card refreshes* 1. **Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)** --- id: bill-date-correction-flow name: Propose Bill-Date Correction — command lifecycle version: 0.1.0 summary: 'One propose_bill_date_correction call end to end — the recommended FIRST live command: single reversible field, QBO-only, naturally idempotent.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: propose label: MCP tool call - id: propose title: Propose Bill-Date Correction message: id: propose-bill-date-correction version: 0.1.0 next_step: id: bus-queue label: queue only — no vendor write - id: bus-queue title: Approval Bus queues the proposal service: id: approval-bus version: 0.1.0 next_step: id: evt-created label: pending + widget token minted - id: evt-created title: Proposal Created message: id: proposal-created version: 0.1.0 next_step: id: human label: approval card renders - id: human title: Cleared human reviews the card actor: name: Approver (RBAC-cleared QMax staff) next_step: id: commit label: human click — approve - id: commit title: Commit Approval (app-only) message: id: commit-approval version: 0.1.0 next_step: id: bus-apply label: RBAC + atomic pending→approved claim - id: bus-apply title: Approval Bus claims + dispatches service: id: approval-bus version: 0.1.0 next_step: id: evt-approved label: exactly-once claim - id: evt-approved title: Proposal Approved message: id: proposal-approved version: 0.1.0 next_step: id: applier label: 'dispatch by kind: bill-date-correction' - id: applier title: 'BillDateCorrectionApplier — re-fetch SyncToken, sparse update' service: id: bill-date-correction-applier version: 0.1.0 next_step: id: w-qbo-update-bill label: re-fetch bill → fresh SyncToken → one-field sparse update - id: w-qbo-update-bill title: QBO Update Bill (sparse) message: id: qbo-update-bill version: 0.1.0 next_step: id: s-quickbooks-online label: the one vendor write - id: s-quickbooks-online title: QuickBooks Online service: id: quickbooks-online version: 0.1.0 next_step: id: evt-applied label: result recorded on the proposal - id: evt-applied title: Mutation Applied message: id: mutation-applied version: 0.1.0 next_step: id: end label: card refreshes - id: end title: 'Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)' --- ## Overview The lowest-blast-radius command in the set — which is why the go-live plan enables it first: one reversible date field on an existing QBO record, no external party sees it, naturally idempotent (re-applying the same date is a no-op), and the applier's SyncToken re-fetch makes the optimistic-lock 409 path real (tested). The apply is two vendor operations: a QBO read for the fresh `SyncToken`, then the sparse update. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Propose Bill-Date Correction** → *queue only — no vendor write* 1. **Approval Bus queues the proposal** → *pending + widget token minted* 1. **Proposal Created** → *approval card renders* 1. **Cleared human reviews the card** → *human click — approve* 1. **Commit Approval (app-only)** → *RBAC + atomic pending→approved claim* 1. **Approval Bus claims + dispatches** → *exactly-once claim* 1. **Proposal Approved** → *dispatch by kind: bill-date-correction* 1. **BillDateCorrectionApplier — re-fetch SyncToken, sparse update** → *re-fetch bill → fresh SyncToken → one-field sparse update* 1. **QBO Update Bill (sparse)** → *the one vendor write* 1. **QuickBooks Online** → *result recorded on the proposal* 1. **Mutation Applied** → *card refreshes* 1. **Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)** --- id: cash-flow-tracker-flow name: Cash Flow Tracker — tool-call lifecycle version: 0.1.0 summary: 'One cash_flow_tracker call end to end: the query hits CashFlowService, which fans into QBO + Fishbowl + ClaritySoft in parallel and assembles the unified cash view. No events — reads are direct.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: q-cash-flow-tracker label: MCP tool call - id: q-cash-flow-tracker title: Cash Flow Tracker message: id: cash-flow-tracker version: 0.1.0 next_step: id: svc label: direct request/response — no events - id: svc title: CashFlowService service: id: cash-flow-service version: 0.1.0 next_steps: - id: r-accounting-read label: cash position + AR/AP aging - id: r-inventory-read label: open SO/PO - id: r-crm-read label: weighted pipeline - id: r-accounting-read title: Accounting Read message: id: accounting-read version: 0.1.0 next_step: id: s-quickbooks-online label: vendor API - id: r-inventory-read title: Inventory Read message: id: inventory-read version: 0.1.0 next_step: id: s-fishbowl-inventory label: vendor API - id: r-crm-read title: CRM Read message: id: crm-read version: 0.1.0 next_step: id: s-claritysoft-crm label: vendor API - id: s-quickbooks-online title: QuickBooks Online service: id: quickbooks-online version: 0.1.0 next_step: id: resp label: results fold in - id: s-fishbowl-inventory title: Fishbowl Inventory service: id: fishbowl-inventory version: 0.1.0 next_step: id: resp label: 'absent → degraded: openOrders omitted, marked unavailable' - id: s-claritysoft-crm title: ClaritySoft CRM service: id: claritysoft-crm version: 0.1.0 next_step: id: resp label: results fold in - id: resp title: CashFlowView assembled → returned in the tool result (divergence + runway alerts; direct response — no event lifecycle on reads) --- ## Overview The answer to "what is it querying specifically": three parallel vendor reads, one assembled view. A **query never touches the event system** — that is the CQRS split; contrast with any command flow (e.g. [[flow|vendor-bill-flow]]). While Fishbowl is absent (the connector long pole), the inventory branch degrades: `openOrders` omitted, `unavailableSources: ['fishbowl']`, divergence alert suppressed — never a failed tool call. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Cash Flow Tracker** → *direct request/response — no events* 1. **CashFlowService** → *cash position + AR/AP aging* / *open SO/PO* / *weighted pipeline* 1. **Accounting Read** → *vendor API* 1. **Inventory Read** → *vendor API* 1. **CRM Read** → *vendor API* 1. **QuickBooks Online** → *results fold in* 1. **Fishbowl Inventory** → *absent → degraded: openOrders omitted, marked unavailable* 1. **ClaritySoft CRM** → *results fold in* 1. **CashFlowView assembled → returned in the tool result (divergence + runway alerts; direct response — no event lifecycle on reads)** --- id: customer-account-flow name: Customer Account Join — tool-call lifecycle version: 0.1.0 summary: 'list_customer_accounts / find_customer_account end to end: the three-system account join (CRM + accounting + inventory) with presence and linkage flags.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_steps: - id: q-list-customer-accounts label: MCP tool call - id: q-find-customer-account label: MCP tool call - id: q-list-customer-accounts title: List Customer Accounts message: id: list-customer-accounts version: 0.1.0 next_step: id: svc label: direct request/response — no events - id: q-find-customer-account title: Find Customer Account message: id: find-customer-account version: 0.1.0 next_step: id: svc label: direct request/response — no events - id: svc title: CustomerAccountService service: id: customer-account-service version: 0.1.0 next_steps: - id: r-crm-read label: ClaritySoft accounts - id: r-accounting-read label: QBO customers - id: r-inventory-read label: Fishbowl customers - id: r-crm-read title: CRM Read message: id: crm-read version: 0.1.0 next_step: id: s-claritysoft-crm label: vendor API - id: r-accounting-read title: Accounting Read message: id: accounting-read version: 0.1.0 next_step: id: s-quickbooks-online label: vendor API - id: r-inventory-read title: Inventory Read message: id: inventory-read version: 0.1.0 next_step: id: s-fishbowl-inventory label: vendor API - id: s-claritysoft-crm title: ClaritySoft CRM service: id: claritysoft-crm version: 0.1.0 next_step: id: resp label: results fold in - id: s-quickbooks-online title: QuickBooks Online service: id: quickbooks-online version: 0.1.0 next_step: id: resp label: results fold in - id: s-fishbowl-inventory title: Fishbowl Inventory service: id: fishbowl-inventory version: 0.1.0 next_step: id: resp label: 'absent → join degrades: folds reachable systems, marked unavailable' - id: resp title: CustomerAccount join assembled → per-system slices + presentIn/linked flags returned in the tool result --- ## Overview The account-join primitive both tools share. While a source is down the join folds the reachable systems and marks `unavailableSources` — `presentIn`/`linked` are never misread as "absent from that system." ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* / *MCP tool call* 1. **List Customer Accounts** → *direct request/response — no events* 1. **Find Customer Account** → *direct request/response — no events* 1. **CustomerAccountService** → *ClaritySoft accounts* / *QBO customers* / *Fishbowl customers* 1. **CRM Read** → *vendor API* 1. **Accounting Read** → *vendor API* 1. **Inventory Read** → *vendor API* 1. **ClaritySoft CRM** → *results fold in* 1. **QuickBooks Online** → *results fold in* 1. **Fishbowl Inventory** → *absent → join degrades: folds reachable systems, marked unavailable* 1. **CustomerAccount join assembled → per-system slices + presentIn/linked flags returned in the tool result** --- id: daily-pipeline-flow name: Daily Pipeline View — tool-call lifecycle version: 0.1.0 summary: 'One daily_pipeline_view call end to end: single-source into ClaritySoft, rolled up by stage. The simplest read path — and the demo-safe one.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: q-daily-pipeline-view label: MCP tool call - id: q-daily-pipeline-view title: Daily Pipeline View message: id: daily-pipeline-view version: 0.1.0 next_step: id: svc label: direct request/response — no events - id: svc title: DailyPipelineService service: id: daily-pipeline-service version: 0.1.0 next_steps: - id: r-crm-read label: all opportunities (paginated) - id: r-crm-read title: CRM Read message: id: crm-read version: 0.1.0 next_step: id: s-claritysoft-crm label: vendor API - id: s-claritysoft-crm title: ClaritySoft CRM service: id: claritysoft-crm version: 0.1.0 next_step: id: resp label: results fold in - id: resp title: DailyPipelineView assembled → company funnel (+ optional per-owner slice) returned in the tool result --- ## Overview Single-source: only ClaritySoft. Unaffected by the Fishbowl gap or a QBO outage — the most resilient read in the set. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Daily Pipeline View** → *direct request/response — no events* 1. **DailyPipelineService** → *all opportunities (paginated)* 1. **CRM Read** → *vendor API* 1. **ClaritySoft CRM** → *results fold in* 1. **DailyPipelineView assembled → company funnel (+ optional per-owner slice) returned in the tool result** --- id: gated-write name: Gated Write — the generic command lifecycle version: 0.1.0 summary: 'The command path every propose_* shares: the agent proposes, a cleared human approves in the card, the kind''s applier executes the vendor write — with every step recorded on the shared store.' owners: - visionary-engineering steps: - id: agent_proposes title: Max (agent) drafts the mutation actor: name: Max agent on claude.ai next_step: id: propose_command label: 'propose_* tool call' - id: propose_command title: Propose Vendor Bill (example) message: id: propose-vendor-bill version: 0.1.0 next_step: id: bus_queues label: queue only — no write - id: bus_queues title: Approval Bus queues the proposal service: id: approval-bus version: 0.1.0 next_step: id: proposal_created label: pending + widget token minted - id: proposal_created title: Proposal Created message: id: proposal-created version: 0.1.0 next_step: id: human_reviews label: approval card renders - id: human_reviews title: Cleared human reviews the card actor: name: Approver (RBAC-cleared QMax staff) next_step: id: commit label: human click — approve/reject - id: commit title: Commit Approval (app-only) message: id: commit-approval version: 0.1.0 next_step: id: bus_applies label: RBAC + atomic claim - id: bus_applies title: Approval Bus dispatches by kind service: id: approval-bus version: 0.1.0 next_step: id: applier label: the kind's applier - id: applier title: Applier service (e.g. VendorBillApplier) service: id: vendor-bill-applier version: 0.1.0 next_step: id: vendor_write label: the one vendor write - id: vendor_write title: QBO Create Bill (example) message: id: qbo-create-bill version: 0.1.0 next_step: id: applied label: result recorded on the proposal - id: applied title: Mutation Applied message: id: mutation-applied version: 0.1.0 --- ## Overview The shape every command shares. For a **specific** tool's path — including its vendor routing and degradation behavior — use the per-tool flows: [[flow|vendor-bill-flow]] · [[flow|bill-date-correction-flow]] · [[flow|invoice-flow]] · [[flow|order-entry-flow]] · [[flow|ar-collections-flow]]. Contrast with the read flows ([[flow|cash-flow-tracker-flow]] et al.), which are direct request/response and never enter the event system. ## The flow ## How it works 1. **Max (agent) drafts the mutation** → *propose_* tool call* 1. **Propose Vendor Bill (example)** → *queue only — no write* 1. **Approval Bus queues the proposal** → *pending + widget token minted* 1. **Proposal Created** → *approval card renders* 1. **Cleared human reviews the card** → *human click — approve/reject* 1. **Commit Approval (app-only)** → *RBAC + atomic claim* 1. **Approval Bus dispatches by kind** → *the kind's applier* 1. **Applier service (e.g. VendorBillApplier)** → *the one vendor write* 1. **QBO Create Bill (example)** → *result recorded on the proposal* 1. **Mutation Applied** --- id: invoice-flow name: Propose Customer Invoice — command lifecycle version: 0.1.0 summary: 'One propose_invoice call end to end: queue → human approve → applier creates the QBO invoice from the approved lines.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: propose label: MCP tool call - id: propose title: Propose Customer Invoice message: id: propose-invoice version: 0.1.0 next_step: id: bus-queue label: queue only — no vendor write - id: bus-queue title: Approval Bus queues the proposal service: id: approval-bus version: 0.1.0 next_step: id: evt-created label: pending + widget token minted - id: evt-created title: Proposal Created message: id: proposal-created version: 0.1.0 next_step: id: human label: approval card renders - id: human title: Cleared human reviews the card actor: name: Approver (RBAC-cleared QMax staff) next_step: id: commit label: human click — approve - id: commit title: Commit Approval (app-only) message: id: commit-approval version: 0.1.0 next_step: id: bus-apply label: RBAC + atomic pending→approved claim - id: bus-apply title: Approval Bus claims + dispatches service: id: approval-bus version: 0.1.0 next_step: id: evt-approved label: exactly-once claim - id: evt-approved title: Proposal Approved message: id: proposal-approved version: 0.1.0 next_step: id: applier label: 'dispatch by kind: invoice-generation' - id: applier title: InvoiceGenerationApplier service: id: invoice-applier version: 0.1.0 next_step: id: w-qbo-create-invoice label: one SalesItemLineDetail per approved line - id: w-qbo-create-invoice title: QBO Create Invoice message: id: qbo-create-invoice version: 0.1.0 next_step: id: s-quickbooks-online label: the one vendor write - id: s-quickbooks-online title: QuickBooks Online service: id: quickbooks-online version: 0.1.0 next_step: id: evt-applied label: result recorded on the proposal - id: evt-applied title: Mutation Applied message: id: mutation-applied version: 0.1.0 next_step: id: end label: card refreshes - id: end title: 'Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)' --- ## Overview The AR-side creation write. Second in the go-live order (after bill-date correction), once U6 idempotency (QBO `RequestId` keyed to the proposal id) ships — a created invoice is customer-visible, so retry-safety is a precondition. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Propose Customer Invoice** → *queue only — no vendor write* 1. **Approval Bus queues the proposal** → *pending + widget token minted* 1. **Proposal Created** → *approval card renders* 1. **Cleared human reviews the card** → *human click — approve* 1. **Commit Approval (app-only)** → *RBAC + atomic pending→approved claim* 1. **Approval Bus claims + dispatches** → *exactly-once claim* 1. **Proposal Approved** → *dispatch by kind: invoice-generation* 1. **InvoiceGenerationApplier** → *one SalesItemLineDetail per approved line* 1. **QBO Create Invoice** → *the one vendor write* 1. **QuickBooks Online** → *result recorded on the proposal* 1. **Mutation Applied** → *card refreshes* 1. **Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)** --- id: kpi-scorecard-flow name: KPI Scorecard — tool-call lifecycle version: 0.1.0 summary: 'One kpi_scorecard call end to end: four sources fold into 7 KPIs; with publish: true the service performs the one direct bounded write (a dated Smartsheet snapshot row).' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: q-kpi-scorecard label: MCP tool call - id: q-kpi-scorecard title: KPI Scorecard message: id: kpi-scorecard version: 0.1.0 next_step: id: svc label: direct request/response — no events - id: svc title: ScorecardService service: id: scorecard-service version: 0.1.0 next_steps: - id: r-accounting-read label: cash + AR/AP - id: r-inventory-read label: open SO/PO values - id: r-crm-read label: open weighted pipeline - id: r-smartsheet-append-scorecard-row label: 'publish: true → direct bounded write (WRITE_MODE-throttled)' - id: r-accounting-read title: Accounting Read message: id: accounting-read version: 0.1.0 next_step: id: s-quickbooks-online label: vendor API - id: r-inventory-read title: Inventory Read message: id: inventory-read version: 0.1.0 next_step: id: s-fishbowl-inventory label: vendor API - id: r-crm-read title: CRM Read message: id: crm-read version: 0.1.0 next_step: id: s-claritysoft-crm label: vendor API - id: r-smartsheet-append-scorecard-row title: 'Smartsheet Append Scorecard Row (publish: true only)' message: id: smartsheet-append-scorecard-row version: 0.1.0 next_step: id: s-smartsheet label: sheets-read locates the sheet first - id: s-quickbooks-online title: QuickBooks Online service: id: quickbooks-online version: 0.1.0 next_step: id: resp label: results fold in - id: s-fishbowl-inventory title: Fishbowl Inventory service: id: fishbowl-inventory version: 0.1.0 next_step: id: resp label: absent → the two order-book KPIs omitted (never zero-filled) - id: s-claritysoft-crm title: ClaritySoft CRM service: id: claritysoft-crm version: 0.1.0 next_step: id: resp label: results fold in - id: s-smartsheet title: Smartsheet service: id: smartsheet version: 0.1.0 next_step: id: resp label: snapshot row appended (formula columns preserved) - id: resp title: ScorecardView assembled → 7 KPIs with status returned in the tool result --- ## Overview Mostly a pure read (three sources → 7 KPIs), with **the one deliberate CQRS exception** in the system: `publish: true` performs [[command|smartsheet-append-scorecard-row]] directly from the read service — bounded (one dated row, formulas preserved), `WRITE_MODE`-throttled, but **not** routed through the approval bus. Flagged in the audit to close or declare (ADR §5 auto-approve list). The [[query|sheets-read]] locate/verify step precedes the append on the publish path. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **KPI Scorecard** → *direct request/response — no events* 1. **ScorecardService** → *cash + AR/AP* / *open SO/PO values* / *open weighted pipeline* / *publish: true → direct bounded write (WRITE_MODE-throttled)* 1. **Accounting Read** → *vendor API* 1. **Inventory Read** → *vendor API* 1. **CRM Read** → *vendor API* 1. **Smartsheet Append Scorecard Row (publish: true only)** → *sheets-read locates the sheet first* 1. **QuickBooks Online** → *results fold in* 1. **Fishbowl Inventory** → *absent → the two order-book KPIs omitted (never zero-filled)* 1. **ClaritySoft CRM** → *results fold in* 1. **Smartsheet** → *snapshot row appended (formula columns preserved)* 1. **ScorecardView assembled → 7 KPIs with status returned in the tool result** --- id: order-entry-flow name: Propose Sales Order — command lifecycle version: 0.1.0 summary: 'One propose_order_entry call end to end: queue → sales approves → applier imports a draft SO (status Estimate) via Fishbowl''s CSV import path.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: propose label: MCP tool call - id: propose title: Propose Sales Order message: id: propose-order-entry version: 0.1.0 next_step: id: bus-queue label: queue only — no vendor write - id: bus-queue title: Approval Bus queues the proposal service: id: approval-bus version: 0.1.0 next_step: id: evt-created label: pending + widget token minted - id: evt-created title: Proposal Created message: id: proposal-created version: 0.1.0 next_step: id: human label: approval card renders - id: human title: Cleared human reviews the card actor: name: Approver (RBAC-cleared QMax staff) next_step: id: commit label: human click — approve - id: commit title: Commit Approval (app-only) message: id: commit-approval version: 0.1.0 next_step: id: bus-apply label: RBAC + atomic pending→approved claim - id: bus-apply title: Approval Bus claims + dispatches service: id: approval-bus version: 0.1.0 next_step: id: evt-approved label: exactly-once claim - id: evt-approved title: Proposal Approved message: id: proposal-approved version: 0.1.0 next_step: id: applier label: 'dispatch by kind: order-entry' - id: applier title: 'OrderEntryApplier — CSV import, draft SO' service: id: order-entry-applier version: 0.1.0 next_step: id: w-fishbowl-import-sales-order label: 'draft SO, status Estimate' - id: w-fishbowl-import-sales-order title: Fishbowl Import Sales Order message: id: fishbowl-import-sales-order version: 0.1.0 next_step: id: s-fishbowl-inventory label: the one vendor write - id: s-fishbowl-inventory title: Fishbowl Inventory service: id: fishbowl-inventory version: 0.1.0 next_step: id: evt-applied label: result recorded on the proposal - id: evt-applied title: Mutation Applied message: id: mutation-applied version: 0.1.0 next_step: id: end label: card refreshes - id: end title: 'Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)' --- ## Overview Fishbowl-bound, so **not in the first live cut**: the connector is the provisioning long pole (on-prem, Cloudflare Tunnel, no sandbox), and VIS-41 (the PO-path RBAC decision) gates any Fishbowl-touching command. While Fishbowl is absent an approved apply degrades to a structured `failed` proposal on the card — never a crash. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Propose Sales Order** → *queue only — no vendor write* 1. **Approval Bus queues the proposal** → *pending + widget token minted* 1. **Proposal Created** → *approval card renders* 1. **Cleared human reviews the card** → *human click — approve* 1. **Commit Approval (app-only)** → *RBAC + atomic pending→approved claim* 1. **Approval Bus claims + dispatches** → *exactly-once claim* 1. **Proposal Approved** → *dispatch by kind: order-entry* 1. **OrderEntryApplier — CSV import, draft SO** → *draft SO, status Estimate* 1. **Fishbowl Import Sales Order** → *the one vendor write* 1. **Fishbowl Inventory** → *result recorded on the proposal* 1. **Mutation Applied** → *card refreshes* 1. **Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)** --- id: project-hunter-flow name: Project Hunter — tool-call lifecycle version: 0.1.0 summary: 'One project_hunter call end to end: score the public candidate feed against the domain rubric, cross-reference ClaritySoft for dedup, return a ranked report. Never creates a lead.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: q-project-hunter label: MCP tool call - id: q-project-hunter title: Project Hunter message: id: project-hunter version: 0.1.0 next_step: id: svc label: direct request/response — no events - id: svc title: ProjectHunterService service: id: project-hunter-service version: 0.1.0 next_steps: - id: r-lead-source-read label: public project candidates - id: r-crm-read label: 'accounts — dedup cross-ref only, never creates a lead' - id: r-lead-source-read title: Lead-Source Read message: id: lead-source-read version: 0.1.0 next_step: id: s-lead-source-web label: vendor API - id: r-crm-read title: CRM Read message: id: crm-read version: 0.1.0 next_step: id: s-claritysoft-crm label: vendor API - id: s-lead-source-web title: Lead Source (Web) service: id: lead-source-web version: 0.1.0 next_step: id: resp label: results fold in - id: s-claritysoft-crm title: ClaritySoft CRM service: id: claritysoft-crm version: 0.1.0 next_step: id: resp label: results fold in - id: resp title: 'ProjectHunterReport assembled → ranked, scored candidates + markdown digest returned in the tool result' --- ## Overview Capture-only: candidates are scored (domain rubric), cross-referenced against the CRM for dedup, ranked and returned. A human decides what enters the pipeline — the workflow never writes. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Project Hunter** → *direct request/response — no events* 1. **ProjectHunterService** → *public project candidates* / *accounts — dedup cross-ref only, never creates a lead* 1. **Lead-Source Read** → *vendor API* 1. **CRM Read** → *vendor API* 1. **Lead Source (Web)** → *results fold in* 1. **ClaritySoft CRM** → *results fold in* 1. **ProjectHunterReport assembled → ranked, scored candidates + markdown digest returned in the tool result** --- id: vendor-bill-flow name: Propose Vendor Bill — command lifecycle version: 0.1.0 summary: 'One propose_vendor_bill call end to end: queue → ProposalCreated → human approve → ProposalApproved → applier routes by PO linkage → Fishbowl import or QBO bill → MutationApplied.' owners: - visionary-engineering steps: - id: agent title: Max (agent) on claude.ai actor: name: QMax staff driving Max on claude.ai next_step: id: propose label: MCP tool call - id: propose title: Propose Vendor Bill message: id: propose-vendor-bill version: 0.1.0 next_step: id: bus-queue label: queue only — no vendor write - id: bus-queue title: Approval Bus queues the proposal service: id: approval-bus version: 0.1.0 next_step: id: evt-created label: pending + widget token minted - id: evt-created title: Proposal Created message: id: proposal-created version: 0.1.0 next_step: id: human label: approval card renders - id: human title: Cleared human reviews the card actor: name: Approver (RBAC-cleared QMax staff) next_step: id: commit label: human click — approve - id: commit title: Commit Approval (app-only) message: id: commit-approval version: 0.1.0 next_step: id: bus-apply label: RBAC + atomic pending→approved claim - id: bus-apply title: Approval Bus claims + dispatches service: id: approval-bus version: 0.1.0 next_step: id: evt-approved label: exactly-once claim - id: evt-approved title: Proposal Approved message: id: proposal-approved version: 0.1.0 next_step: id: applier label: 'dispatch by kind: vendor-bill-intake' - id: applier title: VendorBillApplier — routes by PO linkage service: id: vendor-bill-applier version: 0.1.0 next_steps: - id: w-fishbowl-import-bill label: PO-linked → Fishbowl (auto-pushes to QBO) - id: w-qbo-create-bill label: non-PO → direct QBO bill - id: w-fishbowl-import-bill title: Fishbowl Import Bill message: id: fishbowl-import-bill version: 0.1.0 next_step: id: s-fishbowl-inventory label: the one vendor write - id: w-qbo-create-bill title: QBO Create Bill message: id: qbo-create-bill version: 0.1.0 next_step: id: s-quickbooks-online label: the one vendor write - id: s-fishbowl-inventory title: Fishbowl Inventory service: id: fishbowl-inventory version: 0.1.0 next_step: id: evt-applied label: result recorded on the proposal - id: s-quickbooks-online title: QuickBooks Online service: id: quickbooks-online version: 0.1.0 next_step: id: evt-applied label: result recorded on the proposal - id: evt-applied title: Mutation Applied message: id: mutation-applied version: 0.1.0 next_step: id: end label: card refreshes - id: end title: 'Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)' --- ## Overview The "dispatch for the workflow-based tooling," watchable: the propose queues (no data mutates), the human's click is the only execution path, and the applier picks the vendor route **at apply time** by PO linkage — the two-path rule from vendor discovery. This is the richest command flow (two possible vendor writes); contrast with a read like [[flow|cash-flow-tracker-flow]], which never enters the event system. ## The flow ## How it works 1. **Max (agent) on claude.ai** → *MCP tool call* 1. **Propose Vendor Bill** → *queue only — no vendor write* 1. **Approval Bus queues the proposal** → *pending + widget token minted* 1. **Proposal Created** → *approval card renders* 1. **Cleared human reviews the card** → *human click — approve* 1. **Commit Approval (app-only)** → *RBAC + atomic pending→approved claim* 1. **Approval Bus claims + dispatches** → *exactly-once claim* 1. **Proposal Approved** → *dispatch by kind: vendor-bill-intake* 1. **VendorBillApplier — routes by PO linkage** → *PO-linked → Fishbowl (auto-pushes to QBO)* / *non-PO → direct QBO bill* 1. **Fishbowl Import Bill** → *the one vendor write* 1. **QBO Create Bill** → *the one vendor write* 1. **Fishbowl Inventory** → *result recorded on the proposal* 1. **QuickBooks Online** → *result recorded on the proposal* 1. **Mutation Applied** → *card refreshes* 1. **Terminal: proposal `applied` (a thrown apply records `failed` instead — Mutation Failed)**