Skip to content

Repository files navigation

Teff — Durable AI workflows as data

CI GitHub Stars PyPI Python License: MIT

Durable AI workflows as data.

Every agent project has the same arc: one for loop of LLM calls feels fine, a second agent needs branching, a third needs retries and state — and soon the logic of your product is buried in order-6 loops of async code that nobody can read, nobody can resume, and nobody can debug.

Teff is the other pattern: the workflow is data. Branching, retries, checkpointing and pausing live in the structure of a graph, not in the body of your functions. Run it — and when the process dies, it picks up exactly where it stopped.

Build stateful AI agents as YAML or Python graphs and run them safely in production:

            YAML            Flow (Python)
               │                 │
               └───────┬─────────┘
                       ▼
                  ┌─────────┐
                  │  Graph  │
                  └────┬────┘
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
         LLM          Tools         RAG
          │            │            │
          └────────────┼────────────┘
                       ▼
                  Checkpoint
                       │
                  crash / pause
                       │
                       ▼
                    Resume

Teff is async-first: nodes, tools and LLM calls are async, and a run can be checkpointed, inspected, paused, resumed and evaluated. The flow is data, every run is durable, and nothing is a black box. Read the whole story in Why Teff.

Why Teff?

Workflows are data

Branching, retries and error handling live on edges, not inside loops. Define a workflow in YAML and it becomes a versionable, diffable, reviewable artifact — generated or modified independently from your application code:

name: research

default_model: llama3.1:8b
default_provider: ollama

providers:
  - name: ollama
    type: ollama
    base_url: http://localhost:11434
    chat_path: /api/chat

state:
  initial:
    input: "What's new in durable AI workflows?"

steps:
  - agent:
      id: research
      system: "Research {input}"
      output_key: research

  - llm:
      id: summarize
      system: "Summarize {research}"
      output_key: summary

Run it from the terminal:

teff -f workflow.yaml

Durable by default

Long-running AI workflows fail. Teff writes a checkpoint before every node, so a run continues from the last completed node instead of starting over:

research ✓
search   ✓
LLM      ✓
report   ✗     ← crash / pause

        ↓ restart

research ✓
search   ✓
LLM      ✓
report   → resume   (only this node re-runs)
await graph.run(
    state,
    checkpointer=checkpoint_store,
    checkpoint_id="research-42",
)

File, SQLite and PostgreSQL checkpointers included — see Durable (checkpoints).

Python when you need it

YAML is optional. The same graph builds directly in Python — the Flow API chains nodes with short sugar methods (llm(), transform(), branch(), parallel(), map(), interrupt(), …), so an agent reads top-to-bottom:

from teff.flow import Flow
from teff.provider import ProviderRegistry

flow = (
    Flow(
        "research",
        providers=ProviderRegistry.from_presets("ollama"),
        default_provider="ollama",
        default_model="llama3.1:8b",
    )
    .llm(prompt="Summarize {text}", output_key="summary")
    .transform(action="uppercase", input_key="summary", output_key="result")
)

graph = flow.compile()
result = await graph.run({"text": "Hello world"})

Every node stays inspectable and the whole thing is one YAML export away — see Flow builder.

Beneath the Flow builder sits the low-level Graph APIGraph + Node + Edge — for hand-wiring every arrow, a custom Node subclass, or Command routing when you need full control. YAML, Flow and Graph all compile to the same runnable graph:

from teff.graph import Edge, Graph
from teff.node import Transform

graph = Graph(
    nodes={
        "trim": Transform(action="trim", input_key="title", output_key="loud"),
        "upper": Transform(action="uppercase", input_key="loud", output_key="result"),
    },
    edges=[Edge("trim", "upper")],
    entry_point="trim",
)
result = await graph.run(state={"title": "  hello  "})

See Graph API (low level).

Teff vs the alternatives

Not a comparison of names, but of patterns — the two ways people build agents today, and the one Teff offers:

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

The honest trade-off: you don't get a "library of literally everything" — you get structure, durability and instrumentability, without operating any server. Full story in Why Teff.

Built for real workflows

  • Async-first — nodes, tools and LLM calls are async
  • Agents — ReAct/tool-calling loops, multi-agent supervisors
  • Durable execution — checkpoints and resume from the failed node
  • Human-in-the-loopInterrupt to pause, resume, approve
  • Parallel & fan-out — concurrent branches and dynamic Map
  • RAG — pluggable vector stores and embeddings
  • Structured output — JSON Schema and Python types
  • Observability — traces, token usage, cost, local dashboard
  • MCP & skills — external tools, SKILL.md scoping
  • Evaluations — score workflows against datasets
  • CLI — validate, run, inspect and evaluate workflows
  • Multiple providers — Ollama, OpenAI, Anthropic, OpenAI-compatible
  • Embeddable — a library, not a hosted platform

Each is a full chapter in the docs.

Try it in 30 seconds

pip install teff          # or: uvx teff
teff -f workflow.yaml     # run a workflow

Or clone the repo and run a complete example:

git clone https://github.com/bzdvdn/teff
cd teff && uv sync
uv run teff run --file examples/hello_workflow/workflow.yaml

In action — a durable LLM run, resume and graph render (needs local Ollama):

Run + durable resume + graph (hello_llm example)
Teff CLI demo: run, resume, and graph an LLM workflow

Human-in-the-loop is a first-class citizen — the whole workflow, as data:

name: poem_chat
state:
  initial: { messages: [], poem: "", critic: {}, critic_note: "", decision: "" }
checkpoint: { type: file, path: data/checkpoints }

providers:
  - name: ollama
    type: ollama
    base_url: http://localhost:11434
    chat_path: /api/chat

default_provider: ollama
default_model: llama3.1:8b

steps:
  - context_builder: # topic + latest user feedback -> input
      id: compose
      messages_key: messages
      sections:
        poem: "Current poem"
        critic_note: "Critic feedback"
        answer: "New user feedback"
      output_key: topic
      reset_keys: [poem, critic_note, answer]

  - llm: # a step is just `type: config` — no edges to spell out
      id: poet
      system: >-
        You are an outstanding poet. Below is the topic and, if present,
        feedback — take it into account and rewrite the poem accordingly.
        Reply with ONLY the poem text, no explanations, no headings.
      prompt: "{topic}"
      output_key: poem

  - llm:
      id: critic
      system: >-
        You are a demanding poetry critic. Judge the poem: rhyme, rhythm,
        imagery. Reply with a single JSON object with fields "verdict" and
        "note": "verdict" is "ok" or "fix"; "note" is one-two short sentences
        telling the poet what to improve (or an empty string when it is ok).
      prompt: |-
        Poem:
        {poem}

        Reply with JSON.
      output_key: critic
      parse: true

  - transform:
      {
        id: take_note,
        action: json_get,
        input_key: critic,
        field: note,
        output_key: critic_note,
      }

  - append_assistant: {output_key: poem, messages_key: messages}

  - interrupt:
      id: approval
      key: answer
      prompt: >-
        Here is the poem:

        {poem}

        ---

        Do you like it? Say what you think (yes / of course / make it shorter
        / no)…
      strategy:
        llm: # judges free-form answers - no hard-coded keywords
          system: >-
            Classify how the user feels about the poem they were just shown.
            Return one JSON object with a boolean "ok" field.
            ok=true means the user accepts the poem. Examples of acceptance:
            "yes", "sure", "perfect", "nice", "not bad", "keep it".
            ok=false means they want changes or a rewrite. Examples of
            rejection: "no", "rewrite", "shorter", "change it", "the other".
            When in doubt, prefer ok=true for mildly positive or ambiguous
            replies and ok=false only when a change is clearly requested.
          user: 'The user said: "{answer}". The poem: {poem}'
          model: qwen2.5:7b
          provider: ollama
          schema:
            type: object
            properties: { ok: { type: boolean } }
            required: [ok]
        decision_key: decision
        pass_value: keep
        fail_value: rewrite

  - route:
      id: route
      routes:
        - { when: "decision=keep", goto: done }
        - { when: "decision=rewrite", goto: compose }
      goto: approval

  - transform:
      {
        id: done,
        action: value,
        value: "Poem done — hope you like it!",
        output_key: done,
      }

The steps above chain top-to-bottom automatically — a context_builder injects the topic + latest user feedback into compose, and the loop simply returns there; the router, loop and human gate are plain steps, not control-flow in code.

teff chat examples/poem_chat/workflow.yaml
# "write a poem about autumn"  → bot writes + pauses
# "make it shorter"            → loop rewrites it
# "yes, perfect"               → done
The same workflow, as a chat (poem_chat example)
Teff CLI demo: two-agent poem chat with human approval

Teff vs application code

Without Teff, every app re-implements the same infrastructure:

application
 ├── LLM calls
 ├── tool execution
 ├── retries
 ├── state
 ├── persistence
 ├── routing
 ├── resume logic
 └── observability

With Teff, the infrastructure is the runtime. You own business logic; Teff owns execution:

workflow.yaml ──► Teff ──► state, graph execution, checkpoints,
                            tools, retries, tracing

Production examples

The same few primitives scale to real systems:

More in Examples — and if you want it written end to end, Recipe: from zero to a FastAPI agent in 10 minutes.

CLI

teff -f workflow.yaml                       # run (the default command)
teff -f workflow.yaml --trace              # run + JSON trace to stderr
teff validate workflow.yaml                # validate without running
teff eval workflow.yaml --data dataset.jsonl --exact
teff inspect --checkpoint '{"type":"sqlite","path":"cp.db"}' --checkpoint-id run-1
teff new support-ai                        # scaffold a FastAPI app
teff daemon -f workflow.yaml --interval 60  # restart a run every 60s
teff obs-server --db traces.db --port 8001  # observe trace dashboard
teff version

Install & extras

Python >= 3.11. Core runtime depends only on httpx, jsonschema, pyyaml, and typer.

pip install teff
# extras: teff[stores-qdrant] etc. for one RAG store, teff[embedding] for all,
# teff[pg-checkpoint] for PostgreSQL checkpoints, teff[mcp] for MCP tools,
# teff[tools] for built-in tools, teff[all] for everything except docs

uv tool install teff          # global `teff` CLI
uvx teff -f workflow.yaml     # run on the fly without installing

Docker

Official images on Docker Hub for every v* tag — one build, six variants:

Image Contents Commands
bzdvdn/teff core + teff[tools] the teff CLI
bzdvdn/teff-fastapi core + teff[fastapi] a FastAPI server
bzdvdn/teff-worker core + teff[queue] celery workers
bzdvdn/teff-obs core + teff[observability] teff obs-server dashboard
bzdvdn/teff-rag core + teff[stores-qdrant,tools,rag-pdf] slim RAG build
bzdvdn/teff-all every docs-less extra full optional surface
docker run --rm -v "$PWD:/workflow" bzdvdn/teff:latest run -f /workflow/workflow.yaml

Development

uv sync --all-extras            # install deps (incl. optional extras used by tests)
uv run pytest tests/ -q         # tests — the suite is fully offline (no API keys)
uv run ruff check .              # lint
uv run ruff format --check .     # formatting
uv run mypy .                    # types
uv run mkdocs build              # build these docs

Docs & community

Status

0.2.0 — added the flow.yaml authoring layer: a sugar surface mirroring the Python Flow builder (single-key idiom steps, team: / supervisor:, parallel: / map: / loop:, interrupt: + strategy:), compiled via teff build -f flow.yaml into the low-level graph.yaml artifact; plus the flow.team() Python API and AgentRole. Every example now ships a validated sugar + low-level twin. The low-level workflow.yaml surface remains unchanged and stable.

Coming next: durable conversations with built-in memory, tighter tool ergonomics, and more vector stores + plugins. Want to shape the roadmap or have a workflow Teff can't express yet? Open an issue — every report steers the project.

License

MIT — see LICENSE.

About

Durable AI workflows as data. Build stateful agents as YAML or Python graphs: checkpointing, crash-resume, human-in-the-loop, RAG, MCP, async-first — a library, not a platform.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages