Skip to main content

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 created
  • assigned - Someone is working on it
  • resolved - Issue is fixed
  • closed - 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:

  • TimeoutAt timestamp is calculated
  • After 24 hours, transition to "escalated" (requires scheduler)

Note: Timeout auto-execution requires scheduler integration (coming soon). For now, you can check TimeoutAt manually.


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 tickets
  • Count: 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

  1. Use descriptive state names:

    • pending, processing, completed
    • state1, state2
  2. Keep states simple:

    • ✅ 3-6 states
    • ❌ 20+ states (too complex)
  3. Delete completed instances:

    [Workflow Done] → [state.delete]
  4. Store instance ID in a variable:

    [state.create] → InstanceID

    [Set var.current_order]

❌ Don't

  1. Don't create too many instances:

    • Limit: ~10-20 per user
    • Use state.list to count and clean up
  2. Don't use state machines for simple yes/no:

    • Use flow.condition instead
  3. 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:

  1. state.create - Start workflow
  2. state.transition - Move between states
  3. state.get - Check current state
  4. state.history - View audit trail
  5. state.delete - Clean up
  6. state.list - Find instances
  7. state.update_data - Store metadata

Next Steps: