Event Validation

Causet validates event-related data at three layers: the compiler (before deployment), the runtime (at intent time), and preflight rules (application-defined business logic). Each layer catches different classes of errors.


Layer 1: Compiler validation

The causet-compiler validates your DSL files when you run compile. Compiler errors must be fixed before the IR artifact can be produced and deployed.

What the compiler validates:

  • Event types exist: every source_events reference in a projection or action’s side_effects must match a declared event
  • Field types: field types in event payloads must be valid DSL types (string, int, boolean, float)
  • Derive expressions: expressions in projection derive: blocks must reference valid event payload fields
  • Action schemas: input fields reference valid types, required flags are valid
  • Relationships: emit_events in relationship definitions reference declared events
  • Entity references: entity_expr references must resolve to valid payload fields

A compile error looks like:

[ERROR] Projection 'user_following' references undefined event type 'ARTIST_FOLLOWED_TYPO'
        at projections/follow.projections.causet:3

Fix the error and recompile. There is no mechanism to deploy an IR with compiler errors.


Layer 2: Runtime input validation

When an intent is submitted, the runtime validates the payload against the action’s declared input schema before any rules run.

For an action defined as:

actions:
  FOLLOW_ARTIST:
    input:
      user_id:   { type: string, required: true }
      artist_id: { type: string, required: true }

A request with a missing artist_id is rejected before preflight rules run:

{
  "error": "VALIDATION_ERROR",
  "field": "artist_id",
  "message": "Required field 'artist_id' is missing"
}

Type mismatches (e.g., passing a number for a string field) are also caught at this layer.


Layer 3: Preflight rules

Preflight rules are application-defined business logic that run before core rules. They have access to the current entity state and can reject an intent with a domain-specific error code.

actions:
  FOLLOW_ARTIST:
    preflight:
      rules:
        - name: reject_self_follow
          when: { expr: "intent.user_id == intent.artist_id" }
          then:
            - op: reject
              code: CANNOT_FOLLOW_SELF
 
        - name: reject_if_blocked
          when: { expr: "state.blocked_users contains intent.artist_id" }
          then:
            - op: reject
              code: USER_BLOCKED

Preflight rules execute deterministically inside the rules engine. They cannot make external I/O calls. If you need to validate against data in another entity’s state, use LOOKUP_FIELD:

preflight:
  lookups:
    - name: show_lookup
      state: show
      entity_expr: intent.show_id
  rules:
    - name: reject_if_show_not_found
      when: { expr: "show_lookup == null" }
      then:
        - op: reject
          code: SHOW_NOT_FOUND

What validation cannot catch

Compiler and runtime validation are structural — they check that types are correct and required fields are present. They cannot detect:

Semantically invalid but structurally valid payloads. If artist_id is a valid string but does not correspond to a real artist, runtime validation passes. Use preflight LOOKUP_FIELD to validate referential integrity.

Business rule violations that depend on external state. Preflight rules operate on entity state in the causet database. They cannot query external systems or your own application’s database.

Concurrent modification conflicts. If two intents for the same entity are submitted simultaneously, both may pass validation individually but produce conflicting state. Use optimistic locking or design your events to be commutative if this is a concern.


Cross-stream validation with LOOKUP_FIELD

Use LOOKUP_FIELD in preflight to validate that a referenced entity exists:

# Validate that a show exists before checking in a user
actions:
  CHECK_IN:
    state: user
    input:
      user_id: { type: string, required: true }
      show_id: { type: string, required: true }
    preflight:
      lookups:
        - name: show
          state: show
          entity_expr: intent.show_id
      rules:
        - name: show_must_exist
          when: { expr: "show == null" }
          then:
            - op: reject
              code: SHOW_NOT_FOUND
        - name: show_must_be_announced
          when: { expr: "show.status != 'announced'" }
          then:
            - op: reject
              code: SHOW_NOT_ANNOUNCED

This is the recommended pattern for referential integrity in Causet — not foreign keys in projection tables.


Reserved payload field names

The following field names are reserved and cannot be used in event payloads:

Reserved nameUsed for
typeEvent type discriminator
tsEvent timestamp (microseconds)
entity_idEntity identifier
event_idLedger event identifier
fork_idFork context

Attempting to use these names causes a compiler error.