4 min readResonate HQJust published

Hello World quickstart in Python on Resonate

How a three-function greeting program demonstrates the Resonate Python SDK's local-mode workflow, registration, and ctx.run primitive.

Resonate brand card on a dark background with a plum spectrum wave at the bottom and the post headline in white Sansation.

A new caller picking up the Resonate Python SDK needs a single-file program that exercises the durable-execution surface end-to-end with no server and no infrastructure. The Resonate shape of the solution is a generator-style workflow registered with a local-mode client, which yields each child step through ctx.run so the SDK can checkpoint the invocation and the result. The example wires three functions — foo, bar, baz — into a single greeting and runs them through Resonate.local() in one process.

The shape of the solution

from resonate import Resonate, Context
from typing import Generator, Any
 
resonate = Resonate.local()
 
 
def baz(_: Context, greetee: str) -> str:
    print("running baz")
    return f"Hello {greetee} from baz!"
 
 
def bar(_: Context, greetee: str) -> str:
    print("running bar")
    return f"Hello {greetee} from bar!"
 
 
@resonate.register
def foo(ctx: Context, greetee: str) -> Generator[Any, Any, str]:
    print("running foo")
    foo_greeting = f"Hello {greetee} from foo!"
    bar_greeting = yield ctx.run(bar, greetee=greetee)
    baz_greeting = yield ctx.run(baz, greetee=greetee)
    greeting = f"{foo_greeting} {bar_greeting} {baz_greeting}"
    return greeting
 
 
def main():
    try:
        promise_id = "hello-world-example"
        result = foo.run(promise_id, greetee="World")
        print(result)
    except Exception as e:
        print(e)
# from example-hello-world-py/main.py:1-33

foo is a generator function, not async def. Each yield ctx.run(...) returns control to the SDK so it can record the call against a durable promise, drive the child function, persist the result, and feed that result back into the generator on the next iteration.

The durable primitives in play

  • Resonate.local() — constructs a Resonate client backed by an in-memory LocalStore, with no Resonate Server dependency. main.py:4.
  • @resonate.register — registers foo as a top-level workflow under its function name and returns a Function wrapper that exposes .run(id, ...). main.py:17.
  • ctx.run(func, *args, **kwargs) — Local Function Call (ctx.lfc alias). Schedules bar / baz for an immediate exactly-once / effectively-once execution and checkpoints both the invocation and the result; the workflow blocks on the value at yield. main.py:21-22.
  • yield inside the generator — the suspension point. The SDK resumes the generator only after the child step's durable promise resolves; on replay, the same yield returns the stored result instead of re-running the child. main.py:21-22.
  • foo.run(id, ...) — starts the workflow under a caller-supplied promise id ("hello-world-example"), blocks the calling thread, and returns the final value. Reusing the id within the same process resolves against the existing promise rather than starting a new run. main.py:30.

What the SDK handles vs. what you write

SDK handlesYou write
Allocating a durable promise per ctx.run(...) and persisting invocation and result in the LocalStoreThe two ctx.run(bar, ...) / ctx.run(baz, ...) calls inside foo
Driving the generator: feeding each child's resolved value back into the yield and resuming executionThe plain generator body that composes the final greeting
Mapping the outer promise id ("hello-world-example") to its stored result so a second foo.run(...) with the same id is exactly-once within the processThe id passed to foo.run(promise_id, ...)
Starting the worker threads, message source, and bridge on the first .run(...) callCalling Resonate.local() — no start(), no server URL, no auth
Registering foo by its name and version in the in-memory RegistryThe @resonate.register decorator on foo

Nothing in foo's body indicates distribution. The function body is straight-line Python with two yield points; durability comes from the runtime around it, not from the body.

Failure modes covered

This is a minimum example, so the surface it exercises is small. What main.py actually runs end-to-end is one successful invocation of foo.run("hello-world-example", greetee="World") (main.py:30), with two checkpointed child calls (bar then baz) and one final composed string. The example does not inject a failure, does not call foo.run twice, and does not restart the process.

What the primitives in play guarantee, even though the example doesn't exercise the failure paths:

  • Effectively-once on the outer promise id. A second foo.run("hello-world-example", ...) call in the same Python process resolves against the existing promise instead of re-invoking the workflow body: Function.run calls self.begin_run(id, ...).result(), and begin_run's docstring states "If a promise with the same id already exists, Resonate subscribes to its result or returns it immediately if it has completed" (SDK resonate/resonate.py:1368-1391 at v0.6.7). To see this in main.py, a caller would have to call foo.run twice in main(); as shipped, it is called once.
  • Per-checkpoint persistence inside the process. ctx.run schedules the child against a durable promise and stores both the invocation and the result in the LocalStore. On replay within the same process the SDK feeds the stored value back into the yield rather than re-executing the child. The example does not inject a child-side failure to exercise the replay path; the durable-promise replay machinery is the same one that example-async-rpc-py and the remote-store examples exercise under crash conditions.

What is out of scope for this example, by design:

  • The store is in-memory (LocalStore), so process restarts lose state. Cross-process durability requires Resonate.remote(...) and a running Resonate server (Resonate.remote at SDK resonate/resonate.py:195 at v0.6.7) — out of scope for this example, which explicitly states "does NOT require the use of a Resonate Server" in the README.
  • There is no fan-out, no remote function invocation, no schedule, no compensation. Those are separate examples in the resonatehq-examples org.

When to reach for this pattern

  • If you are bringing up the Resonate Python SDK for the first time and want a single-file program that exercises Resonate.local(), @resonate.register, ctx.run, and Function.run end-to-end.
  • If you need a smoke test that the SDK installs and runs against your Python version (the example pins Python 3.13 via .python-version and resonate-sdk>=0.6.7 via pyproject.toml).
  • If you are evaluating the generator-based workflow shape (yielding ctx.run(...) rather than async def / await) before committing to it in a larger codebase.
  • If you want to confirm the local in-memory store path works in isolation before introducing a Resonate Server.

Sources

This post is written against resonate-sdk v0.6.7 (the version pinned by the example's pyproject.toml). The 0.6.x line exposes Resonate.local() and Resonate.remote() as classmethod constructors. If you are reading this against a later release that has dropped or renamed those constructors, re-verify the four primitives (Resonate.local, @resonate.register, ctx.run, Function.run) against the SDK source at the version you are pinning before generalizing the pattern.