The most-rehearsed workflow on the platform
The bid price update is the canonical example we point every new tool at first. It captures the full pattern: AI proposes a change, Mike approves once, and the system fans the approved value out to every downstream surface that knows about that price. Nothing else writes back to NetSuite by hand.
It exists because bid prices live in four physical places at once — the NS pricing record, the D1 mirror, the original bid line row, and the regenerated bid PDF in R2 — plus the hub cache and the spec sheet. Before this workflow, those drifted apart and Mike spent his days reconciling them. Now there's exactly one approval moment and the fan-out is deterministic.
The contract is stored in workflow_definitions WHERE workflow_type='bid_price_update'. Risk level is 3 (medium). Expected duration is roughly 5 minutes end-to-end once Mike taps approve.
Trigger conditions
- A customer responds to a bid with a counter price and you want it on the record.
- Vendor cost moved and the bid line needs to follow (after margin check).
- USDA entitlement drawdown shifts the net cost on a barrel cheddar line.
- Mike negotiates a price on the phone and needs it reflected in NS + the next quote.
- Spec deviation review concluded the new pack/format justifies a price adjustment.
If the change is >5% of the current price, the workflow auto-flags a spec sheet review as part of the fan-out — that's the guardrail against accidentally moving a price two orders of magnitude.
Driscoll requests $X on item Y for SY2026
Driscoll Foods (NS customer #2147, 36.4% of revenue) emails asking to raise the price on SKU 10472 — Right Start Foods Breakfast Burrito from $1.42 to $1.48 on bid B5875 (NYC DOE Food Distribution, SY2026). The increase is +4.2% — under the 5% spec-review threshold.
In chat, Mike types: "Move 10472 on B5875 to $1.48 effective July 1 2026". Two seconds later a row appears in /proposed-actions.html. Mike taps Approve. The seven-target fan-out begins. Within 12 minutes, the NS pricing record, the D1 mirror, the bid_lines row, the regenerated B5875 PDF in R2, the hub KV cache, and the spec sheet flag have all settled. The customer notification draft is sitting in outbound_email_log awaiting a second Mike approval before send.
The eleven beats
-
01
Mike or chat tool emits a proposed action
An AI tool (most often
propose_price_change) writes a row toproposed_actionswithaction_type='bid_price_update', the price delta, and a structured payload of the seven downstream targets it intends to touch. -
02
Risk tier is calculated
The trigger in
114_risk_level_trigger.sqlevaluates the proposed action against the cumulative cap and assignsrisk_levelL1–L5. A single bid line under 5% movement lands at L3 and shows up in Mike's queue with a yellow band, not red. -
03
Mike sees it in /proposed-actions.html
The page polls every 10s. Mike sees the row with full context — current price, proposed price, customer, bid, spec, margin impact, and a side-by-side preview of the cascade targets. Single-decide or bulk-decide are both available.
-
04
Mike taps Approve — the atomic claim fires
R560's atomic-claim fix: the approve endpoint does
UPDATE proposed_actions SET status='approved', claimed_by='mike', claimed_at=? WHERE id=? AND status='pending'. TheWHERE status='pending'clause prevents the double-approval race that bit us when Mike fat-fingered the bulk-decide button twice. -
05
NS pricing record is updated (push 1 of 7)
A row is enqueued on
NS_PUSH_QUEUEwith the OAuth1 RESTlet payload. The push worker hits thecustomscript_gfs_platform_queryRESTlet with the new price. NS is the system of record — this push is the load-bearing one. Retry policy is exponential, max 3 attempts. -
06
D1 pricing_master mirrors the value (push 2)
Parallel with the NS push, the D1 row in
pricing_masteris updated and a corresponding row appended topricing_historywith the prior value, new value, who, when, and the originating proposed_action_id. This is the audit trail for the next compliance review. -
07
bid_lines is updated (push 3)
If the proposed action carried a
bid_id, the matching row inbid_linesgets the new price and anupdated_atstamp. This is what the bid lifecycle dashboard reads from, so this push is what makes the bid show its new number. -
08
Bid PDF is regenerated in R2 (push 4)
The artifact regen pulls the new
bid_lines, renders the bid response template via Browser Rendering, and stores it asbids/in R2. The hub link is updated to point at the new artifact key./B5875_ .pdf -
09
Hub KV cache is invalidated (push 5)
The
HUB_CACHEKV namespace gets adeletefor any key referencing the affected bid_id, customer_id, or item_id. Next request renders fresh from D1. This is what guarantees customer-facing pages reflect the new price in < 60 seconds. -
10
Spec sheet review flagged if >5% change (push 6)
If
abs(price_change_pct) > 0.05, a row is inserted intospec_review_queueso the spec sheet system can re-validate the pack/format/ingredient line still justifies the new price. This is a soft flag — it doesn't block the cascade. -
11
Customer notification drafted + event emitted (push 7)
A draft email is written to
outbound_email_logwithstatus='pending_review'— Mike still has to approve the send. An eventprice.changedis appended to the events ledger with the full payload so downstream subscribers (margin watcher, customer health) can react.
What's different after the workflow runs
- NetSuite, D1
pricing_master, andbid_linesall show the new price. pricing_historyhas a fresh audit row stamped with Mike's approval.- The hub's customer-facing bid page shows the new price within 60s.
- If >5% change, spec review is on someone's queue.
- A draft customer notification is waiting for Mike's second tap.
- An event in
eventslets margin and health watchers react asynchronously.
What can go wrong and how to recover
D1 has the new price but NS doesn't. The reconciliation cron at 0 */15 * * * catches this within 15 minutes and re-enqueues. Manual recovery: POST /admin/ns-push/retry?action_id=.
Browser Rendering occasionally exceeds 30s on large bids. The old PDF stays in R2 and the hub still links to it; new value is in D1. Next sync tier or a manual POST /admin/bids/ rebuilds it.
Fixed in R560. If the atomic claim sees status≠pending, the second approve becomes a no-op and Mike sees a "already approved" toast. No double-cascade can occur.
Most common quiet failure — Mike approves the price but forgets to approve the email draft. outbound_email_log has a pending_review filter on the admin dashboard so these don't fall off.
Adjacent workflows + diagrams
Code paths + invariants
| Concern | Where |
|---|---|
| Workflow contract | workflow_definitions WHERE workflow_type='bid_price_update' |
| HITL invariant | ADR-031 (data/decisions.json) |
| Atomic claim | R560 — src/index.ts approve handler |
| NS push | src/index.ts NS_PUSH_QUEUE producer |
| D1 cascade | src/chat_tools/impls.ts propose_price_change |
| Event emission | events table, event_type='price.changed' |
| Risk tier trigger | migrations/schema/114_risk_level_trigger.sql |
| Push queue contract | CLAUDE.md invariant #3 — never push to NS without queue |