Skip to content

Correlation, identity, and causation

Every event Chronicle appends carries more than its own data. Three pieces of context travel alongside it, scoped to the current call and captured automatically on every append:

  • Correlation ID — ties together every event, command, and side effect that belong to the same logical operation (a single HTTP request, a message-bus handler invocation).
  • Identity — records who caused the change: a user, a system, or a service.
  • Causation — records why: the chain of operations, from a root action down to the specific command that triggered this event.

All three are set once at the start of an operation (a request, a background job) and flow into every event appended during it, without threading them through every function call.

using Cratis.Execution;
public class CorrelationIdentityCausationCorrelation(ICorrelationIdAccessor accessor, ICorrelationIdModifier modifier)
{
public CorrelationId GetCurrent() => accessor.Current;
public void SetForRequest() => modifier.Modify(CorrelationId.New());
}

View C# snippet source on GitHub

Kotlin’s correlation manager scopes the id per-thread (ThreadLocal); Elixir and TypeScript scope it per logical call context (Elixir’s process dictionary, TypeScript’s AsyncLocalStorage) — in both cases automatically, without extra code, as long as you don’t spawn new threads/processes/async contexts without carrying the value across explicitly.

using Cratis.Chronicle.Identities;
public class CorrelationIdentityCausationIdentity(IIdentityProvider identityProvider)
{
public void SetForRequest(string subject, string name, string userName) =>
identityProvider.SetCurrentIdentity(new Identity(subject, name, userName));
public Identity GetCurrent() => identityProvider.GetCurrent();
}

View C# snippet source on GitHub

Every client provides the same three sentinel identities for well-known cases — System, NotSet, and Unknown — and supports on-behalf-of delegation chains (an identity that acted for another identity) via a withoutDuplicates()/without_duplicates/1 helper that collapses repeated subjects in a long chain.

The name recorded alongside an identity is a display name, and people’s names change. Renaming updates it in one place for every event that identity has ever caused — the events themselves are untouched, since the name is not part of the fact they record.

using Cratis.Chronicle;
public static class RenamingAnIdentity
{
public static async Task Rename(IEventStore eventStore)
{
await eventStore.Identities.Rename("subject-42", "Jane Austen");
}
}

View C# snippet source on GitHub

The subject is the identity’s stable key. It is what events are recorded against, so it does not change.

Metadata reads resolve caused-by identities directly from storage rather than a silo’s identity cache. A rename is visible on the next metadata read, including for events appended before the rename. Each chain link has a Resolution status:

StatusMeaningName
ResolvedThe identity exists and has a display name.The current name.
NameUnavailableThe identity exists but its display name is empty.null.
MissingThe stored identity identifier no longer resolves.null.
NotSetNo caused-by identity was set.null.
SystemThe subject is Chronicle’s well-known system identity.null.

Chronicle never substitutes the subject for an unavailable name. OnBehalfOf retains the chain order, resolving each link separately. The head’s InitiatorType is Agent when a resolved identity acts on behalf of another identity, User for a resolved identity without that chain, System for Chronicle, and Unknown for a missing or unset head. An unavailable display name does not change whether that identity acts as an agent.

Application-defined service identities are not automatically classified as systems. Recognize them by their subjects in your application, rather than relying on display names or the User classification.

PII erasure removes encryption keys for protected event and read-model data; it does not erase identity display names. To make a name unavailable on metadata reads, rename that identity to an empty name. Its subject and username remain available. Your application owns authorization and must enforce it before calling the metadata API.

using Cratis.Chronicle.Auditing;
public class CorrelationIdentityCausationCausation(ICausationManager causationManager)
{
public void RecordPlaceOrder(string orderId) =>
causationManager.Add(
"MyApp.Commands.PlaceOrder",
new Dictionary<string, string> { ["orderId"] = orderId });
}

View C# snippet source on GitHub

TypeScript also has a real causationManager.add(type, properties) — the client-specific pages linked below cover it, along with more Kotlin/Elixir/TypeScript causation detail than fits here (defining/redefining the root cause, reading the full chain, framework-integration examples for scoping context per request).

Each client’s own documentation covers the full API in depth — provider interfaces, framework-integration examples (Express middleware, and similar), and thread/process/async-context caveats specific to that runtime:

  • .NET client — Cratis.Execution.ICorrelationIdAccessor/ICorrelationIdModifier, Cratis.Chronicle.Identities.IIdentityProvider, Cratis.Chronicle.Auditing.ICausationManager
  • Kotlin client — io.cratis.chronicle.correlation, io.cratis.chronicle.identity, io.cratis.chronicle.auditing
  • Elixir client — Chronicle.CorrelationId, Chronicle.Identity, Chronicle.Auditing.CausationManager
  • TypeScript client — correlationIdManager, identityProvider, causationManager