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>
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 auto-link rule, in plain words
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)
- Migrate PocketBase. Run against the live PB:
npm run pb:migrate:order-assistant(addsorders.ai_quote_match,order_items.ai_filland theorder_assistant_*fields onai_settings). If the app is deployed before this, PocketBase drops the unknown fields silently and the assistant's result lands nowhere. - Publish the PB hook change.
ai_quote_matchandai_fillwere added toIGNORED_FIELDSinpocketbase/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). - Deploy the app. Pushing to main does not auto-deploy; it is a manual deploy that takes 5 to 10 minutes.
- 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:
The dry run only reads. Read its report before running it withnpx tsx scripts/2026-10-24-rematch-quote-suggestions.ts npx tsx scripts/2026-10-24-rematch-quote-suggestions.ts --apply--apply. - Activate the n8n clock (below).
- 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:
- n8n, Workflows, Create workflow. Name it
WFDASH Order assistant tick. - Add node, Schedule Trigger. Trigger Interval: Minutes, Minutes Between
Triggers:
5. - 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.
- Method:
- Save. Leave the workflow inactive until the steps above are done.
- 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":{...}}. - 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_matchwith{"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_fillfield 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.