Wiki · Workflow companion

Vendor cost update

Daily ops. Vendor raises (or lowers) a cost; the change cascades through assembly cost rollups, margin checks on open bids, and proposed customer pricing — Mike sees the full impact before approving.

Real · Cascade engine
What this is

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.

When to use it

Trigger conditions

Worked example

Bongards raises butter price 5%

Scenario

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.

Step-by-step what happens

The four beats

  1. 01

    Detect the change (auto)

    vendor_last_cost_change compares the incoming cost to vendor_costs.current_cost. If the delta exceeds the per-item tolerance (default 0.5%), a row is written to proposed_actions with action_type='vendor_cost_update'.

    Reads vendor_costs
    Time ~150ms
  2. 02

    Simulate the impact (auto)

    simulate_cost_increase walks the BOM graph from bom_components, recomputes each affected assembly_cost_rollup in memory (no writes), and joins to bid_lines + quote_lines to identify margin breaches. A structured impact report is attached to the proposed action.

    Reads bom_components, assembly_cost_rollup, bid_lines, quote_lines, pricing_master
    Time ~800ms for typical 14-assembly fanout
  3. 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.

    Writes proposed_actions.status
    HITL step 2
  4. 04

    Apply + recompute (auto)

    propose_bulk_cost_basis writes the new cost to vendor_costs, appends to vendor_cost_history, runs recompute_assembly_cost_rollup on each affected assembly, and emits one vendor.cost_changed event plus an assembly.cost_recomputed event per assembly. Bid-line and quote-line flags are written to margin_breach_queue for follow-up workflows.

    Writes vendor_costs, vendor_cost_history, assembly_cost_rollup, margin_breach_queue, events
    Emits vendor.cost_changed
    Time ~3s for typical fanout
Outcomes

What's different after the workflow runs

Vendor cost
Updated
+ audit history
Assemblies
Recomputed
all dependents
Margin flags
Queued
follow-up needed
Event emitted
Yes
downstream subscribers wake
Failure modes

What can go wrong and how to recover

BOM graph has a cycle

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.

Margin breach not actioned

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.

Concurrent cost updates on same vendor

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.

Related

Adjacent workflows + diagrams

For developers

Code paths + invariants

ConcernWhere
Workflow contractworkflow_definitions WHERE workflow_type='vendor_cost_review'
Change detectionsrc/chat_tools/impls.ts vendor_last_cost_change
Impact simulationsrc/chat_tools/impls.ts simulate_cost_increase
Bulk applysrc/chat_tools/impls.ts propose_bulk_cost_basis
Assembly recomputerecompute_assembly_cost_rollup (recursive CTE depth 6)
Concurrencysrc/durable_objects.ts PushMutexDO
Event emissionevents table — vendor.cost_changed, assembly.cost_recomputed
HITL invariantADR-031 — vendor cost change never auto-applies
Changelog

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.

DateRoundChangeTouched by
2026-05-26R586Added CHANGELOG · SCHEMA · RUNBOOK · BACKLOG sections — wiki became best-in-class operating documentation.Mike + Claude
2026-05-25R584/R585Wiki originally shipped — 8-section structure (hero / what / when / steps / outcomes / failure-modes / related / for-developers).Mike + Claude
If today is more than 60 days past the latest changelog row, treat live system behavior as the source of truth. The doc may have drifted — verify against the workflow contract in workflow_definitions WHERE workflow_type='vendor_cost_update' before acting on these claims.
Schema · data contract

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)

FieldTypeDescription
vendor_idintegerNS vendor internal ID. Required.
item_idintegerThe item whose cost is changing. Required.
new_costdecimalNew unit cost (USD).
effective_datedateWhen cost takes effect.
sourcestringWhere cost came from: 'email', 'portal', 'phone'.

D1 tables written

TableOperationTrigger
proposed_actionsINSERTstage_proposed_action — Mike approves
vendor_costsUPDATEPost-HITL
vendor_cost_historyINSERTAudit row
margin_watchlistINSERTAny item whose margin drops >3%
eventsINSERT (cost.changed)Triggers downstream bid_price_update suggestion

Endpoints called

MethodPathPurpose
POST/api/workflow/executeRunner entrypoint
POST/api/proposed-actions/:id/decideHITL approval
GET/api/vendor-costs/:vendor_idRead live cost snapshot

Events fired

event_typeWhenSubscribers
cost.changedPost-HITL approvalmargin_watcher, bid_price_update suggestion engine
Runbook · when it breaks

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.

  1. Inspect subscription: SELECT * FROM event_subscriptions WHERE event_type_pattern IN ('cost.changed', 'cost.*', '*')
  2. Check the drain: SELECT * FROM events WHERE event_type='cost.changed' ORDER BY created_at DESC LIMIT 5 — look at drained_at.
  3. 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.

  1. 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
  2. If classified as 'general': Re-classify manually; patch the classifier prompt with vendor cost patterns.
  3. Manual entry: Mike enters the cost via propose_vendor_cost_update chat tool.

Scenario · Margin went red but no margin_watchlist row

Trigger may not have fired. Check the threshold.

  1. Verify: SELECT * FROM margin_watchlist WHERE item_id=<id> ORDER BY flagged_at DESC LIMIT 1
  2. Threshold check: Margin drop must be >3% to insert. If below, working as designed.
  3. Manual flag: INSERT INTO margin_watchlist (item_id, flagged_at, reason) VALUES (?, datetime('now'), 'manual')

Logs to check

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:

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.

Backlog · open questions

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.