Hello world — your first durable workflow
A minimal workflow that orchestrates two leaf functions, registered and invoked through Resonate.
The smallest example that exercises the durable execution loop. A registered workflow function calls two leaf functions through ctx.run, and Resonate checkpoints each return value.
TypeScript: @resonatehq/sdk v0.10.1 (current). Python: resonate-sdk v0.6.x. Rust: v0.1.0, in active development.
A workflow with two leaf functions, run via the in-process Resonate client.
A workflow with two leaf functions using Resonate.local() — no server required.
Workflow + leaf function with #[resonate::function], driven by the CLI.
The problem#
Before any "real" example makes sense, you need to see the wiring: a function gets registered, the workflow runtime invokes it, sub-calls go through a Context, and each step's result is checkpointed. Without that mental model, every more elaborate example reads as magic.
Resonate's solution#
Two things, both small. Register a function with the Resonate instance — that's how Resonate knows it can dispatch the function and durably checkpoint its result. Invoke it through the run/RPC API — that's how the function moves from "regular code" to "durable workflow." The rest of every example in this section is variations on those two moves.
Code walkthrough#
The workflow is a generator (TypeScript and Python) or #[resonate::function] async fn (Rust). It receives a Context and calls leaf functions through ctx.run. Leaves can be plain functions — Resonate handles the dispatch and the checkpointing.
import { Resonate } from "@resonatehq/sdk";
import type { Context } from "@resonatehq/sdk";
const resonate = new Resonate();
async function bar(_: Context, greetee: string) {
return `Hello ${greetee} from bar!`;
}
async function baz(_: Context, greetee: string) {
return `Hello ${greetee} from baz!`;
}
function* foo(ctx: Context, greetee: string): Generator<any, string, any> {
// Each ctx.run is a checkpoint — its return value is durably stored.
const barGreeting = yield* ctx.run(bar, greetee);
const bazGreeting = yield* ctx.run(baz, greetee);
return `Hello ${greetee} from foo! ${barGreeting} ${bazGreeting}`;
}
const fooR = resonate.register("foo", foo);
const result = await fooR.run("greeting-workflow", "World");
console.log(result);
resonate.stop();from resonate import Resonate, Context
from typing import Generator, Any
resonate = Resonate.local()
def bar(_: Context, greetee: str) -> str:
return f"Hello {greetee} from bar!"
def baz(_: Context, greetee: str) -> str:
return f"Hello {greetee} from baz!"
@resonate.register
def foo(ctx: Context, greetee: str) -> Generator[Any, Any, str]:
# Each ctx.run is a checkpoint — its return value is durably stored.
bar_greeting = yield ctx.run(bar, greetee=greetee)
baz_greeting = yield ctx.run(baz, greetee=greetee)
return f"Hello {greetee} from foo! {bar_greeting} {baz_greeting}"
result = foo.run("greeting-workflow", greetee="World")
print(result)use resonate::prelude::*;
#[resonate::function]
async fn greet(ctx: &Context, name: String) -> Result<String> {
let greeting = ctx.run(format_greeting, name).await?;
Ok(greeting)
}
#[resonate::function]
async fn format_greeting(name: String) -> Result<String> {
Ok(format!("Hello, {name}! Welcome to durable execution."))
}
#[tokio::main]
async fn main() {
let resonate = Resonate::new(ResonateConfig {
url: Some("http://localhost:8001".into()),
..Default::default()
});
resonate.register(greet).unwrap();
resonate.register(format_greeting).unwrap();
println!("Worker started. Waiting for invocations...");
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl-c");
}The TypeScript and Python examples invoke the workflow in-process — no server needed for this minimal demo. The Rust example registers the function with a long-running worker and waits for an invocation from the Resonate CLI.
Run it locally#
Clone the repo:
git clone https://github.com/resonatehq-examples/example-hello-world-ts
cd example-hello-world-ts
npm installRun the workflow in-process:
npx tsx index.tsYou should see three log lines (running foo, running bar, running baz, possibly with foo repeating as the workflow replays after each yield) followed by the assembled greeting.
Clone the repo:
git clone https://github.com/resonatehq-examples/example-hello-world-py
cd example-hello-world-py
uv syncRun the workflow in-process:
uv run main.pyResonate.local() runs everything in-process — no server required for this minimal demo.
Clone the repo and build:
git clone https://github.com/resonatehq-examples/example-hello-world-rs
cd example-hello-world-rs
cargo buildbrew install resonatehq/tap/resonate
resonate devcargo runresonate invoke greet-1 --func greet --arg '"World"'Inspect the durable call graph:
resonate tree greet-1Try the recovery story (Rust)#
With the worker running in Rust, kill it mid-execution and restart. Resonate replays the workflow from the last checkpointed step rather than from the beginning. resonate tree greet-1 renders the durable call graph as it grows.
Related#
- Durable sleep — adds suspension to the picture.
- Human-in-the-loop — adds external resolution via a durable promise.