> 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: Functions
description: Registered, addressable units of work — the smallest primitive in AGNT5 and the building block steps and tools both reach for.
last_verified: 2026-04-30
---

> 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.

```python
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:

```python
# 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.

## Related concepts

- [Workflows](/docs/concepts/workflows.md) — the durable orchestrator that wraps function calls in step boundaries.
- [Tools](/docs/concepts/tools.md) — how a function becomes available to an agent.
- [Durable execution](/docs/concepts/durable-execution.md) — what the step boundary buys a function call.
- [Picking the right primitive](/docs/concepts/picking-the-right-primitive.md) — when to reach for a function versus a workflow or agent.


**Code primitives**: `@function` decorator (Python), `function(...)` factory (TypeScript); same callable can wear `@tool` for agent use
**Addressable by**: registered name; invoked via `ctx.step(...)` from workflows or attached as `agent.tools`
**Boundary**: a function call from inside a workflow is journaled (durable); a function call from outside (e.g., script) is plain Python/TS

