The cascade that protects margin
A vendor cost update is rarely a single-row change. A 5% butter increase from Bongards moves the landed cost on every assembly that uses butter, which moves the margin band on every active bid line containing those assemblies, which may breach the 22% gross margin floor on customer-facing quotes. This workflow encodes that cascade so Mike sees the second- and third-order impact before he says yes.
The contract lives in workflow_definitions WHERE workflow_type='vendor_cost_review'. Trigger is typically event (a new vendor invoice or RESTlet poll detects a price change), risk level 3, expected duration 20 minutes including Mike's review of the impact report.
The thing this workflow gets right that a manual spreadsheet doesn't: it walks the BOM graph. If Bongards Salted Butter is an ingredient in Right Start Breakfast Burrito and that's a component of the SY2026 NYC DOE bid, the workflow shows you that chain in one view.
Trigger conditions
- Vendor sends a new price list (typically quarterly for ingredient vendors).
- CME trailing-week price moves the Bongards butter peg outside the band.
- USDA entitlement allocation changes the net cost on a barrel cheddar line.
- Mike negotiates a new contract price on a phone call.
- Reconciliation cron detects a posted bill price differs from the master cost.
Bongards raises butter price 5%
Bongards Creameries (vendor #387) emails a new price list: salted butter (item #BTRSLT-1) moves from $2.80/lb to $2.94/lb (+5.0%). The cost ingestion page parses the email attachment; a proposed action lands in Mike's queue.
The workflow runs the impact simulation: 14 assemblies use BTRSLT-1, the landed cost on Right Start Breakfast Burrito moves from $3.42 to $3.51 (+2.6%), the SY2026 NYC DOE bid line for that assembly drops from 22.0% to 19.4% gross margin — below the 20% floor. The workflow surfaces this with a red band on that bid line.
Mike has two choices: accept the cost and re-price the bid (triggers bid price update workflow), or push back on Bongards. He approves the cost change but routes the SY2026 line to a re-pricing follow-up. Cascade total: vendor_costs updated, 14 assemblies recomputed, 31 open bid lines flagged, 4 active customer quotes flagged for review.
The four beats
-
01
Detect the change (auto)
vendor_last_cost_changecompares the incoming cost tovendor_costs.current_cost. If the delta exceeds the per-item tolerance (default 0.5%), a row is written toproposed_actionswithaction_type='vendor_cost_update'. -
02
Simulate the impact (auto)
simulate_cost_increasewalks the BOM graph frombom_components, recomputes each affectedassembly_cost_rollupin memory (no writes), and joins tobid_lines+quote_linesto identify margin breaches. A structured impact report is attached to the proposed action. -
03
Propose + approve (HITL)
Mike sees the impact report in
/proposed-actions.html: cost delta, list of affected assemblies, list of margin-breach bid lines, recommended follow-ups. He approves or rejects. R560 atomic claim applies — no double-approve race. -
04
Apply + recompute (auto)
propose_bulk_cost_basiswrites the new cost tovendor_costs, appends tovendor_cost_history, runsrecompute_assembly_cost_rollupon each affected assembly, and emits onevendor.cost_changedevent plus anassembly.cost_recomputedevent per assembly. Bid-line and quote-line flags are written tomargin_breach_queuefor follow-up workflows.
What's different after the workflow runs
vendor_costsreflects the new price;vendor_cost_historyhas the audit row.- Every dependent row in
assembly_cost_rollupshows the new landed cost. - Margin-breach bid lines and quotes are surfaced for Mike's next-day queue.
- Downstream subscribers (bid price update, customer quote) receive the event and can re-trigger.
- If Mike opts to push the cost back to NS, the vendor record update sibling workflow handles that.
What can go wrong and how to recover
Rare but possible if an assembly accidentally references itself. recompute_assembly_cost_rollup uses a recursive CTE with depth limit 6. Beyond that it aborts with BOM_CYCLE_DETECTED. Manual fix: untangle in bom_components.
If Mike approves the cost but doesn't follow up on the margin-breach queue, customers may see stale (below-floor) prices. The breach queue has a 7-day SLA; daily digest highlights overdue items.
If two proposed actions land on the same vendor_item, the second one's impact simulation may use stale cost from the first. The PushMutexDO Durable Object serializes vendor updates per vendor to prevent this.
Adjacent workflows + diagrams
Code paths + invariants
| Concern | Where |
|---|---|
| Workflow contract | workflow_definitions WHERE workflow_type='vendor_cost_review' |
| Change detection | src/chat_tools/impls.ts vendor_last_cost_change |
| Impact simulation | src/chat_tools/impls.ts simulate_cost_increase |
| Bulk apply | src/chat_tools/impls.ts propose_bulk_cost_basis |
| Assembly recompute | recompute_assembly_cost_rollup (recursive CTE depth 6) |
| Concurrency | src/durable_objects.ts PushMutexDO |
| Event emission | events table — vendor.cost_changed, assembly.cost_recomputed |
| HITL invariant | ADR-031 — vendor cost change never auto-applies |
Dated trail · spot stale claims
Dated trail of when this doc was last touched, what changed, and what to look at if it feels stale.
| Date | Round | Change | Touched by |
|---|---|---|---|
2026-05-26 | R586 | Added CHANGELOG · SCHEMA · RUNBOOK · BACKLOG sections — wiki became best-in-class operating documentation. | Mike + Claude |
2026-05-25 | R584/R585 | Wiki originally shipped — 8-section structure (hero / what / when / steps / outcomes / failure-modes / related / for-developers). | Mike + Claude |
workflow_definitions WHERE workflow_type='vendor_cost_update' before acting on these claims.The machine-readable spec
Canonical fields, table names, endpoint signatures. What code should match, what tests should assert. workflow_type · vendor_cost_update · risk_level · 3.
Inputs (required + optional)
| Field | Type | Description |
|---|---|---|
vendor_id | integer | NS vendor internal ID. Required. |
item_id | integer | The item whose cost is changing. Required. |
new_cost | decimal | New unit cost (USD). |
effective_date | date | When cost takes effect. |
source | string | Where cost came from: 'email', 'portal', 'phone'. |
D1 tables written
| Table | Operation | Trigger |
|---|---|---|
proposed_actions | INSERT | stage_proposed_action — Mike approves |
vendor_costs | UPDATE | Post-HITL |
vendor_cost_history | INSERT | Audit row |
margin_watchlist | INSERT | Any item whose margin drops >3% |
events | INSERT (cost.changed) | Triggers downstream bid_price_update suggestion |
Endpoints called
| Method | Path | Purpose |
|---|---|---|
POST | /api/workflow/execute | Runner entrypoint |
POST | /api/proposed-actions/:id/decide | HITL approval |
GET | /api/vendor-costs/:vendor_id | Read live cost snapshot |
Events fired
| event_type | When | Subscribers |
|---|---|---|
cost.changed | Post-HITL approval | margin_watcher, bid_price_update suggestion engine |
It broke at 2am — what now
Different from "how do I use this." This is the page Mike pulls up when something is wrong: logs to check, recovery steps, who to escalate to.
Scenario · Cost updated but no bid_price_update was suggested downstream
The cost.changed event drained but no subscriber triggered. Check event_subscriptions.
- Inspect subscription:
SELECT * FROM event_subscriptions WHERE event_type_pattern IN ('cost.changed', 'cost.*', '*') - Check the drain:
SELECT * FROM events WHERE event_type='cost.changed' ORDER BY created_at DESC LIMIT 5— look at drained_at. - Likely fix: Add subscription mapping cost.changed → suggest_bid_price_update workflow.
Scenario · Vendor sent a cost increase but it never reached proposed_actions
Likely caught by the email triage classifier and not routed.
- Check inbound_email_log:
SELECT subject, classified_intent, action_taken FROM inbound_email_log WHERE from_email LIKE '%@<vendor-domain>%' ORDER BY received_at DESC LIMIT 10 - If classified as 'general': Re-classify manually; patch the classifier prompt with vendor cost patterns.
- Manual entry: Mike enters the cost via
propose_vendor_cost_updatechat tool.
Scenario · Margin went red but no margin_watchlist row
Trigger may not have fired. Check the threshold.
- Verify:
SELECT * FROM margin_watchlist WHERE item_id=<id> ORDER BY flagged_at DESC LIMIT 1 - Threshold check: Margin drop must be >3% to insert. If below, working as designed.
- Manual flag:
INSERT INTO margin_watchlist (item_id, flagged_at, reason) VALUES (?, datetime('now'), 'manual')
Logs to check
workflow_run_log· top-level run auditworkflow_step_log· per-step traceworkflow_verify_results· post-window verify outcomescron_locks· stuck cron lock detectionevents· workflow.completed / workflow.failed event trailreflexion_log· per-run narrative (if reflexion_enabled)npx wrangler tail· live Worker logs
Kill switch · emergency stop
If this workflow is misbehaving in a high-impact way (creating bad proposed_actions in volume, pushing wrong things to NS), flip a kill switch:
kill:ns_writes· stops every NS push platform-widekill:proposed_apply· stops HITL approvals from executing fan-outkill:high_risk_ops· stops risk_level >= 4 fan-out
See kill-switches-state-machine.html for the full state machine + recovery procedure.
Escalation
Primary: Mike Levine (single-admin) · mikelevine@globalfoodsolutions.co. For prolonged outage during business hours, notify warehouse lead + accounting lead so they can defer dependent work.
What's not done · what's uncertain
What's not done, what's uncertain, what we punted. Captured so it survives context switches and doesn't die in someone's head.
-
STUB
ns_pushkind unwired — vendor cost not written back to NSWe update D1 vendor_costs but don't push to NS vendor record. Mike updates NS manually today.
-
OPEN
Should cost increase auto-suggest a bid_price_update?
Today it just fires event and emits margin_watchlist. Auto-staging a bid_price_update proposed_action would be aggressive — Mike's call.
-
DEFER
Multi-vendor lowest-cost re-ranking
When a vendor raises cost, we should re-rank which vendor is cheapest for that item. Today purely manual.
-
DECISION
Currency / exchange-rate handling for international vendors
Today all costs assumed USD. A few vendors quote CAD — no FX conversion in place.