Skip to main content

Actions

An action is an atomic domain command. Actions own the full write story for the domain: capability calls, collection writes, and signal emissions all happen inside actions.

Defining an action

domain:
actions:
guestbook.submit:
description: "Submit a new message to the guestbook"
effects: [emit, write]
in:
message: text!
do:
- call: ids.generate
with: { prefix: "msg_", count: 1 }
as: generated_ids
- call: collections.write
with:
patch:
op: upsert
collection: hello_messages
id: { $expr: { var: generated_ids.ids.0 } }
value:
id: { $expr: { var: generated_ids.ids.0 } }
message: { $expr: { var: input.message } }
user_id: { $expr: { var: principal.subject } }
created_at: { $expr: { var: ctx.now } }
emits:
- signal: message.created
payload:
id: { $expr: { var: generated_ids.ids.0 } }
message: { $expr: { var: input.message } }
user_id: { $expr: { var: principal.subject } }
created_at: { $expr: { var: ctx.now } }
return:
ok: true

The effects contract

The effects field is a compile-time contract. The runtime enforces it at execution time — any observed side effect not declared in effects causes a hard runtime failure.

EffectCovers
emitSignal emissions via the emits block
writeCollection writes via collections.write

An action that only calls ids.generate and reads session state declares neither emit nor write. An action that writes a record and emits a signal declares [emit, write].

Step execution

The do block is a sequential list of capability calls. Each step binds its output to a named variable with as. That variable is accessible in all subsequent steps and in the emits and return blocks.

do:
- call: message.is_allowed
with:
message: { $expr: { var: input.message } }
as: moderation

- call: collections.write
with:
patch:
op: upsert
collection: hello_messages
id: { $expr: { var: generated_ids.ids.0 } }
value:
moderation_status:
$expr:
if:
- var: moderation.allowed
- approved
- rejected

Available context variables in step with expressions:

VariableDescription
input.*Named inputs declared in in
ctx.nowCurrent timestamp
ctx.idUnique execution ID for this invocation
ctx.traceIdTrace ID for distributed tracing
principal.subjectAuthenticated caller's identity
principal.rolesCaller's assigned roles
principal.claimsCaller's claim map
<step_name>.*Output of a previous step bound with as

Guards

Guards are rules evaluated before or after action step execution. A failed guard aborts the action with an error.

domain:
actions:
guestbook.submit:
guards:
on_fail: abort
pre:
structural:
- description: "Message must not be empty."
rule:
"!==":
- var: input.message
- ""
semantic:
- description: "Message must pass content policy."
confidence: 0.9
call: message.is_allowed
with:
message: { $expr: { var: input.message } }
assert:
"==": [{ var: result.allowed }, true]

Structural guards run synchronously before any step executes. They evaluate $expr rules against inputs.

Semantic guards call a domain capability and assert against the result. They support a confidence threshold for probabilistic checks.

on_fail: abort is the only supported mode for action guards.

Failure semantics

domain:
actions:
guestbook.submit:
failure:
on_step_error: abort
on_signal_error: log_and_continue
rollback:
mode: none
FieldOptionsBehavior
on_step_errorabortStop execution and surface the error
on_signal_errorlog_and_continueEmit failure is non-fatal; continue execution
rollback.modenoneNo automatic rollback of previous writes

Session outcome intent

An action can declare what should happen to the user's session after it completes. This is used by surface adapters to drive post-mutation navigation.

domain:
actions:
guestbook.submit:
session_outcome_intent:
on_success: navigate_to_home
on_failure: stay

Internal and exposed actions

An action without a corresponding mutation is an internal action. Internal actions are callable only by flows and other actions. Their absence from the mutations section is the declaration that they are internal — no additional flag is needed.

domain:
actions:
# Internal — no corresponding mutation entry
guestbook.log_rejection:
description: "Persist an audit record for a rejected message"
effects: [write]
in:
id: id!
reason: text!
do:
- call: collections.write
with:
patch:
op: insert
collection: hello_rejections
id: { $expr: { var: input.id } }
value:
id: { $expr: { var: input.id } }
reason: { $expr: { var: input.reason } }
rejected_at: { $expr: { var: ctx.now } }
return:
ok: true

What's next