Picking the right primitive
Functions, workflows, agents, and tools — what each one is for, and which one to reach for when.
AGNT5 has four primitives: functions (registered units of work), workflows (durable orchestrators that call functions), agents (LLM-driven loops), and tools (capabilities agents can invoke). Reach for the smallest one that does the job.
import httpx
from agnt5 import Agent, Context, FunctionContext, WorkflowContext, function, tool, workflow
@tool
async def fetch_url(ctx: Context, url: str) -> str:
# A tool: a capability the agent can call when it decides it needs to.
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.text
researcher = Agent(
name="researcher",
model="openai/gpt-4o-mini",
instructions="Use fetch_url to read articles. Summarize in three sentences.",
tools=[fetch_url],
)
@function
async def summarize(ctx: FunctionContext, url: str) -> str:
# A function: a registered unit of work the workflow checkpoints.
result = await researcher.run(f"Summarize {url}")
return result.output
@workflow
async def research(ctx: WorkflowContext, url: str) -> str:
# A workflow: the durable orchestrator. Calls functions through ctx.step.
return await ctx.step(summarize, url)The research workflow drives one step. The step runs the summarize function. The function runs the researcher agent. The agent calls the fetch_url tool when its plan requires reading a page. Four primitives, one chain of responsibility.
The mental model
The shortest path through the decision is a question: what is the smallest primitive that does this job?
- Plain Python. No durability, no checkpointing, no agent loop — write a function and call it. AGNT5 does not need to know about it.
- A function (
@function). A unit of work the runtime can call by name, log, retry, and (when invoked throughctx.step) checkpoint. Reach for this when something needs to be addressable from outside the process or callable from a workflow. - A workflow (
@workflow). A durable orchestrator that strings functions together and survives crashes between them. Reach for this when the multi-step process must be resumable — payment → fulfillment → notification, or research → summarize → publish. - An agent (
Agent). An LLM-driven loop that picks actions based on a goal and produces output. Reach for this when you cannot enumerate the steps in advance — the model decides what to do. - A tool (
@tool). A capability you make available to an agent. Reach for this when an agent needs to read or write something the LLM cannot do on its own (HTTP calls, database queries, calculations, other agents).
The four primitives compose along one axis: durability boundaries get coarser as you go up. A workflow’s step boundary is the unit of replay. Inside a step, a function runs. If that function runs an agent, the agent’s loop fires inside the function. The tools the agent invokes fire inside the loop. The runtime’s recovery model sees the step boundary only; everything below it runs fresh on retry.
Why this split
The split exists so each primitive does exactly one job. Workflows orchestrate but do not decide. Agents decide but do not orchestrate. Functions execute but do not loop. Tools provide capabilities but do not own state. When a primitive starts doing two jobs, the durability model breaks: a workflow that calls an LLM directly cannot be replayed without re-billing the prompt, and an agent that orchestrates other agents has no checkpoint between iterations.
Stratifying the four primitives also gives you four places to insert observability. Every workflow run produces a trace. Every step records its input and output. Every agent iteration logs its plan and tool calls. Every tool call logs its arguments and return value. The trace UI walks this hierarchy directly.
Edge cases and gotchas
- A workflow can call an agent directly.
await ctx.step(some_function_that_runs_an_agent, ...)is the canonical shape. The agent’s non-determinism lives inside the step boundary, where it is journaled and replayed. - An agent cannot call a workflow as a tool. Workflows are top-level, addressable units; tools are local capabilities the agent invokes during its loop. Use a
@function(which can itself trigger a sub-workflow) when you need that shape. @functionand@toolare not the same decorator.@functionis a registered, externally callable unit;@toolmarks a callable an agent is allowed to invoke. A handler can be both — register a@functionthat wraps a tool, and the same logic is reachable from clients and from agent loops.- “Step” is a verb, not a primitive.
ctx.step(handler, ...)is the call site that creates a checkpoint inside a workflow. The unit being called is a function; the checkpoint is the step. - Tools that mutate state must be idempotent. An agent’s plan may invoke the same tool multiple times in a single iteration. Tools touching external systems should rely on idempotency keys, conditional updates, or safe-by-design operations.
agentis lowercase in prose. The Python class isAgent; in body text the noun isagent, never “AI agent” or “Agent”.
Related concepts
- Functions — the registered, callable unit.
- Workflows — the durable orchestrator.
- Agents — the LLM-driven loop.
- Tools — capabilities agents can invoke.
- Durable execution — what the step boundary buys you.