Hello World quickstart in TypeScript on Resonate

A program that composes two function calls and survives the process crashing mid-call has to record what it did and how to resume. Resonate provides durable execution as a primitive: each ctx.run inside a generator workflow persists a checkpoint, and a re-entering execution skips already-resolved checkpoints. This example is the smallest TypeScript program that exercises the primitive — one workflow generator that calls two async leaf functions through a Resonate client running in-process with no Resonate Server.

The shape of the solution

import { Resonate } from "@resonatehq/sdk";
import type { Context } from "@resonatehq/sdk";

const resonate = new Resonate();

async function baz(_: Context, greetee: string): Promise<string> {
console.log("running baz");
return `Hello ${greetee} from baz!`;
}

async function bar(_: Context, greetee: string): Promise<string> {
console.log("running bar");
return `Hello ${greetee} from bar!`;
}

function* foo(ctx: Context, greetee: string): Generator<any, string, any> {
console.log("running foo");
const fooGreeting = `Hello ${greetee} from foo!`;
const barGreeting = yield* ctx.run(bar, greetee);
const bazGreeting = yield* ctx.run(baz, greetee);
const greeting = `${fooGreeting} ${barGreeting} ${bazGreeting}`;
return greeting;
}

const fooR = resonate.register("foo", foo);

async function main() {
try {
const result = await fooR.run("greeting-workflow", "World");
console.log(result);
resonate.stop();
} catch (e) {
console.log(e);
}
}

main();
// from example-hello-world-ts/index.ts:1-37

The durable primitives in play

new Resonate() — constructs a client. With no url argument and no RESONATE_URL env var, the client wires itself to an in-memory LocalNetwork and runs without a Resonate Server. Source: resonate-sdk-ts/src/resonate.ts:147 (url resolution), :170-173 (LocalNetwork fallback). Applied at index.ts:4.

resonate.register("foo", foo) — registers the generator function under the name "foo" (version 1) and returns a ResonateFunc<F> handle with run / rpc / beginRun / beginRpc methods bound to that registration. Source: resonate-sdk-ts/src/resonate.ts:253-281. Applied at index.ts:25.

fooR.run("greeting-workflow", "World") — top-level durable invocation. The first argument is the invocation id used to key the durable promise; the rest are passed through to the registered function. Resolves with the workflow's typed return. The handle's run method binds to Resonate.run, which delegates to beginRun(...).result(). Source: resonate-sdk-ts/src/resonate.ts:271-272 (handle binding), :283-288 (Resonate.run). Applied at index.ts:29.

yield* ctx.run(bar, greetee) — local durable child call inside a generator workflow. ctx.run returns an LFC<T> whose [Symbol.iterator] yields the request to the runtime and returns the leaf's typed result. On replay, the recorded checkpoint short-circuits the leaf. Source: resonate-sdk-ts/src/context.ts:45-77 (LFC class + iterator returning T at :72-76), :213-215 (Context.run interface), :403-436 (InnerContext.lfc implementation). Applied at index.ts:19-20.

resonate.stop() — stops the network transport, the heartbeat loop, and clears the subscription refresh interval. After stop() the client must not be used for further operations. Source: resonate-sdk-ts/src/resonate.ts:529-540. Applied at index.ts:31.

What the SDK handles vs. what you write

You write two plain async leaf functions whose first parameter is typed Context (unused here, conventionally _), one generator workflow function whose body is straight-line code with yield* ctx.run(...) at each composition point, and a main that registers the workflow, invokes it by id, and stops the client. Argument and return types are owned by you; they must be JSON-serializable because the SDK encodes them into durable promises through its Codec (src/codec.ts).

The SDK handles registering the function under a name and version (so future invocations of the same name resolve to the same code path), turning each yield ctx.run(...) into a child durable promise keyed by an id derived from the parent invocation id, recording the leaf's return value as the checkpoint resolution, and short-circuiting any leaf whose checkpoint already exists when a workflow re-executes with the same top-level id. With no url / RESONATE_URL, all of that happens against LocalNetwork — the durable-promise machinery is real, but storage lives in process memory. Pointing RESONATE_URL at a server (or passing url to the constructor) switches to HttpNetwork without code changes to the workflow.

Failure modes covered

Same top-level invocation id requested twice in the same process. The top-level promise id is ${idPrefix}${id} — i.e. just "greeting-workflow" when RESONATE_PREFIX is unset (resonate.ts:133 for the prefix assignment, :309 for the concatenation inside beginRun). A second call with the same id observes the existing — and in this in-memory run, already-resolved — promise rather than starting a second execution. The deduplication semantics live in the durable-promise create path.

A leaf throws. async leaves get an exponential retry policy by default; generator leaves get Never(). The default-policy selection lives at context.ts:432 for the ctx.run / lfc path the example actually exercises (and at :397 for lfi); the root computation applies the same default at computation.ts:106-108. The bar/baz leaves are async function, so a throw retries on the SDK's default Exponential schedule before the workflow surfaces the error via the yield* re-throw.

The workflow generator throws after bar resolved but before baz was invoked. bar's child checkpoint is already recorded. The error propagates out of fooR.run into the try/catch in main, where it is logged. Re-running with the same id would replay bar from its checkpoint without re-executing the leaf.

What this example does not cover: crash-mid-execution recovery across processes (that needs RESONATE_URL pointing at a Resonate Server so checkpoints survive the process), compensating actions, external signals, timeouts. Those belong to other example posts.

When to reach for this pattern

If you're spiking on the Resonate TypeScript SDK and want the smallest program that exercises register + invoke + ctx.run + return.

If you need a sanity check that @resonatehq/sdk is wired up correctly in a Bun or Node project before adding a server.

If you're teaching another agent the shape of a Resonate TypeScript workflow — generator function, yield* ctx.run, async leaves — before it tackles a saga, fan-out, or human-in-the-loop pattern.

If you want to demonstrate that the durable-promise programming model works in-process without spinning up infrastructure, and add the server later by setting RESONATE_URL.

If your use-case needs cross-process crash recovery, signals from outside the workflow, compensating actions, or scheduling, this is not the pattern — start from the corresponding example post instead.

Sources

Example repo: resonatehq-examples/example-hello-world-ts

TypeScript SDK repo: resonatehq/resonate-sdk-ts (example pins @resonatehq/sdk@^0.10.0 in package.json:13; citations below are pinned to commit f095729)

Resonate constructor + LocalNetwork fallback: src/resonate.ts (lines 147, 170-173)

Resonate.register and Resonate.run: src/resonate.ts (lines 253-281, 283-288)

Context.run interface and LFC class + iterator: src/context.ts (lines 45-77, 213-215, 403-436)

Default retry-policy selection (Exponential for async, Never for generators): src/context.ts (lines 397, 432) and src/computation.ts (lines 106-108)