Skip to content

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.

Broker eventDCP message_typePayloadNote
job createdtask.createdentity snapshot, status: "open"universal verb
job leasedtask.leaseddelta: status open→leased, responsible_party→workeropen verb; carries x-agentsweaver.attempt
job completedtask.completeddelta: status leased→done; settlement in x-agentsweaverthe verb stays completed; done is the AW status token; carries attempt
job failedtask.faileddelta: status→failed; failure_class/attempt in extensiongeneric pattern for rails with an explicit fail path; AgentsWeaver does not emit it today
job requeuedtask.requeueddelta: status leased→open or quarantined/done/cancelled→open, responsible_party→nullsame entity_id; carries attempt
job cancelledtask.cancelleddelta: status→cancelledbefore first lease, no attempt
job quarantinedtask.quarantineddelta→quarantined + refs: [{rel: "caused_by", ref: "<finding>"}]carries attempt
resubmission as a new jobtask.created + refs: [{rel: "derived_from", ref: "<original task_id>"}]fresh entity_id, entity snapshotnew 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.requeued on the same entity, with responsible_party reset to null. It has two sources: an expired lease (status leased→open, requeue_reason: lease_expired) or an operator retry — the broker’s retry_job resets 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.

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, not message_id. message_id is for audit and correlation; event_id identifies the event itself.
  • No ordering guarantee. A task.leased event can arrive before its corresponding task.created. Use upsert semantics, not an append-only assumption, and reconstruct state from delta.from/delta.to, never from arrival order.
  • Order retries by attempt, never by timestamp. Timestamps are untrusted composition-time hints; attempt is 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_id is the stable lifecycle key. Group and aggregate by it; do not attempt to parse structure out of it.

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.

fieldvaluesmeaning
settle_statesettled · released (provisional — AW intake vocabulary; retry policy for indeterminate settlements still open; AW confirms before first emit)settlement claim (not authority)
failure_classprovisional — field does not exist in the broker yet; values confirmed by AW before first emitfailure category
quarantine_reasonpoison_pill · input_integrity (provisional until AW confirms at first emit)why a job was quarantined
requeue_reasonlease_expired · operator_retrywhy a job was requeued
attempt1-based integer, emitter-maintainedmonotonic 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.

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:

  1. 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.
  2. Choose or mint your verbs. Reuse a controlled verb (created, completed, failed, cancelled) where the meaning genuinely matches; mint a new open verb (like leased, requeued) where your domain has a real distinction the controlled vocabulary doesn’t capture — see the verb reference for what’s controlled versus open.
  3. Decide snapshot versus delta per event. A creation event typically ships a full entity snapshot; every subsequent transition ships a delta describing only what changed.
  4. 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.
  5. 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.

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"
}
}
}
}

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.