Functions
Durable, retryable units of computation that survive failures
A Function is a unit of computation with automatic retries, structured logging, and optional idempotency guarantees.
Think of a Function like a serverless function with built-in retries, observability, and the option to guarantee exactly-once execution.
Why It Exists
When an LLM call times out, you want automatic retries with backoff. When a flaky API fails, you don’t want to write retry logic yourself. When you re-run a payment, you need exactly-once guarantees.
Functions handle this. Failures retry automatically. Every call is traced. Idempotency keys prevent duplicate execution.
When to Use
- Stateless operations like LLM calls, embeddings, or API requests
- Anything that needs automatic retries with backoff
- Expensive computations you don’t want to repeat
- Operations that must survive process crashes
Example
from agnt5 import function, FunctionContext
@function(retries=3, backoff="exponential")
async def summarize_document(ctx: FunctionContext, doc_url: str) -> str:
ctx.log("Fetching document", url=doc_url)
content = await fetch_document(doc_url)
summary = await call_llm(f"Summarize this:\n\n{content}")
return summaryCall it via the client:
from agnt5 import Client
client = Client()
# Basic call (retries on failure, but no idempotency guarantee)
result = client.run("summarize_document", {"doc_url": "https://example.com/paper.pdf"})
# With idempotency key (safe to retry - same key = same result)
result = client.run(
"summarize_document",
{"doc_url": "https://example.com/paper.pdf"},
idempotency_key="summarize-paper-abc123"
)How It Works
Functions provide four guarantees:
- Automatic Retries — Failures retry with configurable backoff (exponential, linear, constant)
- Idempotent Execution — Provide an
idempotency_keyfor exactly-once execution - Crash Recovery — If the process dies mid-execution, the function resumes on restart
- Full Observability — Every invocation emits OpenTelemetry traces with correlation IDs
sequenceDiagram
participant C as Client
participant G as Gateway
participant F as Function
participant S as State Store
C->>G: invoke(name, input, idempotency_key?)
alt Idempotency key provided
G->>S: Check key
alt Key exists
S-->>G: Return cached result
G-->>C: Result (no execution)
else Key not found
G->>F: Execute function
F-->>G: Result
G->>S: Store with key
G-->>C: Result
end
else No idempotency key
G->>F: Execute function
Note over G,F: Retries on failure
F-->>G: Result
G-->>C: Result
end
Configuration
FunctionContext
Add FunctionContext as the first parameter to access runtime utilities:
@function()
async def process_data(ctx: FunctionContext, data: dict) -> dict:
ctx.log("Processing", item_count=len(data)) # Structured logging
if ctx.attempt > 0:
ctx.log("Retry attempt", attempt=ctx.attempt)
return {"processed": True, "run_id": ctx.run_id}| Property | Description |
|---|---|
ctx.log(msg, **kv) | Structured logging with correlation IDs |
ctx.run_id | Unique execution identifier |
ctx.attempt | Current retry attempt (0-indexed) |
ctx.logger | Full Python logger (.debug(), .warning(), .error()) |
Context is optional. Simple functions can omit it:
@function()
async def add(a: int, b: int) -> int:
return a + bRetry Policies
Configure retry behavior with the decorator:
from agnt5 import function, RetryPolicy, BackoffPolicy, BackoffType
@function(
retries=RetryPolicy(max_attempts=5, initial_interval_ms=1000),
backoff=BackoffPolicy(type=BackoffType.EXPONENTIAL, multiplier=2.0)
)
async def call_flaky_api(query: str) -> dict:
return await external_api.search(query)| Strategy | Pattern | Use Case |
|---|---|---|
| Exponential | 1s → 2s → 4s → 8s | Rate limits, overloaded services |
| Linear | 1s → 2s → 3s → 4s | Predictable recovery time |
| Constant | 1s → 1s → 1s → 1s | Fast retry for transient errors |
Idempotency
Use idempotency keys to guarantee exactly-once execution:
# First call executes the function
result = client.run("charge_payment", {"amount": 99.99}, idempotency_key="order-456")
# Retry with same key returns cached result (no duplicate charge)
result = client.run("charge_payment", {"amount": 99.99}, idempotency_key="order-456")When to use: Payment processing, order creation, any operation where retries could cause side effects.
Key format: Use deterministic keys based on the logical operation — payment-{order_id}, email-{user_id}-{campaign_id}
Guidelines
Common Patterns
Function → External API (retries with backoff)
Function → LLM Call (use idempotency key to prevent duplicate charges)
Function → Database Query (naturally idempotent reads)
Workflow → Function (checkpointed, crash-recoverable)Common Pitfalls
-
❌ Don’t forget idempotency keys for non-idempotent operations — Without an idempotency key, retrying a failed request may execute the function again.
-
❌ Don’t store state inside functions — Functions are stateless. Use Entities for persistent state, or Workflows for execution state.
-
❌ Don’t orchestrate multiple functions from within a function — No checkpointing between calls. Use Workflows for multi-step orchestration.
What Functions Don’t Do
- Not for persistent state — Use Entities for state that survives across calls
- Not for multi-step orchestration — Use Workflows to chain functions with checkpointing
- Not for LLM reasoning loops — Use Agents for autonomous decision-making with tools
API Reference
@function()— Decorator to define a durable functionFunctionContext— Runtime context for logging and metadataRetryPolicy— Configure max attempts and intervalsBackoffPolicy— Configure backoff strategy (exponential, linear, constant)