Why Teff¶
Workflow as data. Agents as graphs.
Before we tell you what Teff is, let's talk about the problem it exists to solve — and why the obvious ways of building agents quietly stop working the moment your workflow outgrows a single function.
The problem: agents stop being trustworthy the moment they become real¶
The first LLM call is easy. The first agent is where most projects start bleeding:
- LLM output is non-deterministic — and so is your application. A
conditional that lives inside a prompt loop (
"if the user seems angry…") cannot be tested, traced, or repeated. You get a different answer every run and no way to explain why. - Business flow is buried in control-flow code. The moment you have two or
three agents, branching, retries and error handling become
if/elseandtry/exceptscattered acrossasyncfunctions. The logic of the application is now implicit — you can't read it, you can only step through it in a debugger. - A crash means starting over. A long pipeline dies at step 40 of 42 and the entire run state is gone, because it lived in process memory. You re-run everything, re-pay for every LLM token, and hope it works this time.
- You are flying blind. No per-node timeline, no prompt/response log, no way to replay "the run that returned the wrong answer two hours ago".
- Humans have no seat at the table. The one tool call that needs sign-off either fires silently, or the whole agent collapses because pausing to ask was never designed in.
None of this is about "bad code". They are the natural failure modes of a single pattern: execution logic embedded in imperative code.
The alternative: the graph owns the behaviour¶
Teff is a Python library that moves the intelligence out of the code and into the structure. A workflow is a graph: nodes that transform state, edges that decide what runs next, and a runtime that owns execution.
- The topology is data — YAML/JSON you can version, diff, validate and inspect without running anything.
- Every branch, retry, checkpoint and interrupt is visible in the graph, never hidden inside a loop.
- The runtime owns execution: scheduling, retries, checkpointing, parallelism, resuming, pauses, streaming, observability. You write business logic — never execution infrastructure.
That is where the tagline comes from: Workflow as data. Agents as graphs. An agent is just a graph the runtime happens to execute — inspectable, recoverable and testable, at any scale.
The pain it answers¶
| The pain (from real projects) | How Teff answers it |
|---|---|
| "One run picks a branch, the next doesn't — I can't reproduce it" | Branching lives on edges, a deterministic diagram, not a hidden if |
"The workflow logic is buried in nested async loops" |
The flow is data: you read the YAML, not the stack |
| "He crashed at step 40 — everything restarted from scratch" | Resume from the node that failed — finished work is never re-run |
| "A risky tool call needs a human before it fires" | Interrupt — pause mid-run, review, resume from that point |
| "I neither know it failed, nor what the LLM actually saw" | A checkpointed trace of every node, tool call and LLM request |
| "I want Ollama locally today, OpenAI in prod tomorrow" | Raw-HTTP providers (Ollama, OpenAI-compatible, Anthropic) — no SDKs, no lock |
| "The "framework" really means a platform I must host" | A library you import, not a server you operate |
The killer feature: resume from the node that failed¶
Most "durable" flavours restart the whole pipeline when anything breaks. Teff
is different: a checkpoint is written before every node. If a run crashes,
calling run() with the same checkpoint_id loads the last safe checkpoint
and continues from the exact node that failed — every completed node stays
completed.
from teff import Graph
from teff.checkpoint import SQLiteCheckpointer
from teff.node import Transform
nodes = {"shout": Transform(action="uppercase", input_key="text", output_key="loud")}
graph = Graph(nodes, edges=[], entry_point="shout")
cp = SQLiteCheckpointer("checkpoints.db")
# first run dies somewhere in the middle of the graph
await graph.run(state, checkpointer=cp, checkpoint_id="demo-run")
# same id: resumes from the exact node that crashed, and finishes
await graph.run(state, checkpointer=cp, checkpoint_id="demo-run")
Only the failed node is re-run (it never finished); everything before it is preserved. That means fewer LLM calls, faster recovery, and process failure becoming an annoyance rather than a catastrophe.
The same checkpoints power three more workflows: human-in-the-loop interrupts
(pause → operator → resume), run inspection (teff inspect), and time travel
(replay a finished run from any checkpoint, branch, and continue).
From a script to a full product — on the same primitives¶
The interesting thing is that this isn't only for the small graphs. The same five primitives — nodes, edges, checkpoints, interrupts, reducers — compose into production systems.
- Multi-agent supervisor. A
route()supervisor picks an agent per round and loops until the work is done — planning, coding, QA and an approval gate, all durable. It is described as a plain flow and a team of sub-flows (examples/supervisor_complexruns it offline with no API key). - A five-agent customer repair assistant. RAG over a materials catalog,
tools, streaming, a trace dashboard and a FastAPI app — a real, runnable
composition:
examples/applications/repair-ai-chat. - Support triage with human escalation. A ticket is grounded in your
knowledge base, a supervisor routes it to a specialist, then an escalation
gate pauses for a human when confidence is low — routing is index-something:
docs/recipes/support-triage.md.
The building blocks that made the small example above (a Transform node and
an SQLite checkpointer) are the same ones that make a five-agent assistant
work. The complexity does not leak into the core.
Even the same supervisor can be pure YAML — a team of agents included from their own files, quality-gated by a Map + loop, with an operator interrupt:
name: supervisor_complex
include: [team/lead.yaml, team/planner.yaml, team/coder.yaml]
steps:
- id: review_quality # LLM review step
type: llm_chat
config: { model: llama3.1:8b, output_key: critique, parse: true }
- id: route # conditional routing on the verdict
type: command
config:
routes:
- { when: "verdict=pass", goto: approve }
- { when: "verdict=needs_work", goto: refine }
- id: refine # product-rewrite loop with parallel fixes
type: loop
config:
key: verdict
until: pass
max_rounds: 3
body: [{ type: map, input_keys: [issues], output_key: fixes, processor: ... }]
- id: approve # human-in-the-loop gate
type: interrupt
config: { key: approved, strategy: { any_of: [approve, ok, yes] } }
Full version: examples/supervisor_complex/workflow.yaml.
The principles behind it¶
Teff was designed around a handful of firm rules:
- The graph owns behaviour. Business logic lives in the structure, never in prompts or hidden code.
- Nodes transform state. A node cannot tell "run X"; it only describes how the state should change.
- The runtime owns execution. Scheduling, retries, durability and observability are the framework's job, not yours.
- State is a plain dict. One serializable snapshot per run, no hidden behaviour.
The full frame is in the Constitution.
Not a comparison chart — a comparison of patterns¶
Vendors change, names change, and a table "Teff vs X" goes stale quickly. The comparison that stays true is the one against the pattern you would otherwise reach for:
| Imperative loops | Big platform SDKs | Teff | |
|---|---|---|---|
| Flow is visible | No (in code) | Usually yes | First-class — the graph IS the app |
| Durable / resumable | No | Yes, often on their runtime | Yes, no server: file/SQLite/PG |
| Crash recovery | Start over | Resume at job level | Resume from the failed node |
| Human-in-the-loop | Hand-rolled | Available | Interrupt → resume, a step like any other |
| Dependencies | Your code only | Heavy SDK + runtime | 4 core runtime deps, no SDKs, raw HTTP |
| Embeddable in your app | Yes | Only on their platform | Yes — you import us, a library |
| Observability | print/log | Their dashboard, you write adapters | Built in — traces, token usage, cost |
| Vendor lock-in | None | Strong | None |
That's the trade-off we are honest about: you don't get a "library of literally everything". You get structure, durability and instrumentability — without operating any server.
What we won't promise¶
Honest boundaries build trust:
- Teff is not a batch/ETL scheduler. A large job orchestrator is better served by dedicated tools.
- Teff is not an "everything but the kitchen sink" SDK with an integration for every vendor. We trade that ecosystem for minimal-to-go, inspectable, no-lock-in design.
- Teff is not a managed platform. Many of the biggest dependencies of an app are easy to own precisely because it stays a library.
When it fits¶
- You are shipping agent flows to production, where a silent wrong answer is unacceptable — where you need to observe, replay and pause.
- You want the flow to be reviewable data, not a pile of
asyncfunctions nobody can read. - You are local-first (Ollama in dev, any OpenAI-compatible endpoint in prod) and want no SDK or platform lock-in.
- You run workflows from Docker, cron, a queue or a FastAPI app — as a caller, not a host.