# Resonate Basic Durable World Usage Python

> Core patterns for writing Resonate durable functions in Python — async/await syntax, Context API reference (run, rpc, detached, promise, sleep, dependency injection), and the determinism rules that make recovery work. Use when writing any function body registered with r.register().

- Skill: `resonatehq/resonate-basic-durable-world-usage-python` (Agent Skill)
- Install (CLI): `npx skillmds@latest add resonatehq/resonate-basic-durable-world-usage-python`
- Raw SKILL.md: https://api.skillmd.com/api/skills/resonatehq/resonate-basic-durable-world-usage-python/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: Apache-2.0
- Author: resonatehq (https://skillmd.com/u/resonatehq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/resonatehq/resonate-basic-durable-world-usage-python

---


# Resonate Basic Durable World Usage — Python

## Overview

Durable functions are the payload of Resonate. They are `async def` coroutines that the SDK treats as a sequence of durable checkpoints. Every `await ctx.run(...)` or `await ctx.rpc(...)` is a save point: if the process crashes, the function resumes from the last successful checkpoint on another process or a restart.

This skill covers the Context API (`ctx.*`) that you use inside those functions. The ephemeral-world counterpart (`Resonate` class, registration, top-level invocation) lives in `resonate-basic-ephemeral-world-usage-python`.

## The contract

Every durable function:

1. Is registered with `r.register(fn)` in the ephemeral world.
2. Takes `ctx` (the Context) as its first parameter; any other parameters after that are the invocation args.
3. Uses `await` to interact with `ctx`. Each awaited `ctx.*` call is a durable checkpoint.
4. Is deterministic when replayed: no direct `time.time()`, no `random.random()`, no raw network I/O outside `ctx.run`.

Minimal shape:

```python
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from resonate.context import Context

async def process_order(ctx: Context, order_id: str) -> dict:
    # step 1 — durable: its result is persisted
    order = await ctx.run(load_order, order_id)

    # step 2 — durable: runs locally in the same worker
    charge = await ctx.run(charge_card, order)

    # step 3 — ordinary Python control flow; free to branch, loop, raise
    if not charge["ok"]:
        raise RuntimeError(f"payment failed: {charge['reason']}")

    return {"order_id": order_id, "status": "paid"}
```

If this function crashes after step 1 but before step 2, it resumes at step 2 on restart — `load_order` is NOT re-run. Its result is read from the promise store.

## Same-process invocation: `ctx.run`

`ctx.run` invokes another function in the same process durably. The durable function awaits it and the result is checkpointed:

```python
async def foo(ctx: Context, arg: str) -> str:
    result = await ctx.run(bar, arg)
    return result

async def bar(ctx: Context, arg: str) -> str:
    # bar can be any async def; it runs as a durable leaf
    return arg.upper()
```

To fire off multiple children and await them in parallel, launch them before awaiting:

```python
async def enrich_batch(ctx: Context, order_ids: list[str]) -> list[dict]:
    # Launch all children (returns futures)
    futures = [ctx.run(enrich_one, oid) for oid in order_ids]
    # Await them all
    results = [await f for f in futures]
    return results
```

**Do NOT use `asyncio.gather` here.** `ctx.run(...)` returns a Resonate future, not an asyncio future; `asyncio.gather(ctx.run(...), ctx.run(...))` will not work correctly. The pattern above — collect futures in a list, then `await` each — is the correct idiom.

Note: unawaited `ctx.run(...)` futures are still tracked by the runtime via structured concurrency — the parent cannot settle until all spawned children complete. But explicitly awaiting them gives you the results.

## Remote invocation: `ctx.rpc` and `ctx.detached`

`ctx.rpc` dispatches a registered function to a remote worker process by name:

```python
async def orchestrator(ctx: Context, batch_id: str) -> dict:
    # "worker_fn" must be registered in a reachable worker group
    result = await ctx.rpc("worker_fn", batch_id)
    return result
```

`ctx.detached` is fire-and-forget — the parent does NOT wait for the result:

```python
async def place_order(
    ctx: Context, customer: str, sku: str, qty: int, amount: int
) -> tuple[str, str, str]:
    stock_ref = await ctx.run(reserve_stock, sku, qty)
    charge_ref = await ctx.run(charge_card, customer, amount)

    # Dispatch audit as a detached workflow; get back the promise id only
    audit_future = ctx.detached(
        "write_audit_log",
        customer=customer,
        sku=sku,
        amount=amount,
        stock_ref=stock_ref,
        charge_ref=charge_ref,
    )
    audit_id = await audit_future.id()   # await the id; do NOT await the result
    return stock_ref, charge_ref, audit_id
```

Use `detached` when the side effect should survive independently of the parent's success — post-commit hooks, notifications, audit logs.

## Options

Options configure an individual call. For Context invocations, options come first via `ctx.options(...)`:

```python
from resonate.retry import Exponential, Never

async def foo(ctx: Context, arg: str) -> str:
    result = await ctx.options(
        retry_policy=Exponential(),
        version=2,
    ).run(bar, arg)
    return result
```

To target a specific worker group for `rpc`:

```python
result = await ctx.options(target="db-workers").rpc("query_orders", user_id)
```

## External promises: `ctx.promise`

`ctx.promise` creates a durable promise the function suspends on until externally resolved. This is the primitive for human-in-the-loop workflows.

```python
async def fulfill_order(ctx: Context, order_id: str, amount: int) -> str:
    # Create the promise; its id is deterministic (derived from the workflow id)
    approval = ctx.promise()
    approval_id = await approval.id()

    # Notify whoever needs to resolve (via a leaf — side effects in leaves only)
    await ctx.run(notify_reviewer, order_id, amount, approval_id)

    # Suspend here until an external party resolves or rejects
    decision_raw = await approval

    if decision_raw.get("approve"):
        return await ctx.run(ship_order, order_id)
    return await ctx.run(cancel_order, order_id)
```

The external resolver calls `await r.promises.resolve(approval_id, Value(data=decision))` from outside the worker (a webhook handler, a CLI, a separate process).

## `ctx.sleep`

`ctx.sleep` suspends for a `timedelta`. The Resonate server holds the continuation; no process needs to stay alive during the sleep:

```python
from datetime import timedelta

async def daily_digest(ctx: Context, user_id: str) -> None:
    while True:
        await ctx.sleep(timedelta(hours=24))
        await ctx.rpc("send_digest", user_id)
```

Pass a `timedelta`, not a raw float. For fake async work inside a leaf, use `await asyncio.sleep(...)` directly instead.

## Dependencies

`ctx.get_dependency(MyType)` retrieves a dependency registered via `r.with_dependency(obj)`:

```python
import psycopg

async def write_to_db(ctx: Context, record: dict) -> None:
    db = ctx.get_dependency(psycopg.Connection)
    # Wrap the actual I/O in a helper via ctx.run so the side effect is checkpointed
    await ctx.run(_insert_row, db, record)

async def _insert_row(ctx: Context, db: psycopg.Connection, record: dict) -> None:
    db.execute("INSERT INTO orders VALUES (...)", record)
```

Never instantiate DB clients or HTTP sessions inside a durable function body — they would be recreated on every replay.

## Side effects and the `ctx.run` envelope

Any side effect — HTTP call, DB write, file write, print — belongs inside a function that the durable function reaches via `ctx.run`. This ensures the effect is executed exactly once: the orchestrator awaits the checkpoint, the leaf runs, the result is persisted, and on replay the stored result is returned instead of re-executing.

Bad (replays re-execute the side effect):

```python
async def foo(ctx: Context, arg: str) -> str:
    requests.post(WEBHOOK_URL, json=arg)  # runs on every replay
    return "done"
```

Good (side effect is checkpointed exactly once):

```python
async def send_webhook(ctx: Context, arg: str) -> str:
    requests.post(WEBHOOK_URL, json=arg)
    return "sent"

async def foo(ctx: Context, arg: str) -> str:
    await ctx.run(send_webhook, arg)
    return "done"
```

## Determinism: time and randomness

Never call `time.time()` or `random.random()` directly in a durable function — on replay, the values change and the function branches differently. Wrap them in a leaf via `ctx.run`.

**v0.7.0 note:** `ctx.time.time()` and `ctx.random.random()` were removed in v0.7.0. If you are migrating from v0.6.x, replace any `ctx.time.time()` / `ctx.random.random()` calls with a leaf function dispatched via `ctx.run`, as shown below.

```python
import time, random

async def _get_time(ctx: Context) -> float:
    return time.time()

async def _get_random(ctx: Context) -> float:
    return random.random()

async def foo(ctx: Context) -> None:
    t = await ctx.run(_get_time)
    r = await ctx.run(_get_random)
```

The leaf settles once and its value is persisted; replay returns the stored value.

## Common shapes

**Sequential pipeline with retries:**

```python
from resonate.retry import Exponential

async def ingest(ctx: Context, source_url: str) -> dict:
    data = await ctx.options(retry_policy=Exponential()).run(fetch, source_url)
    parsed = await ctx.run(parse, data)
    await ctx.run(persist, parsed)
    return {"count": len(parsed)}
```

**Parallel fan-out within a single function:**

```python
async def enrich_batch(ctx: Context, ids: list[str]) -> list[dict]:
    futures = [ctx.run(enrich_one, i) for i in ids]
    return [await f for f in futures]
```

**Recursive workflow (dispatching itself via rpc):**

```python
async def crawl(ctx: Context, url: str, depth: int) -> dict:
    page = await ctx.run(fetch, url)
    if depth > 0:
        for link in page["links"]:
            ctx.detached("crawl", link, depth - 1)
    return {"url": url, "id": page["id"]}
```

## Related skills

- `resonate-basic-ephemeral-world-usage-python` — the Client API companion
- `resonate-basic-debugging-python` — diagnosing stuck workflows, missing registrations, non-determinism regressions
- `durable-execution` — the foundational concept
- `resonate-philosophy` — "do not build these things" mindset

