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.
| Effect | Covers |
|---|---|
emit | Signal emissions via the emits block |
write | Collection 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:
| Variable | Description |
|---|---|
input.* | Named inputs declared in in |
ctx.now | Current timestamp |
ctx.id | Unique execution ID for this invocation |
ctx.traceId | Trace ID for distributed tracing |
principal.subject | Authenticated caller's identity |
principal.roles | Caller's assigned roles |
principal.claims | Caller'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
| Field | Options | Behavior |
|---|---|---|
on_step_error | abort | Stop execution and surface the error |
on_signal_error | log_and_continue | Emit failure is non-fatal; continue execution |
rollback.mode | none | No 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
- Flows: how to orchestrate sequences of action calls in response to signals
- Queries and Mutations: how to expose actions publicly
- Capabilities: the building blocks of action step execution