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-33foo 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-memoryLocalStore, with no Resonate Server dependency.main.py:4.@resonate.register— registersfooas a top-level workflow under its function name and returns aFunctionwrapper that exposes.run(id, ...).main.py:17.ctx.run(func, *args, **kwargs)— Local Function Call (ctx.lfcalias). Schedulesbar/bazfor an immediate exactly-once / effectively-once execution and checkpoints both the invocation and the result; the workflow blocks on the value atyield.main.py:21-22.yieldinside the generator — the suspension point. The SDK resumes the generator only after the child step's durable promise resolves; on replay, the sameyieldreturns 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.runcallsself.begin_run(id, ...).result(), andbegin_run's docstring states "If a promise with the sameidalready exists, Resonate subscribes to its result or returns it immediately if it has completed" (SDKresonate/resonate.py:1368-1391at v0.6.7). To see this inmain.py, a caller would have to callfoo.runtwice inmain(); as shipped, it is called once. - Per-checkpoint persistence inside the process.
ctx.runschedules the child against a durable promise and stores both the invocation and the result in theLocalStore. On replay within the same process the SDK feeds the stored value back into theyieldrather 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 thatexample-async-rpc-pyand 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 requiresResonate.remote(...)and a running Resonate server (Resonate.remoteat SDKresonate/resonate.py:195at 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-examplesorg.
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, andFunction.runend-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-versionandresonate-sdk>=0.6.7viapyproject.toml). - If you are evaluating the generator-based workflow shape (yielding
ctx.run(...)rather thanasync 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 examplepyproject.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 ofctx.lfc) - SDK
resonate/resonate.py:1368-1391(v0.6.7) —Function.run
