AIDefining Decisions

Defining Decisions

A decision is a named, schema-validated LLM call declared in your Product DSL. Rules invoke it with op: decision. The runtime renders a prompt, optionally retrieves vector memory, calls the configured provider, validates JSON output, and emits a domain event.


Defining a decision

# decisions/triage.decisions.causet
decisions:
  triage_ticket:
    provider: reasoning              # key from providers:
    prompt: triage_v1                # key from prompts:
    emits: TICKET_AI_TRIAGED         # event type (must exist in events:)
    memories:
      - customer_history             # optional memory refs
    input:
      ticket_id:
        type: string
        required: true
      customer_id:
        type: string
        required: true
      subject:
        type: string
        required: true
      body:
        type: string
        required: true
    output:
      priority:
        type: string
        required: true
      category:
        type: string
        required: true
      summary:
        type: string
        required: true
      confidence:
        type: number
        required: true
FieldRequiredDescription
provideryesLogical provider name from providers:
promptyesPrompt template name from prompts:
emitsyesEvent type emitted on success (declared in events:)
memoriesnoList of memory names to retrieve and inject into the prompt
inputyesInput schema — validated against op: decision bindings
outputyesOutput schema — validated against LLM JSON response

The compiler checks that every ref: in op: decision resolves to a decision, that required inputs are bound, and that unknown input keys are rejected.


Invoking with op: decision

# actions/support.actions.causet
actions:
  CREATE_TICKET:
    state: ticket
    entity_id_expr: intent.ticket_id
    input:
      ticket_id:   { type: string, required: true }
      customer_id: { type: string, required: true }
      subject:     { type: string, required: true }
      body:        { type: string, required: true }
 
    core:
      rules:
        - name: persist_ticket
          then:
            - op: set
              path: /status
              value: open
            - op: emit
              event_type: TICKET_CREATED
              payload:
                ticket_id: intent.ticket_id
                customer_id: intent.customer_id
                subject: intent.subject
                body: intent.body
 
    side_effects:
      rules:
        - name: ai_triage
          then:
            - op: decision
              ref: triage_ticket
              input:
                ticket_id: intent.ticket_id
                customer_id: intent.customer_id
                subject: intent.subject
                body: intent.body

op: decision parameters

ParameterRequiredDescription
refyesDecision name (must match a key under decisions:)
inputnoMap of input field → expression. Required schema fields must be bound.
⚠️

op: decision is side_effects only.

The compiler rejects op: decision in preflight and core. External LLM calls are traced in the Timeline but are not replayed when rebuilding state from the ledger — same rule as other side-effect I/O.

Keep entity mutations and the primary domain event in core. Run AI in side_effects after core commits.


What happens at runtime

  1. Resolve input — evaluate each input: binding against the execution context (intent.*, entity.*, etc.).
  2. Validate input — reject if required fields are missing or wrong type.
  3. Retrieve memory — for each entry in decisions.*.memories, vector-search the partition (e.g. customer_id) and bind results to memories.<name> in the prompt context.
  4. Render prompt — substitute {{ field }} and {{ memories.customer_history }} in the prompt template.
  5. Execute — call the provider (OpenAI, Anthropic, or mock). Credentials resolve from BYOK keys configured in the control plane.
  6. Validate output — parse JSON and check against output: schema.
  7. Emit event — merge input + output into payload and emit the configured event type.
  8. Audit — record the step in timeline with executor, model, tokens, and duration.

The emitted event flows through listeners, projections, and webhooks like any other domain event.


Applying AI output to state

Decisions emit events; they do not mutate entity fields directly. Use a listener to apply structured output:

# listeners/triage.listeners.causet
listeners:
  - on: TICKET_AI_TRIAGED
    priority: 0
    mutate:
      - op: set
        field: priority
        value: event.payload.priority
      - op: set
        field: category
        value: event.payload.category
      - op: set
        field: ai_summary
        value: event.payload.summary
      - op: set
        field: ai_confidence
        value: event.payload.confidence

Listeners run deterministically in-process when the event is emitted. Projections then materialize read models for agent queues and dashboards.


Event payload shape

The emitted event payload merges:

  • All input fields passed to op: decision
  • All output fields returned by the LLM (after validation)

Define the event schema in events: to match:

events:
  TICKET_AI_TRIAGED:
    state: ticket
    entity_expr: event.ticket_id
    payload:
      ticket_id:   string
      customer_id: string
      subject:     string
      body:        string
      priority:    string
      category:    string
      summary:     string
      confidence:  number

Observability

Each decision execution appears in:

  • Timeline — one step per LLM call with input, effects, and compute usage
  • Ledger commit — the configured domain event in the emitted events for that cursor
  • CU metering — memory retrievals + token usage billed as compute units

Use the control plane Decision Log to debug prompt context, model errors, and validation failures.


See also