Wiki · Workflow companion

Bid price update

The canonical worked example. When Mike approves a single bid line price, seven systems update in lockstep so quotes, hub pages, NetSuite, and customer-facing PDFs all tell the same story within minutes.

Real · Live in production
What this is

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.

When to use it

Trigger conditions

Heuristic

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.

Worked example

Driscoll requests $X on item Y for SY2026

Scenario

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.

Step-by-step what happens

The eleven beats

  1. 01

    Mike or chat tool emits a proposed action

    An AI tool (most often propose_price_change) writes a row to proposed_actions with action_type='bid_price_update', the price delta, and a structured payload of the seven downstream targets it intends to touch.

    Writes proposed_actions
    Time ~200ms
  2. 02

    Risk tier is calculated

    The trigger in 114_risk_level_trigger.sql evaluates the proposed action against the cumulative cap and assigns risk_level L1–L5. A single bid line under 5% movement lands at L3 and shows up in Mike's queue with a yellow band, not red.

    Writes proposed_actions.risk_level
    Time ~5ms (trigger)
  3. 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.

    Reads proposed_actions, bid_lines, pricing_master
    Time Mike typically approves within 60min
  4. 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'. The WHERE status='pending' clause prevents the double-approval race that bit us when Mike fat-fingered the bulk-decide button twice.

    Writes proposed_actions.status='approved'
    Invariant ADR-031 HITL discipline
  5. 05

    NS pricing record is updated (push 1 of 7)

    A row is enqueued on NS_PUSH_QUEUE with the OAuth1 RESTlet payload. The push worker hits the customscript_gfs_platform_query RESTlet with the new price. NS is the system of record — this push is the load-bearing one. Retry policy is exponential, max 3 attempts.

    Writes NetSuite pricing record
    Time ~3–8s round trip
    Retry 3 attempts
  6. 06

    D1 pricing_master mirrors the value (push 2)

    Parallel with the NS push, the D1 row in pricing_master is updated and a corresponding row appended to pricing_history with the prior value, new value, who, when, and the originating proposed_action_id. This is the audit trail for the next compliance review.

    Writes pricing_master, pricing_history
    Time ~80ms
  7. 07

    bid_lines is updated (push 3)

    If the proposed action carried a bid_id, the matching row in bid_lines gets the new price and an updated_at stamp. This is what the bid lifecycle dashboard reads from, so this push is what makes the bid show its new number.

    Writes bid_lines
    Conditional only if bid_id present
  8. 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 as bids//B5875_.pdf in R2. The hub link is updated to point at the new artifact key.

    Writes R2 bucket bids/
    Time ~6–10s
    Conditional only if bid is active
  9. 09

    Hub KV cache is invalidated (push 5)

    The HUB_CACHE KV namespace gets a delete for 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.

    Writes KV invalidations
    Time ~40ms
  10. 10

    Spec sheet review flagged if >5% change (push 6)

    If abs(price_change_pct) > 0.05, a row is inserted into spec_review_queue so 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.

    Writes spec_review_queue
    Conditional only if delta > 5%
  11. 11

    Customer notification drafted + event emitted (push 7)

    A draft email is written to outbound_email_log with status='pending_review' — Mike still has to approve the send. An event price.changed is appended to the events ledger with the full payload so downstream subscribers (margin watcher, customer health) can react.

    Writes outbound_email_log, events
    Emits price.changed
Outcomes

What's different after the workflow runs

NS pricing
Updated
≤ 15 min visible
D1 mirror
In sync
≤ 5 s
Bid PDF
Regenerated
R2 versioned
Hub cache
Busted
≤ 60 s
Failure modes

What can go wrong and how to recover

NS push fails (3 retries exhausted)

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=.

Bid PDF regen times out

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//regen rebuilds it.

Double-approve race

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.

Customer never notified

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.

Related

Adjacent workflows + diagrams

For developers

Code paths + invariants

ConcernWhere
Workflow contractworkflow_definitions WHERE workflow_type='bid_price_update'
HITL invariantADR-031 (data/decisions.json)
Atomic claimR560 — src/index.ts approve handler
NS pushsrc/index.ts NS_PUSH_QUEUE producer
D1 cascadesrc/chat_tools/impls.ts propose_price_change
Event emissionevents table, event_type='price.changed'
Risk tier triggermigrations/schema/114_risk_level_trigger.sql
Push queue contractCLAUDE.md invariant #3 — never push to NS without queue
// Cumulative cap on bulk approvals (R560) if (sum_of_approval_dollar_impact > CUMULATIVE_CAP_USD) { require('multi-step-confirmation'); } // HITL invariant — never push to NS without proposed_action_id if (!action_id) throw new Error('HITL bypass attempted');