Wiki · Workflow companion

Customer quote

Daily ops. Customer asks for pricing; AI drafts a quote; Mike approves; rendered HTML/PDF lands in the customer's inbox in under 15 minutes. Three HITL gates keep the loop tight.

Real · Daily ops
What this is

From "can I get pricing?" to a signed PDF in fifteen minutes

The customer quote workflow is the most-run flow on the platform. Customers email or call asking for pricing on assemblies, often by their own internal SKU. The AI gathers context (who they are, what they typically buy, current cost basis, AR status), proposes line-by-line pricing, Mike reviews, the system renders an HTML artifact + PDF, and the email goes out.

The contract is stored in workflow_definitions WHERE workflow_type='draft_quote' — risk level 2 (low-med), expected duration 15 minutes, three HITL gates at steps 1, 2, and 4.

The defining property: nothing about pricing is invented. Every line traces to pricing_master, assembly_cost_rollup, or an explicit override that Mike typed into chat. The AI narrates; the database decides.

When to use it

Trigger conditions

Worked example

Customer Z requests pricing on assembly A123

Scenario

Cardinal Foods (NS customer #1843) emails: "Can you quote A123 — Right Start Foods Breakfast Burrito assembly — for 5,000 cases monthly starting July?"

Mike opens chat, types "Draft quote: Cardinal, A123, 5,000 cases/mo July onwards". The draft_quote workflow starts. Within 90 seconds the AI has pulled Cardinal's customer 360 (30-day order history, AR aging — they're current, their typical bid eligibility), looked up A123's current cost rollup ($3.42 landed), proposed $4.18 per case (22% gross margin per Mike's standard), and rendered a side-by-side comparison.

Mike skims, taps Approve lines, then Approve send. The HTML quote artifact lands at /quote/Q2026-0512, the PDF goes into R2, and the email lands in Cardinal's inbox with a tracked link. Total wall-clock: 11 minutes.

Step-by-step what happens

The five beats

  1. 01

    Gather context (auto)

    The get_customer_360 chat tool pulls the customer record from the D1 mirror, the trailing-90-day order history, current AR aging buckets, and any flags from customer_health_scores. This becomes the prompt context for the line-builder.

    Reads customers, so_lines, customer_health_scores, ar_aging
    Time ~600ms
  2. 02

    Build lines (HITL)

    propose_quote looks up each assembly's cost in assembly_cost_rollup, applies the customer's negotiated margin band (or the platform default if none), and writes draft rows to quote_lines with status='draft'. A proposed_action row lands in Mike's queue.

    Writes quotes, quote_lines (draft), proposed_actions
    HITL step 1
  3. 03

    Approve lines (HITL)

    Mike taps Approve in /proposed-actions.html. The quote rows flip to status='approved' and the atomic claim (R560 pattern) prevents double-approval. Mike can edit individual lines before approving — those edits write back to the same quote_lines rows.

    Writes quote_lines.status='approved'
    Time Mike approves within 5–30 min typically
  4. 04

    Render PDF (auto)

    render_quote_pdf generates the HTML artifact (Pages Function at /quote/) and converts to PDF via Browser Rendering. Both land in R2 under quotes//. The HTML link is what gets put in the email body.

    Writes R2 quotes/
    Time ~4–8s
  5. 05

    Send to customer (HITL)

    A draft email row in outbound_email_log with status='pending_review'. Mike skims, taps Send, the email goes out via Email Routing. Event quote.sent is appended to the events ledger. The customer-facing tracked link is logged for open/click tracking.

    Writes outbound_email_log, events
    HITL step 4 (send)
Outcomes

What's different after the workflow runs

Quote artifact
Live
HTML + PDF in R2
Customer
Notified
Email tracked
Quote lines
Persisted
D1 audit trail
Cycle time
~15 min
Mike's bottleneck
Failure modes

What can go wrong and how to recover

Cost basis missing

If assembly_cost_rollup doesn't have a row for the requested assembly, the proposer refuses to invent a margin. Mike sees a "no cost basis" error and has to add the cost first (vendor cost update workflow).

Customer on credit hold

If the customer's customer_health_scores.risk_tier is red, the workflow surfaces a warning band but doesn't block. Mike decides; this is a soft guardrail not a hard one.

PDF render times out

Browser Rendering occasionally hits 30s. The HTML artifact still works — Mike can send just the link. Re-trigger PDF via POST /admin/quotes//regen-pdf.

Mike never sends

Most common quiet failure. The outbound_email_log dashboard has a "pending_review > 24h" filter to catch these. Daily 9am digest highlights them too.

Related

Adjacent workflows + diagrams

For developers

Code paths + invariants

ConcernWhere
Workflow contractworkflow_definitions WHERE workflow_type='draft_quote'
Customer 360src/chat_tools/impls.ts get_customer_360
Line buildersrc/chat_tools/impls.ts propose_quote
PDF renderrender_quote_pdf — Browser Rendering binding
HTML artifactpages-functions/quote/[id].ts
HITL gatessteps 1, 2, 4 — see hitl_gates_json
Cost rollup invariantnever invent margin — refuse if assembly_cost_rollup empty
Email sendCloudflare Email Routing — outbound_email_log first
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='customer_quote' 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 · customer_quote · risk_level · 2.

Inputs (required + optional)

FieldTypeDescription
customer_idintegerNS customer internal ID. Required.
itemsjsonArray of {item_id, qty, requested_price?}. Required.
effective_datedateQuote effective date; defaults to today.
bid_contextstring?Optional bid_id if quote is bid-anchored.

D1 tables written

TableOperationTrigger
customer_quotesINSERTQuote header row
customer_quote_linesINSERT (one per item)Per-line pricing snapshot
proposed_actionsINSERTIf any line price departs from pricing_master
outbound_email_logINSERT (status=pending_review)Quote PDF email draft
eventsINSERT (quote.generated)Hub + analytics subscribers

Endpoints called

MethodPathPurpose
POST/api/quote/createQuote creation endpoint
GET/quote/:idLive customer-facing quote page
POST/admin/quote/:id/regen-pdfManual PDF regeneration

Events fired

event_typeWhenSubscribers
quote.generatedAfter all lines settlecustomer_health, analytics
quote.sentEmail approved + sentfollow-up scheduler
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 · Quote shows a price different from pricing_master

pricing_master may have updated AFTER the quote snapshot. Quotes are snapshots at creation time.

  1. Check timestamps: SELECT created_at FROM customer_quotes WHERE id=<id> vs SELECT last_updated FROM pricing_master WHERE item_id=?
  2. If quote was created before the update: Working as designed. Tell the customer the quote was a snapshot.
  3. If after: Bug — investigate the snapshot logic in src/chat_tools/impls.ts draft_quote tool.

Scenario · Quote PDF won't render — Browser Rendering 500

Usually a malformed line (missing item_name) or template syntax error.

  1. Check the lines: SELECT * FROM customer_quote_lines WHERE quote_id=<id> AND (item_name IS NULL OR price IS NULL)
  2. Patch: Fill missing fields from items table, then POST /admin/quote/<id>/regen-pdf
  3. Long-term: Tighten the CHECK constraint on customer_quote_lines.

Scenario · Customer never received the quote email

outbound_email_log row likely stuck in pending_review.

  1. Check: SELECT status, approved_at, sent_at FROM outbound_email_log WHERE entity_ref='quote:<id>'
  2. Recover: If pending_review: Mike approves. If approved but not sent: check email-routing logs in wrangler tail.

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.