WorkflowsSide Effects

Side Effects

Side effects are operations that fire after core rules succeed and after the event is committed to the ledger. They handle fan-out: emitting events to other streams, submitting intents to other entities, and scheduling delayed events.

Side effects never write entity state directly. State mutation is strictly the domain of core rules.

Why Side Effects Are Separate from Core

The separation enforces a clean architecture:

  • Core rules: deterministic state mutations — the “what happened” record
  • Side effects: fan-out — “what should happen next as a result”

This split makes rules deterministic and replayable. Core rules can be re-applied to the ledger at any time to rebuild state. Side effects are not replayed during snapshot rebuild (by design — you don’t want to re-send events or re-trigger downstream services).

op: emit — Emit a Domain Event

Emits an event to another stream. Used to notify downstream consumers about a state change.

side_effects:
  rules:
    - name: notify_purchase_complete
      when:
        - path: /status
          op: eq
          value: "complete"
      then:
        - op: emit
          event_type: TICKET_PURCHASE_COMPLETED
          payload:
            user_id: intent.user_id
            show_id: intent.show_id
            ticket_id: intent.ticket_id
            amount: intent.amount

The emitted event is written to the ledger_events table and published to Kafka. Any consumer subscribed to that event type — Causet projections, external services, analytics pipelines — can pick it up.

op: emit_each — Emit per Array Item

Iterates over an array field and emits one event per item.

side_effects:
  rules:
    - name: notify_each_attendee
      when: {}
      then:
        - op: emit_each
          source: intent.attendee_ids
          event_type: ATTENDEE_NOTIFIED
          payload:
            attendee_id: item
            show_id: intent.show_id
            message: intent.notification_message

source is the array to iterate over. item is the current element in the payload expression.

op: submit — Submit a Follow-Up Intent

Submits a new intent to another entity. The submitted intent runs through its own full rules evaluation (preflight, core, side_effects).

side_effects:
  rules:
    - name: trigger_enrichment
      when:
        - path: /status
          op: eq
          value: "parsed"
      then:
        - op: submit
          intent_type: ENRICH_TICKET_METADATA
          payload:
            ticket_id: intent.ticket_id
            artist_id: intent.artist_id
            venue_id: intent.venue_id

Submit is fire-and-forget from the perspective of the originating entity. The originating entity is not notified of success or failure unless the submitted intent emits an event back that the originating entity is watching via a saga.

Multiple submits in the same rule fire in parallel:

then:
  - op: submit
    intent_type: FETCH_VENUE_DATA
    payload:
      venue_id: intent.venue_id
      import_id: intent.import_id
  - op: submit
    intent_type: FETCH_ARTIST_BIO
    payload:
      artist_id: intent.artist_id
      import_id: intent.import_id

op: schedule — Delayed Emit

Enqueues an event to fire after a delay. See Timers & Scheduling for full coverage.

then:
  - op: schedule
    event_type: IMPORT_TIMEOUT_CHECK
    delay_seconds: 3600
    payload:
      import_id: intent.import_id

Multiple Side Effects in One Rule

A single rule can contain any mix of emit, emit_each, submit, and schedule operations. All fire after core succeeds, and all fire regardless of the outcome of each other (no short-circuiting within a side_effects rule).

side_effects:
  rules:
    - name: post_purchase_fan_out
      when: {}
      then:
        - op: emit
          event_type: TICKET_PURCHASED
          payload:
            user_id: intent.user_id
            show_id: intent.show_id
        - op: submit
          intent_type: UPDATE_SHOW_AVAILABILITY
          payload:
            show_id: intent.show_id
            quantity: 1
        - op: schedule
          event_type: PURCHASE_FOLLOWUP_TRIGGERED
          delay_seconds: 259200
          payload:
            purchase_id: intent.purchase_id
            user_id: intent.user_id

External I/O Is Not in Side Effects

Side effects do not support HTTP calls, email sending, webhook calls, or any other external I/O. This is intentional.

Why no external I/O in rules:

  1. Determinism: rules must be deterministic for replay to work. HTTP calls are non-deterministic (network failures, changed responses).
  2. Replayability: during snapshot rebuild, rules are re-evaluated against the ledger. You don’t want external API calls to fire again during replay.
  3. Isolation: the rules engine should not have external dependencies that can fail the intent processing pipeline.

The correct pattern for external I/O:

Emit a domain event from Causet. An external consumer (not part of Causet) subscribes to the Kafka topic and handles the external call. This consumer can have its own retry logic, rate limiting, and failure handling without affecting the Causet rules engine.

Example: Email Notification via Kafka Consumer

side_effects:
  rules:
    - name: emit_purchase_for_email
      when: {}
      then:
        - op: emit
          event_type: TICKET_PURCHASED
          payload:
            user_id: intent.user_id
            user_email: intent.user_email
            show_name: intent.show_name
            show_date: intent.show_date
            venue: intent.venue
            ticket_id: intent.ticket_id

Your email service subscribes to the TICKET_PURCHASED event type on Kafka and sends the confirmation email. If the email service is down, it retries independently — Causet rules are unaffected.

Side Effects Failure Handling

Infrastructure-level failures in emit/submit operations (Kafka unavailable, downstream service down) are retried at the infrastructure layer. The rules engine does not retry side effects at the application level.

If a op: submit fails because the downstream intent’s preflight rejects it, the error is not propagated back to the originating entity. See Workflow Error Handling for recovery patterns.