Get started Functions

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 summary

Call 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_key for 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 + b

Retry 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 function
  • FunctionContext — Runtime context for logging and metadata
  • RetryPolicy — Configure max attempts and intervals
  • BackoffPolicy — Configure backoff strategy (exponential, linear, constant)