> For the complete documentation index, see [llms.txt](/llms.txt).
> A full single-fetch corpus is available at [llms-full.txt](/llms-full.txt).
---
page_type: explanation
audience: both
sdk: python
title: Workflows, steps, and agents
description: The three primitives in AGNT5 — what they are, how they fit together, and which one you reach for when.
last_verified: 2026-04-29
---

> A **workflow** orchestrates work; a **step** is a checkpointed unit of that work; an **agent** is an LLM-driven loop that runs inside a step.

```python
import httpx

from agnt5 import Agent, FunctionContext, WorkflowContext, function, workflow

researcher = Agent(
    name="researcher",
    model="openai/gpt-4o-mini",
    instructions="Summarize the article in three sentences.",
)


@function
async def fetch_article(ctx: FunctionContext, url: str) -> str:
    # Side effect lives in a step. The workflow body never makes the HTTP call.
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.text


@function
async def summarize(ctx: FunctionContext, body: str) -> str:
    # The agent's non-determinism is contained inside this step.
    result = await researcher.run(body)
    return result.output


@workflow
async def research(ctx: WorkflowContext, url: str) -> str:
    article = await ctx.step(fetch_article, url)
    summary = await ctx.step(summarize, article)
    return summary
```

The `research` workflow is the orchestrator. `fetch_article` and `summarize` are steps. The `researcher` Agent is the agent. All three primitives appear in nine lines of orchestration code.

## The mental model

A **workflow** is a function decorated with `@workflow` that drives a sequence of steps to produce a result. Its body looks like ordinary `async` Python — variables, branches, loops, exception handlers — but AGNT5 treats it as a recipe to be executed reliably across crashes. The workflow body must be deterministic: replay must arrive at the same call sites in the same order, every time.

A **step** is the unit of work the workflow delegates. Steps are where side effects happen — HTTP calls, database writes, file I/O, LLM calls. Each call to `ctx.step(...)` checkpoints its input and output to the run's journal. On recovery, replay reads the checkpoint instead of re-running the side effect. You can pass a `@function`-decorated handler (the recommended form, shown above) or a name plus a callable when the step wraps arbitrary async work.

An **agent** is an LLM-driven loop: given instructions, a model, and optional tools, it picks actions and refines its output until it satisfies the goal or hits an iteration limit. Because an agent's output depends on the model's stochastic sampling, it is non-deterministic by definition. The way AGNT5 reconciles that with deterministic workflows is to host the agent's call inside a step. The agent runs once, the step journals its result, and the workflow body sees a deterministic value on replay.

## Why it works this way

Three primitives, one separation of concerns: **orchestrate, execute, decide**. The split exists so each piece can do exactly one job. The workflow stays deterministic and replay-safe; the step is the single chokepoint where non-determinism is allowed and recorded; the agent is free to be as stochastic as the model permits, because its output is captured the first time and replayed thereafter.

You could imagine an alternative where workflows directly call LLMs without a step boundary. AGNT5 rejects that shape because there would be no way to recover a crashed run without re-billing every prompt — and re-running a tool-using agent against the same input does not in general produce the same tool calls. The step boundary is what makes the durability guarantee tractable.

## Edge cases and gotchas

- **`ctx.step` versus `ctx.task`.** Older code in this repository uses `ctx.task(...)`. New code uses `ctx.step(...)`. Both still work; lead with `ctx.step` everywhere.
- **An agent is not a peer of a workflow.** Agents always run inside a step boundary, even when invoked directly from a `@function`. There is no `ctx.agent(...)`; you call `Agent.run(...)` (or its async variant) from inside a `@function`, and the workflow reaches the agent via `ctx.step`.
- **The word "step" is overloaded.** A *step* in a workflow (this page) is a checkpointed call. A *reasoning step* inside an agent loop is one iteration of the agent's plan-act-observe cycle. They are not the same thing — when ambiguity matters, say "workflow step" or "agent iteration".
- **Agents calling agents are still inside steps.** When one agent uses another agent as a tool, or when one agent hands off to another, the whole chain runs inside the step that invoked the first agent. The journal records one step result, not a sub-tree.
- **`agent` is lowercase in prose.** The Python class is `Agent`; in body text the noun is `agent`, never "AI agent" or "Agent".

## Related concepts

- [Durable execution](/docs/concepts/durable-execution.md) — what the step boundary buys you.
- [Determinism — why workflows have rules](/docs/concepts/determinism.md) — what the workflow body is and is not allowed to do.
- [Event sourcing and replay](/docs/concepts/event-sourcing-and-replay.md) — how the journal turns a crashed run into a resumable one.


**Primitives**: `@workflow` (orchestrator), `ctx.step("name", lambda: ...)` (boundary), `Agent` (LLM loop hosted inside a step)
**Composition**: workflow body calls steps; steps wrap agents, function calls, or other side-effecting work; agents invoke tools
**Determinism boundary**: workflow body deterministic; step bodies free to be non-deterministic

