Workflows & transitionsDurable Execution

Durable Execution

Causet sagas are durable by construction. Saga state lives in the entity snapshot, which is persisted to PostgreSQL by the causet-projection-worker. A runtime restart doesn’t lose saga state — the next intent against that entity reads the persisted snapshot and resumes from the correct step.

What Durability Means in Causet

When an entity is mid-saga and the causet-runtime-service restarts:

  1. In-flight intents may fail (their callers receive errors)
  2. Events already committed to the ledger are not lost
  3. The entity snapshot reflects the last successfully processed event
  4. The next intent submitted to that entity reads the persisted snapshot

The saga resumes from wherever it was. No saga step is re-run twice because step advancement is event-driven: a step only fires when the corresponding event is committed, and events are written at-most-once to the ledger.

Why This Differs from Temporal

Temporal provides durable execution through workflow history replay: the workflow function re-executes from the beginning, skipping already-completed activities. Causet does not do this.

CapabilityCauset SagasTemporal
State persistenceEntity snapshotWorkflow history
DurabilityEvent-driven, step-levelFunction replay, activity-level
Restart behaviorResume from snapshotReplay full workflow
Activity retriesNot supportedBuilt-in with backoff
External signalsNot supportedBuilt-in
Long sleepsLimited (delay_seconds)Arbitrary duration timers
CompensationExplicit domain actions; optional reliability.compensating_intent for 2PC abortBuilt-in saga compensations

Causet sagas are simpler and more constrained. They are the right tool when your workflow is entirely event-driven and each step maps to a domain event in your ledger.

Saga Replay from Ledger

Because all entity state is derived from ledger events and rules are deterministic, sagas can be replayed from scratch. Replaying the ledger_events table through the rules engine will reproduce the exact saga state at any point in time.

This is useful for:

  • Debugging saga transitions
  • Auditing what happened during a process
  • Rebuilding entity snapshots after a schema change

Note: Replay does not re-fire side effects by default. op: submit and op: schedule operations do not execute during snapshot rehydration. This is correct behavior — you don’t want to re-send emails or re-trigger downstream services when rebuilding state.

Limitations

Short-Delay Timers Only

op: schedule supports delay_seconds — a relative offset from intent processing time. This is suitable for delays up to a few hours or days.

For delays measured in weeks or months, use an external scheduler (cron, EventBridge, etc.) that submits an intent to Causet when the delay expires:

External Scheduler ──(fire at T+30d)──► submit SHOW_ANNIVERSARY_REMINDER to Causet

The entity stays in its current saga step indefinitely — it doesn’t time out. The scheduler is responsible for re-activating it.

No Built-in Timeout

Sagas do not have built-in step timeouts. An entity can stay in a non-terminal saga step forever. To detect stalls, monitor saga state via projections:

SELECT import_id, current_step, last_updated_at
FROM import_pipeline_status
WHERE current_step BETWEEN 1 AND 3
  AND last_updated_at < extract(epoch from (now() - interval '2 hours')) * 1000
ORDER BY last_updated_at ASC;

Alert on this query result and submit a corrective intent or escalation event when the threshold is exceeded.

Compensation: two layers

DSL sagas: do not automatically undo prior steps when a process fails. Model domain rollback with an explicit reset / compensating action on the same entity:

actions:
  RESET_TICKET_IMPORT:
    state: ticket_import
    entity_id_expr: intent.import_id
    input:
      import_id: { type: string, required: true }
    preflight:
      rules:
        - name: must_be_failed
          when: { expr: "entity._tmp.import_saga.current_step != \"failed\"" }
          then:
            - op: reject
              code: NOT_FAILED
              message: "ticket import is not in failed state"
    core:
      rules:
        - name: clear_import_state
          when: {}
          then:
            - op: set
              path: /status
              value: "idle"
    side_effects:
      rules:
        - name: emit_reset
          then:
            - op: emit
              event_type: TICKET_IMPORT_RESET
              payload:
                import_id: intent.import_id

Runtime reliability is separate: actions may declare reliability.compensating_intent so the coordinator can submit an inverse intent when a multi-entity prepare/commit path aborts. That does not drive your _tmp/ step machine. See Defining Sagas → Runtime reliability and Commit envelopes.

Durability Guarantees Summary

What is durableMechanism
Entity snapshot (saga state)entity_snapshots table in PostgreSQL
Event historyledger_events append-only table
Projection tablesWritten by causet-projection-worker from ledger
Scheduled eventsEnqueued by runtime scheduler service

The entity snapshot is the source of truth for current saga state. The ledger_events table is the source of truth for all history.