Skip to main content

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

teff-0.2.0.tar.gz (2.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

teff-0.2.0-py3-none-any.whl (387.6 kB view details)

Uploaded Python 3

File details

Details for the file teff-0.2.0.tar.gz.

File metadata

  • Download URL: teff-0.2.0.tar.gz
  • Upload date:
  • Size: 2.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for teff-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3103bf21705930cdfd464c913ade487ebaab192590d65a4a1bc5d7cf6920d995
MD5 61852ad46d770b86c1e9d6ee4d8006e3
BLAKE2b-256 23656ca43e52b7e7bb7934e476737c3007cde5240ce9ee9c70871931a393b1f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for teff-0.2.0.tar.gz:

Publisher: release.yml on bzdvdn/teff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file teff-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: teff-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 387.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for teff-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a20047d9ce2136075fb193d713e34e767e33d46b4ddffb8125543a24144b59fb
MD5 dc2079ecbdf4e21c91103ee15b7c2196
BLAKE2b-256 d5d261c7522a2c2bef4a5d733982067c2a739a29a5e7fb30f0c5d90e0850a6fd

See more details on using hashes here.

Provenance

The following attestation bundles were made for teff-0.2.0-py3-none-any.whl:

Publisher: release.yml on bzdvdn/teff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.0

2 files

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page