Resonate async await engine typescript
Skill resonatehq/resonate-skills/resonate-async-await-engine-typescript
Agent skills for building with Resonate — durable execution for long-running, crash-safe workflows.
npx -y skills add resonatehq/resonate-skills --skill resonate-async-await-engine-typescriptAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
The Resonate TypeScript SDK's async/await execution engine — import from @resonatehq/sdk/async, register async functions, eager ctx.run fan-out with Promise.all, Never-default retries and Exponential opt-in, and when to choose the async engine vs the generator engine. Introduced in v0.11.0. Use when writing new TypeScript workflows with async/await instead of function*/yield*.
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.0 KB, as published. Nobody here has run it
Resonate Async/Await Engine (TypeScript)
SDK version: This skill reflects
@resonatehq/sdkv0.11.2 (current on npm). The async/await engine was introduced in v0.11.0.
Overview
The TypeScript SDK ships two execution engines:
| Engine | Import path | Function style | Release |
|---|---|---|---|
| Generator engine | @resonatehq/sdk | function* / yield* | v0.10.x and earlier; still current |
| Async/await engine | @resonatehq/sdk/async | async function / await | v0.11.0+ |
Both engines connect to the same Resonate Server, share the same durable-promise substrate, and support the same patterns (fan-out, human-in-the-loop, saga, etc.). The async engine is opt-in — existing generator-engine code is unaffected.
For the generator engine, see resonate-basic-ephemeral-world-usage-typescript and resonate-basic-durable-world-usage-typescript.
When to use the async engine
- Writing new TypeScript workflows and prefer
async/awaitoverfunction*/yield* - Integrating Resonate into a codebase that already uses
async/awaitthroughout - Browser or edge environments:
@resonatehq/sdk/asyncexports are browser-compatible
When to keep the generator engine
- Existing generator-engine code — there is no reason to migrate; the generator engine is fully supported
- You need
ctx.detached()with the full bounded-replay pattern (async engine also hasdetach()but check the SDK changelog for your version)
Installation
npm install @resonatehq/[email protected]
Both engines are in the same package. The generator engine is the default export; the async engine is the /async sub-export.
Basic usage
import { Resonate } from "@resonatehq/sdk/async";
const resonate = new Resonate({ url: "http://localhost:8001" });
// Register an async function
resonate.register("greet", async (ctx, name: string) => {
return `Hello, ${name}!`;
});
// Start a workflow — resonate.run(id, funcOrName, ...args) → Promise<ResonateHandle<T>>
const handle = await resonate.run("greet-001", "greet", "world");
// Await the result via the durable-promise subscription
const result = await handle.result();
// result === "Hello, world!"
Key differences from the generator engine:
- Import is
from "@resonatehq/sdk/async"notfrom "@resonatehq/sdk" - Functions are
async functionnotfunction* ctx.run(func, ...args)in the async engine has no id argument (ID is auto-generated); in the generator enginectx.run(fn, ...args)is also ID-less at the context level, but the API shape differs- No
beginRun/beginRpcon the async-engine client — everyrunandrpcalready returns aResonateHandle<T>directly, sobeginRunwould be redundant
Eager ctx.run and fan-out with Promise.all
Inside an async workflow function, ctx.run(func, ...args) returns a DurablePromise<T> immediately — the child starts executing right away. Hold several and await them together for parallel fan-out:
resonate.register("processItems", async (ctx, items: string[]) => {
// Start all children eagerly — they run in parallel
const promises = items.map((item) =>
ctx.run(processOne, item)
);
// Await all results
const results = await Promise.all(promises);
return results;
});
resonate.register("processOne", async (ctx, item: string) => {
// ... durable leaf work
return `processed: ${item}`;
});
This is the async-engine equivalent of the generator engine's ctx.beginRun + sequential yield* pattern.
Retries: Never default, Exponential opt-in
The async/await engine defaults to no retry (Never) when no retry policy is specified. To opt into retries on a specific ctx.run call, pass ctx.options({ retryPolicy: ... }):
import { Resonate, Exponential, Never } from "@resonatehq/sdk/async";
resonate.register("reliableStep", async (ctx, taskId: string) => {
// Default: Never retry — a failure propagates immediately
const noRetryResult = await ctx.run(fragileOp, taskId);
// Explicit Exponential retry on a specific child
const retryResult = await ctx.run(
flakeyOp,
taskId,
ctx.options({ retryPolicy: new Exponential() }),
);
return { noRetryResult, retryResult };
});
Never, Exponential, Linear, and Constant are all exported from @resonatehq/sdk/async and can be used with both engines.
Tip: For saga-style compensation, let steps fail immediately with the default Never policy and catch in the outer function.
ctx.options() in the async engine
ctx.options() accepts the same fields as the generator engine:
ctx.options({
retryPolicy: new Exponential(), // retry policy (default: Never)
target: "poll://any@workers", // worker group routing
timeout: 30_000, // ms; defaults to 24h
tags: { "env": "prod" }, // arbitrary tags on the child promise
version: 1, // function version pin
})
Migration from generator engine
| Generator engine | Async/await engine |
|---|---|
import { Resonate } from "@resonatehq/sdk" | import { Resonate } from "@resonatehq/sdk/async" |
function* workflow(ctx: Context, ...) | async function workflow(ctx: Context, ...) |
yield* ctx.run(fn, args) | await ctx.run(fn, args) |
yield* ctx.rpc(fn, args) | await ctx.rpc(fn, args) |
yield* ctx.sleep(ms) | await ctx.sleep(ms) |
const p = yield* ctx.promise(opts) | const dp = ctx.promise(opts) — no await; returns DurablePromise with .id |
const val = yield* p | const val = await dp |
resonate.beginRun(id, fn, ...args) | resonate.run(id, fn, ...args) (returns handle) |
await resonate.run(id, fn, ...args) | (await resonate.run(id, fn, ...args)).result() |
The server-side promise structure is identical; existing generator-engine workflows on the server are unaffected by the async engine.
See the SDK README migration guide for the authoritative changelog.
External promise resolution (human-in-the-loop)
The async engine uses the same resonate.promises.resolve() API as the generator engine:
// Inside a workflow: park on an external decision
resonate.register("awaitApproval", async (ctx, orderId: string) => {
const dp = ctx.promise<{ approved: boolean }>();
// dp.id is available synchronously — surface it to whoever needs to approve
console.log("Approval promise id:", dp.id);
const decision = await dp;
return decision;
});
// In a webhook handler: resolve the parked promise
const data = Buffer.from(JSON.stringify({ approved: true })).toString("base64");
await resonate.promises.resolve(promiseId, { data });
Full runnable example
import { Resonate, Exponential } from "@resonatehq/sdk/async";
const resonate = new Resonate({ url: "http://localhost:8001" });
resonate.register("summarize", async (ctx, texts: string[]) => {
// Fan-out: summarize each text in parallel
const summaries = await Promise.all(
texts.map((t) =>
ctx.run(
summarizeOne,
t,
ctx.options({ retryPolicy: new Exponential() }),
)
)
);
return summaries.join("\n");
});
resonate.register("summarizeOne", async (ctx, text: string) => {
// ... call an LLM or summarization service
return `Summary of: ${text.slice(0, 50)}`;
});
const handle = await resonate.run("summarize-job-001", "summarize", [
"First document ...",
"Second document ...",
]);
console.log(await handle.result());
await resonate.stop();