Causation Metadata
View SourceThis guide explains how to record event lineage using the causation_id and
correlation_id metadata fields built into every reckon-gater event.
Overview
Every event record exposes two lineage fields:
| Field | Purpose | Example |
|---|---|---|
causation_id | ID of the event or command that directly caused this event | "evt-001" |
correlation_id | A shared ID grouping all events in one business process | "order-12345" |
These fields are writer-set metadata — the store records them as-is alongside the event payload. There is no built-in query API that indexes by them. If you need to query "all events caused by X" or "all events in order 12345", build a read-model projection that indexes by these fields as events flow through subscriptions.
Setting Causation Metadata
-module(order_handler).
handle_command(Command, _State) ->
CommandId = maps:get(id, Command),
CorrelationId = maps:get(correlation_id, Command, CommandId),
Event = #{
event_type => <<"order_placed_v1">>,
data => #{order_id => <<"order-123">>, items => [...]},
metadata => #{
causation_id => CommandId, %% this command caused this event
correlation_id => CorrelationId %% business-process grouping ID
}
},
reckon_gater_api:append_events(my_store, StreamId, [Event]).Propagating Causation in Event Handlers
When an event handler or process manager reacts to an event and produces new events, it must forward both IDs to maintain the chain:
handle_event(#{id := EventId, metadata := Meta} = _OrderPlaced, _State) ->
CorrelationId = maps:get(correlation_id, Meta),
NewEvent = #{
event_type => <<"payment_requested_v1">>,
data => ...,
metadata => #{
causation_id => EventId, %% the upstream event caused this
correlation_id => CorrelationId %% same business process
}
},
reckon_gater_api:append_events(my_store, PaymentStream, [NewEvent]).Querying by Causation (Read-Model Pattern)
There is no built-in API to query events by causation_id or correlation_id.
Lineage queries are a read-model concern. The recommended pattern:
- Subscribe to the store's event stream.
- In the projection, index
causation_idandcorrelation_idinto a local SQLite (or any) table as events arrive. - Query your projection for all events caused by a given ID or belonging to a given correlation group.
%% Projection example — insert into your read-model as events flow in
project_event(#{id := Id, metadata := Meta, event_type := Type}) ->
CausationId = maps:get(causation_id, Meta, undefined),
CorrelationId = maps:get(correlation_id, Meta, undefined),
db:exec("INSERT INTO event_lineage(id, type, causation_id, correlation_id)
VALUES (?, ?, ?, ?)", [Id, Type, CausationId, CorrelationId]).
%% Then query your projection directly
get_effects(CausingEventId) ->
db:query("SELECT * FROM event_lineage WHERE causation_id = ?",
[CausingEventId]).
get_correlated(CorrelationId) ->
db:query("SELECT * FROM event_lineage WHERE correlation_id = ?",
[CorrelationId]).Event Metadata Schema
Recommended metadata structure for events that participate in causation chains:
create_event_metadata(CausingId, CorrelationId, ActorId) ->
#{
causation_id => CausingId,
correlation_id => CorrelationId,
actor_id => ActorId,
timestamp => erlang:system_time(microsecond),
source => atom_to_binary(node())
}.Common Pitfalls
Missing causation_id in event handlers
%% BAD: causation chain is broken
handle_event(_OrderPlaced, _State) ->
NewEvent = #{event_type => <<"payment_requested_v1">>, data => ...},
reckon_gater_api:append_events(Store, Stream, [NewEvent]).
%% GOOD: chain is preserved
handle_event(#{id := EventId, metadata := Meta}, _State) ->
NewEvent = #{
event_type => <<"payment_requested_v1">>,
data => ...,
metadata => #{
causation_id => EventId,
correlation_id => maps:get(correlation_id, Meta)
}
},
reckon_gater_api:append_events(Store, Stream, [NewEvent]).Using event ID as correlation ID
%% BAD: correlation changes with each event
metadata => #{causation_id => EventId, correlation_id => EventId}
%% GOOD: correlation is a stable business-process ID
metadata => #{causation_id => EventId, correlation_id => OrderId}Best Practices
- Always set causation_id — every non-root event should reference its cause.
- Use a stable business ID as correlation_id — the order ID, saga ID, or request ID works well; never a generated event ID.
- Build lineage projections early — once events are in the store without causation metadata, retroactive lineage is impossible.
- Include in error logs — log both IDs alongside any error for fast triage.
Related Guides
- Event Sourcing — core concepts
- Subscriptions — how to build read-model projections
- CQRS — command/query separation patterns