EventsDispatching Events

Dispatching Events

In Causet, events are emitted by rules — not dispatched directly by clients. Clients submit intents; rules decide which events result.


The Emit Model

Client submits intent
  → preflight rules run (validate)
  → core rules run (emit events on own entity)
  → side_effects rules run (emit to other entities, submit intents, schedule)
  → ledger_events appended
  → Kafka publish (projection fan-out)

You never emit events directly via an API call. The only way to produce a ledger event is for a rule to execute an emit operation during intent processing.


op: emit — Emit a Single Event

The primary way to produce events.

In core mode (own entity)

core:
  rules:
    - name: emit_followed
      when: {}
      then:
        - op: emit
          event_type: ARTIST_FOLLOWED
          payload:
            user_id:   intent.user_id
            artist_id: intent.artist_id

In core mode, emit produces an event on the same entity that the intent targets. The entity ID is resolved from the event’s declared entity_expr.

In side_effects mode (another entity)

In side_effects mode, emit can target a different entity:

side_effects:
  rules:
    - name: notify_artist_of_follower
      when: {}
      then:
        - op: emit
          event_type: FOLLOWER_ADDED
          entity_id: intent.artist_id   # emit to the artist entity
          payload:
            artist_id:   intent.artist_id
            follower_id: intent.user_id
            followed_at: intent.ts

The entity_id field in a side_effects emit specifies which entity receives the event.

Note: The entity_id in a side_effects emit must match the entity_expr resolution for the target event type. The runtime validates this at evaluation time.


op: emit_each — Emit Per Item in a Collection

emit_each iterates over an array and emits one event per item:

side_effects:
  rules:
    - name: notify_all_followers
      when: { expr: "state.followers != null" }
      then:
        - op: emit_each
          over: state.followers        # iterate over this array
          item_as: follower_id         # bind each item to this variable
          event_type: SHOW_NOTIFICATION_SENT
          entity_id: follower_id       # emit to each follower's entity
          payload:
            user_id:   follower_id
            artist_id: intent.artist_id
            show_id:   intent.show_id

emit_each produces N events — one per element in the over array. All events are appended to the ledger in the same transaction as the parent intent.


op: submit — Submit a Downstream Intent

submit enqueues a new intent targeting another entity. The downstream intent is processed asynchronously after the current intent completes:

side_effects:
  rules:
    - name: trigger_ticket_confirmation
      when: {}
      then:
        - op: submit
          action: SEND_PURCHASE_CONFIRMATION
          entity_id: intent.user_id
          payload:
            ticket_id: intent.ticket_id
            show_id:   intent.show_id
            email:     intent.email

The submitted intent is independent — it has its own cursor lock, rule evaluation, and ledger write. If it fails, it does not roll back the parent intent.

Use submit for cross-entity workflows where the downstream entity needs its own state mutation.


op: schedule — Delayed Intent Submission

schedule submits an intent to be processed after a specified delay:

side_effects:
  rules:
    - name: send_followup_24h
      when: {}
      then:
        - op: schedule
          action: SEND_FOLLOW_UP
          entity_id: intent.user_id
          delay_seconds: 86400    # 24 hours
          payload:
            user_id:   intent.user_id
            artist_id: intent.artist_id

The scheduled intent is stored and will be submitted after delay_seconds elapses. It goes through the full intent processing pipeline when it fires.

Note: Scheduled intents are a feature of causet-saas-cloud. They require the SaaS layer. Scheduling is not available when calling causet-runtime directly.


What Cannot Be Done in Rules

Rules are deterministic and side-effect-free within the evaluation context:

ProhibitedWhy
HTTP calls to external APIsNon-deterministic, breaks replay
Database reads (other than via lookup)Non-deterministic
Date.now() or system timeNon-deterministic — use intent.ts
Writing to a queue directlyNot available in the expression language
Random number generationNon-deterministic

For external integration (webhooks, emails, third-party APIs), use the submit op to trigger a dedicated action that handles the integration, and track the send state on the entity to guard against duplicates.


Direct Event Injection (Internal/Admin Only)

There is no supported public API for injecting events directly into the ledger without going through intent processing. This is intentional — skipping rules evaluation means the snapshot and downstream projections may become inconsistent.

In operational emergencies, the admin layer can write directly to ledger_events. This is a last-resort procedure that must be followed by a full snapshot rebuild and projection rebuild for the affected entity.


Projection Events (Runtime to Kafka)

After all ledger_events are committed, causet-runtime publishes enriched projection events to causet.projection-events.v1. This is internal infrastructure — your rules do not publish to Kafka directly. The projection event envelope is generated automatically from the ledger events.