Skip to content

Actors & Workflows

Vox provides two first-class concurrency primitives: Actors for lightweight message-passing and Workflows for orchestrating activities. Both are expressed as standard fn declarations — the runtime provides the mailbox dispatch, journaling, and replay infrastructure automatically.


Actors are isolated processes with their own state and a mailbox for receiving messages. They communicate exclusively via message passing — no shared memory.

An actor is modeled as one function per handler. The naming convention ActorName_HandlerName makes handlers discoverable and groups them semantically:

fn CounterActor_Increment(current: int, amount: int) to int {
return current + amount
}
fn CounterActor_GetCount(current: int) to int {
return current
}
fn CounterActor_Reset() to int {
return 0
}

Key concepts:

  • Each handler takes current state as the first parameter and returns the new state
  • The runtime maintains the mailbox and dispatches messages to the appropriate handler
  • No shared memory — state flows through parameters and return values

Define typed messages for inter-actor communication using ADTs:

type CounterMsg =
| Increment(amount: int)
| GetCount
| Reset

State persistence is handled by the interpreted runtime (ADR-019). The function receives the last-known state and returns the next state — the runtime checkpoints it automatically.

fn PersistentCounter_Increment(current: int) to int {
return current + 1
}

This pattern compiles to database-backed state management — the actor’s count survives process restarts because the runtime journals the state after each handler invocation.

Vox ConceptCompiled Output (Rust)
fn ActorName_Handler(state: T, ...) to TTokio task + mpsc::channel mailbox
Actor spawn (runtime)ProcessHandle via ProcessRegistry
Message send (runtime)Channel send + optional oneshot for reply
State parameterStruct field with default, checkpointed by runtime

Activities are retryable units of work that may fail. They are the recommended place for side effects within workflows.

fn fetch_user_data(user_id: str) to Result[str] {
return Ok("User data for " + user_id)
}
fn send_notification(email: str, body: str) to Result[bool] {
return Ok(true)
}

Activities must always return a Result type, since they represent operations that can fail.


ConceptPatternSurvivalState
Actorfn ActorName_Handler(state: T, ...) to TLives in memory; revive with same IDRuntime checkpoints return value
Workflowfn workflow_name(...) to Result[T]Interpreted runtime can replay completed stepsJournal in Codex
Activityfn activity_name(...) to Result[T]Individual retryable step within a workflowNone (idempotent)

Workflows orchestrate activities with retry and journaling intent. A workflow is a plain function — the runtime provides durability.

Current state:

  • Implemented semantics: function-based workflow pattern, with { ... } parsing/typechecking, generated async Rust functions, interpreted workflow planning/journaling, stored step-result replay, and retry/backoff for interpreted mesh_* activities.
  • Planned semantics: full durable state-machine execution for the generated Rust path and richer replay models for branching/loops.
  • Escape hatch / current durable path: the interpreted workflow runtime used by vox mens workflow ....
// vox:skip — `return` in match arm body is a parser limitation; illustrative only
fn onboard_user(user_id: str, email: str) to Result[str] {
let profile = fetch_user_data(user_id)
match profile {
Error(msg) => return Error("profile fetch failed: " + msg)
Ok(data) => {
let notif = send_notification(email, "Welcome! " + data)
match notif {
Error(msg) => return Error("notification failed: " + msg)
Ok(_) => return Ok("Onboarding complete for " + user_id)
}
}
}
}

The with expression carries workflow activity options when calling activities through the interpreted runtime. Some are honored today, while others only matter on specific runtime paths:

OptionTypeDescription
retriesintHonored for interpreted mesh_* activity execution
timeoutstrParsed for interpreted runtime activity planning
initial_backoffstrHonored for interpreted mesh_* retries
activity_idstrExplicit durable/journal key
idstrAlias for activity_id
mensstrMesh control override for interpreted mesh_* activities

The interpreted workflow runtime can skip previously completed activities when restarted with the same workflow, run id, and activity ids because it records journal/tracker data before replay and stores step result payloads for linear replay. Generated Rust workflows do not yet compile into a durable state machine.

Durable spine (today): the supported replay/idempotency story is the interpreted vox mens workflow … runtime (see ADR-019). Rust-emitted async fn workflows are orchestration helpers only until generated code adopts the same journaling contract. Generated-workflow parity remains intentionally out of scope until Vox has a formal replay model and ADR for it (see ADR-021).

Vox ConceptCurrent generated / runtime behavior
fn workflow_name(...)Generated as a plain async fn in Rust codegen
fn activity_name(...)Generated as a plain async fn; with lowering adds helper wiring
with { retries: 3 }Interpreted runtime honors it for mesh_* activity execution
Step completionInterpreted runtime journals versioned events and stores replayable step results

A complete workflow combining activities with different retry policies:

// vox:skip — `return` in match arm body is a parser limitation; illustrative only
type OrderResult =
| OrderOk(order_id: str)
| OrderError(message: str)
fn validate_order(order_data: str) to Result[str] {
let validated = "validated-" + order_data
return Ok(validated)
}
fn charge_payment(amount: int, card_token: str) to Result[str] {
let tx = "tx-" + card_token
return Ok(tx)
}
fn send_confirmation(recipient: str, order_id: str) to Result[str] {
let msg = "Order " + order_id + " confirmed for " + recipient
return Ok(msg)
}
fn process_order(customer: str, order_data: str, amount: int) to Result[str] {
let validated = validate_order(order_data)
match validated {
Error(msg) => return Error(msg)
Ok(_) => {
let payment = charge_payment(amount, "card-123")
match payment {
Error(msg) => return Error(msg)
Ok(tx) => send_confirmation(customer, tx)
}
}
}
}

Understanding the types of durability is crucial when reasoning about failure recovery in Vox:

  1. Persistent Actors (runtime checkpointing): State survives restarts because the runtime checkpoints the return value of each handler. When the actor respawns, it resumes with the last saved state.
  2. Workflow Durability (Interpreted Runtime): When running via vox run or vox mens workflow, the engine tracks execution steps natively in the database. If the process dies and restarts, completed activities are short-circuited.
  3. Compiled Rust Workflows (Future Parity): Workflows that are compiled strictly down to standard Rust async equivalents do not automatically benefit from step-level replayable durability yet. This remains an active implementation target for parity with the interpreted path (see ADR-021).