Sagas, State Machines, and Long-Running Workflows¶
This guide covers three related patterns for managing complex, multi-step processes:
- Sagas — distributed transactions with automatic compensation (rollback)
- State Machines — state-based entity lifecycle management
- Long-Running Workflows — sagas that pause for timers or external events
Sagas¶
A saga orchestrates a multi-step distributed transaction. Each step has an action (forward operation) and an optional compensate (rollback). If any step fails, compensations run in reverse order for all previously completed steps.
saga "create_order" {
from {
connector = "api"
operation = "POST /orders"
}
step "order" {
action {
connector = "orders_db"
operation = "INSERT"
target = "orders"
data = { status = "pending", user_id = "input.user_id" }
}
compensate {
connector = "orders_db"
operation = "DELETE"
target = "orders"
where = { id = "step.order.id" }
}
}
step "payment" {
action {
connector = "stripe"
operation = "POST /charges"
body = { amount = "input.amount", currency = "input.currency" }
}
compensate {
connector = "stripe"
operation = "POST /refunds"
body = { charge = "step.payment.charge_id" }
}
}
step "inventory" {
action {
connector = "inventory_db"
operation = "UPDATE"
target = "inventory"
set = { reserved = "reserved + input.quantity" }
where = { product_id = "input.product_id" }
}
compensate {
connector = "inventory_db"
operation = "UPDATE"
target = "inventory"
set = { reserved = "reserved - input.quantity" }
where = { product_id = "input.product_id" }
}
}
on_complete {
connector = "orders_db"
operation = "UPDATE"
target = "orders"
set = { status = "confirmed" }
where = { id = "step.order.id" }
}
on_failure {
connector = "notifications"
operation = "POST /send"
template = "order_failed"
data = { user_id = "input.user_id", reason = "error.message" }
}
}
How Sagas Work¶
- Steps execute in order
- Each completed step's result is available as
step.NAME.*in subsequent steps - If a step fails, all previously completed compensations run in reverse order
on_completeruns after all steps succeedon_failureruns after all compensations complete (regardless of compensation result)
Step-Level Error Handling¶
Mark non-critical steps with on_error = "skip" to continue without triggering compensation:
step "send_notification" {
on_error = "skip"
action {
connector = "email"
operation = "send"
}
# No compensate block — skip means: failure is acceptable
}
Simple Sagas (No Delay/Await)¶
Simple sagas execute synchronously and return a response when all steps complete. No extra configuration is needed — just define the saga block.
State Machines¶
A state machine defines valid states and transitions for an entity. State is persisted in the entity's status column. Transitions can have guards (CEL conditions) and side-effect actions.
state_machine "order_status" {
initial = "pending"
state "pending" {
on "pay" { transition_to = "paid" }
on "cancel" { transition_to = "cancelled" }
}
state "paid" {
on "ship" {
transition_to = "shipped"
guard = "input.tracking_number != ''"
action {
connector = "notifications"
operation = "POST /send"
template = "order_shipped"
data = { tracking = "input.tracking_number" }
}
}
on "refund" { transition_to = "refunded" }
}
state "shipped" {
on "deliver" { transition_to = "delivered" }
}
state "delivered" { final = true }
state "cancelled" { final = true }
state "refunded" { final = true }
}
Triggering Transitions¶
Use the state_transition block in a flow:
flow "update_order_status" {
from {
connector = "api"
operation = "POST /orders/:id/events"
}
state_transition {
machine = "order_status" # Name of the state_machine block
entity = "orders" # Database table
id = "input.params.id"
event = "input.event" # e.g., "pay", "ship", "cancel"
data = "input.data" # Additional data for guards and actions
}
to {
connector = "db"
target = "orders"
}
}
State Machine Features¶
- Guards — CEL expressions that prevent invalid transitions (e.g., require tracking number before shipping)
- Actions — side effects executed during a transition (e.g., send notification)
- Final states — states that cannot transition further (
final = true) - Initial state — applied when the entity has no
statusvalue yet
Error Handling¶
If a guard fails (returns false), the transition is rejected with an error. If no valid transition exists for the event in the current state, an error is returned.
Long-Running Workflows¶
When a saga includes steps that pause execution, Mycel persists the workflow state to a database so it survives restarts.
Configuration¶
Enable workflow persistence in the service block:
service {
name = "order-service"
version = "1.0.0"
workflow {
storage = "db" # Database connector name
table = "mycel_workflows" # Table name (default)
auto_create = true # Create table on startup
}
}
Delay Steps¶
Pause execution for a duration:
saga "onboarding" {
from {
connector = "api"
operation = "POST /onboard"
}
step "create_account" {
action {
connector = "db"
operation = "INSERT"
target = "accounts"
}
}
step "wait_24h" {
delay = "24h"
}
step "send_welcome_email" {
action {
connector = "email"
operation = "send"
data = { template = "welcome", user_id = "step.create_account.id" }
}
}
}
When the saga reaches the delay step, it saves state to the database and pauses. A background ticker checks for workflows whose delay has expired and resumes them automatically.
Await/Signal Steps¶
Pause until an external system sends a signal:
saga "loan_approval" {
timeout = "7d"
from {
connector = "api"
operation = "POST /loans"
}
step "submit" {
action {
connector = "db"
operation = "INSERT"
target = "loans"
}
}
step "wait_approval" {
await = "loan_approved" # Pause until this event is signaled
timeout = "48h" # Step-level timeout
}
step "disburse" {
action {
connector = "banking"
operation = "POST /transfers"
}
}
on_failure {
connector = "notifications"
operation = "POST /send"
template = "loan_rejected"
}
}
Signal the workflow via the auto-registered REST API:
POST /workflows/{workflow_id}/signal/loan_approved
Content-Type: application/json
{ "approved_by": "manager@company.com", "note": "Approved" }
The signal data is available in subsequent steps as input.signal.
Workflow REST API¶
The engine auto-registers three endpoints:
| Endpoint | Description |
|---|---|
GET /workflows/{id} |
Get workflow status, current step, timestamps |
POST /workflows/{id}/signal/{event} |
Resume a paused workflow awaiting an event |
POST /workflows/{id}/cancel |
Cancel an active workflow (runs compensation) |
When a saga with delay/await steps is triggered, the response is HTTP 202 with the workflow ID:
Supported Databases¶
Workflow state can be persisted to: PostgreSQL, MySQL, SQLite.