Get started Functions

Functions

Registered, addressable units of work — the smallest primitive in AGNT5 and the building block steps and tools both reach for.

A function is a @function-decorated handler — a registered, addressable unit of work the runtime can call by name. It is AGNT5’s smallest primitive.

import httpx

from agnt5 import FunctionContext, function


@function
async def fetch_article(ctx: FunctionContext, url: str) -> str:
    """Fetch the body of a URL."""
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.text

The same handler is reachable from two call sites:

# Direct invocation: a client calls the function by name.
result = await client.run("fetch_article", {"url": "https://example.com"})

# Workflow invocation: the same handler is checkpointed inside a step.
@workflow
async def research(ctx: WorkflowContext, url: str) -> str:
    body = await ctx.step(fetch_article, url)
    return summarize(body)

The mental model

A function is a Python async def you have decorated and registered. Once decorated, the runtime knows the handler exists, knows its name, and can route invocations to it. The decorator does two jobs: it adds the handler to the global registry (_FUNCTION_REGISTRY in the SDK source), and it gives the handler the FunctionContext that the runtime needs to thread tracing, retries, and logging through.

The same registered function plays two roles. Standalone, a client invokes it by name (client.run("fetch_article", ...)); the runtime spins up one execution, hands the handler a FunctionContext, and returns the result. Inside a workflow, the workflow calls ctx.step(fetch_article, url); the workflow’s runtime captures the input, runs the function, and writes the output to the journal. Same code, different host.

Durability is not in the decorator. A function called standalone runs once: if its process crashes mid-execution, the call fails and there is no automatic resume. Durability comes from the ctx.step boundary in a workflow, which is what causes the input and output to be journaled and the call to be skipped on replay. The @function decorator gives you registration; the workflow’s ctx.step gives you durability.

Why it works this way

Splitting registration from durability lets one handler serve every role AGNT5 needs from it. A handler can be called by a client, called by a workflow inside a step, used as a tool by an agent (when also decorated with @tool), or scheduled by cron — without changing its signature. The runtime treats the handler as a leaf node and the caller decides what guarantees wrap it.

The split also keeps @function cheap. Not every callable in your application warrants the cost of journaling. A pure deterministic helper (parsing a string, computing a hash) gains nothing from being checkpointed. Decorating it with @function registers it for invocation but does not impose durability overhead unless a workflow opts in.

Edge cases and gotchas

  • A standalone function is not durable. If you client.run("fetch_article", ...) and the worker crashes, you get an error and no automatic retry. Durable execution requires wrapping the call in a workflow’s ctx.step.
  • FunctionContext is not WorkflowContext. The function context is stateless: it has log(), sleep() (non-durable), and tracing helpers, but no step(). To checkpoint inside business logic, write a workflow and call the function from it.
  • ctx.sleep() inside a function is plain asyncio.sleep. It will not survive a crash. Use a workflow if you need durable timers.
  • Names must be unique in the registry. Two @function async def fetch_article declarations in the same worker raise at registration time. Pass @function(name="fetch_article_v2") to disambiguate.
  • A @function can also be a @tool. Decorating a handler with both makes it externally callable (registry entry) and agent-callable (tool list). The decorators do different jobs and stack cleanly.
  • Functions return whatever they return. The runtime serializes the return value when the function is the target of a step or a remote call. Stick to JSON-serializable shapes (primitives, dicts, lists, dataclasses) — opaque Python objects round-trip poorly.
  • Workflows — the durable orchestrator that wraps function calls in step boundaries.
  • Tools — how a function becomes available to an agent.
  • Durable execution — what the step boundary buys a function call.
  • Picking the right primitive — when to reach for a function versus a workflow or agent.