State Machines
State machines are the foundation of AgentML. By explicitly defining states, transitions, and behaviors, you create agents with deterministic, auditable, and formally verifiable behavior.
Why State Machines?
Traditional LLM applications rely on emergent behavior from prompts, leading to unpredictable and difficult-to-debug systems. State machines provide:
- Deterministic Behavior: Same inputs produce same state paths
- Explicit Control Flow: Clear understanding of agent behavior
- Formal Verification: Prove correctness of agent logic
- Better Debugging: Trace exact state transitions
- Compositional: Build complex agents from simple machines
Basic Concepts
States
A state represents what the agent is currently doing:
<agentml xmlns="github.com/agentflare-ai/agentml"
datamodel="ecmascript">
<!-- The agent starts in the 'idle' state -->
<state id="idle">
<!-- State content here -->
</state>
<state id="processing">
<!-- State content here -->
</state>
<state id="responding">
<!-- State content here -->
</state>
</agentml>
Transitions
Transitions move the agent from one state to another when events occur:
<state id="idle">
<!-- When 'user.message' event occurs, transition to 'processing' -->
<transition event="user.message" target="processing" />
</state>
Entry and Exit Actions
Execute code when entering or leaving a state:
<state id="processing">
<!-- Execute when entering this state -->
<onentry>
<log expr="'Started processing'" />
<assign location="startTime" expr="Date.now()" />
</onentry>
<!-- Execute when leaving this state -->
<onexit>
<log expr="'Finished processing in ' + (Date.now() - startTime) + 'ms'" />
</onexit>
<transition event="complete" target="idle" />
</state>
Complete Example
Here's a simple customer support agent:
<agentml xmlns="github.com/agentflare-ai/agentml"
datamodel="ecmascript"
xmlns:gemini="github.com/agentflare-ai/agentml-go/gemini">
<datamodel>
<data id="user_input" expr="''" />
<data id="response" expr="''" />
</datamodel>
<!-- Start in 'idle' state -->
<state id="idle">
<onentry>
<log expr="'Waiting for user input'" />
</onentry>
<transition event="user.message" target="processing">
<assign location="user_input" expr="_event.data.message" />
</transition>
</state>
<state id="processing">
<onentry>
<log expr="'Processing: ' + user_input" />
<gemini:generate
model="gemini-2.0-flash-exp"
location="_event"
promptexpr="'You are a helpful assistant. User: ' + user_input" />
</onentry>
<transition event="action.response" target="responding">
<assign location="response" expr="_event.data.message" />
</transition>
<transition event="action.error" target="error" />
</state>
<state id="responding">
<onentry>
<log expr="'Bot: ' + response" />
</onentry>
<transition target="idle" />
</state>
<state id="error">
<onentry>
<log expr="'An error occurred'" />
</onentry>
<transition target="idle" />
</state>
</agentml>
Hierarchical States
Nest states within states to manage complexity:
<agentml xmlns="github.com/agentflare-ai/agentml"
datamodel="ecmascript">
<!-- Top-level state -->
<state id="authenticated">
<!-- Nested states -->
<state id="browsing">
<transition event="select.product" target="viewing_product" />
</state>
<state id="viewing_product">
<transition event="add.to.cart" target="cart" />
<transition event="back" target="browsing" />
</state>
<state id="cart">
<transition event="checkout" target="payment" />
<transition event="continue.shopping" target="browsing" />
</state>
<state id="payment">
<transition event="payment.success" target="order_complete" />
<transition event="payment.failed" target="cart" />
</state>
<final id="order_complete" />
<!-- Global transition for authenticated states -->
<transition event="logout" target="logged_out" />
</state>
<state id="logged_out">
<transition event="login.success" target="authenticated" />
</state>
</agentml>
Benefits of hierarchical states:
- Shared transitions: Common transitions apply to all nested states
- Shared entry/exit actions: Execute code for entire state groups
- Better organization: Group related states together
Parallel States
Execute multiple state machines simultaneously:
<agentml xmlns="github.com/agentflare-ai/agentml"
datamodel="ecmascript">
<!-- Both state machines run in parallel -->
<parallel id="monitoring">
<!-- First parallel region: Health checks -->
<state id="health_monitor">
<state id="checking">
<onentry>
<log expr="'Running health check'" />
</onentry>
<transition event="health.ok" target="healthy" />
<transition event="health.degraded" target="unhealthy" />
</state>
<state id="healthy">
<transition target="checking">
<after delay="30s" />
</transition>
</state>
<state id="unhealthy">
<onentry>
<raise event="alert.health_issue" />
</onentry>
<transition target="checking">
<after delay="10s" />
</transition>
</state>
</state>
<!-- Second parallel region: Performance monitoring -->
<state id="performance_monitor">
<state id="measuring">
<onentry>
<log expr="'Measuring performance'" />
</onentry>
<transition event="perf.good" target="optimal" />
<transition event="perf.slow" target="degraded" />
</state>
<state id="optimal">
<transition target="measuring">
<after delay="60s" />
</transition>
</state>
<state id="degraded">
<onentry>
<raise event="alert.performance_issue" />
</onentry>
<transition target="measuring">
<after delay="30s" />
</transition>
</state>
</state>
</parallel>
</agentml>
Parallel states are useful for:
- Monitoring systems: Multiple independent checks
- Multi-modal interfaces: Handle voice and text simultaneously
- Background tasks: Process data while maintaining UI state
Conditional Transitions
Use conditions to control which transition executes:
<state id="validate">
<onentry>
<assign location="input" expr="_event.data.input" />
</onentry>
<!-- Conditions evaluated in order -->
<transition cond="input.length === 0" target="empty_input" />
<transition cond="input.length < 5" target="too_short" />
<transition cond="input.length > 100" target="too_long" />
<transition target="valid" /> <!-- Default: no condition -->
</state>
History States
Remember and return to the last active substate:
<state id="application">
<history id="app_history" type="shallow" />
<state id="editing">
<state id="text_mode" />
<state id="voice_mode" />
</state>
<state id="paused">
<transition event="resume" target="app_history" />
</state>
<transition event="pause" target="paused" />
</state>
History types:
- shallow: Remember top-level state only
- deep: Remember entire nested state configuration
Final States
Mark the end of a state machine or sub-machine:
<state id="processing_task">
<state id="step1">
<transition event="step1.done" target="step2" />
</state>
<state id="step2">
<transition event="step2.done" target="complete" />
</state>
<!-- Final state -->
<final id="complete">
<donedata>
<param name="result" expr="task_result" />
</donedata>
</final>
</state>
<!-- This transition executes when 'processing_task' reaches final state -->
<transition event="done.state.processing_task" target="next_task">
<assign location="last_result" expr="_event.data.result" />
</transition>
Delayed Transitions
Execute transitions after a delay:
<state id="waiting">
<!-- Transition after 5 seconds -->
<transition target="timeout">
<after delay="5s" />
</transition>
<!-- Can still transition immediately on event -->
<transition event="user.input" target="processing" />
</state>
Delay formats:
- Milliseconds:
1000ms
- Seconds:
5s
- Minutes:
2m
- Hours:
1h
State Machine Composition
Invoke other state machines as services:
<state id="orchestrator">
<onentry>
<!-- Invoke a worker agent -->
<invoke id="worker1" type="scxml" src="worker_agent.aml">
<param name="task_id" expr="current_task.id" />
</invoke>
</onentry>
<!-- Handle completion -->
<transition event="done.invoke.worker1" target="aggregate_results">
<assign location="worker_result" expr="_event.data" />
</transition>
<!-- Handle errors -->
<transition event="error.execution.worker1" target="handle_error" />
</state>
This enables:
- Code reuse: Share common state machines
- Separation of concerns: Isolate functionality
- Parallel execution: Multiple workers simultaneously
- Fault isolation: Errors don't crash parent
Best Practices
- Keep states focused: Each state should have one clear purpose
- Use meaningful names:
awaiting_user_input
notstate_1
- Document transitions: Add comments explaining event flow
- Provide fallback transitions: Handle unexpected events gracefully
- Use hierarchical states: Group related states for maintainability
- Limit nesting depth: Too many levels make debugging difficult
- Test state coverage: Ensure all states are reachable
- Use final states: Clearly mark completion points
- Log state transitions: Aid debugging with
<log>
statements - Validate early: Check conditions before expensive operations
Debugging State Machines
Validation
Validate your state machine before running:
agentmlx validate agent.aml
Runtime Snapshots
Capture snapshots to see the current state:
agentmlx run agent.aml --save-snapshots ./debug --snapshot-interval 1
Logging
Add logging to track state transitions:
<state id="processing">
<onentry>
<log expr="'Entered processing state at ' + Date.now()" />
</onentry>
<onexit>
<log expr="'Exiting processing state'" />
</onexit>
</state>
Next Steps
- Explore Events & Schemas for data validation
- Learn about Namespaces for extending functionality
- Understand Token Efficiency for optimization
- Read Document Structure for syntax details