Wiki · Bid Center Path 1 · R597

Path 1 — email intake

A bid lands in the bids@ai-globalfoodsolutions.co mailbox. src/email.ts parses it, the document converter normalizes the attachment, name_synonyms.json fuzzy-matches the sender to a known customer, and a proposed_actions row stages "new bid inbound — customer match X — run review?" for Mike. On approve, a chat session opens with the PDF + customer.json + latest bid_quote_versions preloaded, and the standardized 11-skill review runs. Trace thread: customer + SY-version.

Path 1 · email-driven intake HITL on customer-match (ADR-031)
What this is

The mailbox-driven path into the Bid Center

Of the three Bid Center intake paths, Path 1 is the asynchronous one. A customer (or a vendor on behalf of a program) emails a bid, RFP, or pricing update to bids@ai-globalfoodsolutions.co. Cloudflare Email Routing forwards the message to the Worker, where src/email.ts handles the inbound. Headers + body are parsed, supported attachments (PDF / XLSX) are detected and routed through document_converter to Markdown, and the source bytes are persisted to the bid-attachments R2 bucket keyed by email_id.

Customer identity is then resolved via name_synonyms.json — exact match first, then alias, then fuzzy (Levenshtein). Confidence ≥ 0.85 auto-suggests; anything below forces Mike to pick. The customer's latest sy_version is read from bid_quote_versions (or a fresh SY-version-v0.1 is minted if first-time), and the pair customer_id + sy_version becomes the trace thread for every downstream row — the Bid Center equivalent of the SO PO# (otherrefnum) thread.

A single proposed_actions row stages the whole intake for HITL approval (ADR-031). On approve, a chat session is opened with the bid Markdown, customer.json, and the latest bid_quote_versions row preloaded into context, and the dispatcher fires one of the 11 standardized skills (LOG_EXTERNAL_BID, ADD_UPDATE_CUSTOMER_PRICING, …). Outputs are 8 D1 bid_* tables (migration 136), a regenerated .docx quote, a refreshed STATUS.json, and a re-rendered dashboard at gfs-pricing.pages.dev. No NetSuite write happens here — NS writes are deferred to Path 3 (Jul 1 rollover).

Diagram: ns-bid-center-path-1-email-intake.html. Sibling paths: Path 2 direct chat · Path 3 July 1 rollover. Master: master wiki.

When to use it

Trigger conditions

No NS write on this path

Path 1 only writes to D1 + R2 + folder + dashboard. NetSuite writes are deferred to Path 3 (Jul 1 rollover) where the year of accumulated SY-versions becomes named price lists. This is intentional — Path 1 captures & stages, Path 3 commits.

★ The customer + SY-version trace thread

From the moment the email is parsed, customer_id + sy_version is the trace thread. Sample: ACC Distributors / SY2627-v2.5. Grep this pair across proposed_actions, bid_quote_versions, bid_price_snapshots, and STATUS.json for a complete audit trail — analogous to the SO otherrefnum (PO#) thread.

Worked example

ACC Distributors emails a cheddar re-quote

Scenario

April 14, 2026, 10:42 AM. Cheddar cost moved 8% in the prior two weeks. ACC Distributors' AP clerk emails bids@ai-globalfoodsolutions.co with subject "Price refresh request — cheddar items" and a 3-page PDF listing 47 SKUs.

Cloudflare Email Routing forwards to the Worker. src/email.ts writes an events.bid.email_received row, attaches the PDF bytes to R2 under bid-attachments/2026-04-14/<email_id>.pdf, and routes the PDF through document_converter to get Markdown.

Customer resolution runs: sender domain is accdist.com, body mentions "ACC Distributors", and name_synonyms.json resolves to ACC Distributors at confidence 0.94. bid_quote_versions shows the latest as SY2627-v2.4 for ACC, so we'll bump to SY2627-v2.5 on approve.

The payload is assembled (email_id, customer_id, sy_version, bid_markdown, suggested_skill=ADD_UPDATE_CUSTOMER_PRICING) and a proposed_actions row is staged: kind=bid_email_intake, status=pending, risk_level=3. Mike sees the card on admin-dashboard at 10:46 AM and taps Approve.

A chat session opens. Preloaded context: the bid Markdown, ACC's customer.json, and the SY2627-v2.4 row from bid_quote_versions. The dispatcher routes to ADD_UPDATE_CUSTOMER_PRICING, which snapshots the 47 line items, applies the HPS MAP GPO allowance for SY2627, and writes UPSERTs into bid_customers, bid_customer_prices, bid_quote_versions (new row marked is_latest=1, prior row demoted), and bid_price_snapshots (47 rows). quotes/SY2627-v2.5.docx regenerates. STATUS.json updates ACC's freshness to "today, v2.5". The dashboard at gfs-pricing.pages.dev re-renders.

Two events fire: events.bid.review_completed and events.bid.quote_versioned. No NS write. The new pricing sits in the Bid Center until July 1, when Path 3 picks up ACC's latest SY-version (v2.5) and stages it into the Jul 1 cohort.

Step-by-step what happens

Email arrives → HITL approve → review runs

  1. 01

    Email arrives at bids@

    Cloudflare Email Routing rule forwards to the Worker's email handler.

  2. 02

    src/email.ts parses

    Extracts from / to / subject / body / attachments. Emits events.bid.email_received.

  3. 03

    Detect attachments

    PDF / XLSX routed to document_converter. Other types ignored + logged. Body-only fallback if no supported attachment.

  4. 04

    document_converter → Markdown

    Bytes → Markdown string. Source bytes persisted to R2 (bid-attachments bucket) keyed by email_id.

  5. 05

    Resolve customer

    name_synonyms.json + bid_customers. exact → alias → fuzzy. ≥ 0.85 auto-suggest, < 0.85 ambiguous (Mike picks).

  6. 06

    Pick SY-version

    SELECT sy_version FROM bid_quote_versions WHERE customer_id=? AND is_latest=1. None → currentSchoolYear() + '-v0.1'. The pair customer_id + sy_version is the trace thread from here on.

  7. 07

    Build proposed payload

    Bundle email_id, customer_id + confidence, sy_version, bid_markdown, r2_source_key, and a suggested_skill (router heuristic on bid content).

  8. 08

    ★ Stage proposed_actions (HITL gate)

    INSERT row: kind=bid_email_intake, status=pending, risk_level=3. ADR-031 invariant.

  9. 09

    Notify Mike

    admin-dashboard pending HITL queue surfaces the card: "new bid inbound — customer X (conf 0.91) — run review?"

  10. 10

    ★ Mike approves / rejects / reassigns

    APPROVE → fires bid.intake_approved; REJECT → closes row, email stays in R2; REASSIGN → update customer or skill, then approve.

  11. 11

    Open chat session

    Session preloaded with bid Markdown + customer.json + latest bid_quote_versions row.

  12. 12

    Dispatch 11-skill review

    One of: CREATE_NEW_CUSTOMER, ADD_UPDATE_CUSTOMER_PRICING, ADD_UPDATE_ITEM, FILL_REGIONAL_PRICES, REBUILD_REGIONAL_SHEETS, COMPARE_CUSTOMER_QUOTES, LOG_EXTERNAL_BID, BID_PIPELINE, ARCHIVE_CUSTOMER, SYSTEM_HEALTH_CHECK, PREPARE_NEXT_SCHOOL_YEAR.

  13. 13

    UPSERT D1 bid_* tables

    Migration 136 (Agent M owns). Until landed, writes go to folder + STATUS.json only.

  14. 14

    Regen .docx + refresh STATUS.json

    Per-customer per-SY-version .docx; freshness metadata updated.

  15. 15

    Dashboard re-renders

    gfs-pricing.pages.dev reads latest STATUS.json on next request.

  16. 16

    Events fired

    events.bid.email_intake_completed, events.bid.review_completed, events.bid.quote_versioned.

Outcomes

What's different after Path 1 runs

HITL gate
1
customer match approval
D1 tables
8 bid_*
UPSERT on approve
NS write
none
deferred to Path 3
Events
5
bid.* fired
Failure modes

What can go wrong

Customer-match confidence below threshold

Sender domain unrecognized + body doesn't mention a known alias → ambiguous match. Today HITL surfaces the ambiguity to Mike; auto-classifier confidence is under bake.

Email lands but no attachment

Body-only parse runs; suggested_skill defaults to ambiguous → Mike picks. Common with one-line "please refresh" emails.

document_converter fails on a corrupt PDF

PDF bytes → Markdown throws; proposed_actions still stages with bid_markdown empty + error flag. Mike sees "intake parse failed — open R2 source" and decides next step.

Spam / unrelated email

Mike rejects the row; events.bid.intake_rejected fires; email stays in R2 for audit. Recovery: admin tool can re-queue if rejected by mistake.

Migration 136 not yet landed

D1 bid_* writes no-op gracefully; folder + STATUS.json remain authoritative until Agent M lands the migration.

Related

Adjacent flows + diagrams

For developers

Code paths + invariants

ConcernWhere
Mailboxbids@ai-globalfoodsolutions.co
Email pipelinesrc/email.ts
Document convertersrc/document_converter.ts
Name synonymsdata/name_synonyms.json
R2 bucketbid-attachments (keyed by email_id)
HITL row kindproposed_actions.kind = 'bid_email_intake'
D1 tablesbid_customers, bid_quote_versions, bid_price_snapshots, …
Dispatcherrouter.dispatch(suggested_skill, payload)
Events firedbid.email_received, bid.intake_approved, bid.review_completed, bid.quote_versioned
// Trace thread invariant - Path 1 type ThreadKey = { customer_id: string, sy_version: string }; // On approve, the dispatcher runs: async function runPath1Approval(env: Env, action: ProposedAction) { const { customer_id, sy_version, bid_markdown, suggested_skill } = action.payload; const session = await openChatSession(env, { customer_id, preload: [bid_markdown, await loadCustomerJson(customer_id), await loadLatestVersion(env, customer_id)] }); await dispatcher.run(suggested_skill, { session, customer_id, sy_version }); await emit(env, 'bid.review_completed', { customer_id, sy_version }); }
Changelog

Dated trail

DateRoundChangeTouched by
2026-05-26R597Wiki ships alongside Path 1 diagram. JSON-LD mirrored from diagram. Customer + SY-version threading documented as the Bid Center analog of the SO PO# trace thread.Mike + Claude (Agent L)
Schema · data contract

The machine-readable spec

workflow_type · bid_center_email_intake_path · risk_level 3. Parent: bid_center_lifecycle (master).

Customer + SY-version threading (the trace thread)

Analogous to the SO PO# (otherrefnum) thread. Grep this pair across every Path 1 artifact for a complete audit trail.

Record / tableField carrying customer + SY-versionSample value
proposed_actionspayload.customer_id + payload.sy_version"ACC Distributors / SY2627-v2.5"
chat_sessioncontext.customer_id + context.sy_version"ACC Distributors / SY2627-v2.5"
bid_customerscustomer_id"ACC Distributors"
bid_quote_versionscustomer_id + sy_version (thread origin)"ACC Distributors / SY2627-v2.5"
bid_price_snapshotscustomer_id + sy_version + sku"ACC / SY2627-v2.5 / SKU-419"
STATUS.jsoncustomers[customer_id].latest_sy_version"SY2627-v2.5"
eventspayload.customer_id + payload.sy_versionevery bid.* row

D1 tables written (Path 1)

TableOperationPurpose
proposed_actionsINSERT then UPDATEHITL gate row
bid_customersUPSERTcustomer normalize on first sight
bid_quote_versionsINSERT + demote priornew SY-version row, prior loses is_latest
bid_price_snapshotsINSERT-manyline-level snapshot for the version
bid_customer_pricesUPSERTworkshop layer (locked per version)
eventsINSERTappend-only audit trail

Endpoints

MethodPathPurpose
POST/api/proposed-actions/decidesingle approve / reject / reassign
GET/api/proposed-actions?kind=bid_email_intakepending Path 1 queue
GET/quote/<slug>/<version>customer-facing rendered quote

Events fired

event_typeWhen
bid.email_receivedemail lands at Worker
bid.intake_approvedMike approves the proposed_action
bid.intake_rejectedMike rejects
bid.review_completed11-skill dispatcher finishes
bid.quote_versionednew bid_quote_versions row written
Runbook · when it breaks

It broke — what now

Scenario · Customer emailed but no proposed_actions row exists

Customer reports they emailed a bid; admin-dashboard shows nothing.

  1. Check email landed: SELECT * FROM events WHERE event_type='bid.email_received' AND created_at > datetime('now','-1 day')
  2. Check Email Routing rule: CF dashboard → Email Routing → bids@ rule active?
  3. Check Worker logs: npx wrangler tail filter on email events
  4. Check R2 bucket: npx wrangler r2 object list bid-attachments --prefix=$(date +%Y-%m-%d)
  5. Manual re-stage: upload PDF via admin-dashboard chat, run Path 2 instead

Scenario · Customer-match confidence stuck at 0.0 for a known customer

Long-known customer suddenly fails to match; ambiguous queue grows.

  1. Inspect synonyms: cat data/name_synonyms.json | jq '.<customer>'
  2. Add alias: append the new sender alias and re-deploy or hot-reload
  3. Re-run intake: reassign the staged row to the right customer in admin-dashboard
  4. Bake auto-classifier: if happening frequently, raise the issue in the auto-classifier backlog

Scenario · document_converter throws on a malformed PDF

proposed_actions stages but bid_markdown is empty / error flagged.

  1. Open R2 source: npx wrangler r2 object get bid-attachments/<email_id>.pdf
  2. Try local convert: open in Preview / use pdftotext to confirm corruption
  3. Decide: reject + ask customer to resend, OR copy-paste body into Path 2 chat

Logs to check

Kill switches

Backlog · open questions

What's not done · what's uncertain