> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getmillwork.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify an agent-to-agent quote handoff

> Run an offline example where an output check approves an agent's emailed quote handoff and the receiving agent verifies the record.

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](https://docs.primitive.dev/docs)
with a Millwork output check: agents can clarify work over ordinary email,
while your rules decide whether the next agent should receive the result.

<Note>
  **Offline example.** Supplier data, Primitive mail, and Millwork receipts are
  simulated. The commands send no email and start no Millwork run or charge.
</Note>

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

```mermaid theme={null}
---
config:
  flowchart:
    nodeSpacing: 20
    rankSpacing: 22
    padding: 10
    diagramPadding: 8
    curve: stepAfter
  themeVariables:
    textColor: "#211F1B"
---
flowchart TD
  A("<b>Sending agent</b><br/>emails your service") --> P(["<b>Primitive</b><br/>delivers the email"])
  P --> C("<b>Your service</b><br/>keeps the case record")
  C ~~~ X(" ")
  C -->|Ready to hand off| V("<b>Millwork</b><br/>calls your output check")
  V -->|Only if it passes| H("<b>Your service</b><br/>signs the case record")
  H --> T(["<b>Primitive</b><br/>delivers the handoff"])
  T --> R("<b>Receiving agent</b><br/>verifies before acting")
  C <-.->|"Detail missing,<br/>sender replies"| Q("Ask the sender<br/>via Primitive")

  classDef external fill:#F3F0F8,stroke:#9A86B8,color:#211F1B,stroke-width:1px
  classDef customer fill:#EEF4F6,stroke:#7895A0,color:#211F1B,stroke-width:1px
  classDef decision fill:#F8F6F3,stroke:#B92B2B,color:#211F1B,stroke-width:1px
  classDef aside fill:#EEF4F6,stroke:#7895A0,color:#211F1B,stroke-width:1px,stroke-dasharray:4 3
  classDef ghost fill:none,stroke:none,color:transparent
  class A,P,T external
  class C,H,R customer
  class Q aside
  class V decision
  class X ghost
  linkStyle default stroke:#4B5563,stroke-width:1px
  linkStyle 2 stroke:transparent,stroke-width:0px
```

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.

<div className="cookbook-action-card">
  <Card title="Run the offline cases" href="#run-the-example" arrow="true">
    Download the checked example, run its cases with Node 22, and verify a record yourself.
  </Card>

  <Card title="Hand it to your coding agent" href="#run-the-example" arrow="true">
    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.
  </Card>
</div>

<span id="run-the-example" />

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

<Steps>
  <Step title="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.

    <Accordion title="Copy the downloader">
      ```javascript theme={null}
      // Generated from the Primitive agent-email example. Review before running.
      import { createHash } from "node:crypto";
      import { existsSync, mkdirSync, writeFileSync } from "node:fs";
      import { dirname, resolve, sep } from "node:path";

      const target = resolve("primitive-agent-email");
      const url = "https://docs.getmillwork.dev/downloads/primitive-agent-email.json";
      const expected = "62418fab4aa0d2026a1f716f215554e5ca31c70278ee8cb0f578beb95a11b8b4";
      const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
      if (existsSync(target)) throw new Error("Target directory already exists; choose a clean location");
      const response = await fetch(url);
      if (!response.ok) throw new Error(`Example download failed (HTTP ${response.status})`);
      const limit = 1_000_000;
      if (Number(response.headers.get("content-length") ?? 0) > limit) {
        await response.body?.cancel();
        throw new Error("Example bundle is too large");
      }
      if (!response.body) throw new Error("Example download returned no body");
      const reader = response.body.getReader();
      const chunks = [];
      let received = 0;
      for (;;) {
        const { done, value } = await reader.read();
        if (done) break;
        received += value.byteLength;
        if (received > limit) {
          await reader.cancel();
          throw new Error("Example bundle is too large");
        }
        chunks.push(value);
      }
      const bytes = Buffer.concat(chunks);
      const bundle = JSON.parse(bytes.toString("utf8"));
      if (!Array.isArray(bundle.files) || bundle.files.length !== 55) throw new Error("Unexpected example file inventory");
      const seen = new Set();
      const contents = [];
      for (const { path: name, sha256: digest, content } of bundle.files) {
        if (typeof name !== "string" || !name || name.startsWith("/") || name.includes("\\") || name.split("/").some((part) => !part || part === "." || part === "..") || seen.has(name)) throw new Error("Unsafe or duplicate file path");
        if (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest) || typeof content !== "string") throw new Error("Invalid file entry");
        const path = resolve(target, name);
        if (!path.startsWith(target + sep)) throw new Error("Unsafe target path");
        const data = Buffer.from(content, "utf8");
        if (sha256(data) !== digest) throw new Error(`Checksum mismatch: ${name}`);
        seen.add(name);
        contents.push({ name, digest, path, data });
      }
      const actual = sha256(contents.map(({ name, digest }) => `${name}\0${digest}\n`).join(""));
      if (actual !== expected || bundle.aggregate_sha256 !== expected) throw new Error("Bundle manifest mismatch");
      for (const { path, data } of contents) {
        mkdirSync(dirname(path), { recursive: true });
        writeFileSync(path, data, { flag: "wx" });
      }
      process.stdout.write(`Created ${target} with ${contents.length} checked files.\n`);
      ```
    </Accordion>
  </Step>

  <Step title="Run the cases">
    ```bash theme={null}
    cd primitive-agent-email
    npm ci && npm run 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:

    ```text theme={null}
    record verified: no gaps or truncation through seq 9 against head b5c161c9e77bbd0b2bae980cd3ab0225129b9072a8d18c801c88e44e2a535de3 (source: witness); received action sha256:55dadcf371f4a52dce64fe80a4c0c8d01d210a7c80a19862204b92cbfcedeef3 bound; later entries unknown
    ```

    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.
  </Step>

  <Step title="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:

    ```bash theme={null}
    npm run verify-record -- fixtures/rfq-042/record.jsonl \
      --witness fixtures/rfq-042/witness.eml \
      --head b5c161c9e77bbd0b2bae980cd3ab0225129b9072a8d18c801c88e44e2a535de3 --seq 9 \
      --chain-pub fixtures/keys/chain.pub \
      --chain-key 106c0118fa939a174356d82548c15b2fe8c1663996e21ef4db4fe4113ceec3bb \
      --check-pub fixtures/keys/check.pub \
      --check-key 18d1161ffb0a5d229b6ad043e69baa090df0af6e8cd8a41118cce7dd1fee29c9
    ```

    **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.
  </Step>
</Steps>

<span id="make-this-your-case" />

## Make this your case

Edit three files. Everything else is the record, the `verify-record` check and
the simulated services, which you keep.

| File | What you change |
| - | - |
| `policy.mjs` | Your bound supplier, buyer and receiving inboxes, clock windows, caps on replies, transfers and attempts, and the send limits. The shipped numbers are teaching values. |
| `reply-template.mjs` | The one clarification reply. It can ask only for a field your rules name; no text from incoming mail enters it. |
| `ruleset.mjs` | The exact quote rules your output check applies: required fields, approved suppliers, currencies, ship-to addresses and lead time. |

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](/cookbook/output-checks/completion-evidence)
for the evidence-read pattern this check follows.

<span id="when-a-check-fails" />

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

| What `verify-record` prints | What it means | Next action |
| - | - | - |
| `record verified: ...` | Every entry through that sequence number is intact, matches the head in the retained mail, and the mail you received is the signed action. | Act under your own policy. Entries after that number are unknown. |
| `hash_mismatch` | An entry was edited after it was written. | Do not act. Ask the sender for the record again. |
| `witness_mismatch` | The record was rewritten and re-signed, so it no longer matches the head in the mail you kept. | Do not act. The mail you retained is the evidence. |
| `sequence_gap`, `sequence_order`, `duplicate_seq` or `truncated` | Entries are missing, reordered or repeated, or the record stops before the mail's head. | Ask for the complete record through the mail's sequence number. |
| `bad_signature`, `bad_observation_signature` or `revoked_key` | A head or check observation was not signed by the key you pinned, or the key was revoked at that point. | Confirm the public keys with the sender out of band. |
| `action_mismatch` | The recipient, subject, text or attachments you received differ from what was checked and signed. | Do not act on this mail. |
| `equivocation` | Two different signed heads exist at one sequence number. | Stop and contact the sender. |
| `noncanonical`, `unknown_kind`, `genesis_mismatch`, `clock_regression` or `invalid_transition` | The record is malformed, belongs to another case, or breaks the case rules, for example a send without a passing check. | Do not act. Report the code to the sender. |

<span id="how-it-works" />

## How the handoff works

The example uses these words for its parts:

| Term | Meaning |
| - | - |
| **Keeper** | Your system that stores the case record. Its **controller** is the code that issues check attempts and reserves sends. |
| **Case record** | The keeper's append-only chain of entries for one approved RFQ. Entries hold digests and decisions, not mail text. |
| **Transfer** and **transfer packet** | One proposed handoff to the next agent, and the frozen `email_transfer/v1` snapshot of its evidence, policy, recipient and action. |
| **Check** | Your output check. It reads the transfer packet by the controller's attempt ID and signs one observation per attempt. |
| **Arm** and **dock** | The saved agent, which the API calls an `arm`, returns only the attempt ID. The output-check endpoint, called the dock in the [endpoint contract](/cookbook/output-checks/build-the-dock#the-endpoint-contract), receives only `{candidate}`. |
| **Receipt** | Millwork's record of the run, its check and its usage. It does not contain or sign the email. |
| **Checkpoint** and **witness** | A checkpoint is a signed head of the case record. A witness is a party outside the keeper, here the receiving agent, that keeps the raw handoff mail carrying that head. |

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.

```json theme={null}
{
  "mode": "live",
  "task": {
    "objective": "Return exactly the attempt ID you were given as JSON: {\"attempt_id\": \"<id>\"}.",
    "inputs_ref": {
      "kind": "inline_json",
      "json": {
        "attempt_id": "d9848df6487df71a445bc8c2068cf1bfc4916ad2200b5c8ab7dc3dec6dc211d5"
      }
    }
  },
  "routing": {
    "required_arm_id": "arm_rfq_attempt_relay"
  },
  "policy": {
    "budget": {
      "max_cost_usd": 0.05,
      "max_runtime_s": 120
    },
    "data_classes": [
      "sandbox"
    ],
    "on_eval": []
  },
  "verifier_id": "vrf_rfq_quote_check"
}
```

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

<div className="completion-evidence-overview">
  | Evidence | Shows | Does not show |
  | - | - | - |
  | Hash chain and a known head | Earlier entries were not edited or reordered relative to that head. | That a keeper who can replace the whole chain and head is honest. |
  | Signed checkpoint | Your pinned key endorsed that sequence, head, policy and action. Two signed heads at one sequence reveal a fork to a witness. | That the judgment was correct, the real time, or that the key was not compromised. |
  | Head in the retained handoff mail | A party outside the keeper kept the head. | Business truth or delivery. Primitive's sending-domain signature shows its relay signed those bytes. |
  | Millwork receipt | What Millwork returned when the keeper fetched the receipt. | A link to this transfer that a third party can check offline. The keeper asserts that link. |
  | Check observation | Your check key endorsed one digest, outcome and ruleset for an attempt. | An independent attestation by Millwork. You keep the signing key. |
</div>

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](/concepts/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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.