Resonate Basic Ephemeral World Usage
SDK version: This skill reflects
@resonatehq/sdkv0.11.4 (current on npm).Two execution engines (v0.11.0+): The SDK now ships two engines. This skill documents the generator engine (imported from
@resonatehq/sdk), which usesfunction*/yield*and is the basis of all existing Resonate examples. An async/await engine (imported from@resonatehq/sdk/async) was added in v0.11.0 and is documented inresonate-async-await-engine-typescript.
Overview
The Ephemeral World is where your application code lives that is NOT inside generator functions. This is where you:
- Initialize the Resonate Client
- Register functions
- Start top-level executions
- Manage promises externally
- Set dependencies
Key distinction: Ephemeral World code uses resonate (the Client). Durable World code uses ctx (the Context).
Mental Model
┌─────────────────────────────────────────┐
│ EPHEMERAL WORLD (Stateless) │
│ │
│ • Resonate Client initialization │
│ • Function registration │
│ • Top-level run/rpc calls │
│ • Promise management (create/resolve) │
│ • Dependency injection │
│ │
│ Uses: resonate.run(), resonate.rpc() │
└─────────────────────────────────────────┘
↓ invokes ↓
┌─────────────────────────────────────────┐
│ DURABLE WORLD (Stateful) │
│ │
│ • Generator functions with yield* │
│ • Context APIs for sub-invocations │
│ • Durable coordination and recovery │
│ │
│ Uses: ctx.run(), ctx.rpc() │
└─────────────────────────────────────────┘
Core Rule
You CANNOT use Context APIs in the ephemeral world, and you CANNOT use Client APIs in durable functions.
Initialization Patterns
Local Development Mode (Zero Dependencies)
import { Resonate } from "@resonatehq/sdk";
// No URL = local in-memory mode
const resonate = new Resonate();
Key behaviors:
- In-memory storage (no persistence across restarts)
- No external dependencies required
- Perfect for testing and development
- CRITICAL: When using
new Resonate()without a URL, function arguments are wrapped in an array
Connected to Resonate Server
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate({
url: "http://localhost:8001",
group: "workers",
token: process.env.RESONATE_TOKEN, // JWT bearer token, if the server is secured
});
When to use:
- Multiple worker processes
- Persistence across restarts
- Distributed execution
- Production deployments
There is no basic-auth option on current releases. The constructor accepts
token(a JWT bearer token) and nothing else auth-related — there is noauth,username, orpasswordfield, and noRESONATE_USERNAME/RESONATE_PASSWORDenvironment variable. Passing an unknown option does not throw, so credentials handed to a field that does not exist are silently dropped and the client sends unauthenticated requests.Basic auth did exist through
0.9.6, whereauth: { username, password }sent anAuthorization: Basicheader that the server of that era validated. The0.10.0networking rewrite removed it with no deprecation warning. If you are upgrading from0.9.x, a workingauthblock does not fail loudly — it stops authenticating.
Postgres Instead of a Server
The SDK can also run durable execution directly on Postgres, with no Resonate Server process. Apply the resonate.sql schema from resonate-pg to a Postgres 16+ database, install the pg peer dependency, and pass a PostgresNetwork as the network option:
import { Resonate } from "@resonatehq/sdk";
import { PostgresNetwork } from "@resonatehq/sdk/postgres";
const network = new PostgresNetwork({
connectionString: process.env.DATABASE_URL ?? "postgres://localhost:5432/mydb",
group: "workers",
});
const resonate = new Resonate({ network });
Works with both engines — pass the same network to the Resonate class from @resonatehq/sdk/async.
Key behavior to know: with no server process, due timers are advanced by the workers themselves. Durable sleeps and timeouts progress only while at least one worker is connected; with every worker down they suspend until one reconnects.
Environment Variables
export RESONATE_URL="http://localhost:8001"
export RESONATE_TOKEN="<jwt>"
const resonate = new Resonate(); // Picks up env vars automatically
Resolution order:
- Constructor arguments (highest priority)
- Environment variables
- Built-in defaults (local mode)
Registration Patterns
Correct: Register with Function Reference
function* myWorkflow(ctx: Context, arg: string) {
return `Hello ${arg}`;
}
// ✅ CORRECT
const stub = resonate.register(myWorkflow);
Wrong: Custom String Names
// ❌ WRONG - Don't use custom string names
resonate.register("my_workflow", myWorkflow);
// ❌ WRONG - Don't use snake_case
resonate.register("db_get_user", dbGetUser);
Why this matters:
- Resonate uses function names for routing
- Custom names break RPC resolution
- Snake_case violates naming conventions
Using the Returned Stub
const workflowStub = resonate.register(myWorkflow);
// Later, invoke directly via stub
const result = await workflowStub.run("execution-1", "World");
Top-Level Invocation Patterns
Run (Local Execution)
// Blocks until result is ready
const result = await resonate.run(
"execution-id",
myWorkflow,
"arg1",
"arg2"
);
Use when:
- Running in the same process
- Need the result immediately
Begin Run (Non-Blocking Local)
// Returns immediately with handle
const handle = await resonate.beginRun(
"execution-id",
myWorkflow,
"arg1"
);
// Do other work...
// Get result later
const result = await handle.result();
Use when:
- Starting work but not waiting immediately
- Running multiple executions concurrently
RPC (Remote Execution)
// Blocks until remote execution completes
const result = await resonate.rpc(
"execution-id",
"myWorkflow", // String name!
"arg1",
resonate.options({
target: "poll://any@workers"
})
);
Critical differences from run:
- Uses string function name (not function reference)
- Requires target specification
- Executes in different process/group
Begin RPC (Non-Blocking Remote)
const handle = await resonate.beginRpc(
"execution-id",
"myWorkflow",
"arg1",
resonate.options({
target: "poll://any@workers"
})
);
const result = await handle.result();
Options Pattern
await resonate.run(
"execution-id",
myWorkflow,
"arg1",
resonate.options({
timeout: 60_000, // 60 seconds in ms
tags: { userId: "123" },
version: 1
})
);
Available options:
timeout: Max execution time in millisecondstags: Metadata for filtering/searchingversion: Function version for schema evolutiontarget: RPC routing (poll://any@group-name)
Promise Management
Create Promise
await resonate.promises.create(
"approval-123",
Date.now() + 30000 // timeout 30s from now
);
Get Promise
const promise = await resonate.promises.get("approval-123");
Resolve Promise (Human-in-the-Loop)
// Elsewhere (webhook, UI, CLI):
await resonate.promises.resolve("approval-123", {
data: JSON.stringify({
approved: true,
approver: "alice@example.com",
}),
});
Reject Promise
await resonate.promises.reject("approval-123", {
data: JSON.stringify({
reason: "Insufficient funds",
}),
});
Cancel Promise
await resonate.promises.cancel("approval-123");
Migration note (v0.10.0 → v0.10.1+): The single
resonate.promises.settle(id, state, value)method was split intoresolve,reject, andcancelin v0.10.1. The oldsettle()is private in v0.10.2 and remains private in v0.11.4. Update any code that calls.settle()directly.
Dependency Injection
Set Dependencies
import { createClient } from "@supabase/supabase-js";
const db = createClient(url, key);
// Set in ephemeral world
resonate.setDependency("db", db);
resonate.setDependency("config", { apiKey: "..." });
Rules:
- Only set dependencies in ephemeral world
- Set before registering functions that use them
- Pass non-serializable objects (DB connections, etc.)
Access in Durable World
function* myWorkflow(ctx: Context, userId: string) {
// Get in durable world
const db = ctx.getDependency("db");
const user = yield* ctx.run(async (_ctx, id) => {
return await db.from("users").select().eq("id", id).single();
}, userId);
return user;
}
Scheduling
const schedule = await resonate.schedule(
"daily-report",
"0 8 * * *", // Every day at 8am
generateReport,
"arg1"
);
// Later, delete the schedule
await schedule.delete();
Subscription (Get Existing Execution)
// Subscribe to existing execution
const handle = await resonate.get("execution-id");
// Wait for result
const result = await handle.result();
// Check if done
const isDone = await handle.done();
Use when:
- Checking status of long-running executions
- Reconnecting to executions after restart
- Monitoring from external systems
Common Pitfalls
1. Mixing Ephemeral and Durable APIs
// ❌ WRONG - Using ctx in ephemeral world
async function main() {
const result = await ctx.run(myFunc); // ctx doesn't exist here!
}
// ✅ CORRECT
async function main() {
const result = await resonate.run("id", myFunc);
}
2. Using Client APIs Inside Generators
// ❌ WRONG
function* myWorkflow(ctx: Context) {
const result = await resonate.run("id", otherFunc); // Wrong API!
}
// ✅ CORRECT
function* myWorkflow(ctx: Context) {
const result = yield* ctx.run(otherFunc);
}
3. Missing Target on RPC
// ❌ WRONG - RPC needs target
await resonate.rpc("id", "funcName", "arg");
// ✅ CORRECT
await resonate.rpc(
"id",
"funcName",
"arg",
resonate.options({ target: "poll://any@workers" })
);
4. Forgetting In-Memory Mode Wraps Args in Array
// In-memory mode (no URL)
const resonate = new Resonate();
function* workflow(ctx: Context, state: MyState) {
// ❌ WRONG - state will be [MyState], not MyState!
console.log(state.someField); // undefined!
}
// ✅ CORRECT - Handle array wrapping in local mode
function* workflow(ctx: Context, state: any) {
const actualState = Array.isArray(state) ? state[0] : state;
console.log(actualState.someField); // Works!
}
5. Using Async Instead of Generators
// ❌ WRONG - Durable functions must be generators
async function* workflow(ctx: Context) { }
// ✅ CORRECT
function* workflow(ctx: Context) { }
Complete Example
import "dotenv/config";
import { Resonate, type Context } from "@resonatehq/sdk";
// Initialize client (ephemeral world)
const resonate = new Resonate({
url: process.env.RESONATE_URL,
group: "workers"
});
// Set dependencies (ephemeral world)
resonate.setDependency("apiKey", process.env.API_KEY);
// Register functions (ephemeral world)
const workflowStub = resonate.register(myWorkflow);
// Start execution (ephemeral world)
async function main() {
const handle = await resonate.beginRun(
"workflow-1",
myWorkflow,
{ userId: "123" }
);
console.log("Started:", handle.id);
const result = await handle.result();
console.log("Result:", result);
}
// Durable function (durable world - uses Context APIs)
function* myWorkflow(ctx: Context, input: any) {
const apiKey = ctx.getDependency("apiKey");
const data = yield* ctx.run(fetchData, input.userId);
const processed = yield* ctx.run(processData, data);
return processed;
}
async function fetchData(_ctx: Context, userId: string) {
// Fetch logic
return { userId, data: "..." };
}
async function processData(_ctx: Context, data: any) {
// Process logic
return { processed: true };
}
main().catch(console.error);
Decision Tree
Where am I?
- In
main()or app entry point → Ephemeral World (use Client APIs) - In
function*withctxparameter → Durable World (use Context APIs)
What do I need to do?
- Initialize Resonate →
new Resonate() - Register a function →
resonate.register(func) - Start a workflow →
resonate.run()orresonate.beginRun() - Start remote work →
resonate.rpc()orresonate.beginRpc() - Manage promises →
resonate.promises.*() - Share resources →
resonate.setDependency() - Schedule cron →
resonate.schedule() - Check execution status →
resonate.get()
Summary
The Ephemeral World is your control plane:
- One Client per process
- Stateless orchestration
- Entry and exit points
- External promise resolution
- Dependency setup
Keep it simple, keep it separate from durable functions.