Prompt-defined State Machines
How to build agents that handle complex workflows without writing a single line of code. Everything lives in the prompt.
The Big Idea
Forget writing if-statements and for-loops. With Gola, you describe what should happen in plain English (well, structured English), and the LLM figures out how to do it.
Here's what replaces traditional code:
- Your prompt becomes the program
- MCP tools are your standard library (files, APIs, databases)
- The LLM is your interpreter
Why this is actually cool:
- Change logic without redeploying code
- Non-programmers can understand and modify workflows
- The same pattern works for any workflow
- No debugging state machines at 3 AM
The Problem: Agents Are Chaotic
Ever built a multi-step agent? Then you've seen these problems:
- "What was I doing?" - Agent forgets its current task mid-conversation
- Groundhog Day - "Let me search for flights" (for the 5th time)
- Amnesia - "Sorry, what was your destination again?" (user said it 3 messages ago)
- Domino effect - One API error and everything falls apart
- Non-deterministic nightmares - Works great 80% of the time (useless in production)
The Solution: Make the Prompt Your State Machine
Instead of hoping the agent remembers what to do, force it to follow a state machine defined entirely in the prompt. No code, just very specific instructions.
Traditional vs Prompt-Defined
In a traditional system, you'd write code like:
# Traditional approach - requires coding
def handle_booking(state):
if state.status == "gathering_details":
details = get_user_input()
state.update(details)
state.status = "searching_flights"
elif state.status == "searching_flights":
flights = search_api(state.origin, state.destination)
# ... etc
With Gola's prompt-defined approach:
# Prompt-defined approach - no code needed
mcp_servers:
- name: "filesystem" # Provides state persistence
- name: "http" # Provides API access
prompts:
system: |
When status is "gathering_details":
1. Ask user for origin, destination, dates
2. Use filesystem.write to save state
3. Update status to "searching_flights"
See the difference? No compilation, no deployment. Change the prompt and the behavior changes.
Core Design Patterns
Pattern 1: The Four-Step Execution Cycle
This is the secret sauce. Force your agent to follow these steps every single time:
Your execution on every turn MUST follow this exact 4-step cycle:
1. **ALWAYS READ THE STATE FIRST**: Use the `read_file` tool to load the JSON from `./itineraries/session_{session_id}/current_state.json`
2. **CHECK THE `status` FIELD**: The value determines your single next action
3. **EXECUTE ONE STEP**: Perform the single action required for the current status
4. **ALWAYS WRITE THE STATE LAST**: Update the JSON state and save it back
Why agents stay controlled:
- Can't act without checking state first
- Can only do one thing per turn (no confusion)
- State always gets saved (no memory loss)
Pattern 2: State-to-Action Mapping
Tell the agent EXACTLY what to do in each state. No room for creativity:
### Status: `gathering_trip_details`
**Action:**
1. Ask user for: origin, destination, departure date, if round trip
2. **IMMEDIATELY** call `report_progress` with reason "awaiting_input"
3. Do NOT write state or perform any other actions - just wait
**When user provides details:**
1. Update state with their information
2. If cities are ambiguous, ask for clarification
3. Convert cities to IATA codes
4. Write state with status "searching_departing_flights"
The tricks that work:
- Yell at it with CAPS ("MUST", "IMMEDIATELY", "NEVER")
- Number everything (agents follow numbered lists)
- Be explicit about next states ("set status to X")
- Add guards ("When user responds..." not "If user responds...")
Pattern 3: Progress Reporting Hooks
These built-in functions let you control when the agent waits vs continues:
Use `report_progress` with these specific reasons:
### When to STOP and WAIT:
- **"awaiting_input"**: When asking for trip details
- **"pending_choice"**: When user needs to select from options
- **"need_clarification"**: When resolving ambiguity
### When to CONTINUE:
- **"response_complete"**: After providing information
- **"results_displayed"**: After showing search results
What this gives you:
- Agent actually waits for user input (novel concept)
- Your UI knows what's happening
- No more agents racing ahead without the user
Pattern 4: Tell It What NOT to Do
Sometimes you need to be the bad cop:
## CRITICAL RULES
1. **NEVER output JSON or technical details to the user**
2. **NEVER call report_progress before showing content**
3. **NEVER try to modify a completed itinerary**
4. **NEVER write the same status repeatedly**
5. **ALWAYS show information FIRST, then call report_progress**
How to be effectively negative:
- List the DON'Ts explicitly
- Put critical rules at the top (agents read top-down)
- Show examples of wrong behavior to avoid
Pattern 5: Define Your State Like a Database Schema
Give the agent the COMPLETE structure, even with null values:
All state files must include these fields, even if their values are `null` initially.
{
"session_id": "session_1234567890",
"status": "gathering_trip_details",
"current_date": "2025-08-04",
"origin_city": null,
"destination_city": null,
"departure_date": null,
"return_date": null,
"flights": {
"departure": [],
"return": []
},
"accommodation_options": [],
"selected_accommodation": null
}
Critical points:
- Include every field from the start (even if null)
- Use real example values (agents learn by example)
- Keep the same structure throughout (no surprises)
Advanced Patterns
Conditional Branching
Yes, you can do if-then-else in prompts:
### Status: `selecting_accommodation`
**When user selects:**
1. Parse the selected property
2. Update state with accommodation details
3. If round trip, status "searching_return_flights"
4. If one-way, status "confirming_itinerary"
The LLM handles the logic. You just describe it clearly.
Error Recovery
Build resilience into state transitions:
### Error Handling
After 3 consecutive API failures:
1. Inform the user of the issue
2. Set status to "error_state"
3. Call `assistant_done` with status "error"
If you receive "loop detected" errors:
1. Read the current state file
2. Check what status you're in
3. Move to the next appropriate status
Good news: Gola's guardrails catch most of these automatically. Loop detection, context overflow, API failures - all handled. You just need to tell the agent what to do when things work.
Session Lifecycle
How to handle "start over" gracefully:
### After Completion (assistant_done called)
If the user sends another message:
1. Forget the previous session_id
2. Start fresh as if it's a new conversation
3. Do NOT attempt to read the previous state file
4. Begin with status `gathering_trip_details`
Make the Agent Check Its Own Work
## VALIDATION CHECKLIST
Before calling `report_progress`, ask yourself:
- Have I shown the user what they need to see?
- Have I asked a clear question they can answer?
- Will the user understand what they're confirming?
If NO to any of these, show the information FIRST.
Building Your Own
MCP Tools You'll Need
At minimum, you need state persistence:
mcp_servers:
# Essential: State persistence
- name: "filesystem"
tools: ["read_file", "write_file"]
# Optional: External APIs
- name: "http"
tools: ["get", "post"]
# Optional: Database access
- name: "postgresql"
tools: ["query", "insert", "update"]
The Template That Actually Works
Copy this, fill in the blanks:
You are a [AGENT ROLE] that [PRIMARY FUNCTION]. You use a state file to track progress.
## CRITICAL RULES
1. [RULE 1 - Most important]
2. [RULE 2]
3. [etc...]
## WORKFLOW OVERVIEW
Your workflow follows these main steps:
1. [High-level step 1]
2. [High-level step 2]
3. [etc...]
## STATE MANAGEMENT
### Execution Cycle
[4-step cycle customized for your use case]
### State Transitions
#### Status: `[state_name_1]`
**Action:**
1. [Specific action 1]
2. [Specific action 2]
**Next Status:** `[next_state]`
#### Status: `[state_name_2]`
[etc...]
## ERROR HANDLING
[Error recovery instructions]
## STATE STRUCTURE
[Complete JSON schema with examples]
## VALIDATION
[Self-check instructions]
Real Examples That Work
These have been built and actually run in production:
Booking Systems
- States: searching → selecting → confirming → complete
- MCP Tools:
filesystem
(state),http
(APIs),email
(confirmations) - No Code: Entire flow defined in prompts, executed via MCP
Application Wizards
- States: personal_info → employment → references → review → submit
- MCP Tools:
filesystem
(drafts),database
(submissions),pdf
(documents) - Validation: LLM validates against schemas in prompts
Customer Support
- States: identify_issue → gather_details → attempt_solution → escalate_or_resolve
- MCP Tools:
zendesk
(tickets),slack
(escalation),knowledge_base
(solutions) - Dynamic Routing: LLM interprets issue and chooses appropriate MCP tools
Onboarding Flows
- States: welcome → profile_setup → preferences → tutorial → ready
- MCP Tools:
database
(user data),email
(welcome),analytics
(tracking) - Zero Backend Code: MCP servers handle all infrastructure
Common Failures and Fixes
Agent ignores state file
Fix: Start every state instruction with "FIRST, read the state file..."
Agent does 5 things at once
Fix: Add "STOP HERE" after each action
Agent forgets previous conversation
Fix: Add a conversation_summary
field that accumulates
Stuck in a loop
Fix: Gola's loop detection catches this, but also add "If you see this state 3 times, move to error_state"
Unclear what happens next
Fix: End every state with "Set status to [specific_next_state]"
Context gets too big
Fix: Context management handles this automatically, but keep your state files small
Agent wanders off-script
Fix: Workflow drift protection helps, but be explicit: "You may ONLY set status to X or Y"
Testing Your State Machine
Test the Happy Path First
- Run through the entire flow perfectly
- Verify each state transitions correctly
- Check the final state file is complete
Then Break Things
- Give invalid input ("pizza" when asked for a date)
- Stop the agent mid-flow and restart
- Simulate API failures (disconnect network)
- Try to confuse it ("go back", "start over", "wait no")
Check the Safeguards
- Does it wait for user input?
- Does it recover from errors?
- Can you resume an interrupted session?
- Does completion actually complete?
Pro-tip: The best test? Give it to someone who's never seen it before. They'll break it in ways you never imagined.
Why This Actually Works
The state machine pattern that's been around forever has been moved from code to prompts. The result?
- Business logic in English - Product managers can actually read it
- No deployment - Change prompt, save file, done
- Version control - Your prompts ARE your documentation
- Debugging - Read the state file, read the prompt, find the problem
Is it perfect? No. Will it handle every edge case? No. But for 90% of multi-step workflows, it's simpler, faster, and more maintainable than traditional code.
The key insight: LLMs are really good at following detailed instructions. So give them detailed instructions. The state machine pattern just makes those instructions systematic and reliable.
Start simple: Build a two-state agent first. Get that working. Then add complexity. Before you know it, you'll have a production-ready workflow running entirely from a prompt.