Context Management: Correlation, Identity, and Causation
Chronicle Elixir provides process-scoped context management for tracking correlation, identity, and causation information as you append events. See Correlation, identity, and causation for what these three mean and why Chronicle tracks them. This page covers the Elixir-specific API in depth.
All three are process-scoped: set them once at the start of a request or operation and every subsequent append call in the same process will include them automatically. You can always override on a per-call basis.
Correlation IDs
Section titled “Correlation IDs”Correlation IDs group related operations together, making it easy to trace a user action through multiple services.
Creating Correlation IDs
Section titled “Creating Correlation IDs”alias Chronicle.CorrelationId
# Generate a new random UUID-based correlation IDcorrelation_id = CorrelationId.create()
# Parse a string or UUID as a correlation IDcorrelation_id = CorrelationId.parse("92a130f7-16e2-44f7-a8e3-79e76f5df3e1")Process-Scoped Correlation
Section titled “Process-Scoped Correlation”Set a correlation ID for your process, and all subsequent append calls will include it:
alias Chronicle.CorrelationId
# Set for the current processcorrelation_id = CorrelationId.create()Chronicle.set_correlation_id(correlation_id)
# All appends now use this correlation ID:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{...}):ok = Chronicle.append("account-42", %MyApp.Events.FundsDeposited{...})
# Get the current correlation IDcurrent = Chronicle.current_correlation_id()
# Clear it when doneChronicle.clear_correlation_id()One-Off Correlation Override
Section titled “One-Off Correlation Override”For a single append, override the process-scoped correlation ID:
:ok = Chronicle.append("account-42", event, correlation_id: "92a130f7-16e2-44f7-a8e3-79e76f5df3e1")Identity
Section titled “Identity”Identity answers the question: who caused this state change? It is stored with every event appended to the Chronicle event log, giving you a permanent, queryable record of authorship. This is essential for compliance, debugging, and building audit UIs that show “last changed by”.
An identity has three fields:
| Field | Description |
|---|---|
subject | Stable unique identifier (e.g. a user UUID from your identity provider) |
name | Human-readable display name |
user_name | Login / username |
Creating Identity
Section titled “Creating Identity”alias Chronicle.Identity
# Create with subject, display name, and usernameidentity = Identity.new("user-42", "Alice Cooper", "alice")Chronicle also provides three sentinel identities for well-known cases:
Identity.system() # automated processes or background workersIdentity.not_set() # identity was not provided (default when none is set)Identity.unknown() # identity information could not be determinedProcess-Scoped Identity
Section titled “Process-Scoped Identity”Set identity for your process, and all subsequent appends will include it automatically:
alias Chronicle.Identity
identity = Identity.new("user-42", "Alice Cooper", "alice")Chronicle.set_identity(identity)
# All appends now record Alice as the causer:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{...}):ok = Chronicle.append("account-42", %MyApp.Events.FundsDeposited{...})
# Get the current identitycurrent = Chronicle.current_identity()
# Clear it when doneChronicle.clear_identity()One-Off Identity Override
Section titled “One-Off Identity Override”For a single append, override the process-scoped identity:
identity = Identity.new("system", "Batch Processor", "batch")
:ok = Chronicle.append("account-42", event, identity: identity)Switching Identity
Section titled “Switching Identity”In applications where a single process serves multiple users (e.g. a CLI tool, a test harness, or an admin console), you can switch the process-scoped identity between operations. Chronicle records the identity at the moment of each append, so different events in the same process can record different causers:
alice = Identity.new("u-alice", "Alice Smith", "alice.smith")bob = Identity.new("u-bob", "Bob Jones", "bob.jones")
# Alice approves a leave requestChronicle.set_identity(alice):ok = Chronicle.append(request_id, %LeaveApproved{approved_by: "u-alice"})
# Bob then archives itChronicle.set_identity(bob):ok = Chronicle.append(request_id, %RequestArchived{})The event log now shows Alice caused the approval and Bob caused the archival, even though both happened in the same process.
On-Behalf-Of / Delegation Chains
Section titled “On-Behalf-Of / Delegation Chains”When a system actor performs an action on behalf of a human user, you can express this as a delegation chain. The on_behalf_of field links the acting identity to the originating identity:
alias Chronicle.Identity
# The human who initiated the requesthuman = Identity.new("u-alice", "Alice Smith", "alice.smith")
# The system process acting on her behalfsystem_acting = Identity.new( "s-workflow", "Workflow Engine", "workflow-engine", human # on_behalf_of)
Chronicle.set_identity(system_acting):ok = Chronicle.append("order-99", %OrderFulfilled{})Chronicle records the full chain: “Workflow Engine (on behalf of Alice Smith)”. Use Identity.without_duplicates/1 if you need to deduplicate subjects in a long chain.
Causation Chains
Section titled “Causation Chains”Causation tracks the sequence of operations that led to an event. Each entry in the chain represents one step — a root action (e.g. an incoming HTTP request), followed by the commands or messages that triggered subsequent state changes. This chain is stored with every event, giving you a navigable audit trail you can replay forward from any point.
Building Causation Chains
Section titled “Building Causation Chains”alias Chronicle.Auditing.CausationManager
# Define the root cause (the initial action that started the chain)CausationManager.define_root(%{application: "banking-api", version: "1.0"})
# Add the command that this request is executingCausationManager.add("Banking.Commands.OpenAccount", %{account_id: "account-42"})
# Append events — each one is stored with the full causation chain:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{...}):ok = Chronicle.append("account-42", %MyApp.Events.FundsDeposited{...})
# Clear for the next operationCausationManager.clear()Causation Entries
Section titled “Causation Entries”Each entry in the chain is a Chronicle.Auditing.CausationEntry with a type and a map of properties. The type can be any string that identifies the operation — typically a fully-qualified command or message name:
alias Chronicle.Auditing.CausationManager
# Root entry with metadata about the originating contextCausationManager.define_root(%{request_id: "req-1", source: "my-api"})
# Command stepCausationManager.add("MyApp.Commands.PlaceOrder", %{order_id: "order-99"})
# A second command triggered by the first (translation / saga step)CausationManager.add("MyApp.Commands.ReserveInventory", %{sku: "ABC123", qty: 2})One-Off Causation Override
Section titled “One-Off Causation Override”For a single append, pass a causation list directly:
alias Chronicle.Auditing.CausationEntry
causation = [ CausationEntry.new("MyApp.Commands.InitialRequest", %{request_id: "req-1"}), CausationEntry.new("MyApp.Commands.ProcessRequest", %{item_id: "item-1"})]
:ok = Chronicle.append("resource-42", event, causation: causation)Typical Usage Pattern
Section titled “Typical Usage Pattern”Set up correlation, identity, and causation at the boundary of each request or operation. All appends inside that boundary inherit the context automatically.
defmodule MyApp.Web.AccountController do alias Chronicle.{CorrelationId, Identity, Auditing.CausationManager}
def open_account(conn, params) do # --- Set up request context --- Chronicle.set_correlation_id(CorrelationId.create()) Chronicle.set_identity(Identity.new( conn.assigns[:user_id], conn.assigns[:user_display_name], conn.assigns[:user_name] ))
CausationManager.clear() CausationManager.define_root(%{request_id: conn.assigns[:request_id]}) CausationManager.add("MyApp.Commands.OpenAccount", %{account_id: params["account_id"]})
# --- Execute the operation --- case Chronicle.append(params["account_id"], %MyApp.Events.AccountOpened{...}) do :ok -> # --- Clean up --- Chronicle.clear_correlation_id() Chronicle.clear_identity() CausationManager.clear() send_resp(conn, 201, "Account created")
{:error, reason} -> send_resp(conn, 400, inspect(reason)) end endendAsync/Spawn Considerations
Section titled “Async/Spawn Considerations”Context is process-scoped using Elixir’s process dictionary. Spawned processes do not inherit context from their parent — capture and pass it explicitly:
# Capture before spawningcorrelation_id = Chronicle.current_correlation_id()identity = Chronicle.current_identity()
spawn(fn -> Chronicle.set_correlation_id(correlation_id) Chronicle.set_identity(identity) Chronicle.append(...)end)See Also
Section titled “See Also”Chronicle.CorrelationId— correlation ID value type and parsingChronicle.Correlation.CorrelationIdManager— process-scoped correlation handlingChronicle.Identity— identity value type, sentinel values, andon_behalf_ofchainsChronicle.Identity.IdentityProvider— process-scoped identity handlingChronicle.Auditing.CausationManager— process-scoped causation chain buildingChronicle.Auditing.CausationEntry— individual causation chain stepsChronicle.Auditing.CausationType— built-in causation entry type constants