Commit EnvelopesOverview

Commit Envelopes

A commit envelope is Causet’s declarative pattern for coordinating writes across multiple entity streams atomically. Think of it as a two-phase commit (2PC) — but expressed entirely in the DSL without any distributed transaction manager.

The most common use cases are wallet transfers (debit sender, credit receiver) and inventory reserves (hold stock, capture payment, confirm shipment).

┌─────────────────────────────────────────────────────────────┐
│                      Commit Envelope                         │
│                                                              │
│   start_action ──► PREPARE ──► all participants ACK         │
│                                     │                        │
│                              ┌──────┴──────┐                 │
│                              ▼             ▼                 │
│                           COMMIT        ABORT                │
│                        (all apply)   (all rollback)          │
│                                             │                │
│                                       timeout / failure      │
└─────────────────────────────────────────────────────────────┘

When to use commit envelopes

Use a commit envelope when:

  • You need an atomic debit + credit across two entity streams
  • You need to reserve inventory and capture payment as a unit
  • A multi-step operation must either fully complete or fully abort
  • You want the coordination logic declared in the DSL, not hand-coded in service code

Do not use a commit envelope for simple single-entity state updates — those are just normal actions. Commit envelopes are for cross-stream atomicity.


DSL shape

commit_envelopes:
  wallet_transfer:
    # Action that kicks off the envelope
    start_action: INITIATE_TRANSFER
 
    # Entity stream and field path used to track envelope state
    envelope_state: transfer
    envelope_state_path: /transfer_state
 
    # The four events that drive the lifecycle
    lifecycle:
      prepare_event:   TRANSFER_PREPARED
      prepared_event:  TRANSFER_PREPARE_CONFIRMED
      commit_event:    TRANSFER_COMMITTED
      abort_event:     TRANSFER_ABORTED
 
    # Auto-abort if participants don't all ACK within this window
    timeout:
      abort_after_seconds: 30
      tick_event: CLOCK_TICK          # a kind: system event that fires periodically
 
    # Optional: sequence ordering across participants
    causal_ordering:
      mode: strict
      cursor_field: cursor
      seq_source: envelope
 
    # Who participates in the envelope
    participants:
      - name: sender
        state: wallet
        entity_expr: event.sender_id
 
      - name: receiver
        state: wallet
        entity_expr: event.receiver_id

Top-level fields

FieldRequiredDescription
start_actionyesThe action name that initiates the envelope
envelope_stateyesEntity type used for the envelope ledger record
envelope_state_pathyesField path on envelope_state entities that stores envelope state
lifecycleyesThe four event types that drive the prepare/commit/abort cycle
timeoutyesAuto-abort configuration
participantsyesThe entity streams that participate in the coordination
causal_orderingnoOptional strict sequencing across participants

Lifecycle

A commit envelope always moves through four phases driven by four events:

Event fieldDescription
prepare_eventEmitted by start_action. Tells all participants to stage their changes.
prepared_eventEmitted when all participants have ACK’d their staging phase. Triggers commit.
commit_eventEmitted to all participants to apply staged changes permanently.
abort_eventEmitted when a participant rejects, or when timeout fires. Triggers rollback.

See the Lifecycle & Timeout page for a complete state diagram and timeout configuration.


Participants

Each participant declares which entity stream it touches and how to route the envelope events to the right entity.

participants:
  - name: sender
    state: wallet
    entity_expr: event.sender_id
 
  - name: receiver
    state: wallet
    entity_expr: event.receiver_id

Participants can also declare:

  • visibility_gate — expression that must be true for this participant to join (e.g. conditional enrollment)
  • delta_expr — the value being staged (for audit / abort reconciliation)
  • participant_type — optional tag for specialized runtime routing
  • staging / apply / abort_cleanup — advanced rule blocks for full control over each phase

See the Participants page for the full field reference.


Timeout

If any participant fails to ACK the prepare_event within abort_after_seconds, the runtime emits the abort_event and all participants execute their abort_cleanup rules.

timeout:
  abort_after_seconds: 30
  tick_event: CLOCK_TICK

tick_event must be a kind: system event that fires on a regular schedule (typically every few seconds). The runtime uses it to check elapsed time and trigger the abort path.

⚠️

Make sure your tick_event interval is shorter than abort_after_seconds. If the tick fires every 30 seconds and your timeout is 30 seconds, you may get a 2× window in practice.


Causal ordering

Optional strict sequencing ensures that participant events are processed in order, preventing race conditions on high-throughput streams.

causal_ordering:
  mode: strict
  cursor_field: cursor
  seq_source: envelope
FieldDescription
modestrict — the runtime enforces monotonic sequence ordering
cursor_fieldThe field name in the event payload that carries the sequence number
seq_sourceenvelope — the envelope assigns sequence numbers to participant events

See Causal Ordering for details.


Minimal example: wallet transfer

This example debits a sender and credits a receiver atomically.

commit_envelopes:
  wallet_transfer:
    start_action: INITIATE_TRANSFER
    envelope_state: transfer
    envelope_state_path: /transfer_state
    lifecycle:
      prepare_event:   TRANSFER_PREPARED
      prepared_event:  TRANSFER_PREPARE_CONFIRMED
      commit_event:    TRANSFER_COMMITTED
      abort_event:     TRANSFER_ABORTED
    timeout:
      abort_after_seconds: 30
      tick_event: CLOCK_TICK
    participants:
      - name: sender
        state: wallet
        entity_expr: event.sender_id
        delta_expr: "-event.amount"
      - name: receiver
        state: wallet
        entity_expr: event.receiver_id
        delta_expr: "event.amount"

The matching action and events look like this:

events:
  TRANSFER_PREPARED:
    state: transfer
    entity_expr: event.transfer_id
    payload:
      transfer_id: string
      sender_id:   string
      receiver_id: string
      amount:      number
 
  TRANSFER_PREPARE_CONFIRMED:
    state: transfer
    entity_expr: event.transfer_id
    payload:
      transfer_id: string
 
  TRANSFER_COMMITTED:
    state: transfer
    entity_expr: event.transfer_id
    payload:
      transfer_id: string
 
  TRANSFER_ABORTED:
    state: transfer
    entity_expr: event.transfer_id
    payload:
      transfer_id: string
      reason: string
 
actions:
  INITIATE_TRANSFER:
    state: transfer
    entity_id_expr: intent.transfer_id
    input:
      transfer_id: { type: string, required: true }
      sender_id:   { type: string, required: true }
      receiver_id: { type: string, required: true }
      amount:      { type: number, required: true }
    preflight:
      rules:
        - name: reject_self_transfer
          when:
            expr: "event.sender_id == event.receiver_id"
          then:
            - op: reject
              code: SELF_TRANSFER
        - name: reject_zero_amount
          when:
            expr: "event.amount <= 0"
          then:
            - op: reject
              code: INVALID_AMOUNT
    core:
      rules:
        - name: emit_prepared
          when: {}
          then:
            - op: set
              path: /status
              value: preparing
            - op: emit
              event_type: TRANSFER_PREPARED
              payload:
                transfer_id: event.transfer_id
                sender_id:   event.sender_id
                receiver_id: event.receiver_id
                amount:      event.amount

See the Examples page for a full inventory reserve pattern with staging and abort cleanup.


Compiler output

The compiler expands a commit_envelope block into:

  1. Rules on the envelope entity stream — tracking prepare/ACK/commit/abort state transitions
  2. Rules on each participant stream — staging, applying, and cleaning up deltas
  3. A timeout check rule — triggered by the tick_event, emits abort_event if elapsed > abort_after_seconds

You do not write these rules by hand — the compiler generates them from your commit_envelope declaration.