Every state declares what it must accomplish. The framework validates before advancing. Failed states retry with feedback. Like TDD for your agent pipeline.
| Concept | What it means |
|---|---|
| StateSpec | A node in your workflow. Has an objective, tools, an execute function, and acceptance criteria (key results). |
| KeyResult | A verifiable deliverable. Can be a programmatic check (lambda) or an LLM-evaluated description. The "test" in TDD. |
| Transition | How states connect. Static (str), conditional (dict with branches), or dynamic (callable). Supports loops and bidirectional flows. |
| BoundLLM | LLM adapter scoped to a state's tools. ctx.llm.run_with_tools() handles the full tool-call loop. |
| SharedContext | Key-value store shared across all states. No globals, no singletons. |
| Validator | Checks key results after execution. Rule-based (code), LLM-based (subjective), or custom protocol. |
When a state fails validation, it re-executes with ctx.feedback explaining exactly what went wrong. The LLM self-corrects.
States can branch, loop back, call each other. Return {"_transition": "branch"} to route conditionally at runtime.
No required runtime deps. Bring your own LLM client. OpenAI and LiteLLM adapters ship as optional extras.
One command. Pick an LLM adapter or bring your own.
pip install fsm-agent-flow[openai]
uv add fsm-agent-flow[openai] works too. For LiteLLM: pip install fsm-agent-flow[litellm]
Two states. Research gathers info, writing produces a report. Each state declares what "done" looks like.
from fsm_agent_flow import Workflow, StateSpec, KeyResult from fsm_agent_flow.llm.openai import OpenAIAdapter # Tools are plain functions — auto-converted to JSON Schema def search(query: str) -> str: """Search the web for information.""" return your_search_api(query) # Each state declares its acceptance criteria research = StateSpec( name="research", objective="Gather information on the topic", key_results=[ KeyResult("length", "At least 200 chars", check=lambda o: len(str(o)) > 200), KeyResult("sourced", "Cites real sources"), # LLM-validated ], execute=lambda ctx: ctx.llm.run_with_tools( "Research the topic thoroughly.", ctx.input), tools=[search], max_retries=2, is_initial=True, ) writing = StateSpec( name="writing", objective="Write a structured report", key_results=[ KeyResult("sections", "Has headings", check=lambda o: str(o).count("#") >= 2), ], execute=lambda ctx: ctx.llm.run_with_tools( "Write a structured report.", str(ctx.input)), is_final=True, ) # Wire it up and run llm = OpenAIAdapter(model="gpt-4o") result = Workflow( objective="Research and report", states=[research, writing], transitions={"research": "writing"}, llm=llm, validator_llm=llm, ).run("quantum computing") print(result.history[-1].output)
Real state machines go back and forth. Return {"_transition": "label"} from execute to pick a branch.
transitions = {
"check": {"need_weather": "fetch", "ready": "respond"},
"fetch": {"wrong_city": "fetch", "default": "check"},
"respond": None,
}
# In your execute function, pick the branch:
def check(ctx):
if ctx.shared.get("weather"):
return {"_transition": "ready", "answer": "Sunny!"}
return {"_transition": "need_weather"}
Launch the node editor in your browser. Drag states, connect transitions, set key results. Export as Python or JSON.
python -m fsm_agent_flow.editor
Click + State in the toolbar. Each node becomes a StateSpec.
Drag from an output port to an input port. For branching, add named conditions in the sidebar.
Double-click a node. Set objective, tools, key results, max retries. Toggle initial/final.
Hit Export .py for runnable Python code, or Save for portable JSON. Ctrl+S works too.
Paste this into your project's CLAUDE.md. Claude Code will understand the full API and write workflows for you.
# fsm-agent-flow TDD/OKR-driven agentic workflow framework. See the reference docs: @https://raw.githubusercontent.com/NewJerseyStyle/FSM-agent-flow/main/CLAUDE.md @https://raw.githubusercontent.com/NewJerseyStyle/FSM-agent-flow/main/docs/claude/rules/adapters.md @https://raw.githubusercontent.com/NewJerseyStyle/FSM-agent-flow/main/docs/claude/rules/workflows.md @https://raw.githubusercontent.com/NewJerseyStyle/FSM-agent-flow/main/docs/claude/rules/validation.md @https://raw.githubusercontent.com/NewJerseyStyle/FSM-agent-flow/main/docs/claude/rules/tools.md
@ syntax tells Claude Code to fetch and read those files as context. It will learn the full architecture — state specs, transitions, tools, validation, adapters — and can write complete workflows, debug issues, and build custom adapters in your project.
| Pattern | Code |
|---|---|
| LLM + tools loop | ctx.llm.run_with_tools(system, user_msg) |
| Single LLM call | ctx.llm.chat([Message(role="user", content="...")]) |
| Share data | ctx.shared.set("key", val) / ctx.shared.get("key") |
| Conditional route | return {"_transition": "branch_name", ...} |
| Nested OODA agent | run_ooda(ctx, task="...", tools=[...], max_cycles=3) |
| Retry feedback | Check ctx.feedback — it explains why the last attempt failed |
| Save workflow state | wf.context.to_dict() / WorkflowContext.from_dict(d) |
| Visual editor | python -m fsm_agent_flow.editor |
| JSON roundtrip | workflow_to_json(wf) / workflow_from_json(data, llm=...) |
| Generate Python | workflow_to_python(json_data) |
Read the README, browse the examples, or just pip install and go.