Workflow
A Workflow is a graph of steps that you define in code. Your code,
not a model, decides which step runs next. Use one when you know the
steps in advance and want every run to follow them predictably; use an
Agent when the model should decide what to do.
The two share the same telemetry and audit log, and you can put one
inside the other.
| Agent | Workflow | |
|---|---|---|
| Who controls the loop | The LLM | You |
| Iteration shape | Open-ended (think/act/observe) | Explicit graph (nodes + edges) |
| Branching | Tool calls + reasoning | add_router + classifiers |
| Predictability | Hard to bound | Deterministic given inputs |
| Best for | Research, free-form reasoning | Compliance flows, multi-step approval chains, fixed pipelines |
Pick based on who should be in control, not on style. See Agent vs Workflow. Production systems usually need both, composed together.
Three ways to build one
from loomflow import Workflow, step1. Plain Python with @step
Write the flow as ordinary async Python. Decorating a function with
@step gives it its own telemetry span and audit entries when it runs
inside a workflow; control flow stays as if/await/return:
from loomflow import step
@step
async def classify(text: str) -> str: ...
@step
async def respond(text: str, label: str) -> str: ...
async def my_workflow(text: str, user_id: str) -> str:
label = await classify(text)
return await respond(text, label)2. Sugar constructors for common shapes
No graph builder needed; one call returns a fully-wired Workflow:
wf = Workflow.chain([classify, respond])
wf = Workflow.route(classifier, {"a": fn_a, "b": fn_b})
wf = Workflow.parallel([fn_a, fn_b, fn_c], merge=combine)3. Explicit graph builder
For loops, multi-stage branching, or flows where the graph’s shape is itself what gets reviewed (multi-stage approval chains, for example):
from loomflow import Workflow, END
wf = Workflow("triage")
wf.add_node("classify", classify) # returns {"team": ..., "ticket": ...}
wf.add_node("billing", billing)
wf.add_node("tech", tech)
wf.set_start("classify")
wf.add_router(
"classify",
lambda item: item["team"],
{"billing": "billing", "tech": "tech"},
)
wf.add_edge("billing", END)
wf.add_edge("tech", END)Read more
anyio task groups.Explicit graph builderadd_node / add_edge / add_router / set_start. Cycles supported with safety caps.The @step decoratorTelemetry + audit on plain Python control flow. Transparent outside a live workflow.Composition with AgentAgent-as-step (pass Agent to add_node); workflow-as-tool (wf.as_tool()).Visualizationwf.to_mermaid() / to_dot() / Jupyter inline rendering. The graph as a diagram, no extra tools.Agent vs WorkflowWhen to reach for which. Cost / determinism / audit trade-offs.Shared memory across the graph
Pass memory= to a workflow and every nested Agent step that
didn’t bring its own inherits it. Episodes / facts written by one
agent become recall-able by the next without per-agent wiring:
from loomflow import Workflow, Agent, InMemoryMemory
mem = InMemoryMemory()
agent_a = Agent("Stage 1", model="gpt-4.1-mini")
agent_b = Agent("Stage 2", model="gpt-4.1-mini")
wf = Workflow.chain([agent_a, agent_b], memory=mem)
await wf.run("hi", user_id="alice", session_id="conv-1")
# Both agents wrote to / read from `mem`.Explicit always wins. The resolution order is Agent(memory=...) >
Workflow(memory=...) > the agent’s per-instance default.
Ambient response tone
Same propagation pattern, different concern. response_tone= on a
workflow flows to every nested Agent that didn’t bring its own:
wf = Workflow.chain(
[researcher, writer, reviewer],
response_tone="executive",
)
# All three agents inherit "executive" tone unless one set its own.Resolution: agent.run(response_tone=) per-call > Agent(response_tone=) >
Workflow(response_tone=) > none. Uses the same contextvar
propagation as memory=, reset in finally so tones do not leak
across runs. See Agent reference: response_tone
for the preset effects and free-form passthrough.
Composition with Agent
- Agent inside a Workflow. Pass an
Agentinstance toadd_node; the framework calls.run(input)automatically and threads the liveRunContext(user_id/session_id/ metadata) through to the inner agent run. - Workflow inside an Agent. Call
wf.as_tool()to get aToolthe agent can invoke. The whole workflow runs as one tool call from the agent’s perspective.
Both directions share the same telemetry spans, audit-log entries and
user_id partitioning. In a trace, workflow steps are
loom.workflow.step spans tagged pattern="workflow", so you can tell
which decisions your code made and which the model made.
Cycles are built-in. Refinement loops (A → B → classify → (C|D|END) → B) are supported with two safety caps: max_steps
(total steps per run) and max_visits_per_node (cap on any single
node’s re-entries). Hit either and the workflow raises
RuntimeError with the offending node named. Hitting one usually
means your routing logic never picks the branch that ends the loop.