4 min readResonate HQJust published

Hello World quickstart in TypeScript on Resonate

The smallest unit of a Resonate TypeScript program — a generator workflow, two leaf functions, a registered handle — annotated for retrieval.

Resonate brand card on a dark background with a teal spectrum wave at the bottom and the post headline in white Sansation.

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