Commands
Commands are the entry point for all mutations. They are pure, deterministic functions that validate input, check invariants against event history, and emit new events. Commands are the only mechanism for writing to the event store.
Anatomy of a command
A command declares typed input, the folds it needs, and an execute body that checks invariants and emits events.
A command is a function annotated with #[export_command]. It receives typed input and a CommandContext, then uses the Command builder to declare folds and run logic.
use umari::prelude::*;
use schemars::JsonSchema;
use serde::{Serialize, Deserialize};
use validator::Validate;
#[derive(DomainIds, Validate, JsonSchema, Serialize, Deserialize)]
pub struct Input {
#[domain_id]
pub user_id: u64,
#[domain_id]
pub project_id: Uuid,
#[validate(length(min = 1, max = 200))]
pub title: String,
#[validate(range(min = 1, max = 120))]
pub duration_months: u32,
}
#[export_command]
pub fn execute(input: Input, context: CommandContext) -> anyhow::Result<ExecuteOutput> {
// 1. Validate input
input.validate()?;
// 2. Build command with folds, execute
Command::new(input, context)
.fold::<UserExistsFold>()
.fold::<ProjectFold>()
.execute(|input, (user_exists, project_state)| {
// 3. Check invariants
anyhow::ensure!(user_exists, "user does not exist");
anyhow::ensure!(!project_state.exists, "project already exists with this ID");
anyhow::ensure!(
!project_state.archived,
"a project with this ID was previously archived"
);
// 4. Emit events
Ok(emit![ProjectCreated {
project_id: input.project_id,
user_id: input.user_id,
title: input.title,
duration_months: input.duration_months,
status: ProjectStatus::Draft,
}])
})
}
Declaring folds and logic
The Command builder chains fold registrations, then execute:
Command::new(input, context): creates the builder.inputmust implementDomainIds..fold::<T>(): registers a fold.Tmust implementFold + FromDomainIds<Args = ()>; it’s constructed from the input’s domain ID bindings automatically..fold_args::<T>(args): same, but passes extra constructor arguments toT::from_domain_ids(args, bindings)..fold_with(|input| MyFold { ... }): manual construction from the raw input..execute(|input, fold_states| { ... }): runs the command. The closure receives the input by value and the fold states as a tuple (registration order, up to 12 folds). Check invariants withanyhow::ensure!/bail!, then returnOk(emit![...])(orOk(emit![])for a no-op).
Emitting events
The emit! macro collects the events to commit:
emit![] // No events
emit![SomeEvent { field: value }] // Single event
emit![EventA { .. }, EventB { .. }] // Multiple events
Each expression must be a struct implementing Event. You can also build an Emit manually:
Emit::new()
.event(FirstEvent { .. })
.event(SecondEvent { .. })
Command idempotency
Commands support built-in idempotency through an idempotency key on the context. When present, the runtime checks whether any event in the fold scope already carries this key. If a match is found, the command exits early: your logic never runs and no events are emitted. This deduplication happens at the event store level, so it survives crashes and restarts; you can safely retry commands without producing duplicates.
You can also implement domain-level idempotency inside the body, returning an empty emit when the desired state already holds:
.execute(|input, project_state| {
if project_state.exists && project_state.title.as_deref() == Some(&input.title) {
return Ok(emit![]); // already exists with same data, idempotent
}
// ... emit ProjectCreated
})
The context key is set with CommandContext::new().with_idempotency_key(Some(request_id)).
CommandContext
The context carries the causal-chain metadata threaded through every command.
pub struct CommandContext {
pub correlation_id: Uuid, // request that started the chain
pub causation_id: Uuid, // this specific execution
pub triggering_event_id: Option<Uuid>, // the event that called us, if any
pub idempotency_key: Option<Uuid>,
}
You almost never construct this by hand; use CommandContext::new() and override fields as needed:
CommandContext::new()
.with_correlation_id(id)
.with_triggering_event_id(id)
.with_idempotency_key(key)
The right values are populated automatically depending on where the command runs:
| Where the command runs | What’s produced |
|---|---|
| HTTP / CLI entry point | fresh correlation_id, fresh causation_id, no triggering_event_id |
Inside an effect’s handle() | inherits correlation_id and triggering_event_id from the effect’s current event; fresh causation_id |
Public vs private commands
Commands fall into two categories by convention, not by type:
- Public commands: part of the domain API. Called by external services, HTTP handlers, or scheduled jobs. They live in the
commands/directory and are uploaded to the runtime. - Private commands: implementation details of effect idempotency, only called from within effects. They use the same definition pattern but aren’t uploaded as standalone modules; effects call them to record that a side effect has happened.
// In effects/register-webhooks/src/commands.rs
use umari::prelude::*;
#[derive(DomainIds)]
pub struct RecordWebhookInput {
#[domain_id] pub user_id: u64,
#[domain_id] pub topic: String,
}
pub fn record_webhook(
input: RecordWebhookInput,
context: CommandContext,
) -> anyhow::Result<ExecuteOutput> {
Command::new(input, context)
.fold::<UserExistsFold>()
.execute(|input, user_exists| {
anyhow::ensure!(user_exists, "user does not exist");
Ok(emit![WebhookRegistered {
user_id: input.user_id,
topic: input.topic,
}])
})
}
Private commands are plain functions (not #[export_command]): they aren’t exported as WASM modules.
Validation
Input validation runs before your logic and surfaces failures as an invalid-input error.
Use the validator crate and call validate() first:
#[derive(DomainIds, Validate, Serialize, Deserialize)]
pub struct Input {
#[validate(length(min = 1, max = 200))]
pub title: String,
#[validate(range(min = 1, max = 120))]
pub duration_months: u32,
}
#[export_command]
pub fn execute(input: Input, context: CommandContext) -> anyhow::Result<ExecuteOutput> {
input.validate()?; // call this first
// ...
}
Custom validators:
fn non_nil_uuid(value: &Uuid) -> Result<(), validator::ValidationError> {
if value.is_nil() {
return Err(validator::ValidationError::new("uuid")
.with_message("must not be nil".into()));
}
Ok(())
}
#[derive(DomainIds, Validate, Serialize, Deserialize)]
pub struct Input {
#[validate(custom(function = "non_nil_uuid"))]
pub project_id: Uuid,
}
Inspecting what a command emitted
A private command guarding an effect needs to answer: “did anything actually get written, or was this a duplicate?”
Every command returns ExecuteOutput, which effects inspect with has_event:
pub struct ExecuteOutput {
pub position: Option<u64>, // event store position after commit
pub events: Vec<EmittedEvent>, // events that were emitted
}
pub struct EmittedEvent {
pub id: Uuid,
pub event_type: String,
pub domain_ids: IndexMap<String, String>, // field_name → value
}
let receipt = ScheduleWebhookRegistration::execute(&input)?;
if !receipt.has_event::<WebhooksRegistrationScheduled>() {
return Ok(()); // already scheduled, skip the side effect
}
If the command short-circuited (idempotency hit or empty emit), the receipt is empty and the effect bails out cleanly.
Complete command example
A register-or-reactivate command: it emits UserRegistered the first time and UserReactivated on subsequent calls.
#[derive(DomainIds, Validate, JsonSchema, Serialize, Deserialize)]
pub struct Input {
#[domain_id]
pub user_id: u64,
#[validate(email)]
pub email: String,
#[validate(length(min = 1))]
pub name: String,
}
#[export_command]
pub fn execute(input: Input, context: CommandContext) -> anyhow::Result<ExecuteOutput> {
input.validate()?;
Command::new(input, context)
.fold::<EventFold<UserRegistered>>()
.execute(|input, registered| {
if registered.exists() {
Ok(emit![UserReactivated {
user_id: input.user_id,
email: input.email,
name: input.name,
}])
} else {
Ok(emit![UserRegistered {
user_id: input.user_id,
email: input.email,
name: input.name,
}])
}
})
}
Both events carry the same data but mean different things downstream: projectors and effects can treat a reactivation differently from a first registration.