Skip to content

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 group related operations together, making it easy to trace a user action through multiple services.

alias Chronicle.CorrelationId
# Generate a new random UUID-based correlation ID
correlation_id = CorrelationId.create()
# Parse a string or UUID as a correlation ID
correlation_id = CorrelationId.parse("92a130f7-16e2-44f7-a8e3-79e76f5df3e1")

Set a correlation ID for your process, and all subsequent append calls will include it:

alias Chronicle.CorrelationId
# Set for the current process
correlation_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 ID
current = Chronicle.current_correlation_id()
# Clear it when done
Chronicle.clear_correlation_id()

For a single append, override the process-scoped correlation ID:

:ok = Chronicle.append("account-42", event,
correlation_id: "92a130f7-16e2-44f7-a8e3-79e76f5df3e1"
)

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:

FieldDescription
subjectStable unique identifier (e.g. a user UUID from your identity provider)
nameHuman-readable display name
user_nameLogin / username
alias Chronicle.Identity
# Create with subject, display name, and username
identity = Identity.new("user-42", "Alice Cooper", "alice")

Chronicle also provides three sentinel identities for well-known cases:

Identity.system() # automated processes or background workers
Identity.not_set() # identity was not provided (default when none is set)
Identity.unknown() # identity information could not be determined

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 identity
current = Chronicle.current_identity()
# Clear it when done
Chronicle.clear_identity()

For a single append, override the process-scoped identity:

identity = Identity.new("system", "Batch Processor", "batch")
:ok = Chronicle.append("account-42", event,
identity: 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 request
Chronicle.set_identity(alice)
:ok = Chronicle.append(request_id, %LeaveApproved{approved_by: "u-alice"})
# Bob then archives it
Chronicle.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.

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 request
human = Identity.new("u-alice", "Alice Smith", "alice.smith")
# The system process acting on her behalf
system_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 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.

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 executing
CausationManager.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 operation
CausationManager.clear()

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 context
CausationManager.define_root(%{request_id: "req-1", source: "my-api"})
# Command step
CausationManager.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})

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)

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
end
end

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 spawning
correlation_id = Chronicle.current_correlation_id()
identity = Chronicle.current_identity()
spawn(fn ->
Chronicle.set_correlation_id(correlation_id)
Chronicle.set_identity(identity)
Chronicle.append(...)
end)
  • Chronicle.CorrelationId — correlation ID value type and parsing
  • Chronicle.Correlation.CorrelationIdManager — process-scoped correlation handling
  • Chronicle.Identity — identity value type, sentinel values, and on_behalf_of chains
  • Chronicle.Identity.IdentityProvider — process-scoped identity handling
  • Chronicle.Auditing.CausationManager — process-scoped causation chain building
  • Chronicle.Auditing.CausationEntry — individual causation chain steps
  • Chronicle.Auditing.CausationType — built-in causation entry type constants