← Greenfield Pipeline Design · Greenfield design
Order Confirmation on the Existing System
Superseded on 10 September 2026. IMP is being built on the greenfield design at /order-confirmation/. This page is kept as the record of the alternative that was designed and not pursued.
The build that takes IMP live first. It delivers the same desk as the greenfield order-confirmation design, with the same rules and the same experience for IMP and its customers, on the system that runs in production today. The greenfield remains the target; what follows works out, from the problem and the constraints, what the interim build has to be.
1. The problem
IMP buys printed tin-can sheets from one supplier, RIC in Turkey, and sells the finished cans to six customers. The sheets carry each customer's artwork. IMP therefore cannot order sheets for an order until the customer has confirmed the artwork on every line of it.
Today one person does this by hand: find the current design for each line, send the set to the customer, chase for a reply, file whatever comes back, work out sheet quantities from can quantities, draft the supplier purchase order. About two hours per order, repeated on every order because customers change artwork between orders. A wrong design on a print run costs around $10,000, and it has happened with both IMP and the customer having confirmed the wrong version.
Sari, the decision maker at IMP, gave one hard requirement. When a customer sends him a new version of a design, he forwards it, and he needs to be certain that this version goes out for confirmation and the old one does not. He cannot tell versions apart by eye, he will not remember which is current, and he does not want to compare files or read version lists. Ask him only when the system is genuinely unsure.
Two facts about how IMP works decide much of what follows. Almost all of Sari's traffic, orders and design files alike, arrives on WhatsApp. And every order is reconfirmed, even when the same artwork was approved last week, because the cost of a wrong run is higher than the cost of asking again.
2. Constraints
These are the facts that decide the design, as distinct from preferences.
- The runtime is what runs today. Engine B, the visual workflow builder with its Go interpreter, is frozen for new authoring but keeps running. There is no Temporal in any environment. The greenfield spine does not exist yet.
- Long waits. A customer can take weeks to confirm. Engine B parks runs on rows and a scheduled sweep fails runs parked more than seven days.
- WhatsApp is built but not configured. The channel exists in code on a platform-owned Twilio account; no environment has a number. Configuring one is an operator task (ARD-728), not code, but its timing is not ours.
- IMP has no ERP integration in Aradus. Its brand library exists: 45 brands across 6 customers and 7 products, every one at version 1 with a file. It has two connected mailboxes and no orders in the system.
- Small team. The full desk is more than one engineer builds in three months on the existing system. Whatever is built must ship in pieces, each useful alone.
- It must move. Anything written here will be migrated to the greenfield tables. Data the greenfield cannot map is waste.
3. What the system has to do
Derived from the problem, before any choice of mechanism.
- Know that an order has arrived, with its lines, from the message it arrived in.
- Know, per line, which artwork version the customer is being asked to confirm.
- Put that version in front of the right person at the customer, and keep asking until each line has an answer.
- Record each answer with evidence that would settle a dispute a year later: who said it, in which message, in which words.
- Accept a new design file from Sari and make it the current version without his involvement, unless the system cannot tell which brand it belongs to.
- Convert confirmed can quantities to sheets, covers and bottoms, show the working, and draft the supplier order for a person to approve.
- Do all of the above per customer, since each customer has its own contacts, patience, and rules about what counts as a yes.
Requirement 4 rules out any design where confirmation is a status flag. Requirement 5 rules out any design where Sari picks a version. Requirement 7 rules out a graph per customer.
4. Decisions
For each decision: the options considered, and why one won.
Scope first. Two scopes were possible: the full desk of the prototype, or the smallest thing that chases a confirmation, which knows an order came in, sends the brand files for its lines, and asks until they confirm. The full desk is built. The reasoning is Sari's own: order confirmation is one link in a chain he wants to run without employees, and a link that still needs a person to convert quantities and draft the supplier order saves one of the two hours. The supplier order is also where his "Caramel branding" mistake happened, and requirement 6 exists because of it. What makes the full scope affordable is the build order in section 10, where each piece ships alone.
4.1 Where the loop runs
The loop is a long-running conversation with timers: send, wait, remind, read, repeat. Three ways to run it on the existing system.
An Engine B graph plus Go handlers for the steps no node can
do. The graph has the nodes a chase needs: a trigger,
request_approval, wait_for_event,
delay, send_email, notify. New Go
code is limited to reading and deciding. Costs: the seven-day sweep must
be raised for this graph; a graph is data, so it is tested by replay
rather than by unit test.
A pure Go handler with its own state rows and the existing cron for timers, the way approval timeouts already work. Cleaner to port to Temporal and independent of a frozen engine. Cost: it rebuilds waiting, reminders and reply matching that the interpreter already has, so it is the slowest to a live order.
A hybrid, Go owning the state machine and the graph only sending and collecting approvals. Cost: two owners of one loop, which is how the 26 August incident happened, where the pipeline wrote bills the workflow was supposed to own.
The graph wins on time to a live order, and the cost is acceptable because the graph is thin: sequencing and waiting only. Every step that reads or decides is a Go handler with a typed result, which is the part that ports to a Temporal activity unchanged.
4.2 How an order enters
The prototype had a Start button because nothing upstream existed. In production the order is a message: the customer's purchase order as a PDF or a photo, on Sari's WhatsApp, forwarded to Aradus the way he will forward artwork. Nobody re-types it. The existing pipeline classifies it as a purchase order and order entry creates the sales order with a confidence per line.
A misread quantity that becomes a pack, then a sheet count, then a supplier order is the one mistake the desk must not let through unreviewed. So a gate exists between order creation and the chase. It is a per-customer threshold rather than a step every order takes: when every line clears it the run starts on its own; otherwise the order waits in Needs you with the weak lines highlighted and one tap starts it. A customer who sends clean PDFs should not pay for one who sends photographs.
Until the production WhatsApp number exists, forwarding goes to a connected mailbox or an upload. The handler is the same; only the door changes.
4.3 The unit of confirmation is the line
Confirmation could be recorded per order (one yes), per brand (this design is approved), or per line (this version, on this order, for this quantity). Per order fails requirement 4 when the customer confirms three lines and rejects one. Per brand fails Sari's second fact: every order is reconfirmed, so an approval from last week does not carry. Per line survives both a partial reply and a per-order reconfirmation, and it is what the tracker shows.
4.4 Replacement by agreement of three signals
Requirement 5 is the hard one. A file arrives with no order attached and a caption at best. Which brand does it belong to, and is it new?
One signal is not enough. The bytes alone say only whether this exact file has been seen. The text in the artwork says which brand it is for, but a re-export of the same design carries the same text, and two brands from one customer can share most of their text. Visual similarity to the current version says whether the design changed, but a customer's two flavours can look alike. The origin, whose WhatsApp number or mailbox it came from, narrows the customer but not the brand.
So the rule is agreement. Same bytes as the current version: a duplicate, no version, a one-line "already current" back to Sari. Otherwise read the brand name out of the artwork, compute a perceptual fingerprint and compare it with the current version of each candidate brand for that customer, and note the origin. When all three point at one brand, it becomes the new current version and Sari gets a receipt naming what replaced what. When they disagree, or the file is near-identical to the current version so that "new version or the same file?" is a fair question, Sari gets a one-tap ask listing the candidates. That is what "only ask when genuinely unsure" means in mechanism.
Whether the rule may act alone is a per-customer setting, because IMP's answer is one customer's answer. A customer with a compliance team wants every replacement approved.
4.5 One mechanism for asking a person
The desk asks a person in three places: a line that matches no brand,
a replacement the rule is unsure about, and the supplier PO review. The
existing order_approvals table already carries a proposal,
an AI confidence, an assignee, a decision and an expiry, with kinds for
branding confirmation and file replacement. Inventing a second ask store
would fork the record of decisions across two tables. All three asks are
rows here.
4.6 Rules are data, per customer
Who to send to, who to escalate to, how often to remind, what a
marked-up screenshot counts for, whether the rule may replace on its
own: each is a customer's answer, and each is something a person will
change after an intervention. They are stored as one versioned row per
customer, read by the graph at run time. That is what lets one graph
serve six customers and the next tenant. Learned mappings, "Ramadan
Edition Gold at this customer means this brand," go to
entity_aliases, which the matcher already reads.
5. Architecture
flowchart TB
M[forwarded message<br/>mail, upload; WhatsApp when the number exists] --> P[document pipeline<br/>classify, order entry]
P -- purchase order --> O[sales order created]
P -- artwork file --> R[replacement handler<br/>three-signal rule]
O --> G[Engine B graph<br/>one run per order]
G --> MT[match_brands]
MT -- unmatched line --> A1[order_approvals ask]
MT --> PK[send_branding_pack]
PK --> W[wait_for_event<br/>reply or link click]
W -- delay --> RM[reminder, escalation]
W --> RD[read_signoff_reply]
RD -- rejected --> N[needs you]
RD -- all confirmed --> CV[convert_bom]
CV --> PO[draft_supplier_po]
PO --> A2[request_approval<br/>PO review]
A2 --> S[send from IMP's address]
R --> V[new brandings version<br/>or one-tap ask]
UI[Command Center desk<br/>queue, tracker, rulebook, PO review] -.reads and decides.-> GThree parts, one owner each.
The graph owns sequencing and waiting. One run per
order, started by the order-created event or the operator's tap. It
calls the matcher, sends the pack, parks on wait_for_event,
fires reminders on delay, escalates after the rulebook's
cap, and hands converted quantities to request_approval.
One template per tenant, reading the rulebook at run time. The stale-run
sweep gets a per-graph maximum of 60 days for this template.
Go handlers own reading and deciding. Five new node types, each a deterministic function or a model call returning a typed result with a confidence:
| node | does | returns |
|---|---|---|
match_brands |
matches each order line to the customer's current brand versions: aliases first, then name similarity | per line: brand version or none, confidence, candidates |
send_branding_pack |
one email to the sign-off contact with a preview and a tokenized link per line | the dispatch id; lines moved to sent |
read_signoff_reply |
reads a reply or a link click into per-line verdicts | per line: confirmed, rejected with instruction, deferred, unclear, with the quoted words |
convert_bom |
turns confirmed cans into body sheets, covers and bottoms through
bill_of_materials |
quantities per component with the working, frozen |
draft_supplier_po |
drafts the purchase order, rounded to the supplier's minimum | a purchase orders row in draft |
A sixth handler, the replacement rule of 4.4, runs outside any order on an artwork file arriving through the pipeline.
The desk is Next.js pages in the Command Center: the queue split Needs you, With customer, Supplier PO, Sent; the order view with the per-line tracker, Remind now and record-by-phone; the rulebook editor; the PO review with Approve and send. Pages read the rows the graph writes and act through the approvals API. They do not read run state, so the runtime can change underneath them.
6. Data
Each change traces to a requirement in section 3.
order_brandings becomes per line
(requirements 2, 3, 4). Today it is keyed by order and brand with a
status. It gains order_line_item_id, a pin to the exact
brandings version row, a line state (matched, needs_you,
sent, reminded, confirmed, rejected, deferred, superseded),
reminder_count, last_reminded_at,
sent_at, and signoff_evidence: the inbox
message id and the quoted words, or "by phone, recorded by Layla at
14:02". One open row per line, enforced by a partial unique index.
brandings gains what the rule needs
(requirement 5). content_hash, phash,
identity_text, source and
source_message_id, is_current, and
decided_by (the rule, or the person who answered the ask).
Versions stay one row each; a replacement is a new row and the old one
loses is_current.
order_approvals is reused (decision
4.5) with two kinds added, branding_match and
supplier_po_review, beside the two that exist.
proposal holds candidates and confidence;
decision holds the answer and a save-as-rule flag.
entity_aliases is reused (decision 4.6)
for learned line-name-to-brand mappings.
branding_rulebooks is new (requirement
7). One versioned row per tenant and customer: sign-off and escalation
contacts, reminder interval and cap, deferred handling, whether a
marked-up screenshot counts, archive days, replacement mode, match
threshold, order-entry threshold for auto-start, and whether open lines
are reconfirmed when their brand gets a new version. For IMP that last
one is yes.
Supplier PO (requirement 6). A purchase
orders row with source_order_id pointing at
the sales order, each line carrying the frozen BOM working as JSON so
the review can show how 3,893 body sheets came to be.
7. Flows
Intake. A forwarded purchase order lands through a mailbox, an upload, or later the WhatsApp number. The pipeline classifies it, order entry extracts the lines with a confidence each, and the sales order is created with the customer resolved from the forwarded header or the sender. The order-created event starts the run if the lines clear the customer's threshold; otherwise the order waits in Needs you.
Matching. match_brands runs over every
line. Above the threshold the line is pinned to the brand's current
version. Below it, or with no candidate, an ask opens with three
choices: new brand and ask the customer for the file, map to an existing
brand, or pause the order. The answer can be saved as an alias. Nothing
is sent while a line is unresolved.
Pack. One email to the sign-off contact: a preview
per line and one link per line, each link carrying a token for order,
line and pinned version, so a click is a per-line verdict with nothing
to read. Lines move to sent. delay fires at the reminder
interval; each reminder re-sends only the unconfirmed lines and
increments the count; past the cap the escalation contact is added and
Sari is told.
Replies. A reply or a click raises the event the run
is parked on. read_signoff_reply returns per-line verdicts
with the words that decide each. Confirmed pins the line with evidence.
Rejected with an instruction moves the line to Needs you and tells
Sari's designer; when the new file arrives it goes through replacement
and only that line is re-sent. Deferred follows the rulebook. Unclear
opens an ask. A confirmation relayed by phone is recorded from the
tracker with who said it and when.
Replacement. As in 4.4, at any time. When a version changes and the rulebook says reconfirm, open lines pinned to the old version are re-sent with the new one.
Convert and PO. When every line is confirmed,
convert_bom produces components with the working per line,
draft_supplier_po rounds to the minimum order quantity and
drafts the purchase order, and a request_approval opens the
review: Approve and send, or Change an item. Approve sends the PO from
IMP's own address, posts the purchase to the sales order, and logs the
receipt.
8. Failure modes and invariants
- A model call that errors, times out, or lands under its confidence floor becomes an ask. It does not skip, and it does not guess.
- Every send is idempotent on order, line and pinned version. A retried step cannot send the same pack twice; a reminder after a confirmation is a no-op.
- A run that fails is shown in Needs you with the step named. The sweep cannot make one disappear inside 60 days.
- Two files with the same bytes are one version. A file that matches no current version and agrees on no brand is an ask, and an unanswered ask blocks nothing except that file.
- Only the handlers write
brandingsversions andorder_brandingsrows. A test after every run asserts it, the check that would have caught the 26 August incident.
9. Tests and evals
Every model-facing piece has a golden set before it ships, in the two-layer shape the repository already uses: deterministic replay in CI, live-model accuracy on demand.
- Matcher. Scored on IMP's 45 brand files against line names from purchase orders IMP has received: precision at 1, and the ask rate below threshold. The target is agreed with IMP before the build.
- Replacement rule. Scored on pairs built from the same files: the same file re-sent, a true new version, a near-identical re-export, a file for a different brand from the same customer. Two numbers: how often it asks, and how often it replaces the wrong brand when it acts alone. The second must be zero on the set.
- Reply reader. Scored per line on replies from IMP's threads, including relayed text such as "items 2 and 3 confirmed by phone".
- Order entry. Scored field by field on IMP's
purchase orders with the extraction comparer in
test/evals/lib/compare.ts. - Graph replay. One recorded order driven through
start, pack, two reminders, a partial reply, a rejection, a replacement,
and close; asserts the
order_brandingsrows at each step and that every send happened once. Runs against the local stack like the invoice-versioning eval.
10. Build order
Each step is usable on its own and the next does not undo it.
- Replacement handler. Sari's hard requirement, and it works with no order in the system. Tuned on the 45 files.
- Intake and matching, tracker read-only. Forwarded POs become orders; lines are matched and pinned; the desk shows the tracker and sends nothing.
- Pack, reminders, reply reading. The chase goes live by email.
- Convert and PO review. The supplier order closes the loop.
- Rulebook editor and queue views. Until then the rulebook is a row an engineer edits.
WhatsApp attaches to steps 1 and 3 when the production number exists. Nothing waits on it.
11. The path to the greenfield
The data here was chosen so that the move is a column mapping.
order_brandings per line is line_signoffs.
brandings with is_current and the hashes is
brands plus brand_versions flattened into one
table. branding_rulebooks is a policies row.
Each order_approvals row is a proposal and its decision in
one row. Each Go handler is an activity with the same input and output;
the graph's sequence becomes OrderConfirmationWorkflow and
the replacement handler becomes ArtworkReplacementWorkflow.
What does not move is the graph itself, which is the part with the least
logic in it.
12. Inputs still needed
- Five to ten purchase orders IMP's customers have sent, for order entry and the matcher. IMP's mailbox in Aradus holds 12 messages and no purchase orders; they are on Sari's phone.
- A few of the reply threads that came back, for the reply reader.
- ARD-728's operator steps on the Twilio console, so WhatsApp lands before the pilot rather than after.
The 45 artwork files are in hand, copied from the production bucket.