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.2 (current). Python: resonate-sdk v0.7.0 (current). Rust: 0.4.0, in active development.
A workflow with two leaf functions, run via the in-process Resonate client.
A workflow with two leaf functions, run via the Resonate Server.
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) or async function (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);
await resonate.stop();from __future__ import annotations
import asyncio
import os
import time
from typing import TYPE_CHECKING
from resonate.resonate import Resonate
if TYPE_CHECKING:
from resonate.context import Context
async def bar(ctx: Context, greetee: str) -> str:
return f"Hello {greetee} from bar!"
async def baz(ctx: Context, greetee: str) -> str:
return f"Hello {greetee} from baz!"
async def foo(ctx: Context, greetee: str) -> str:
# Each ctx.run is a checkpoint — its return value is durably stored.
bar_greeting = await ctx.run(bar, greetee=greetee)
baz_greeting = await ctx.run(baz, greetee=greetee)
return f"Hello {greetee} from foo! {bar_greeting} {baz_greeting}"
async def main() -> None:
url = os.environ.get("RESONATE_URL", "http://localhost:8001")
r = Resonate(url=url)
r.register(foo)
try:
handle = r.run(f"greeting-{time.time_ns()}", foo, "World")
result = await handle.result()
print(result)
finally:
await r.stop()
if __name__ == "__main__":
asyncio.run(main())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 example invokes the workflow in-process — no server needed. The Python and Rust examples register the function with a long-running worker and connect to a Resonate Server.
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 syncbrew install resonatehq/tap/resonate
resonate devuv run main.pyYou should see the assembled greeting printed after all three durable steps checkpoint.
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.