How to Emit Job-Lifecycle Events
Mapping a job queue onto DCP task events means treating each job as one stable task entity for its entire life, and reporting lease, retry, completion, and failure as delta events on that same entity_id. DCP’s entity_type set is closed, but verbs are open tokens, so a job broker needs no spec change to describe its lifecycle — only a considered mapping plus a vendor extension for anything rail-specific. This guide walks through that pattern, ratified between AgentsWeaver (a job broker) and Tokonomix (a consumer) after four rounds of independent review plus counterparty code-verified corrections — the first external agreement to adopt DCP for a production event rail, reusable by any queue or broker.
The mapping
Section titled “The mapping”| Broker event | DCP message_type | Payload | Note |
|---|---|---|---|
| job created | task.created | entity snapshot, status: "open" | universal verb |
| job leased | task.leased | delta: status open→leased, responsible_party→worker | open verb; carries x-agentsweaver.attempt |
| job completed | task.completed | delta: status leased→done; settlement in x-agentsweaver | the verb stays completed; done is the AW status token; carries attempt |
| job failed | task.failed | delta: status→failed; failure_class/attempt in extension | generic pattern for rails with an explicit fail path; AgentsWeaver does not emit it today |
| job requeued | task.requeued | delta: status leased→open or quarantined/done/cancelled→open, responsible_party→null | same entity_id; carries attempt |
| job cancelled | task.cancelled | delta: status→cancelled | before first lease, no attempt |
| job quarantined | task.quarantined | delta→quarantined + refs: [{rel: "caused_by", ref: "<finding>"}] | carries attempt |
| resubmission as a new job | task.created + refs: [{rel: "derived_from", ref: "<original task_id>"}] | fresh entity_id, entity snapshot | new broker row |
| job heartbeat | (no DCP event) | — | see below |
| job delivered | (no DCP event) | — | see below |
heartbeat and delivered are not DCP events
Section titled “heartbeat and delivered are not DCP events”Both are excluded on the same single-responsibility grounds: a heartbeat or lease-renewal is a liveness/delivery signal, not a change in project-coordination state. SPEC §1.2 excludes delivery guarantees and retry/ack semantics from DCP’s scope, and §4.2 assigns freshness to the transport layer, not the protocol; delivered is rail-internal delivery semantics for the same reason. Both already live where they belong — on the broker’s own tables (job_broker.heartbeat, lease_expires_at). A DCP event per heartbeat would just be noise in a coordination log; a periodic task.snapshot is the closest fit for an audit trail, and even that is a “should not” by default.
entity_id is stable across the whole lifecycle
Section titled “entity_id is stable across the whole lifecycle”The load-bearing decision in this treaty is that entity_id does not change across retries. Early drafts minted a fresh entity_id per attempt; ground-truth research into AgentsWeaver’s broker code showed a requeue is actually UPDATE aw_jobs SET status='open' … on the same row — the fresh-id approach invented an entity that doesn’t exist on the rail, contradicting the SPEC’s own rule that a task’s ID does not change when its status changes. The final design:
entity_id=task_awjob_<job-id>, opaque and producer-asserted, stable for the job’s entire life.- A requeue is
task.requeuedon the same entity, withresponsible_partyreset tonull. It has two sources: an expired lease (statusleased→open,requeue_reason: lease_expired) or an operator retry — the broker’sretry_jobresets a quarantined, done, or cancelled row back to open in place (requeue_reason: operator_retry). Either way it is a retry, not a regression — upsert logic must not treat a later state as stale just because it revisits a status the entity already passed through. refs: [{rel: "derived_from", ref: "…"}]is reserved for genuinely new jobs, never for a retry on the same row.
The decision rule is mechanical: same broker row → task.requeued; new row → task.created + derived_from (a star, never a chain). Both paths are open from quarantined: an operator retry reuses the row (requeued), a resubmission creates a new row with a fresh entity_id (derived_from). See job.requeued.json and job.resubmitted-derivation.json below.
attempt: the normative monotonic disambiguator
Section titled “attempt: the normative monotonic disambiguator”A stable entity_id makes the state machine cyclic (…leased→open→leased… can repeat every retry), and DCP gives no ordering guarantee on delivery — a reordered or replayed stream could leave a consumer’s derived state non-deterministic, unsure whether a given leased event is attempt 2 or attempt 4.
The fix is x-agentsweaver.attempt: a 1-based, monotonically increasing integer that every event after the first lease carries. It is an emitter-maintained event counter, explicitly not a pass-through of the broker’s claim_attempts column — that column resets on operator retry and is therefore not monotonic; the DCP-facing counter must be. Consumers order retries by attempt, never by (untrusted) timestamps — binding, not advisory, since the x-agentsweaver vocabulary is normative for AgentsWeaver-emitted events. task.cancelled events occurring before any lease carry no attempt, since none exists yet to disambiguate.
correlation_id: a star to the initiating message
Section titled “correlation_id: a star to the initiating message”Every event in a job’s life — including the failure branch (cancelled, quarantined, and failed on rails that emit it) — carries correlation_id set to the message_id of that job’s original task.created message. A link-to-previous-message design breaks the moment one message is dropped (a real risk under §1.2’s no-delivery-guarantee stance); anchoring to a fixed origin survives it. The three identifiers now have distinct jobs: entity_id is the stable lifecycle key, correlation_id the star back to the job’s origin, and derived_from the star between separate jobs (original → resubmission).
Rail mechanics stay in the extension, and stay descriptive
Section titled “Rail mechanics stay in the extension, and stay descriptive”Everything specific to how AgentsWeaver’s broker runs — lease_expires_at, reservation_id, job_kind, settlement fields — lives under x-agentsweaver in extensions, never as top-level event fields. This isn’t style: DCP carries no trust, so nothing in the stream can be concurrency or settlement authority. A fence_token was proposed and then removed from the stream entirely during review — the broker database stays the sole source of truth for lease state, and a consumer using a stream-carried fence token for locking would be trusting data DCP disclaims. lease_expires_at stays, purely as an informative time hint, never a lock.
Settlement follows the same rule: the default payload is an opaque, non-enumerable receipt_ref plus a settle_state token (pending/settled/failed/reversed) — a claim, not a payment authority, and a consumer must not finalize a financial action off a DCP event without independent confirmation via the broker or receipt channel. Amounts are opt-in and travel a transport-restricted, bilateral channel, never the shared stream — see job.completed.opt-in-costs.json, which marks itself with cost_disclosure and a disclosure_note warning any stream subscriber can still see the field; the restriction is enforced by the transport, not the marker. The same logic extends to producer confidentiality generally: no secrets, credentials, or personal data in free-text fields like title, description, or extension values — assume every event is visible to every consumer in scope.
Consuming this stream
Section titled “Consuming this stream”If you consume AgentsWeaver job events (or design a consumer for any similarly-shaped rail), the general consumer guidance applies, plus these job-specific rules:
- Dedupe on
event_id, notmessage_id.message_idis for audit and correlation;event_ididentifies the event itself. - No ordering guarantee. A
task.leasedevent can arrive before its correspondingtask.created. Use upsert semantics, not an append-only assumption, and reconstruct state fromdelta.from/delta.to, never from arrival order. - Order retries by
attempt, never by timestamp. Timestamps are untrusted composition-time hints;attemptis the normative disambiguator for this namespace. - Tolerate unknown verbs and status values. New open tokens are additive, not breaking; a consumer that hard-fails on an unrecognized verb or status has over-fit to today’s vocabulary.
entity_idis the stable lifecycle key. Group and aggregate by it; do not attempt to parse structure out of it.
The x-agentsweaver vocabulary
Section titled “The x-agentsweaver vocabulary”These fields are documented and normative for events AgentsWeaver itself emits — a consumer may rely on the documented keys and values when reading this specific namespace, even though extension content is not DCP-normative in general. AgentsWeaver’s status vocabulary is open · leased · done · cancelled · quarantined — note that the completion event carries a delta to done while the verb stays task.completed.
| field | values | meaning |
|---|---|---|
settle_state | settled · released (provisional — AW intake vocabulary; retry policy for indeterminate settlements still open; AW confirms before first emit) | settlement claim (not authority) |
failure_class | provisional — field does not exist in the broker yet; values confirmed by AW before first emit | failure category |
quarantine_reason | poison_pill · input_integrity (provisional until AW confirms at first emit) | why a job was quarantined |
requeue_reason | lease_expired · operator_retry | why a job was requeued |
attempt | 1-based integer, emitter-maintained | monotonic event counter at the moment of the event |
These are open tokens: new values are additive, not a breaking change, and a consumer should tolerate values it doesn’t yet recognize.
Map your own rail: a 5-step start
Section titled “Map your own rail: a 5-step start”The design-review process that ratified this treaty converged on a short checklist for anyone mapping a different queue, broker, or job system onto DCP:
- Choose your entity. Jobs and similar units of work almost always map to
task— resist inventing a new entity type; the eight-entity set is closed by design. - Choose or mint your verbs. Reuse a controlled verb (
created,completed,failed,cancelled) where the meaning genuinely matches; mint a new open verb (likeleased,requeued) where your domain has a real distinction the controlled vocabulary doesn’t capture — see the verb reference for what’s controlled versus open. - Decide snapshot versus delta per event. A creation event typically ships a full
entitysnapshot; every subsequent transition ships adeltadescribing only what changed. - Own an
x-<vendor>namespace, and document its values. Put rail-specific mechanics there, keep it descriptive rather than authoritative, and write down what each field means — that documentation is what lets you later declare parts of it normative for your own emitted events, as AgentsWeaver did. - Validate, and prove both directions. Run your accept cases through the validator, but also build reject fixtures — malformed payloads your schema should refuse. This treaty shipped both, all in the conformance corpus.
Worked examples
Section titled “Worked examples”Three complete, real events from the ratified example set. job.leased.json — the first lease, carrying attempt: 1:
{ "dcp_version": "1.0", "message_id": "msg_aw_01JZK0002", "message_type": "task.leased", "issued_at": "2026-07-03T06:21:00Z", "correlation_id": "msg_aw_01JZK0001", "body": { "event_id": "evt_aw_01JZK0002", "entity_type": "task", "verb": "leased", "entity_id": "task_awjob_7f3a9c", "project_id": "project_aw_tokonomix_rail", "attributed_to": "worker_tokonomix_w17", "delta": { "status": { "from": "open", "to": "leased" }, "responsible_party": { "to": "worker_tokonomix_w17" } }, "extensions": { "x-agentsweaver": { "lease_expires_at": "2026-07-03T06:36:00Z", "note": "descriptive only; the broker DB is authoritative for lease state", "attempt": 1 } }, "occurred_at": "2026-07-03T06:20:58Z" }}job.requeued.json — same entity_id as its original job, status reverts to open after a lease expiry, attempt increments:
{ "dcp_version": "1.0", "message_id": "msg_aw_01JZK0007", "message_type": "task.requeued", "issued_at": "2026-07-03T06:34:00Z", "correlation_id": "msg_aw_01JZK0000", "body": { "event_id": "evt_aw_01JZK0007", "entity_type": "task", "verb": "requeued", "entity_id": "task_awjob_8b2d1a", "project_id": "project_aw_tokonomix_rail", "occurred_at": "2026-07-03T06:33:57Z", "attributed_to": "aw-job-broker", "delta": { "status": { "from": "leased", "to": "open" }, "responsible_party": { "to": null } }, "extensions": { "x-agentsweaver": { "attempt": 2, "requeue_reason": "lease_expired" } } }}job.resubmitted-derivation.json — a genuinely new job (fresh entity_id) that stars back to the quarantined original via derived_from:
{ "dcp_version": "1.0", "message_id": "msg_aw_01JZK0008", "message_type": "task.created", "issued_at": "2026-07-03T06:34:00Z", "body": { "event_id": "evt_aw_01JZK0008", "entity_type": "task", "verb": "created", "entity_id": "task_awjob_c91f04", "project_id": "project_aw_tokonomix_rail", "attributed_to": "aw-job-broker", "entity": { "id": "task_awjob_c91f04", "project_id": "project_aw_tokonomix_rail", "title": "Resubmission of quarantined job: summarize corpus batch 41", "status": "open" }, "refs": [ { "rel": "derived_from", "ref": "task_awjob_8b2d1a" } ], "occurred_at": "2026-07-03T06:33:57Z", "extensions": { "x-agentsweaver": { "resubmission_of": "task_awjob_8b2d1a" } } }}Where the rest lives
Section titled “Where the rest lives”All nine worked examples for this treaty — job.created.json through job.quarantined.json, including the opt-in-costs variant — sit in examples/v1/ alongside four reject fixtures in the conformance corpus, and every one is validator-verified. x-agentsweaver is the first registered external extension namespace: see Extensions for how namespace registration works and what “descriptive, not authoritative” means more generally. For the rule that makes open verbs and open status tokens safe to build on, see Status Is a Vocabulary. For the base mechanics this pattern builds on, start with How to Emit a DCP Message, and to see every event type side by side, browse the examples index.
By InterIP Networks · Last updated 2026-07-03.