---
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)**