Hello World quickstart in Python on Resonate

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 handles | You write
Allocating a durable promise per ctx.run(...) and persisting invocation and result in the LocalStore | The 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 execution | The 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 process | The id passed to foo.run(promise_id, ...)
Starting the worker threads, message source, and bridge on the first .run(...) call | Calling Resonate.local() — no start(), no server URL, no auth
Registering foo by its name and version in the in-memory Registry | The @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.

Example repo: https://github.com/resonatehq-examples/example-hello-world-py

Python SDK repo: https://github.com/resonatehq/resonate-sdk-py

Python SDK at the pinned version: https://github.com/resonatehq/resonate-sdk-py/tree/v0.6.7

Resonate documentation: https://docs.resonatehq.io

Files cited in this post:

main.py:1-37 — the entire example

pyproject.toml:6-8 — Python and SDK pins

.python-version:1 — Python pin

SDK resonate/resonate.py:139-192 (v0.6.7) — Resonate.local() classmethod

SDK resonate/resonate.py:195 (v0.6.7) — Resonate.remote() classmethod

SDK resonate/resonate.py:332-400 (v0.6.7) — resonate.register

SDK resonate/resonate.py:863-886 (v0.6.7) — Context.run (alias of ctx.lfc)

SDK resonate/resonate.py:1368-1391 (v0.6.7) — Function.run