IntentRules

Intent Rules

Rules are evaluated in strict phase order: preflight → core → side_effects. Each rule has a name, optional when condition, and a then list of operations.

preflight:
  rules:
    - name: my_validation_rule
      when: { expr: "intent.quantity <= 0" }
      then:
        - op: reject
          code: INVALID_QUANTITY
 
core:
  rules:
    - name: my_mutation_rule
      when: {}
      then:
        - op: set
          path: /status
          value: active
 
side_effects:
  rules:
    - name: my_fanout_rule
      then:
        - op: emit
          event_type: ORDER_PLACED
          payload:
            order_id: intent.order_id

Rule structure

- name: reject_duplicate_checkin   # snake_case; required
  priority: 100                    # lower runs first; default 100
  when:
    expr: "entity.last_checkin == event.show_id"   # optional; omit or use when: {}
  then:
    - op: reject
      code: DUPLICATE_CHECKIN
FieldRequiredDescription
nameyesUnique rule name within the phase (snake_case)
whenno{ expr: "..." } — rule runs only when expression is truthy. when: {} always runs
prioritynoLower number runs first (default 100)
thenyesOrdered list of operations

Warning: Rule when: uses { expr: "..." }. Array op where: fields (filter, remove, find) use plain strings: where: "it.active == true". Using { expr: ... } under where: causes a compile-time ClassCastException.


Phase summary

PhasePurposeCan mutate entity?Can reject?
preflightValidate before any writenoyes
coreMutate target entity + emit ledger eventsyes (own stream)no
side_effectsFan-out after ledger commitno direct writesno

Execution order at runtime:

preflight → core → (ledger commit) → side_effects

If preflight rejects, core and side_effects never run.


Preflight

Read-only validation. Abort the intent with reject when a business rule fails. No state mutations, no emits, no fan-out.

Allowed operations

OpPurpose
rejectAbort intent with error code
ifConditional validation branches
lookupLoad another entity for checks
findBind an array element to a variable (read-only use)
for_eachIterate when each item needs validation
stop / continueControl for_each loops

Use LOOKUP_FIELD(...) inside expr for inline cross-stream reads without lookup.

Forbidden in preflight

set, add, sub, push, merge, unset, remove, filter, map, sort, clone, emit, emit_each, submit, schedule, decision, lock, unlock, relationship_create, relationship_remove

The compiler rejects these with VERR_PREFLIGHT_SIDE_EFFECT.


reject

Abort the intent with a typed error code. Use in preflight to return structured errors to callers.

- op: reject
  code: TICKET_LIMIT_EXCEEDED
  message: "You can only purchase 4 tickets per show"
FieldRequiredDescription
codeyesSCREAMING_SNAKE_CASE rejection code returned to the client
messagenoHuman-readable message
preflight:
  rules:
    - name: reject_self_follow
      when: { expr: "intent.user_id == intent.artist_id" }
      then:
        - op: reject
          code: CANNOT_FOLLOW_SELF
          message: "A user cannot follow themselves"
 
    - name: check_stock
      when: {}
      then:
        - op: if
          expr: "LOOKUP_FIELD('show_stream', intent.show_id, 'available_tickets') < intent.quantity"
          then:
            - op: reject
              code: INSUFFICIENT_STOCK

lookup (preflight)

Load another entity’s state into a scratch path for validation.

- op: lookup
  into: /_tmp/show
  stream: show_stream
  entity_id_expr: intent.show_id
  fields:
    - title
    - available_tickets
 
- op: if
  expr: "_tmp.show == null"
  then:
    - op: reject
      code: SHOW_NOT_FOUND
FieldRequiredDescription
intoyesPath to write result — use /_tmp/... for scratch
streamyesTarget stream name
entity_id_expryesExpression for entity ID
fieldsnoOptional field subset to load

Expression alternative: LOOKUP_FIELD('show_stream', intent.show_id, 'title') inline in expr without binding a variable.


if (preflight)

Branch validation logic. Nested ops inside then/else must also be preflight-safe.

- op: if
  expr: "intent.quantity <= 0"
  then:
    - op: reject
      code: INVALID_QUANTITY
  else:
    - op: if
      expr: "intent.quantity > 10"
      then:
        - op: reject
          code: BULK_LIMIT_EXCEEDED
FieldRequiredDescription
expryesCondition string
thenyesOps when truthy
elsenoOps when falsy

find (preflight)

Bind the first matching array element to a variable — useful for duplicate detection.

- op: find
  path: /following
  where: "it == intent.artist_id"
  as: existing_follow
 
- op: if
  expr: "existing_follow != null"
  then:
    - op: reject
      code: ALREADY_FOLLOWING
FieldRequiredDescription
pathyesArray field path
whereyesPlain string; it = current element
asyesVariable name (null if not found)

Core

Primary state mutations on the target entity. Relationship edges, array transforms, and event emission that participates in the ledger commit belong here.

Allowed operations

CategoryOps
Mutationset, add, sub, unset, merge
Arraypush, remove, filter, find, map, sort, clone
Relationshipsrelationship_create, relationship_remove
Eventsemit, emit_each
Concurrencylock, unlock
Control flowif, for_each, stop, continue

emit in core is common — state change and domain event are committed together.

Forbidden in core

reject, submit, schedule, decision — use side_effects for fan-out and async work. Cross-stream set/add/sub on another entity’s stream is forbidden; use submit in side_effects instead.


set

Set a field to a value (overwrites existing).

- op: set
  path: /status
  value: purchased
 
- op: set
  path: /owner_id
  value: intent.user_id
FieldRequiredDescription
pathyesField path (/cart/total)
valueyesLiteral or expression string
if_missingnocreate (default), fail, skip
target_streamnoCross-stream write (avoid — prefer submit)
target_entitynoEntity ID for cross-stream write

add / sub

Increment or decrement a numeric field.

- op: add
  path: /view_count
  value: 1
  cap: { min: 0 }
 
- op: sub
  path: /inventory/on_hand
  value: intent.quantity
  cap: { min: 0 }
FieldRequiredDescription
pathyesNumeric field path
valueyesAmount (literal or expression)
capno{ max: N, min: N } — clamp result
if_missingnocreate, fail, skip

unset

Remove a field from entity state.

- op: unset
  path: /tmp/scratch

merge

Shallow-merge an object into an existing field or bound variable.

- op: merge
  path: /metadata
  value:
    last_seen: event.ts
    source: intent.source

Nested objects are replaced, not deep-merged. Use multiple set ops for nested paths.


push

Append to an array.

- op: push
  path: /following
  value: intent.artist_id
 
- op: push
  path: /cart/items
  value:
    product_id: intent.product_id
    quantity:   intent.quantity

remove

Remove array elements matching where. where is a plain string.

- op: remove
  path: /social/friends
  where: "it == event.show_id"

filter

Keep only array elements matching where.

- op: filter
  path: /cart/items
  where: "it.quantity > 0"

find

Find first matching element and bind to variable — often paired with merge or if.

- op: find
  path: /cart/items
  where: "it.product_id == intent.product_id"
  as: existing_item
 
- op: if
  expr: "existing_item != null"
  then:
    - op: merge
      path: existing_item
      value:
        quantity: "existing_item.quantity + intent.quantity"
  else:
    - op: push
      path: /cart/items
      value:
        product_id: intent.product_id
        quantity:   intent.quantity

map

Transform each array element in place.

- op: map
  path: /items
  as: it
  value:
    sku:      it.sku
    qty:      it.qty
    subtotal: "it.qty * it.price"

sort

Sort an array. Prefix - for descending.

- op: sort
  path: /items
  by: it.price
 
- op: sort
  path: /items
  by: "-it.price"

clone

Deep-copy a value into a variable for use later in the same rule.

- op: clone
  path: /items
  as: original_items

relationship_create

Create a relationship edge from the current entity.

- op: relationship_create
  relationship: artist_followers
  to_id: intent.artist_id
 
# one_to_many — id required
- op: relationship_create
  relationship: user_friends
  to_id: intent.friend_id
  id: "concat(intent.user_id, '_', intent.friend_id)"
FieldRequiredDescription
relationshipyesDeclared relationship name
to_idyesTarget entity ID expression
idsometimesRequired for one_to_many edges

relationship_remove

Remove a relationship edge.

- op: relationship_remove
  relationship: artist_followers
  to_id: intent.artist_id

emit (core)

Emit a domain event as part of the ledger commit. Event type must be declared in events:.

- op: emit
  event_type: ARTIST_FOLLOWED
  payload:
    user_id:   intent.user_id
    artist_id: intent.artist_id
    ts:        event.ts
FieldRequiredDescription
event_typeyesDeclared event type
payloadyesKey-value map; values are expressions or literals
target_streamnoEmit on a different stream
target_entitynoTarget entity ID expression

emit_each (core)

Emit one event per collection item. Use item in payload expressions.

- op: emit_each
  from: "entity.cart.items"
  event_type: INVENTORY_RESERVE_REQUESTED
  target_stream: inventory_stream
  target_entity: "item.product_id"
  payload:
    order_id:   intent.order_id
    product_id: item.product_id
    quantity:   item.quantity

lock / unlock (core)

Acquire and release a concurrency lock within a rule. Pair lock/unlock inside the same for_each scope.

- op: lock
  key: item.sku
 
- op: unlock
  key: item.sku

Not allowed in preflight.


if / for_each / stop / continue (core)

- op: for_each
  for_each: "entity.notifications ?: []"
  then:
    - op: emit
      event_type: NOTIFICATION_SENT
      payload:
        notification_id: item.id
 
- op: if
  expr: "entity.status == 'banned'"
  then:
    - op: stop

stop halts the current rule. continue skips to the next for_each iteration (only valid inside for_each).


Side effects

Fan-out after the ledger commit. Failures here do not roll back core mutations.

Use side effects for: cross-entity work (submit), delayed work (schedule), external AI (decision), and emits that should not block the primary commit path.

Allowed operations

OpPurpose
emitEmit domain event
emit_eachEmit per collection item
submitSubmit follow-up intent on another entity
scheduleDeferred event or intent
decisionAI decision (external LLM)
if, for_each, stop, continueControl flow wrapping the above
lock, unlockConcurrency (rare in side effects)

Forbidden in side effects

Direct state writes (set, add, sub, push, etc.), reject, relationship_create, relationship_remove


emit (side effects)

Same syntax as core. Use when the emit should run after commit or must not block validation/mutation.

side_effects:
  rules:
    - name: emit_purchase
      then:
        - op: emit
          event_type: TICKET_PURCHASED
          payload:
            ticket_id: intent.ticket_id
            user_id:   intent.user_id

emit_each (side effects)

- op: emit_each
  from: "entity.line_items"
  event_type: LINE_ITEM_FULFILLED
  payload:
    order_id: intent.order_id
    sku:      item.sku

submit

Submit a follow-up intent — invokes another action on a target entity. Side effects only.

- op: submit
  intent_type: CREATE_NOTIFICATION
  target_stream: user_stream
  target_entity: intent.user_id
  payload:
    user_id: intent.user_id
    message: "Your ticket has been confirmed"
FieldRequiredDescription
intent_typeyesTarget action name
payloadyesInput map for the target intent
target_streamnoStream override
target_entitynoEntity ID expression

Submitted intents run asynchronously after the parent intent completes.


schedule

Schedule a deferred event or intent.

- op: schedule
  event_type: TICKET_REMINDER_SENT
  delay_seconds: 86400
  payload:
    ticket_id: intent.ticket_id
    user_id:   intent.user_id
 
- op: schedule
  intent_type: SEND_POST_SHOW_RECAP
  delay_seconds: 86400
  payload:
    user_id: intent.user_id
    show_id: intent.show_id
FieldRequiredDescription
delay_secondsyesDelay before execution
event_typeone ofSchedule a deferred event
intent_typeone ofSchedule a deferred intent

Exactly one of event_type or intent_type is required.


decision

Invoke a compiled AI decision. External LLM call — not replayed from ledger. Side effects only.

- op: decision
  ref: triage_ticket
  input:
    ticket_id:   intent.ticket_id
    customer_id: intent.customer_id
    subject:     intent.subject
    body:        intent.body
FieldRequiredDescription
refyesDecision name under decisions: in your DSL
inputnoMap of decision input field → expression

The runtime validates input, retrieves vector memories, calls the provider, and emits the event declared on the decision (emits:).

See AI Decisions and Support Copilot.


if (side effects)

Wrap fan-out ops conditionally.

side_effects:
  rules:
    - name: notify_on_large_order
      when: { expr: "intent.total_cents > 10000" }
      then:
        - op: submit
          intent_type: ALERT_FRAUD_TEAM
          payload:
            order_id: intent.order_id

Complete example

actions:
  PURCHASE_TICKET:
    state: ticket
    entity_id_expr: intent.ticket_id
    input:
      ticket_id:  { type: string,  required: true }
      user_id:    { type: string,  required: true }
      show_id:    { type: string,  required: true }
      quantity:   { type: integer, required: true }
 
    preflight:
      rules:
        - name: validate_quantity
          when: { expr: "intent.quantity <= 0" }
          then:
            - op: reject
              code: INVALID_QUANTITY
        - name: check_stock
          when: {}
          then:
            - op: if
              expr: "LOOKUP_FIELD('show_stream', intent.show_id, 'available_tickets') < intent.quantity"
              then:
                - op: reject
                  code: INSUFFICIENT_STOCK
 
    core:
      rules:
        - name: mark_purchased
          when: {}
          then:
            - op: set
              path: /status
              value: purchased
            - op: emit
              event_type: TICKET_PURCHASED
              payload:
                ticket_id: intent.ticket_id
                user_id:   intent.user_id
                show_id:   intent.show_id
                quantity:  intent.quantity
 
    side_effects:
      rules:
        - name: notify_user
          then:
            - op: submit
              intent_type: CREATE_NOTIFICATION
              target_stream: user_stream
              payload:
                user_id: intent.user_id
                message: "Your ticket has been confirmed"
        - name: schedule_reminder
          then:
            - op: schedule
              event_type: TICKET_REMINDER_SENT
              delay_seconds: 86400
              payload:
                ticket_id: intent.ticket_id

Next steps