durable handoffs
why
when work crosses a boundary — step, queue, agent, user — the sender can crash and the receiver can be retried or duplicated. if the canonical fact was not persisted first, the handoff promises state that may never exist and notifications built on it are unverifiable. persisting first makes every reload, retry, and projection converge on one durable truth.
flowchart LRA[caller] -->|1 validate transition| D[(durable record)]D -->|2 typed reference| B[next step · workflow · agent · user]B -->|3 reload canonical fact| DD -->|4 publish projection| C[query invalidation · event]
rule
persist the canonical fact before handing it to another step, workflow, agent, queue, or user. a notification is a projection of durable truth, never the truth itself.
required handoff
- validate the current durable state and permitted transition.
- write the canonical record, artifact, or event with a stable idempotency key.
- return or send only typed references and bounded summaries.
- publish progress, invalidate a query, or broadcast after the write.
- the consumer reloads the canonical fact, never a stale in-memory payload.
payload discipline
- pass
runId,artifactId,workflowInstanceId, or a similarly exact stable reference. - store large bodies in the owning database or object store; never shuttle them between steps or child workflows.
- make eligibility a query over durable facts (stage status, unconsumed record), not a local "already processed" array.
decision process
the order is deliberate: write, then notify. a message can be delivered twice, late, or not at all; a ui can disconnect; a worker can restart between lines. none of these delivery outcomes may change whether the handoff happened.
| design | failure | correct boundary |
|---|---|---|
| broadcast "completed" before the write. | a consumer renders success that no authoritative reader can find. | commit the fact, then publish a projection of it. |
| put the only artifact in a queue message. | redelivery, size limits, and expiration make the artifact non-durable. | store the artifact once; pass its stable reference. |
| let the receiver trust the payload as current state. | the sender can be stale by the time the receiver runs. | receiver reloads and validates the canonical fact. |
| keep consumed IDs in memory. | a restart replays work; concurrent workers disagree. | query durable eligibility or enforce a unique/idempotent write. |
good and bad
bad
await broadcast("stage completed", result);await db.insert(stageResult).values(result);
if the write fails or the process restarts between the lines, the notification is a lie: users act on a completion that never happened, support finds nothing in the store, and a retry re-runs the stage with no stable identity to deduplicate against.
good
await db.insert(stageResult).values({ ...result, idempotencyKey });await broadcast("stage completed", { stageResultId: result.id });
the fact commits first under a stable key; the notification is a typed-reference projection of it. duplicate delivery, delayed delivery, and consumer restarts all converge on the same committed record, so no delivery outcome changes whether the handoff happened. every consumer reloads one authority, keeping retries safe and projections consistent by construction.
verification reasoning
test successful handoff, duplicate delivery, delayed delivery, and consumer restart. in each, the receiver must reload the same canonical fact or stop with a typed diagnostic — never create a second semantic completion or act on an unverified payload.
do not
- broadcast success before the source-of-truth write commits.
- use agent state, a queue payload, or a model transcript as the only record of a completed stage.
- carry a full mutable aggregate across a long-running workflow boundary.