Choosing your abstraction¶
Teff gives you several ways to build the same graph. The rules below are the decision map — pick the smallest abstraction that expresses your problem, and escalate only when you need more control. "Smaller" here means: more declarative, more checked, less boilerplate.
1. Build the graph: YAML vs Flow DSL vs graph.py¶
All three produce the same Graph. They differ in where the structure lives
and how much control you have.
| You want to… | Use |
|---|---|
| Declare topology as data, own it via git/diff | YAML (workflow.yaml) |
| Build ergonomically from Python, still export to YAML | Flow DSL (Flow) |
| Use custom node classes, dynamic wiring, full runtime control | graph.py (Graph + Node) |
Rules of thumb:
- Start in YAML if the workflow is mostly built-in nodes (
llm,transform,map,parallel,interrupt) and you want ops ownership of the topology.teff validatechecks it;observability:/checkpoint:are one line. - Use the Flow DSL when you are already in Python:
flow.llm(...)/flow.transform(...)/flow.branch(...)/flow.map(...)give you type-checked construction, andflow.to_yaml()still lets you export the graph to YAML when ops needs it. - Drop to
graph.pywhen you need a customNodesubclass, imperative wiring (edges computed at build time), or hooks only the low-level API exposes. YAML and Flow are views over the same runtime — you can mix them (from_yaml()→ run,flow.compile()→ same engine).
2. Pick the agent shape¶
The most common confusion. An "agent" in Teff is just a node that calls an LLM in a loop.
| Shape | What it does | Use when… |
|---|---|---|
LLM node |
One model call, no loop | A single step in a larger graph |
Agent |
Conversational loop (multi-turn chat state) | Chat assistant; you manage turns via state |
ReActAgent |
Tool-calling loop (think → act → observe → repeat) | Agent that must call tools to answer |
ToolExec |
Executes a single tool call from state | Build your own tool loop at low level |
route() (Flow) |
Supervisor: route state[key] to one of N sub-agents |
Multi-agent / heterogeneous workers |
harness() / react() (Flow) |
Opinionated agent loops with tool orchestration | Get a capable agent in 2 lines |
Supervisor |
Node that delegates one turn to a worker | Hand-rolled supervisor with custom policy |
Decision flow:
- One model call? →
LLM. - Loops, and needs tools? →
ReActAgent(orFlow.react()). - Multiple specialist agents? →
Flow.route()(see supervisors). - Loop but no tools, chat-like? →
Agent. - Need full control? → build a
@node-based loop withToolExec.
3. Concurrency and fan-out¶
| Construct | Semantics | Use when… |
|---|---|---|
Parallel |
Fixed set of branches run concurrently; results merged by reducers | You know the branches at build time |
Map |
Dynamic fan-out over state[input_keys], then reduce |
The number of items is only known at runtime |
SubFlow / add_flow() |
Reuse a sub-graph as one node | Sharing a reusable sub-workflow |
Map and Parallel are not interchangeable: Map iterates a runtime list
(plan → one pass per plan item), Parallel fans out fixed branches (e.g.
summarize title and body at once). If the fan-out is data-driven, use
Map; if it is structural, use Parallel.
4. Durable state: checkpoints¶
| Checkpoint type | Persistence | Use when… |
|---|---|---|
| none | in-memory | Short, stateless demo graphs; tests |
file |
JSON files on disk | Dev, simple deploys, no DB wanted |
sqlite |
single .db file |
Default production choice; multi-tenant via owner |
pg |
PostgreSQL | Multi-instance, central state, shared DB |
sqlite_history / pg_history |
full run history | Time travel (time_travel example), audit |
Rules:
- Anything interactive or long-running must be durable — add
checkpoint:(or aCheckpointerin Python). ownerscoping gives you multi-tenant checkpoints on one store (JSONFileCheckpointer/SQLiteCheckpointer/PGCheckpointertake an owner).- Start with
sqlite, move topgwhen you outgrow one process. See durable.
5. Humans in the loop¶
| Approach | What happens | Use when… |
|---|---|---|
Flow.interrupt(key) |
Run pauses, resumes with a new value | Approve / edit a result (HITL) |
Agent approval |
Each tool call requires sign-off | Safety-critical tool usage |
Ask validation |
Validate the answer (regex/equals/LLM) before resuming | Bad input must be rejected/re-asked |
Combine them: interrupt for who decides, Ask for is the answer valid.
See human_in_loop and
ask_strategies.
6. Resilience¶
| Mechanism | Scope | Use when… |
|---|---|---|
Workflow retry: / Retry node |
step-level retries with backoff | Transient failures (rate limits, timeouts) |
__error__ edge / Fallback |
catch and redirect on failure | Handle a failing node explicitly |
Extract |
repair malformed LLM output | Structured output that can fail |
Layer them: Retry for transient, __error__ for expected-but-handleable,
model failover at the provider level for whole-model issues.
7. Retrieval¶
| Vector store | When to use |
|---|---|
memory / sqlite |
Local dev, small corpora, no external service |
chroma/faiss/lance/… |
Feature parity on your infra |
qdrant/milvus/weaviate/pinecone/pgvector |
Scale, teams, managed services |
Pick the store, not the app: the RAG agent is identical across stores — see rag_stores.
Quick-reference matrix¶
| I need to… | Start with | Escalate to |
|---|---|---|
| Declare a pipeline | YAML | Flow DSL |
| Custom node logic | @node / Node |
graph.py wiring |
| One tool-using agent | Flow.react() |
ReActAgent / custom loop |
| Several agents | Flow.route() |
Supervisor node |
| Fixed parallel work | Parallel |
— |
| Fan out over runtime data | Map |
— |
| Long/interactive run | checkpoint: sqlite |
pg + history |
| Pause for human input | Flow.interrupt() |
Ask validation |
| Survive flaky LLMs | workflow retry: |
provider failover |
| Ground answers in documents | RAGTool + sqlite |
managed vector store |