WFDASH-WEBAPP/docs/order-assistant.md
Dave 85533245bf feat(orders): order assistant routes, MCP tool and runbook
Task 7 of the AI order assistant plan.

- POST /api/v1/orders/assistant/tick (n8n, assertAgentAuth)
- POST /api/v1/orders/{id}/assistant/run (browser session; 409 for
  "AI is disabled" and "not set up yet")
- MCP orders.assistant_match { orderId } -> matchOrder(force) with a
  TOOL_HINTS entry; deliberately NOT in READ_TOOLS
- docs/order-assistant.md runbook and the AGENTS.md section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:06:51 +02:00

8.1 KiB

AI order assistant runbook

Design: docs/superpowers/specs/2026-10-24-ai-order-assistant-design.md. Plan: docs/superpowers/plans/2026-10-24-ai-order-assistant.md.

What it does

An order that came from a quote should point at that quote. Today only an order created from a quote carries the link; every other order — a walk-in, an email intake, a counter sale — has to be linked by hand.

The order assistant works out which quote an order came from. On a schedule it looks at orders that have no quote yet, asks the AI which of the candidate quotes the order came from, and then either:

  • links the order to the quote itself (when it is sure), or
  • suggests the quote, and a person accepts it.

The AI never touches prices, never changes a line's status, never creates a purchase order and never sends anything. It only decides which quote an order belongs to.

The assistant links an order to a quote only when all of these are true:

  • The "Auto-link when sure" setting is on (it is on by default).
  • The AI says it is sure about the match.
  • Every real order line is paired to a quote line, and each of those pairs is a sure match.
  • Every pair points at a line of the one chosen quote (not a mixture of two quotes).
  • The chosen quote belongs to the same company as the order.

If any of those fail, the assistant only writes a suggestion. The order page then shows "AI suggests Q…" with the reason, the doubts and the line-by-line pairing, and the existing Link button is there to accept it.

The assistant also refuses to link when PocketBase says no — for example when a line on the order has already moved past pending, or when another order already points at that quote. A refusal is not an error: the assistant falls back to a suggestion.

Going live (manual steps, in this order)

  1. Migrate PocketBase. Run against the live PB: npm run pb:migrate:order-assistant (adds orders.ai_quote_match, order_items.ai_fill and the order_assistant_* fields on ai_settings). If the app is deployed before this, PocketBase drops the unknown fields silently and the assistant's result lands nowhere.
  2. Publish the PB hook change. ai_quote_match and ai_fill were added to IGNORED_FIELDS in pocketbase/pb_hooks/audit_log_utils.js, so the AI's own writes do not flood the audit log. Publish it the same way the other hook changes are published (npm run pb:hooks:publish).
  3. Deploy the app. Pushing to main does not auto-deploy; it is a manual deploy that takes 5 to 10 minutes.
  4. Clean up the stale suggestions. The old rule-based matcher wrote soft suggestions that were not backed by a matching line (one quote was suggested to 16 different orders). Run the clean-up script against the live PB — dry run first:
    npx tsx scripts/2026-10-24-rematch-quote-suggestions.ts
    npx tsx scripts/2026-10-24-rematch-quote-suggestions.ts --apply
    
    The dry run only reads. Read its report before running it with --apply.
  5. Activate the n8n clock (below).
  6. Turn it on in Settings, AI: the global "AI enabled" switch must be on, then switch on "Order assistant enabled" in the Order assistant section and click Save. Defaults: every 10 minutes, auto-link on.

The tick does nothing until both switches are on, so steps 5 and 6 are safe in either order.

n8n workflow: "WFDASH Order assistant tick"

n8n only acts as a clock. The app decides whether a run is due (AI and the assistant on, and at least "Run every" minutes since the last run), so the setting in the app controls the real frequency.

Spec:

Node Setting
Name WFDASH Order assistant tick
Trigger Schedule Trigger, every 5 minutes
Action HTTP Request
Method / URL POST https://wfdash.apphub.co.za/api/v1/orders/assistant/tick
Authentication Generic Credential Type, Header Auth, the same credential the quote assistant tick workflow uses (an Authorization: Bearer <AGENT_API_KEY> header). Do not create a new secret.
Body none
Settings tab Retry On Fail: off; Timeout: 120000 ms (120 s)
Status inactive until the app is deployed and migrated

Click-by-click:

  1. n8n, Workflows, Create workflow. Name it WFDASH Order assistant tick.
  2. Add node, Schedule Trigger. Trigger Interval: Minutes, Minutes Between Triggers: 5.
  3. Add node, HTTP Request, connect it after the trigger.
    • Method: POST.
    • URL: https://wfdash.apphub.co.za/api/v1/orders/assistant/tick.
    • Authentication: Generic Credential Type, Generic Auth Type: Header Auth. Pick the credential used by the quote assistant tick workflow (open that workflow and read its HTTP Request node to see which one).
    • Options, Add option, Timeout: 120000.
    • Settings tab on the node: turn Retry On Fail off.
  4. Save. Leave the workflow inactive until the steps above are done.
  5. After deploy, click Execute workflow once. Expected response while the assistant is off: {"ran":false,"reason":"disabled"}. When on and not yet due: {"ran":false,"reason":"not_due"}. While an earlier run is still going: {"ran":false,"reason":"running"}. When it ran: {"ran":true,"summary":{...}}.
  6. Toggle the workflow Active.

A 401 means the credential is wrong (it must send the app's AGENT_API_KEY as a Bearer token). A tick that takes long is normal when there is work. Each run is bounded: at most 10 orders, oldest first, and it stops starting new orders once 90 seconds have passed, so a run normally answers inside the 120 s timeout; leftovers carry to the next run.

Runs never overlap. A run stamps its start time before it does any work and writes its summary when it ends. While a start is newer than the last summary and less than 10 minutes old, other ticks answer {"ran":false,"reason":"running"}. If a run dies without writing its summary, the next tick after those 10 minutes takes over.

Running it by hand

  • Order page — the "Match with AI" button on the quote match card runs the assistant for that one order and reloads the order.
  • MCP tool — orders.assistant_match with {"orderId": "..."}.

Both force a run past the "already looked at these lines" rule.

Reading ai_quote_match

orders.ai_quote_match (JSON) is the record of what the assistant decided about an order. Read it in the PB admin.

Field Meaning
status matched (the order was linked), suggested, no_match or error
quote_id, quote_number The quote the assistant chose
confidence high, medium or low
reason, doubts The AI's plain-language reasoning and anything to check
lines The pairing: order line id → quote line id, with same_item (sure / likely / unsure)
auto_linked True only when the assistant itself created the link
lines_hash Fingerprint of the order lines the run looked at — the same lines are not looked at twice
model The model that answered
run_at When the run happened
error Set when status is error

Settings (Settings → AI → Order assistant)

Setting Default Meaning
Order assistant enabled off The tick does nothing while this is off
Run every (minutes) 10 How often the tick may run
Auto-link when sure on May the assistant link the order itself?
Fill lines from the quote on Reserved for the next phase — the assistant does not fill lines yet
Prompt empty Overrides the built-in prompt when set

orders.ai_quote_match and order_items.ai_fill are in IGNORED_FIELDS of the audit hook, so the assistant's own writes do not flood the audit log.

Not in this phase

  • The assistant does not fill order lines from the quote yet — that is the next phase. The order_items.ai_fill field and the "Fill lines from the quote" switch are already in place for it.
  • The assistant does not touch prices, suppliers or SKUs.
  • It never creates a purchase order, never sends an email and never changes a line's status.