The rule-based matcher no longer writes or shows suggestions: on
2026-10-24 it had left 2,888 on unlinked orders, one quote suggested
to 309 of them.
- createOrder runs the AI order assistant (matchOrder) straight after
intake when it is switched on, instead of the rule-based suggestion
- order page: Re-match / Try again and the rule-based suggestion card
are gone; Match with AI is the action, and "Match by reference" saves
the reference then runs the AI (the run route takes { reference })
- the unauthenticated /orders/[id]/quote-match route is removed
- the sweep route and MCP orders.quote_match_sweep run the AI batch
(matchUnlinkedOrders, shared with the tick)
- scripts/2026-10-24-clear-rule-quote-suggestions.ts clears the old
suggestions (dry run unless --apply); replaces the rematch script,
which would have written rule-based ones again
- also fixes the setState-in-effect lint error on the order page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 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.
Once the order points at a quote, the assistant can also fill the order's lines from that quote — the cost, the supplier, the SKU and the part number — leaving them in purple until a person checks them.
The AI never changes a line's status, never creates a purchase order and never sends anything. It decides which quote an order belongs to, and it may pre-fill a line's prices from that quote.
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.
Filling the lines from the quote
With "Fill lines from the quote" on, the assistant prices the order's lines from the quote they are paired with. This is a copy, not a new price: no AI call is involved, the pairing is already on the lines.
A line may be filled only when all of these are true:
- It is still
pending. - It carries no cost of its own (
cost_priceis 0). - Its
pricing_sourceis notmanualand notquote— a price a person set is never overwritten. - It has no reviewed AI fill on it.
- It is paired to a quote line (
quote_itemis set) that actually has a cost.
What the fill writes:
| Field | Rule |
|---|---|
cost_price |
The quote line's cost |
selling_price |
Only when the order line has none of its own |
supplier, sku, matched_price_list_id |
Only when the order line has none of its own |
markup + markup_percent |
Written together, from cost and selling |
pricing_source |
ai_quote |
ai_fill |
The audit record of the fill (see below) |
Every line is re-read immediately before it is written, so a cost a person typed in the meantime wins over the AI's.
If the customer's price and the quoted price both exist and differ by more
than 5%, the fill records a doubt, e.g. customer price R16.64, quoted R18.20. Doubts show on the line's marker.
The purple review
A filled line is pending review while all three are true:
ai_fill.statusisfilled,ai_fill.reviewed_atis empty, andpricing_sourceis stillai_quote.
While a line is pending it shows up purple on the order Process page, with the bot marker next to it. Hover or focus the marker to see the quote it was filled from, the cost, the selling price, the markup, the reason and any doubts.
The purple ends the moment somebody touches the line:
- Saving the line records the review on
ai_fill(reviewed_at,reviewed_by, and whether the prices were accepted unchanged or changed). The pricing source staysai_quote. - The one-click "use this cost" copy writes
pricing_source: "quote"— the operator chose the quote's cost themselves — so the AI's provenance ends too. - A line whose price the operator types by hand becomes
pricing_source: "manual".
The board badge
The Orders board shows an "AI x/y" badge on an order when the AI
filled some of its lines: x is how many lines still need a review, y
is how many the AI filled. The badge is hidden when nothing is pending.
order_items.ai_fill
| Field | Meaning |
|---|---|
status |
filled, skipped or error |
source |
quote |
quote_id, quote_number, quote_item_id |
Where the numbers came from |
cost_price, selling_price, markup |
What was written |
supplier_id, sku |
What was written |
reason |
Why the line was paired with that quote line |
doubts |
Anything a person should check (e.g. the 5% price difference) |
model, attempted_at |
Which model, and when |
reviewed_at, reviewed_by |
Set when a person checked the line |
accepted_unchanged |
True when the person kept both prices |
supplier_changed |
True when the person changed the supplier |
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.
- Clear the old rule-based suggestions. Quote matching is AI-only:
the rule-based matcher no longer runs (not at intake, not on the order
page, not in the sweep). The suggestions it left behind are wrong far
too often — on 2026-10-24 there were 2,888 on unlinked orders, one
quote suggested to 309 of them. Clear them — dry run first:
It only clearsnpx tsx scripts/2026-10-24-clear-rule-quote-suggestions.ts npx tsx scripts/2026-10-24-clear-rule-quote-suggestions.ts --applypotential_quoteon orders with no hard link whosequote_match_methodis not an AI one; links and AI suggestions stay. - 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, fill lines 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: it matches at most 10 orders and
fills at most 10 orders, 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.
An order the tick has nothing to do with does not use up that budget: the run looks through up to 200 unlinked orders to find 10 it can work on, and an order whose lines the fill could not write is skipped rather than counted.
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. It also fills the lines when the order ends up linked.
- 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 | May the assistant pre-fill the order's lines from the linked quote? |
| 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 price a line from scratch — it only copies the cost, supplier, SKU and part number from the quote line the order line is already paired with.
- It never creates a purchase order, never sends an email and never changes a line's status.
- There is no POS fallback: the assistant fills only from a quote.