Correlation & Causation
Every committed event links back to the intent that produced it. That link is causation — which intent caused this event. When one user action fans out across many intents and entities, you also need correlation — a shared ID that ties the whole chain together.
Causation: which intent caused this?
Causation answers: which intent produced this event?
The runtime records the intent ID on every ledger commit. If you see an unexpected ARTIST_FOLLOWED event, look up that intent ID to find the original FOLLOW_ARTIST submission — input payload, outcome, and timestamp.
In the control plane:
- Intent status — final result for a given intent ID
- Decision Log — how rules evaluated for that intent
- Ledger history — patch and emits at the commit cursor
Causation is always available. You do not declare anything extra in the DSL for a single intent → event path.
Correlation: which user request started the chain?
Correlation answers: which top-level request produced this entire cascade?
One purchase might produce:
TICKET_PURCHASED → INVENTORY_DECREMENTED → CONFIRMATION_EMAIL_QUEUED → REVENUE_RECORDEDEach step may be a different intent on a different entity. They share a common origin — the user’s purchase request — but the runtime does not automatically assign a cross-chain correlation ID. You pass one explicitly when you need end-to-end traceability.
| Concept | Scope | Built-in? |
|---|---|---|
| Causation | One intent → its events | Yes — intent ID on every commit |
| Correlation | Many intents → one user request | No — add to payloads when needed |
Passing correlation IDs in payloads
For multi-hop flows, declare correlation_id and optionally caused_by on your events:
events:
TICKET_PURCHASED:
state: ticket
entity_expr: event.ticket_id
payload:
ticket_id: string
show_id: string
user_id: string
price_cents: int
correlation_id: string # top-level request ID
caused_by: string # intent that triggered this
CONFIRMATION_EMAIL_QUEUED:
state: notification
entity_expr: event.notification_id
payload:
notification_id: string
user_id: string
ticket_id: string
correlation_id: string # propagated from parent
caused_by: string # intent ID of the purchasePropagate from the submitting intent:
actions:
PURCHASE_TICKET:
state: show
entity_id_expr: intent.show_id
input:
show_id: { type: string, required: true }
user_id: { type: string, required: true }
ticket_id: { type: string, required: true }
correlation_id: { type: string, required: false }
side_effects:
rules:
- name: emit_purchase
when: {}
then:
- op: emit
event_type: TICKET_PURCHASED
payload:
ticket_id: intent.ticket_id
show_id: intent.show_id
user_id: intent.user_id
price_cents: intent.price_cents
correlation_id: intent.correlation_idThe caller supplies correlation_id at the top of the chain (HTTP request ID, client-generated UUID, etc.). Each downstream intent and event copies it forward.
When side_effects submit a follow-up intent, set caused_by to the current intent’s ID so the parent-child link is visible in event payloads without manual ledger inspection.
What gets stored
| Stored as | What it is |
|---|---|
| Intent ID | Causation — always on every commit and in intent status |
| Intent type | Action name (e.g. PURCHASE_TICKET) |
| Event type | Domain event name (e.g. TICKET_PURCHASED) |
| Payload | Your declared fields, including optional correlation_id and caused_by |
| Cursor | Position on the entity timeline for this commit |
| Timestamp | When the commit was recorded |
Envelope fields like event type and timestamp are automatic. Correlation across multiple intents is only present if you put it in the payload.
Why it matters
Debugging projection failures. Failure records include event type and payload. A correlation_id in the payload links a bad projection row back to the originating user request.
Tracing fan-out chains. A FOLLOW_ARTIST intent may trigger listeners, side effects, and downstream submits. Without correlation metadata, you follow the chain hop by hop via intent IDs. With a shared correlation_id, filter once in the Decision Log, projections, or logs.
Cross-entity causation. Intent A emits event A, which triggers intent B. The ledger ties event A to intent A; caused_by in event B’s payload makes the A → B relationship explicit in your domain data.
How to trace a chain
By causation (single intent)
- Start from an event or entity state change in the control plane.
- Note the intent ID on that commit.
- Open intent status and the Decision Log for that ID — see input, rule steps, and outcome.
By correlation (multi-intent)
- Ensure your flows declare and propagate
correlation_id. - Filter projection tables or application logs on that value.
- Optionally materialize
correlation_idin a projection for query-service lookups.
See Debugging for control plane forensics.
Best practices
Include correlation_id for complex flows. When one user action spans multiple entity types and intents, propagate a stable string through the entire chain.
Set caused_by on submitted follow-up intents. When side effects submit another intent, record which intent triggered it.
Project correlation for read-side tracing. Derive correlation_id into a projection if dashboards or support tools need to search by request ID.
Don’t over-engineer simple flows. Single-entity actions with one or two events — intent ID alone is enough for debugging.
See also
- Debugging — Decision Log, ledger commits, intent status
- Idempotency — retry-safe intent design
- Workflows —
submitand multi-intent chains - Events — emitting and payload design