Tutorial — build your first workflow¶
This walks you through writing, running, and reading a real workflow. We use a
deterministic example — no LLM, no network, no API key — so you can run it
the moment teff is installed. Everything here lives, runnable, in
examples/hello_workflow/.
1. The job we want to automate¶
Given a note (some text), decide whether it is a single-line or multi-line
note and tag it accordingly, then mark the job done.
That's deliberately boring. The point is the shape — a count, a branch, and a converge — which every real workflow repeats.
2. Express it as YAML (the "workflow as data" view)¶
name: hello-offline
state:
initial:
text: "just one line"
steps:
- id: count
type: transform
config: {action: count_lines, input_key: text, output_key: lines}
- id: single
type: transform
config: {action: value, value: single-line note, output_key: note}
- id: multi
type: transform
config: {action: value, value: multi-line note, output_key: note}
- id: status
type: transform
config: {action: value, value: done, output_key: status}
edges:
- {from: count, to: single, condition: "lines=1"}
- {from: count, to: multi, condition: "lines!=1"}
- {from: single, to: status}
- {from: multi, to: status}
The nodes are the steps; the arrows are the edges. An edge can carry a
condition — here lines=1 routes to single, anything else to multi.
Run it with the built-in CLI:
teff run --file examples/hello_workflow/workflow.yaml
{"text": "just one line", "lines": "1", "note": "single-line note", "status": "done"}
That's the whole result: every node's output_key has landed on the state.
YAML needs only the data — teff infers everything else from the node types.
3. The same graph with the Flow builder¶
For workflows with real logic, the Flow DSL reads top-to-bottom
like a script. Transform is invoked as .transform(...), and the branch is
.branch("lines", Case("1").add(...), default=...); both ends re-join at
.converge(...):
import asyncio
from teff.flow import Flow
from teff.flow.case import Case
from teff.node import Transform
flow = (
Flow("hello-offline")
.transform(action="count_lines", input_key="text", output_key="lines", id="count")
.branch(
"lines",
Case("1").add(
Transform(action="value", value="single-line note", output_key="note"),
id="single",
),
default=Transform(action="value", value="multi-line note", output_key="note"),
)
.converge(Transform(action="value", value="done", output_key="status"), id="status")
)
graph = flow.compile()
result = asyncio.run(graph.run(state={"text": "two\nlines"}))
print(result["note"]) # multi-line note
Same topology — but now the code tells the story left-to-right.
4. The same graph, hand-wired¶
The low-level API is explicit about every node and every edge. Useful when you need full control (custom edges, one-off behaviour):
from teff.graph import Edge, Graph
from teff.node import Transform
graph = Graph(
nodes={
"count": Transform(action="count_lines", input_key="text", output_key="lines"),
"single": Transform(
action="value", value="single-line note", output_key="note"
),
"multi": Transform(action="value", value="multi-line note", output_key="note"),
"status": Transform(action="value", value="done", output_key="status"),
},
edges=[
Edge("count", "single", "lines=1"),
Edge("count", "multi", "lines!=1"),
Edge("single", "status"),
Edge("multi", "status"),
],
entry_point="count",
)
result = asyncio.run(graph.run(state={"text": "only"}))
print(result["note"]) # single-line note
5. See the topology¶
Neither guess the shape — ask teff:
teff graph examples/hello_workflow/workflow.yaml
prints the same graph as Mermaid, and teff run shows the state through the whole run.
6. Where this leads¶
Now swap the Transform count/value nodes for real work:
llm_chat/ agent for a reasoning step,- a
tool(search, fetch) for an action, checkpointto make it durable (pause on anInterrupt, resume later),parallel/mapfor fan-out.
Easiest next step: open
choosing-your-abstraction to pick
when each view is right, then build the runnable
support_triage example (supervisor
+ human-in-the-loop + checkpoints), which uses this same branch-and-converge
shape under the hood.