- Updated `docs/api-mcp-layer.md` to reflect the current state of the API, including new endpoints and tools. - Added `category` field to `QUOTE_ITEM_PATCH_FIELDS` and `ORDER_ITEM_PATCH_FIELDS` to support item categorization. - Introduced 22 new MCP tools to cover previously missing REST functionalities, increasing the total tool count from 103 to 125. - Improved descriptions for several existing tools to clarify their usage and parameters. - Regenerated the API documentation to ensure accuracy and completeness.
9 KiB
MCP Workflow Cookbook
End-to-end recipes for the most common operator requests that come through the WFDASH MCP. Each recipe lists the goal, the tool calls in order, and the gotchas — so an agent that has never seen the business before can drive the workflow to completion without further prompting.
Tool names below are the MCP tools. Inputs match the
inputSchemayou get fromtools.discovery. Output shapes are described indocs/api-mcp-layer.md§3.3.
1. "I have a supplier invoice — what do I do?"
Goal: take a supplier's invoice PDF (or delivery note), match it to the right PO, and book the goods into stock.
Sequence:
- Identify the PO — read "Your Ref" / "Order No" from the
document. It's either a stock PO (
PO{yy}{6d-seq}STK) or a customer PO (PO-YYYY-MM-NNN). - Diff first (read-only) —
purchase_orders.diff({ po_reference, document: { document_no, date, lines: [...] }, mode: "invoice" | "grn" }). Returns aMatchReportwith a verdict + per-lineLineComparison[]. - Decide:
verdict === "ready_to_receive"→ commit withstock_purchases.match_and_receive_from_invoice({ po_reference, document, attachment, confirm: true }). The same call attaches the scanned invoice to the PO.- Any other verdict (
cost_mismatch,qty_mismatch,unmatched_po_lines,unmatched_invoice_lines,supplier_mismatch,wrong_status,po_not_found) → stop and report. Neverconfirm: truewith a non-clean verdict.
- For stock POs only — the invoice receive books stock into
stock_items.current_qtyand writes astock_ledgerrow.stock_purchases.receiveis the lower-level tool for partial receives; the diff+match path handles the common case in one call. - For customer POs — the receive path flips
order_items. item_statustocollectedand stampssupplier_invoice_number. Usecollection_confirm.upload_invoiceif the invoice wasn't attached during step 3, thencollection_confirm.confirm_order_itemsto move the lines toready_invoice.
Gotchas:
mode: "grn"is delivery-note mode — skips price validation, accepts missing/zero prices. Use when the supplier sends a packing slip before the invoice.- The matched
document_nobecomes thesupplier_invoice_numberon the order_items. Get it right; don't paraphrase. confirm: truewith a non-clean verdict silently refuses — you get a report back but no write. Read the verdict before flippingconfirm.
2. "Split this order into a continuation"
Goal: some lines on the order are stuck (waiting on a back-ordered part); the rest can ship. Move the ready lines onto a fresh ORD number.
Sequence:
orders.get({ id, expand: "items" })— confirm which lines areitem_status === "pending"(only pending can be moved).orders.split({ order_id, item_ids: [<the lines to move>] }). Returns the new continuation order's ORD number. The parent order keeps its number; each moved item gets a breadcrumb innotes.
Gotchas:
- Any item not in
pendingrefuses silently (it would orphan a PO or driver assignment). Filter the candidates down topendingbefore splitting —order_items.list({ filter:order="" && item_status="pending"}). - The continuation order inherits customer, company, rep, contact — no need to re-state them.
- If you only have one line to move, splitting is overkill. Just update the parent.
3. "Confirm collection on a partial delivery"
Goal: the customer received 8 of 10 units today; 2 are on a
back-order. Mark the row collected with the right qty_collected
without losing the partial quantity.
Sequence:
order_items.get({ id, expand: "order" })— read the currentquantityandqty_collected. Thequantity_collectedparameter is the amount to add, not the new total.- Compute
to_add = received_qty - qty_collected(clamp to ≥ 0). order_items.confirm_collected({ item_ids: ["<id>"], quantity_collected: { "<id>": <to_add> }, note: "Partial delivery 8 of 10, 2 back-ordered", actor: { id: "<user_id>", name: "<display_name>" } }).
Gotchas:
- Eligibility: row must be in
collection_poolorassigned. Anything past the pool refuses withwrong_status. actoris required — it's stamped on every audit entry. Look it up viausers.lookup({ q: "<name>" })if you don't have the id.noteis optional but recommended for partial receives — the default "agent confirmed" note loses information about why only part was received.
4. "Bulk-price a quote that just landed"
Goal: the AI has extracted items but didn't price them. Fill in cost + selling prices from the catalogue in one MCP round-trip.
Sequence:
quote_items.list({ filter:quote="<quote_id>", expand: "matched_price_list_id" })— fetch every line.- For each line with
matched_price_list_idpopulated, read the catalogue'scost_priceandmarkup. bulk_update_quote_items({ items: [ { id, patch: { cost_price, selling_price, pricing_source: "ai", pricing_notes: "<date> by <agent>: matched catalogue row" }, force_overwrite: false }, ... ]}).quotes.re_price({ id: <quote_id>, items: [...] })— same call shape, scoped to one quote; useful when the agent wants to keep the diff inside the quote context.
Gotchas:
force_overwrite: falseis the safe default — rows withcost_price > 0 AND selling_price > 0are skipped withalready_priced. Passtrueonly when the operator explicitly asked to re-price.pricing_notesis required for the audit trail. The agent identity- date is yours to stamp — the tool does NOT auto-inject it.
selling_priceis auto-derived fromcost_price * (1 + markup/100)if omitted. Pass it explicitly when the catalogue has a stored selling price you want to preserve.- Whitelist (both bulk and re-price):
cost_price,selling_price,description,sku,unit,supplier,category,markup,pricing_source,pricing_notes,matched_price_list_id,user_edited. Anything else returnsforbidden_field.
5. "Customer rejected some lines — partial approve + split"
Goal: the customer accepts 6 of 8 lines. Approve the 6 on the original quote, clone the rejected 2 into a fresh quote for re-pricing.
Sequence:
quotes.get({ id, expand: "items" })— read all line ids.quotes.partial_approve({ quote_id, decisions: [ { item_id: "<accepted_id>", decision: "approved" }, { item_id: "<rejected_id>", decision: "rejected", note: "Customer preferred a different supplier" }, ... ]}).quotes.split({ source_quote_id, groups: [ { name: "Re-quote rejected lines", item_ids: ["<rej_id1>", "<rej_id2>"] } ]}).quotes.re_price({ id: <new_quote_id>, items: [...] })— adjust pricing on the continuation.
Gotchas:
quotes.partial_approverecords the customer's choice but doesn't move the items. To action them, follow withquotes.split.- Splitting preserves the rejected lines on a new quote with the same
customer + company + letterhead, so a
re_priceround feels like a continuation, not a fresh RFQ. - If the customer rejected everything, prefer
quotes.cloneand starting over — splitting into a single empty group is a no-op.
6. "Find sprint-pricing items I can claim"
Goal: the buyer-side sprint shows items waiting for pricing proofs. Find one that's unclaimed and lock it for 30 minutes.
Sequence:
sprint.list({ filter:claim_status=null || claim_expires_at<"now", expand: "quote_item.quote", sort: "-created" })— list unclaimed sprint items.- Pick one — prefer items with
quote_item.quote.customeralready known (faster resolution). sprint.claim({ id: <sprint_item_id>, claimed_by: "<agent_name>" }). Returns 409 if another agent already owns it.
Gotchas:
- The claim is 30-min default — finish your work before it expires, or re-claim to extend.
sprint.attachment_counts({ item_ids: [...] })is the bulk counterpart tosprint.item_attachments— use it to render paperclip badges in any list view without N round-trips.- After pricing, use
bulk_update_quote_itemsto push the new prices; the sprint item auto-resolves when the linkedquote_item.user_editedflips totrue.
Quick reference: tool families by workflow
| Workflow | Primary tools |
|---|---|
| Supplier invoice | purchase_orders.diff, stock_purchases.match_and_receive_from_invoice, stock_purchases.receive (partial) |
| Order split | orders.get, order_items.list, orders.split |
| Partial collection | order_items.get, order_items.confirm_collected, users.lookup (for actor) |
| Bulk pricing | quote_items.list, bulk_update_quote_items, quotes.re_price |
| Partial approve | quotes.get, quotes.partial_approve, quotes.split, quotes.re_price |
| Sprint claim | sprint.list, sprint.claim, bulk_update_quote_items |
For the full tool catalogue (auto-generated), see
docs/api-mcp-layer.md.