# Resonate Async Await Engine Typescript

> 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*.

- Skill: `resonatehq/resonate-async-await-engine-typescript` (Agent Skill)
- Install (CLI): `npx skillmds@latest add resonatehq/resonate-async-await-engine-typescript`
- Raw SKILL.md: https://api.skillmd.com/api/skills/resonatehq/resonate-async-await-engine-typescript/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: resonatehq (https://skillmd.com/u/resonatehq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/resonatehq/resonate-async-await-engine-typescript

---


# Resonate Async/Await Engine (TypeScript)

> **SDK version:** This skill reflects `@resonatehq/sdk` v0.11.4 (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/await` over `function*`/`yield*`
- Integrating Resonate into a codebase that already uses `async/await` throughout
- Browser or edge environments: `@resonatehq/sdk/async` exports 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 has `detach()` but check the SDK changelog for your version)

---

## Installation

```bash
npm install @resonatehq/sdk@0.11.4
```

Both engines are in the same package. The generator engine is the default export; the async engine is the `/async` sub-export.

---

## Basic usage

```typescript
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"` not `from "@resonatehq/sdk"`
- Functions are `async function` not `function*`
- `ctx.run(func, ...args)` in the async engine has **no id argument** (ID is auto-generated); in the generator engine `ctx.run(fn, ...args)` is also ID-less at the context level, but the API shape differs
- No `beginRun` / `beginRpc` on the async-engine client — every `run` and `rpc` already returns a `ResonateHandle<T>` directly, so `beginRun` would 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:

```typescript
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: ... })`:

```typescript
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:

```typescript
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](https://github.com/resonatehq/resonate-sdk-ts#migrating-from-generators) 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:

```typescript
// 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

```typescript
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();
```

