# 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) 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**: ```bash 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 ` 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.