Skip to main content
A supplier agent emails a quote without a lead time. The buyer agent asks for the missing detail, checks the completed quote against the source mail, and sends a signed handoff to a sourcing agent. The sourcing agent verifies that record before acting. This pattern pairs Primitive’s agent email with a Millwork output check: agents can clarify work over ordinary email, while your rules decide whether the next agent should receive the result.
Offline example. Supplier data, Primitive mail, and Millwork receipts are simulated. The commands send no email and start no Millwork run or charge.

How an email handoff becomes verifiable

An agent can ask for missing details by email; once the work is ready, your output check decides whether it can move to the next agent. Your output check is a check applied to a run’s output; the API calls it a verifier. Millwork records its result. Goal: run a supplier-to-buyer quote handoff between email agents for an approved request for quotation (RFQ), check the quote before it moves on, and verify the record the receiving agent gets. You are done when: npm run cases exits 0 and prints a record verified line, and changing one character in an earlier entry of a copy of the record makes verification fail.

Run the offline cases

Download the checked example, run its cases with Node 22, and verify a record yourself.

Hand it to your coding agent

Give this page to your coding agent. Ask it to run the offline cases, verify that changing a copy of the record fails, and report the result.

Run the example

You need Node.js 22. The example installs its own pinned Millwork CLI with npm ci; it needs no API key, no Primitive account and no Millwork organization.
1

Download the checked example

Copy the downloader into primitive-example.mjs, review it, then run it with node primitive-example.mjs. It creates a new primitive-agent-email directory, downloads only this docs site’s example file, and checks every file against the digest it pins before writing anything.
2

Run the cases

Expected result: the command exits 0. It prints the clarification reply, the checked handoff, a timeline of the case record, the decision for every negative case, and this line for the first transfer:
It also prints each fixture’s record and mail paths, its signed head and sequence number, both public-key fingerprints and a complete verify-record command with those values filled in. The last step runs the published millwork verifier test --local against the example’s check.
3

Verify a record yourself

Run the verify-record command that npm run cases printed for fixtures/rfq-042. The keys in it are labelled test-only keys from the example:
Expected result: the same record verified line. Now copy record.jsonl, change one hex digit of the packet_sha256 value on the transfer_prepared entry in the copy, and run the command against the copy: it prints hash_mismatch and exits 1. Change the quote text in a copy of witness.eml instead: it prints action_mismatch. Run npm run cases again; the heads it prints do not change. The second transfer, fixtures/rfq-077, has its own record, mail and command in the same output.

Make this your case

Edit three files. Everything else is the record, the verify-record check and the simulated services, which you keep. After an edit, run npm run cases. A rule change changes the recorded heads, so the run reports which fixture no longer matches its committed record. Rewrite one fixture’s record on purpose with npm run cases -- --update-golden <fixture>; it prints the old and new heads and leaves every other fixture’s record byte for byte unchanged. An ordinary run never rewrites a record. To serve the check from your own HTTPS endpoint later, start from Recipe D. This page has no CLI recipe of its own, so there is no --recipe value for it; use millwork verifier init quote-check --recipe d --json, then read Verify agent or pipeline completion for the evidence-read pattern this check follows.

When something fails

Each negative case in npm run cases prints a decision for the buyer agent or a code from verify-record. A decision of reject stops that mail or handoff, quarantine holds mail until a person releases it, and pause:<reason> stops automation until a person reconciles it. The controller never starts a second send or a new paid run to recover; it first looks up the earlier request by its request key, and it stops when that lookup cannot settle what happened.

How the handoff works

The example uses these words for its parts: For one handoff, the buyer agent freezes the transfer packet and records an attempt ID. It sends Millwork a run request that carries only that ID. Millwork calls your output check with {candidate: {attempt_id}}, and the check reads the frozen packet from your own store and returns its verdict. Millwork returns a receipt with that verdict. The buyer agent then signs the record head and sends the handoff mail carrying it, and the sourcing agent runs verify-record against the mail it kept. Routine mail, such as the clarification reply, has no Millwork run. Only the consequential handoff is checked. Primitive moves the mail, you own the record, the controller and the check, and Millwork supplies the check call and the receipt. There is no central coordinator.

What the handoff mail carries

The handoff mail’s plain-text part ends with one fixed line, -- MILLWORK WITNESS v1 --, followed by the signed head, sequence number and action. The controller refuses to send a packet whose quote, filename or other part already contains that line. The receiving agent rebuilds the action from its own mailbox address, the decoded subject, the checked text and the attachment digests, and requires it to equal the signed action. Raw mail bytes do not need to equal the submitted body. The keeper serves its record entries separately. The receiver checks them against chain and check public keys it pinned out of band and against the head in the mail it kept. verify-record cannot tell you who sent a copied mail, whether a later entry replaced the handoff, or whether the same head went out in a second send. Decide separately whether to trust mail from the keeper’s sender.

The run request

In a separately approved live run, the controller sends this request with a request key, so a retry does not start the work twice. The saved agent returns only the attempt ID, and the check reads the frozen packet from your own store. The sandbox data class labels your own made-up test data; it is not a protection and adds no isolation.
With on_eval: [], a failed check records needs_review and starts no fallback or repair. The controller accepts a receipt only when it is a completed live run on the pinned saved agent, its final check is a pass from your output check, and the check’s own signed observation agrees. A test run, a basic output-presence check, or a missing receipt holds the handoff. The example’s receipts are simulated; a real receipt match is a later step that you approve separately.

What each record shows

Digest-only entries are not confidential: a short value can be guessed from its digest. Keep mail bodies and attachments under their own retention and access policy. Capture any receipt you need before it expires; read Results and receipts for retention.

Limits of the example

The example derives its signing keys from a labelled test-only seed. Do not use them for real mail, and keep production keys in your own key service. The receiving agent’s checks are offline and synthetic. Primitive publishes no general rule for grouping inbound mail into threads, so the example binds mail to a case by its references and an allowlisted sender, and it quarantines mail that matches neither. A real send, a real Millwork run, or real supplier data each needs your separate approval.