> Markdown version of https://agledger.ai/docs/guides/work-context/
> Full index of this site for AI assistants: https://agledger.ai/llms.txt

# Durable work state for agents

An agent's conversation is ephemeral. The Work Context recipe makes the *work* durable: the state
of an in-progress piece of work is checkpointed as immutable, signed records, so a fresh session
with no prior conversation reads the latest checkpoint and resumes. Any model, any harness; the
checkpoints are ordinary notarized records, tamper-evident and verifiable offline like everything
else on the chain.

The recipe ships in the [`agledger-ai/install`](https://github.com/agledger-ai/install) repository
at `examples/recipes/work-context/`: one notarize-only Type (`work-context-v1`), a register script,
an importable manifest, and a lineage checker. Its README carries the full convention set; this
page is the working loop. Installing a recipe in general is covered in
[Install a Recipe](/docs/guides/recipes/); why work state rides the ledger, and what that buys over
a store you trust, is covered on [agent memory](/agent-memory/).

## The identity model, first

A registered agent is a durable identity. The **sessions** that do its work (different models,
harnesses, runs) are ephemeral; the Server sees only the key a session presents, and the key names
the agent. "A fresh agent resumes the work" means a new session of the SAME agent, presenting that
agent's key. Do not register a second agent to take over work: a key bound to a different agent
can neither read nor continue another agent's records, by design.

## Register the type

Pick ONE path per org (both registered makes a bare `type` ambiguous on record create):

```bash
git clone --branch v1.5.0 https://github.com/agledger-ai/install.git
cd install/examples/recipes/work-context
./register.sh          # POST /v1/schemas, admin key, lands under the `local` publisher
```

Or import the manifest when the type should carry a matching `manifestDigest` across servers:

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/schemas/import" \
  -H "Authorization: Bearer $ADMIN_KEY" -H 'Content-Type: application/json' \
  -d "{\"manifest\": $(cat manifests/01-work-context.json | jq .manifest)}"
```

## Start a piece of work

One root record represents the work. Every checkpoint will be a child of this root. Put the
navigation hint in the root's criteria: cold-start sessions read the root first, and the hint is
what tells them where the state lives.

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $AGENT_KEY" -H 'Content-Type: application/json' -d '{
  "type": "delegated-workflow-v1",
  "criteria": {
    "workflowName": "Q3 vendor migration",
    "workContext": "Durable work state lives in work-context-v1 CHILD records of this root. The head is the one still-current checkpoint: GET /v1/records/search?parentRecordId=<thisRecordId>&type=work-context-v1&superseded=false (expect exactly one row; two means the work forked). Read the head, follow its resumeInstructions, then write your own checkpoint under this root, setting supersedesRecordId to the head id both at the top level of the create body and inside criteria."
  }}'
```

The response `id` is the root id. It plus the agent key is the entire brief a resuming session
needs.

## Write the first checkpoint

The first checkpoint declares `checkpointReason: "initial"` and is the only one that supersedes
nothing:

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $AGENT_KEY" -H 'Content-Type: application/json' -d '{
  "type": "work-context-v1",
  "parentRecordId": "<rootId>",
  "criteria": {
    "objective": "Migrate all vendor records to the new schema",
    "summary": "Kickoff. Inventory complete: 240 records across 3 systems.",
    "pendingWork": ["Migrate system A", "Migrate system B", "Reconcile totals"],
    "checkpointReason": "initial",
    "resumeInstructions": "Start with system A; credentials are in the ops vault under vendor-migration."
  }}'
```

It lands `RECORDED`: notarize-only, terminal on create, one call.

## Resume cold

A new session holding the agent's key, given only the root id:

```bash
# 1. Find the head: the work-context child of the root that nothing supersedes
curl -s "$AGLEDGER_API_URL/v1/records/search?parentRecordId=<rootId>&type=work-context-v1&superseded=false" \
  -H "Authorization: Bearer $AGENT_KEY" | jq '.data'
```

`&superseded=false` is what makes this the *current* state rather than the newest row. The chain
keeps every state the work ever held, so without it a filter matches checkpoints that stopped being
true three resumes ago. (Do not use the root's `childRecordIds` either; that array is oldest
first.) One row back is the head. Two rows back is a genuine fork, which the query reports instead
of picking a winner: see "Keep the lineage honest" below.

Read the head's `resumeInstructions` and `pendingWork` (the first entry is the next action), do the
work, then write your own checkpoint superseding the head:

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $AGENT_KEY" -H 'Content-Type: application/json' -d '{
  "type": "work-context-v1",
  "parentRecordId": "<rootId>",
  "supersedesRecordId": "<headId>",
  "criteria": {
    "objective": "Migrate all vendor records to the new schema",
    "summary": "System A migrated: 96 records, 0 failures. B and reconciliation remain.",
    "completedWork": ["Migrate system A (records notarized under <recordId>)"],
    "pendingWork": ["Migrate system B", "Reconcile totals"],
    "checkpointReason": "milestone",
    "supersedesRecordId": "<headId>",
    "resumeInstructions": "Migrate system B next; reuse the batch size from system A."
  }}'
```

The head id goes in two places, and they do different jobs. The **top-level** `supersedesRecordId`
is the record field the engine acts on: it is immutable, inside the create-time signature, and
resolved at create, so a checkpoint can never carry a dangling lineage claim onto the chain (name a
record that is not in your org and the write is refused with a 404). It is what retires the old
head from the `superseded=false` view. The **criteria copy** carries the same claim inside the
signed record *content*, where an offline auditor walking exported criteria can see the lineage
without the engine's indexes. Do not put the id in criteria alone: criteria is content the engine
does not interpret, so nothing gets linked and the old head stays current.

`parentRecordId` and `supersedesRecordId` also do different jobs: the parent says which piece of
work this checkpoint belongs to, the supersedes says which earlier checkpoint it makes stale.

What the engine does *not* do is judge whether the claim makes sense for this recipe. A session
that misidentifies the head and omits the field gets an ordinary `RECORDED` back. The omission is
not lost, though: the head it failed to supersede stays current, so the very next head query
returns two rows and the fork is visible immediately. That is a better guard than a write-time
refusal, which could only ever catch a MISSING claim and never a WRONG one.

When a pending item created records, carry their ids in the checkpoint (as above). That turns
"I did it" into a claim anyone can check against the chain: either the records are under the root
or they are not.

## Finish

The last checkpoint declares `checkpointReason: "final"` and must have an empty `pendingWork`;
a final with work remaining is refused (400). That guard and the `checkpointReason` enum live in
the Type's schema, so they hold on every write path.

## Or drive the whole loop over A2A

Nothing above is REST-only. An A2A-native session holding the agent's key writes checkpoints with
`create_record`, which carries both `parentRecordId` and `supersedesRecordId`, and reads the head
with `ListTasks`:

```json
{"filter": "parentRecordId = \"<rootId>\" AND type = \"work-context-v1\" AND superseded = false"}
```

The returned Task's metadata reports the result (`agledger:parentRecordId`,
`agledger:supersedesRecordId`, `agledger:supersededByCount`), so a writer can tell a live head from
a replaced record without a REST read. Do not fall back to `tasks[0]`: that is the newest row, and
the whole point of `superseded = false` is that newest and current are different questions. The
fields A2A does not carry are `references`, `metadata`, `dependsOn` and `riskClassification`, so a
fleet that wants A2A task ids checkable from the chain writes those checkpoints over REST. Fleet
triage is also REST's job: the `ListTasks` filter grammar has no criteria term, so a question like
"which work items are awaiting approval" is one criteria search over REST but one `ListTasks` call
per work item over A2A.

## Keep the lineage honest

The server resolves a supersession claim but does not judge it: it proves the target is a record in
your org, not that superseding it was the right move. Two sessions racing the same head both land,
and each believes it wrote the newest checkpoint. The recipe ships the check the schema cannot do:

```bash
./verify-lineage.py <rootId>
```

It proves exactly one initial, every supersedes resolves under the root, no record superseded
twice, and one chain from initial to head. Run it at every resume if concurrent sessions are
possible. A detected fork is not tamper (every branch is genuinely signed); recover by writing a
checkpoint that supersedes the branch you keep and records the merge.

Offline verification works like everywhere else on the chain: export each record's audit bundle
and verify against out-of-band keys with no server trust. See
[recovery](/docs/operations/recovery/) for the verifier; checkpoint lineage is content, so an
offline auditor walks the exported criteria.

## Checkpoint size

Keep a working head at 2 to 4KB of criteria. **Snapshot, do not append**: superseded checkpoints
stay on-chain forever, so the head only needs the working set; summarize older progress in
`summary` and let the supersedes chain be the archive. The server caps `criteria` (default 10,240
bytes; the 400 carries your org's limit), and in cold-start runs large heads degraded resume
fidelity well before the cap: compact heads were resumed correctly by every model that completed
the protocol.

## Handoff with visible succession

Plain key reuse resumes work but leaves succession unrecorded. When the chain should show *which
session* wrote each checkpoint, mint an additional key bound to the same agent for the successor
(`POST /v1/admin/api-keys`, admin-mediated). Signed attribution is two-level, agent and key, both
inside the signature, so succession is tamper-evident at key granularity and each successor is
independently revocable.

---

*Validated against API v1.5.0 on 2026-08-21. The transcripts are from the recipe's end-to-end
walkthrough, driven against a fresh install of the published v1.5.0 release. Record and search
shapes are owned by the [API reference](/api/); confirm field-level detail there against your own
Server.*
