State Machines
Implement state machines for managing complex workflows and business logic in your application.
Workflow Automation
State machines provide a structured way to model workflows with discrete states and transitions. They help prevent invalid state transitions and make complex business logic more maintainable.
1 Creating a State Machine
Use the create_state_machine() function to create a new state machine instance:
let states = ["pending", "confirmed", "processing", "shipped", "delivered", "cancelled"];
let transitions = [
{"event": "confirm", "from": "pending", "to": "confirmed"},
{"event": "process", "from": "confirmed", "to": "processing"},
{"event": "ship", "from": "processing", "to": "shipped"},
{"event": "deliver", "from": "shipped", "to": "delivered"},
{"event": "cancel", "from": "pending", "to": "cancelled"}
];
let order = create_state_machine("pending", states, transitions);
print(order.current_state()); # "pending"
Parameters
| Parameter | Type | Description |
|---|---|---|
| initial_state | String | The starting state (must be in the states array) |
| states | Array | Array of valid state names |
| transitions | Array | Array of transition definitions |
Transition Definition
| Key | Description |
|---|---|
| event | Name of the event/method to create |
| from | Source state(s) - can be a string or array of strings |
| to | Target state |
| if / guard | Optional guard condition field name |
2 Instance Methods
current_state()
Returns the current state as a string.
print(order.current_state()); # "pending"
is(state)
Check if the state machine is in a specific state.
if (order.is("pending")) {
print("Order is waiting for confirmation");
}
is_in([states])
Check if the state machine is in any of the given states.
if (order.is_in(["shipped", "delivered"])) {
print("Order is on its way or delivered");
}
set(key, value) & get(key)
Store and retrieve custom data in the state machine context.
order.set("customer_id", 12345);
order.set("total", 99.99);
order.set("items", ["Product A", "Product B"]);
let customer_id = order.get("customer_id");
let total = order.get("total");
history()
Get the state transition history.
let hist = order.history();
print(hist);
last_transition()
Returns information about the last transition as a hash.
order.confirm();
let last = order.last_transition();
# {from => pending, to => confirmed, event => confirm}
print(last["from"]); # "pending"
print(last["to"]); # "confirmed"
print(last["event"]); # "confirm"
can(event)
Checks if an event is available from the current state.
if (order.can("ship")) {
print("Order can be shipped");
}
# Returns: true or false
available_events()
Returns an array of events available from the current state.
let events = order.available_events();
# ["confirm", "cancel"] from pending state
3 Automatic Transition Methods
When you define transitions, Soli automatically creates methods for each event:
# From the transitions array above
order.confirm(); # Transitions from "pending" to "confirmed"
order.process(); # Transitions from "confirmed" to "processing"
order.ship(); # Transitions from "processing" to "shipped"
order.deliver(); # Transitions from "shipped" to "delivered"
order.cancel(); # Transitions from "pending" to "cancelled"
Calling an event from an invalid state will raise an error with a helpful message.
order.ship();
# Error: Cannot transition 'ship' from state 'pending'. Valid states: processing
4 Guard Conditions
Guard conditions allow you to conditionally enable transitions based on context values:
let transitions = [
{"event": "confirm", "from": "pending", "to": "confirmed"},
{"event": "ship", "from": "processing", "to": "shipped", "if": "can_ship"},
{"event": "deliver", "from": "shipped", "to": "delivered", "guard": "is_deliverable"},
{"event": "cancel", "from": "pending", "to": "cancelled"}
];
let order = create_state_machine("pending", states, transitions);
# Set guard conditions
order.set("can_ship", true);
order.set("is_deliverable", true);
# Transitions will fail if guard is false
order.set("can_ship", false);
order.ship();
# Error: Guard condition 'can_ship' is false
The if and guard keys are aliases and work identically. They specify a context field that must be true (or undefined) for the transition to proceed.
5 Example: Order Processing Workflow
class OrderWorkflow
def create_order(items: Array, total: Float)
let states = ["pending", "confirmed", "processing", "shipped", "delivered", "cancelled"];
let transitions = [
{"event": "confirm", "from": "pending", "to": "confirmed"},
{"event": "process", "from": "confirmed", "to": "processing"},
{"event": "ship", "from": "processing", "to": "shipped"},
{"event": "deliver", "from": "shipped", "to": "delivered"},
{"event": "cancel", "from": "pending", "to": "cancelled"}
];
let order = create_state_machine("pending", states, transitions);
order.set("items", items);
order.set("total", total);
order.set("created_at", clock());
order
end
def process_order(order: Any)
if (order.is("pending"))
order.confirm();
end
if (order.is("confirmed"))
order.process();
end
order
end
end
# Usage
let workflow = new OrderWorkflow();
let order = workflow.create_order(["Widget A", "Widget B"], 149.99);
workflow.process_order(order);
6 Example: Payment State Machine
let payment_states = ["pending", "authorized", "captured", "failed", "refunded"];
let payment_transitions = [
{"event": "authorize", "from": "pending", "to": "authorized"},
{"event": "capture", "from": "authorized", "to": "captured"},
{"event": "fail", "from": ["pending", "authorized"], "to": "failed"},
{"event": "refund", "from": ["captured", "failed"], "to": "refunded"},
{"event": "retry", "from": "failed", "to": "pending"}
];
let payment = create_state_machine("pending", payment_states, payment_transitions);
payment.set("amount", 99.99);
payment.set("currency", "USD");
payment.authorize();
print("Payment status: " + payment.current_state()); # "authorized"
payment.capture();
print("Payment status: " + payment.current_state()); # "captured"
7 Database Persistence
State machines can be persisted to the database for long-running workflows:
# Save state to database
let state_data = {
"machine_type": "Order",
"machine_id": order.get("id"),
"current_state": order.current_state(),
"context": {
"total": order.get("total"),
"items": order.get("items")
}
};
Order.update(order.get("id"), state_data);
# Load state from database
let saved_data = Order.find(order.get("id"));
let loaded_order = create_state_machine(
saved_data["current_state"],
["pending", "confirmed", "processing", "shipped", "delivered"],
[...]
);
loaded_order.set("total", saved_data["context"]["total"]);
loaded_order.set("items", saved_data["context"]["items"]);
8 Best Practices
Define Clear States
Use descriptive state names that reflect business states clearly.
Validate Transitions
Let the state machine reject invalid transitions automatically.
Use Context for Data
Store related data in the state machine context for easy access.
One Machine Per Workflow
Use separate state machines for independent workflows.
API Reference
create_state_machine(initial_state, states, transitions)
Creates a new state machine instance.
| Parameter | Type | Description |
|---|---|---|
| initial_state | String | Starting state |
| states | Array | List of valid state names |
| transitions | Array | Transition definitions with event, from, to keys |
Instance Methods
| Method | Description |
|---|---|
| current_state() | Returns current state as string |
| is(state) | Returns true if in given state |
| is_in([states]) | Returns true if in any of given states |
| set(key, value) | Stores data in context |
| get(key) | Retrieves data from context |
| history() | Returns transition history |
| last_transition() | Returns info about last transition |
| can(event) | Returns true if event is available |
| available_events() | Returns array of available events |
9 Interactive Demo
Try out the state machine interactive demo to see how transitions work in real-time:
Open State Machine Demo