Reference

Implementation patterns and code examples for each CHAIN maturity level. SQL schemas, Rust patterns, and practical guidance for building reflective systems.

This chapter provides implementation patterns and code examples for each CHAIN maturity level. Use it as a reference when building systems that need specific levels of reflectivity.

Tip · Inspiration, not prescription

There are dozens of ways to achieve each maturity level. The examples here are illustrative, not definitive. Your stack, your domain, and your constraints will shape your implementation. Use these patterns as a starting point for discussion and design, not as a spec to copy verbatim.


Causality Implementation Patterns

Causality is about linking effects to their causes. At lower maturity levels, you store state with no context. At higher levels, every state change carries metadata that explains what triggered it, enabling you to trace any outcome back to its origin.

C0: Opaque Causality

At C0, you’re storing current state with no causal context. This is the default for most CRUD applications. You can answer “what is the balance?” but not “how did it get there?”

-- Only the current state is preserved, with no causal metadata
SELECT balance FROM accounts WHERE account_id = 42;

No trail or chain exists. If something goes wrong, you’re left reconstructing history from logs, backups, or tribal knowledge.

C1: Single-Writer Journals

At C1, you introduce append-only event journals with monotonic ordering. Each entity has one writer, which eliminates race conditions and makes the sequence of events deterministic. This is the foundation of event sourcing.

The key addition here is monotonic ULIDs: identifiers that encode timestamp and guarantee lexicographic ordering. This means you can always determine which event came first by comparing IDs.

CREATE TABLE user_account_events (
    event_ulid      text PRIMARY KEY,     -- monotonic ULID (lexicographic order)
    user_id         int,
    event_time      timestamp,            -- redundant but handy for queries
    event_type      text,
    data            jsonb
);

INSERT INTO user_account_events (
    event_ulid,               -- e.g. '01HZ7YV7P3TK0A6RJKXY3GZP0H'
    user_id,
    event_time,
    event_type,
    data
) VALUES (
    gen_monotonic_ulid(),     -- client-side helper guarantees monotonicity
    42,
    toTimestamp(now()),
    'FundsDeposited',
    { "amount": 1000 }
);

/* Read-side projection: running balance */
CREATE TABLE balance_by_user (
    user_id  int PRIMARY KEY,
    balance  decimal
);

You could also use Kafka with a single partition per entity, EventStoreDB streams, or any append-only store that guarantees ordering. The schema above is one way to do it in Postgres.

ObzenFlow models this pattern with a Journal trait that enforces the single-writer, append-only contract:

#[async_trait]
pub trait Journal<T: JournalEvent>: Send + Sync {
    /// Get the owner of this journal (single writer)
    fn owner(&self) -> Option<&JournalOwner>;

    /// Append an event to the journal (append-only, no updates)
    async fn append(&self, event: T, parent: Option<&EventEnvelope<T>>)
        -> Result<EventEnvelope<T>, JournalError>;

    /// Create a reader for sequential access
    async fn reader(&self) -> Result<Box<dyn JournalReader<T>>, JournalError>;
}

/// Cursor-based reader for efficient sequential access
pub trait JournalReader<T: JournalEvent>: Send + Sync {
    /// Read the next event (O(1) regardless of journal size)
    async fn next(&mut self) -> Result<Option<EventEnvelope<T>>, JournalError>;

    /// Current position for checkpointing
    fn position(&self) -> u64;
}

The JournalReader solves the O(n²) problem that plagues naive journal implementations: instead of re-reading from the beginning each time, it maintains a cursor position and keeps file handles open. This is essential for journals that grow large over time.

C2: Causal Linking

At C2, events carry explicit references to what caused them. The two key fields are:

  • causal_parent_ulid: points to the upstream event or command that triggered this one
  • correlation_ulid: groups related events into a workflow or transaction

This lets you answer “what triggered this event?” by following the causal_parent link.

CREATE TABLE trade_events (
  event_ulid         text PRIMARY KEY,  -- monotonic ULID for ordering & idempotency
  trade_id           int,
  event_time         timestamp,
  event_type         text,
  causal_parent_ulid text,              -- ULID pointing at command or prior event
  correlation_ulid   text,              -- ULID grouping this workflow
  payload            jsonb
);

INSERT INTO trade_events (
  event_ulid, trade_id, event_time, event_type,
  causal_parent_ulid, correlation_ulid, payload
) VALUES (
  gen_monotonic_ulid(),             -- e.g. '01HZXW5J5C0K1QZ8G5WR8QF2KX'
  9001,                             -- trade_id
  toTimestamp(now()),               -- event_time
  'TradeSettled',                   -- event_type
  '01HZXW4M4B9J0A7X2F3VK1H8ZD',     -- causal_parent_ulid (could be a command or prior event)
  '01HZXVQ0T7M2K9P4C6ZD2G1R0X',     -- correlation_ulid (workflow ID)
  {...}                             -- payload
);

Alternative approaches include storing causal metadata in message headers (Kafka), using OpenTelemetry’s span parent/child relationships, or embedding causality in your domain events as first-class fields.

ObzenFlow implements causal linking using Lamport’s happened-before relation with vector clocks. Each event carries a VectorClock that maps writer IDs to sequence numbers, enabling precise causal ordering even across distributed writers:

/// Vector clock: maps writer ID → sequence number
pub struct VectorClock {
    pub clocks: BTreeMap<String, u64>,
}

impl CausalOrderingService {
    /// Check if event a happened before event b
    pub fn happened_before(a: &VectorClock, b: &VectorClock) -> bool {
        // a happened-before b if:
        // 1. For all writers in a: a[w] <= b[w]
        // 2. There exists at least one writer where a[w] < b[w]
        // ...
    }

    /// Check if two events are concurrent (neither happened before the other)
    pub fn are_concurrent(a: &VectorClock, b: &VectorClock) -> bool {
        !Self::happened_before(a, b) && !Self::happened_before(b, a)
    }
}

This approach has an advantage over simple parent ULIDs: it can detect concurrency. Two events are concurrent if neither happened before the other. This distinction matters when merging branches or resolving conflicts in distributed systems.

C3: Graph Navigation

At C3, causality becomes a queryable graph. Instead of just following single links, you can traverse relationships in both directions: “what did this event cause?” and “what caused this event?”

The “graph” here refers to the causal relationship graph of your events, not a graph database. You don’t need Neo4j or Amazon Neptune to achieve C3. The goal is bidirectional traversal of cause-and-effect relationships, which can be achieved with simpler tools.

One approach is a causal edges table that explicitly models parent-child relationships:

-- Events table (any C2-compatible structure)
CREATE TABLE trade_events (
  event_id           uuid PRIMARY KEY,
  event_type         text,
  event_time         timestamp,
  correlation_id     uuid,        -- e.g, workflow scope
  payload            jsonb
);

-- Causal edges table enables graph queries within the same scope
CREATE TABLE causal_edges (
  parent_id     uuid,
  child_id      uuid,
  parent_type   text,   -- 'event' or 'command'
  child_type    text,   -- usually 'event'
  scope         text,   -- optional: e.g, policy or saga like 'portfolio-rebalance'
  PRIMARY KEY (parent_id, child_id)
);

Another approach is materialized views that pre-compute common traversals. This is the direction ObzenFlow takes: rather than forcing you to query raw event journals, materialized views project causal relationships into queryable structures that answer questions like “show me all effects of this command” or “what led to this failure?”

Recursive CTEs in Postgres can also work for ad-hoc graph traversal, though they become expensive at scale.

C4: Enforced Causality with Vector Clocks

At C4, causality is enforced across distributed systems. Events carry vector clocks or hybrid logical clocks (HLCs) that encode their causal dependencies. The system can reject or defer events that arrive before their prerequisites.

-- Events now include distributed causal metadata
CREATE TABLE trade_events (
  event_id             uuid PRIMARY KEY,
  event_type           text,
  originating_service  text,
  event_time           timestamp,
  vector_clock         jsonb,    -- {"OrderSvc": 12, "BrokerSvc": 9}
  hlc                  text,     -- Optional: "2025-05-13T14:07:11.456Z#0001234"
  payload              jsonb
);

A vector clock tracks each service’s view of causality:

{
  "OrderSvc": 12,
  "BrokerSvc": 9,
  "PortfolioSvc": 3
}

This means: “this event knows about OrderSvc event 12, BrokerSvc event 9, and PortfolioSvc event 3.” If an event arrives claiming to be caused by OrderSvc event 15, but you’ve only seen event 12, you know something is out of order.

Warning · Enforcement is an architectural achievement
Frameworks like ObzenFlow can provide vector clock primitives (C2) and materialized views for causal traversal (C3), but enforcement requires system-wide coordination. All services must use consistent writer IDs, propagate vector clocks correctly across boundaries, and agree to reject or buffer events that violate causal order. No single framework can enforce causality across a dozen microservices. That’s an architecture and implementation responsibility.

History Implementation Patterns

History is about preserving state transitions over time. At lower maturity levels, you rely on periodic snapshots with gaps. At higher levels, you have complete, replayable, evolvable records that survive schema changes.

H0: Basic Archival

At H0, you take periodic snapshots but accept data loss between them. This is typical of systems that rely on nightly pg_dump or hourly exports.

-- Three updates before the snapshot
UPDATE accounts SET balance = 1000 WHERE user_id = 42;
UPDATE accounts SET balance = 3000 WHERE user_id = 42;
UPDATE accounts SET balance = 2000 WHERE user_id = 42;
# Snapshot taken here
pg_dump mydb > snapshot_2024_04_17.sql
-- If the WAL after the dump is now lost or corrupted, this update is unrecoverable
UPDATE accounts SET balance = 4000 WHERE user_id = 42;

If something goes wrong between snapshots, you lose that history. H0 is a starting point, not a target.

H2: Deterministic Replay

At H2, you can re-derive state by replaying events. This requires deterministic logic. Given the same events in the same order, you always get the same result.

Projection rebuilds are one form of replay. You discard a read model and recompute it from the event log:

-- Rebuild projection from the raw event log
TRUNCATE account_balance_projection;

INSERT INTO account_balance_projection (account_id, balance, last_sequence)
SELECT
  e.account_id,
  SUM(e.amount)         AS balance,
  MAX(e.sequence)       AS last_sequence
FROM account_events e
GROUP BY e.account_id;

Pipeline replay is another form. ObzenFlow provides this via the -⁠-⁠replay-from CLI flag. When a flow completes, it archives source events to disk. To replay:

# Original run archives events to disk
$ obzenflow run my_flow.rs
FlowApplication complete!
To replay, add: --replay-from ./runs/2025-01-27T14-30-00Z

# Replay processes the same events deterministically
$ obzenflow run my_flow.rs --replay-from ./runs/2025-01-27T14-30-00Z

Currently, replay starts from source stages, which are typically the most expensive part of a flow (external API calls, database queries, file I/O). The ReplayDriver reads archived source events and feeds them through the pipeline, bypassing the original data sources entirely. This enables regression testing, debugging production issues, and verifying that pipeline changes produce expected outputs.

Future versions will support replay from any vector clock position, allowing mid-pipeline resumption.

H3: Evolvable History with Upcasters

At H3, your event schema can evolve without breaking replay. The key technique is upcasting: transforming old event versions into the current schema during replay.

This Rust example shows how to upcast a V1 event to V2 by supplying a default value for the new field:

enum Event {
    OrderFilledV1 { order_id: String, qty: u32, timestamp: DateTime<Utc> },
    OrderFilledV2 { order_id: String, qty: u32, timestamp: DateTime<Utc>, execution_price: f64 },
}

fn upcast(event: Event) -> Event {
    match event {
        Event::OrderFilledV1 { order_id, qty, timestamp } => {
            // supply default execution_price for v1 events
            Event::OrderFilledV2 { order_id, qty, timestamp, execution_price: 0.0 }
        }
        other => other,
    }
}

// Usage in your replay or consumer pipeline
fn handle(raw: Event) {
    let event = upcast(raw);
    match event {
        Event::OrderFilledV2 { order_id, qty, execution_price, .. } => {
            // business logic sees only the latest shape
        }
        _ => {}
    }
}

You could also use schema registries (Confluent, AWS Glue), Avro/Protobuf with schema evolution rules, or versioned event types with explicit migration functions.

H4: Time-Travel Provenance

H4 is rare. Most systems never need this level of rigor, and most teams never build it. But for regulated finance, healthcare, legal discovery, and high-stakes simulation, H4 is the target.

At H4, history becomes a cryptographically verifiable, point-in-time queryable truth machine. Events are immutable, hash-chained, and sealed into segments that can be verified by third parties.

Hash-chained events link each event to its predecessor:

CREATE TABLE events (
  event_id      uuid PRIMARY KEY,
  prev_hash     bytea NOT NULL,           -- hash of previous event
  content_hash  bytea NOT NULL,           -- hash of this event's content
  payload       jsonb NOT NULL
);

-- Verification: recompute hashes and check chain integrity
-- If any event is deleted, modified, or reordered, the chain breaks

Merkle-sealed segments group events into verifiable batches:

Segment: [evt1, evt2, evt3, ..., evt1000]
         ↓
    Merkle Root (32 bytes)
         ↓
    Cryptographic Signature (proves who sealed it, when)

This enables O(log n) verification of any event and detection of deletions or tampering.

Point-in-time reconstruction lets you query state at any causal frontier:

# "Show me the system state as of this vector clock"
query --as-of '{"OrderSvc": 500, "InventorySvc": 320}'

# "What if we replayed from this checkpoint with different rules?"
replay --from-checkpoint checkpoint_01HABC --with-config new_rules.yaml

Few systems reach H4. The infrastructure requirements are substantial: content hashing, Merkle trees, cryptographic signing, checkpoint indexing, and tooling for point-in-time queries. But for systems where “prove this history is authentic” is a regulatory requirement, H4 is non-negotiable.


Agency Implementation Patterns

Agency is about attributing actions to actors. At lower maturity levels, actions happen anonymously. At higher levels, every action is tied to a verified identity with auditable authorization.

Agency operates at two levels:

  • System-level agency: Which component produced this event? Which service, stage, or process?
  • Human-level agency: Which user triggered this action? Via which auth mechanism? With what permissions?

ObzenFlow provides system-level agency automatically via WriterId. Every event knows which stage or system component wrote it:

pub enum WriterId {
    Stage(StageId),   // A pipeline stage produced this event
    System(SystemId), // A system component (e.g., control plane) produced this event
}

Human-level agency (OIDC tokens, policy decisions, workflow approvals) is implemented in your handlers and attached to event payloads. The SQL examples below illustrate the conceptual fields needed at each maturity level. In ObzenFlow, these fields would be part of your wide event payloads in immutable journals rather than relational tables.

A0: Unattributed

At A0, events record what happened but not who did it. This is common in legacy systems where the application trusts that only authorized users can reach certain endpoints.

INSERT INTO account_events (
  event_type,
  amount,
  subject_user_id
) VALUES (
  'FundsWithdrawn',
  1000,
  42
);

The subject_user_id tells you which account was affected, but not who performed the withdrawal. Was it the account holder? An admin? A bug? You can’t tell.

A1: Logged

At A1, you record an actor ID, but it’s not verified. Someone passed principal_actor_id: 42 in the request, but you don’t know if that’s accurate.

INSERT INTO account_events (
  event_type,
  amount,
  subject_user_id,
  principal_actor_id
) VALUES (
  'FundsWithdrawn',
  1000,
  42,   -- subject account
  42    -- principal echoed as subject, but may be spoofed
);

This is better than nothing, but the identity could be spoofed or simply wrong. You have data for debugging, but not for audit.

ObzenFlow operates at A1 out of the box. Every ChainEvent carries system-level attribution automatically:

pub struct ChainEvent {
    pub id: EventId,                      // Unique event identifier
    pub writer_id: WriterId,              // Which stage/service created this (A1)
    pub content: ChainEventContent,       // The actual event payload
    pub causality: CausalityContext,      // Causal linking (parent_ids)
    pub flow_context: FlowContext,        // Flow and stage context
    pub processing_info: ProcessingContext,
    pub intent: Option<IntentContext>,    // Explicit intent (I1+)
    pub correlation_id: Option<CorrelationId>,
    // ...
}

The writer_id field provides automatic system-level attribution: you always know which pipeline stage produced an event. Human-level attribution (A2+) would be part of your event payload, implemented in your handlers.

Frameworks generally don’t provide verified identity, policy decisions, or cryptographic signatures out of the box. Those are application concerns that depend on your auth infrastructure (OIDC, mTLS, policy engines). The framework gives you the event structure; you fill in the human-level agency fields.

A2: Bound

At A2, actions are tied to cryptographically verified identities. The actor’s identity comes from a validated token (OIDC, mTLS, SPIFFE) and is recorded with proof of verification.

INSERT INTO account_events (
  event_type,
  amount,
  subject_user_id,
  principal_actor_id,        -- the verified actor's unique ID
  principal_verified,        -- true iff identity was validated
  identity_provider,         -- e.g. "OIDC", "mTLS", "SPIFFE"
  principal_subject_claim,   -- e.g. the "sub" claim from the JWT
  principal_token_jti        -- unique token identifier for replay/audit
) VALUES (
  'FundsWithdrawn',
  1000,
  42,                        -- subject account
  99,                        -- actual actor (distinct from subject)
  TRUE,                      -- identity was cryptographically verified
  'OIDC',                    -- this came from an OpenID Connect token
  'user-alice@example.com',  -- the token's "sub" claim
  '3f47a8e2-token-id-xyz'    -- JWT ID for audit linkage
);

Now you can prove who performed the action. The token fields let you trace back to the original authentication event if needed.

A3: Authorized with OPA/Rego

At A3, you record not just who acted, but whether they were authorized to act. The policy decision and its inputs are stored alongside the event.

The key fields are:

  • policy_input: The complete context sent to the policy engine. Authorization often depends on more than identity: time of day, IP address, device, resource attributes. Capture all of it.
  • policy_decision: The engine’s verdict (ALLOW/DENY).
  • policy_version: Which version of the policy was in effect. Policies change; you need to know which rules applied at decision time.
INSERT INTO account_events (
  event_type,
  amount,
  subject_user_id,
  principal_id,
  principal_verified,
  policy_input,
  policy_decision,
  policy_version
) VALUES (
  'FundsWithdrawn',
  1000,
  42,
  99,
  TRUE,
  '{
    "event":     {"event_type":"FundsWithdrawn","amount":1000,"subject_user_id":42},
    "principal": {"id":99,"roles":["teller"],"attrs":{"dept":"retail"}},
    "environment":{"ip":"203.0.113.12","time":"2025-05-10T21:00:00-04:00","device_id":"abc123"}
  }'::jsonb,
  'ALLOW',
  'rev-20250510a'
);

A corresponding OPA policy might look like:

package banking.withdraw

default allow = false

allow {
    input.principal.roles[_] == "teller"
    input.event.amount <= 10000
    input.environment.time >= "09:00:00"
    input.environment.time <= "17:00:00"
}

The value of A3 is replayability: given the same inputs and policy version, you can re-evaluate the decision. During an audit, you can prove not just what happened, but that the action was (or wasn’t) authorized under the rules in effect at the time.

You could use OPA, AWS Cedar, Google Zanzibar, or any policy engine that produces auditable decisions. OPA’s decision logs are designed exactly for this pattern.

A4: Verifiable

At A4, the entire agency chain is cryptographically signed and verifiable. Every step from identity to policy to action carries a signature that can be independently verified.

INSERT INTO account_events (
  event_type,
  amount,
  subject_user_id,
  principal_id,
  principal_verified,
  identity_provider,
  principal_subject_claim,
  policy_input,
  policy_decision,
  policy_version,
  workflow_id,                    -- e.g. "wf-20250510-closure"
  workflow_approver_id,           -- human who OK'd the workflow
  workflow_approved_at,
  event_signature,                -- signature over the entire row payload
  signer_cert_thumbprint,
  proof_chain                     -- JSON array of prior signatures/decisions
) VALUES (
  'TradeClosed',
  1000,
  42,
  99,
  TRUE,
  'OIDC',
  'user-alice@example.com',
  '{"event":{...},"principal":{...},"environment":{...}}'::jsonb,
  'ALLOW',
  'rev-20250510a',
  'wf-20250510-closure',
  1234,
  '2025-05-10T20:59:00-04:00',
  'MEUCIQDox...base64-signature...',
  'AB:12:CD:34:EF:56:78:90',
  '[{"seq":5321,"signature":"...","signer":"policy-engine"},
    {"seq":5319,"signature":"...","signer":"workflow-service"}]'::jsonb
);

The proof_chain field carries signatures from earlier steps in the workflow. An auditor can verify each signature independently and confirm the entire chain of custody.

A4 is rare. Most systems stop at A2 (verified identity) or A3 (authorized). Full cryptographic proof chains with workflow approvals and signature verification are typically found only in regulated industries where non-repudiation is a legal requirement: securities trading, healthcare records, government systems. For most applications, A2 or A3 is sufficient.


Intent Implementation Patterns

Intent is about capturing why actions were taken, not just what happened. At lower maturity levels, intent lives in tickets and Slack threads. At higher levels, intent is embedded in events and can be validated against outcomes.

I2a: Human-Looped Intent

At I2a, you capture intent that originated from a human decision, even when automation executes it. The example below shows a stop-loss order: the human set the threshold, and the system triggered it automatically when conditions were met.

-- Record the StopLossTriggered event, embedding the stop-loss rule intent
-- and linking back to the original PlaceStopLoss command (ID 4278)
INSERT INTO trade_events (
  id,
  event_type,
  trade_id,
  intent,
  causal_event_id,
  triggered_price,
  created_at
) VALUES (
  5322,                      -- this event's PK
  'StopLossTriggered',       -- stop-loss actually fired
  'T-1001',
  '{
     "stopLossPercentage": 0.05,    -- the 5% threshold set by the user
     "stopLossType": "percentage"
   }'::jsonb,
  4278,                      -- references the original StopLossPlaced event
  145.23,                    -- market price at trigger time
  NOW()
);

The intent field preserves the human’s original reasoning. The causal_event_id links back to when that intent was declared.

I2b: Agentic Intent

At I2b, you capture intent that the system derived on its own, based on rules or policies configured by humans. The example below shows an automated portfolio rebalance: the system decided to act based on a drift from target allocation.

-- Record an automated portfolio rebalance action,
-- capturing why the agent rebalanced and linking to its trigger
INSERT INTO portfolio_events (
  id,
  event_type,
  account_id,
  intent,
  causal_event_id,
  created_at
) VALUES (
  5987,
  'PortfolioRebalance',
  'acct-12345',
  '{
     "purpose": "maintain_target_allocation",
     "targetAllocation": {
       "AAPL": 0.25,
       "GOOG": 0.15,
       "CASH": 0.60
     }
   }'::jsonb,
  5310,   -- e.g. ID of a MarketPriceUpdated event that prompted this rebalance
  NOW()
);

The intent field explains the system’s goal. Later, you can compare the actual outcome to this declared purpose.


Narrative Implementation Patterns

Narrative is about reconstructing and communicating the story of what happened. At lower maturity levels, you grep logs. At higher levels, the system can query timelines and even generate explanations.

N2: Structured Timeline Query

At N2, you can reconstruct a timeline with a single query. Events carry correlation IDs that tie them together into workflows.

SELECT event_type, actor_id, ts
FROM   events
WHERE  correlation_id = 'order-wf-9931'
ORDER  BY ts;

Example output:

event_typeactor_idts
PaymentRequestedapi-gateway2025-05-15 12:07:13.021
ChargeInitiatedsvc-payment2025-05-15 12:07:13.044
ChargeSucceededsvc-payment2025-05-15 12:07:14.382
OrderConfirmedorder-core2025-05-15 12:07:14.599

This gives you the sequence of events. You can see what happened and who did it, but not yet why.

N4: Explainable Narrative Templates

At N4, the system can generate human-readable explanations from structured data. One approach is template-based generation:

{{actor}} executed {{action}} at {{timestamp}} due to {{intent}}.

More sophisticated systems use the causal graph and intent data to generate full narratives:

“On April 15 at 12:01 UTC, the Portfolio Service detected a sector imbalance. A rebalancing command was issued to reduce tech exposure. The Trade Service submitted a sell order for AAPL. The Compliance Service blocked the trade due to rule R-19: risk limit exceeded. No retry occurred because the SLA window had expired.”

This narrative was generated from CHAI-linked data, not written by hand. The system traversed the causal graph, extracted intent from each event, and composed a coherent explanation.

You could implement this with template engines, rule-based generators, or LLMs grounded in structured data. The key is that the narrative is derived from the event graph, not invented.


Summary

This reference provides implementation patterns across the CHAIN maturity spectrum. Key techniques include:

  • Monotonic ULIDs for single-writer journal ordering
  • Causal parent references for linking events to their triggers
  • Causal edge tables for graph navigation
  • Vector clocks for distributed causal enforcement
  • Upcasters for schema evolution without history loss
  • Policy input envelopes for auditable authorization
  • Proof chains for verifiable agency
  • Intent fields for preserving goals across event chains
  • Correlation IDs for timeline reconstruction

These are starting points, not specifications. Your implementation will depend on your stack, your domain, and your constraints. The goal is to understand what each maturity level requires, then find the approach that works for your system.

You’ve completed the CHAIN series. Return to the philosophy overview to revisit ORA or the Case for Change.