State Machines
What Are State Machines?
A state machine is a way to model workflows that have different stages (states) and rules for moving between them.
Think of it like a traffic light:
- States: Red, Yellow, Green
- Transitions: Red → Green, Green → Yellow, Yellow → Red
- Rules: You can't go from Red directly to Yellow
In your bot, state machines help you manage complex processes like:
- Order fulfillment (pending → paid → shipped → delivered)
- Support tickets (open → assigned → resolved → closed)
- User trials (trial → active → cancelled)
Why Use State Machines?
The Problem
Without state machines, managing workflows is messy:
❌ Old Way (30+ nodes):
Get var.ticket_status
↓
If status == "open"?
↓ Yes
Set status = "assigned"
Get history
Add entry
Set history
Update timestamp
Send notification
↓ No
Send error...
You have to:
- Manually track the current state
- Manually validate every transition
- Manually log history
- Manually handle errors
Result: Lots of repeated code, easy to make mistakes
The Solution
With state machines, it's automatic:
✅ New Way (3 nodes):
[state.create: support_ticket]
↓
[state.transition: "assigned"]
↓ Success
[Send: "Ticket assigned!"]
The system automatically:
- ✅ Validates the transition is allowed
- ✅ Logs the history
- ✅ Updates timestamps
- ✅ Saves to session
Result: 90% fewer nodes, no mistakes
Real-World Example: Support Ticket Bot
The Workflow
Let's build a simple support ticket system.
States:
open- Ticket just createdassigned- Someone is working on itresolved- Issue is fixedclosed- Ticket is done
Allowed Transitions:
- open → assigned
- open → cancelled (user cancels)
- assigned → resolved
- assigned → escalated (urgent)
- resolved → closed
- resolved → reopened (wasn't actually fixed)
Step 1: Create the State Machine
When a user creates a ticket, use state.create:
Node: state.create
Config:
{
"machine_type": "support_ticket",
"instance_id": "ticket_{{var.ticket_counter}}",
"states": {
"open": {
"transitions": ["assigned", "cancelled"]
},
"assigned": {
"transitions": ["resolved", "escalated"]
},
"resolved": {
"transitions": ["closed", "reopened"]
},
"closed": {
"transitions": []
}
},
"initial_state": "open",
"data": {
"description": "{{input_text}}",
"user_id": "{{user.id}}"
}
}
What This Does:
- Creates a new ticket instance
- Sets initial state to "open"
- Stores the ticket description and user ID
- Returns the instance ID
Flow:
[/ticket command] → [state.create] → NEXT
↓
InstanceID: ticket_123
State: open
Step 2: Assign the Ticket
When an admin assigns the ticket, use state.transition:
Node: state.transition
Config:
{
"instance_id": "{{var.current_ticket}}",
"to_state": "assigned",
"metadata": {
"assigned_to": "{{admin.id}}",
"assigned_at": "{{sys.timestamp}}"
}
}
What This Does:
- Checks if transition from "open" → "assigned" is allowed
- If yes: Updates state, logs history, saves to session
- If no: Returns "Invalid" output
Flow:
[/assign command] → [state.transition] → Success
↓ ↓
FromState: open [Send: "Assigned!"]
ToState: assigned
Invalid
↓
[Send: "Can't assign {{ErrorMessage}}"]
Step 3: Check Current State
Use state.get to check what state a ticket is in:
Node: state.get
Config:
{
"instance_id": "{{var.current_ticket}}"
}
Flow:
[/status command] → [state.get] → Found
↓ ↓
CurrentState [Send: "Status: {{CurrentState}}"]
↓
NotFound
↓
[Send: "No ticket found"]
Step 4: View History
Use state.history to see all transitions:
Node: state.history
Config:
{
"instance_id": "{{var.current_ticket}}",
"limit": 5
}
Output:
{
"history": [
{"from": "open", "to": "assigned", "timestamp": "2025-12-25T10:00:00Z"},
{"from": "assigned", "to": "resolved", "timestamp": "2025-12-25T15:00:00Z"}
]
}
Advanced Features
Guards (Conditional Transitions)
Sometimes you only want to allow a transition if a condition is true.
Example: Only allow closing a ticket if it's been resolved for 24+ hours
{
"states": {
"resolved": {
"transitions": ["closed", "reopened"],
"guards": {
"closed": "hours_since_resolved >= 24"
}
}
}
}
What Happens:
- If guard passes → Transition succeeds
- If guard fails → "GuardFailed" output
Flow:
[state.transition: to="closed"]
↓
Guard: hours_since_resolved >= 24?
↓ Yes
Success
↓ No
GuardFailed
↓
[Send: "Must wait 24h before closing"]
Actions (Triggers)
Execute actions when entering or exiting a state.
Example: Send notification when ticket is assigned
{
"states": {
"assigned": {
"on_enter": "notify_assignee",
"on_exit": "log_reassignment"
}
}
}
Outputs:
OnEnterAction: "notify_assignee"OnExitAction: "log_reassignment"
Flow:
[state.transition: to="assigned"]
↓ Success
OnEnterAction = "notify_assignee"
↓
[trigger.webhook: event={{OnEnterAction}}]
Timeouts (Auto-Transitions)
Automatically transition after a time period.
Example: Auto-escalate if not resolved in 24 hours
{
"states": {
"assigned": {
"timeout": {
"duration": "24h",
"to_state": "escalated"
}
}
}
}
What Happens:
TimeoutAttimestamp is calculated- After 24 hours, transition to "escalated" (requires scheduler)
Note: Timeout auto-execution requires scheduler integration (coming soon). For now, you can check
TimeoutAtmanually.
Helper Nodes
state.delete
Remove a completed workflow.
Example:
[Ticket Closed]
↓
[state.delete: instance_id="{{var.ticket_id}}"]
Use Cases:
- Clean up completed orders
- Remove cancelled processes
state.list
Find all instances matching criteria.
Example: Count pending tickets
{
"machine_type": "support_ticket",
"state": "pending"
}
Output:
Instances: Array of matching ticketsCount: Number found
Use Cases:
- Dashboard: "You have {{Count}} pending tickets"
- Filter: Show all "shipped" orders
state.update_data
Add or update metadata.
Example: Store customer email
{
"instance_id": "{{var.ticket_id}}",
"data": {
"customer_email": "user@example.com",
"priority": "high"
}
}
Behavior:
- Merges data (doesn't replace)
- New fields are added
- Existing fields are updated
Complete Bot Example
Here's a full support ticket bot flow:
1. User creates ticket:
[/ticket <description>]
↓
[state.create: support_ticket, data={description}]
↓ NEXT
[Save InstanceID to var.current_ticket]
↓
[Send: "Ticket #{{InstanceID}} created"]
2. Admin assigns ticket:
[/assign <ticket_id>]
↓
[state.transition: to="assigned", metadata={admin_id}]
↓ Success
[Send: "Ticket assigned to you"]
↓ Invalid
[Send: "Can't assign: {{ErrorMessage}}"]
3. Admin resolves ticket:
[/resolve <ticket_id>]
↓
[state.transition: to="resolved"]
↓ Success
[Send: "Ticket marked as resolved"]
4. User confirms:
[/close <ticket_id>]
↓
[state.transition: to="closed"]
↓ Success
[state.delete: instance_id]
↓
[Send: "Ticket closed. Thank you!"]
Common Use Cases
1. E-Commerce Orders
States: pending → paid → shipped → delivered
Transitions:
- pending → paid (payment received)
- paid → shipped (order dispatched)
- shipped → delivered (confirmation)
- Any → cancelled (user cancels)
2. User Lifecycle
States: trial → active → premium → churned
Transitions:
- trial → active (signup complete)
- active → premium (upgrade)
- active → churned (cancelled)
- premium → churned (downgrade + cancel)
3. Game Quests
States: locked → available → active → completed → claimed
Transitions:
- locked → available (level requirement met)
- available → active (quest started)
- active → completed (objective done)
- completed → claimed (reward collected)
4. Approval Workflows
States: draft → submitted → review → approved → published
Transitions:
- draft → submitted (user submits)
- submitted → review (admin reviews)
- review → approved (admin approves)
- review → draft (rejected, back to editing)
- approved → published (goes live)
Best Practices
✅ Do
-
Use descriptive state names:
- ✅
pending,processing,completed - ❌
state1,state2
- ✅
-
Keep states simple:
- ✅ 3-6 states
- ❌ 20+ states (too complex)
-
Delete completed instances:
[Workflow Done] → [state.delete] -
Store instance ID in a variable:
[state.create] → InstanceID
↓
[Set var.current_order]
❌ Don't
-
Don't create too many instances:
- Limit: ~10-20 per user
- Use
state.listto count and clean up
-
Don't use state machines for simple yes/no:
- Use
flow.conditioninstead
- Use
-
Don't forget to handle "NotFound":
[state.get]
↓ NotFound
[Send: "No active workflow"]
Troubleshooting
"Instance not found"
Cause: Instance was deleted or never created
Solution:
[state.get]
↓ NotFound
[state.create: new instance]
"Transition not allowed"
Cause: Trying invalid transition (e.g., "closed" → "open")
Solution: Check your state definitions. Add the transition if needed.
"Guard failed"
Cause: Guard expression evaluated to false
Solution:
[state.transition]
↓ GuardFailed
[Send: "Cannot proceed: {{ErrorMessage}}. Guard: {{GuardExpr}}"]
Summary
State machines let you:
- ✅ Manage complex workflows with ease
- ✅ Reduce 25+ nodes to 3 nodes
- ✅ Get automatic validation and history
- ✅ Never worry about invalid transitions
7 Nodes:
state.create- Start workflowstate.transition- Move between statesstate.get- Check current statestate.history- View audit trailstate.delete- Clean upstate.list- Find instancesstate.update_data- Store metadata
Next Steps:
- Check Node Reference