resonate-basic-durable-world-usage-typescript — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited resonate-basic-durable-world-usage-typescript (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
SDK version: This skill reflects @resonatehq/sdk v0.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.
The Durable World is inside generator functions where you write durable, recoverable execution logic using the Context object. Every operation checkpoints progress, enabling automatic recovery after failures.
Key distinction: Durable World code uses ctx (Context). Ephemeral World code uses resonate (Client).
Generator Function (function*)
↓
yield* ctx.run() ← Checkpoint
↓
yield* ctx.sleep() ← Checkpoint + Suspend
↓
yield* ctx.rpc() ← Checkpoint + Cross Process
↓
return result ← Final Checkpoint*Every `yield` is a durable checkpoint.**
If the process crashes, the execution replays from the last checkpoint using stored results.
function* (Generator Functions)// ✅ CORRECT - Generator function
function* myWorkflow(ctx: Context, arg: string) {
// Durable code here
}
// ❌ WRONG - Async function
async function myWorkflow(ctx: Context, arg: string) {
// Not durable!
}
// ❌ WRONG - Async generator
async function* myWorkflow(ctx: Context, arg: string) {
// Invalid syntax for Resonate
}yield* Context Callsfunction* myWorkflow(ctx: Context) {
// ✅ CORRECT
const result = yield* ctx.run(someFunc, "arg");
// ❌ WRONG - Missing yield*
const result = ctx.run(someFunc, "arg");
// ❌ WRONG - Using await
const result = await ctx.run(someFunc, "arg");
}// ✅ CORRECT
function* myWorkflow(ctx: Context, userId: string, data: any) {
// ...
}
// ❌ WRONG - Missing Context
function* myWorkflow(userId: string) {
// Can't call Context APIs!
}Signature: ctx.run(func, ...args) → yield* → result
Behavior:
function* workflow(ctx: Context, userId: string) {
// Call synchronously, get result immediately
const user = yield* ctx.run(fetchUser, userId);
const orders = yield* ctx.run(fetchOrders, userId);
return { user, orders };
}
async function fetchUser(_ctx: Context, userId: string) {
// Regular async function - can throw, use await, etc.
return { id: userId, name: "Alice" };
}function* workflow(ctx: Context) {
const db = ctx.getDependency("db"); // DB connection object
// ✅ OK - db object doesn't need to serialize
const user = yield* ctx.run(queryDatabase, db, "123");
return user;
}
async function queryDatabase(_ctx: Context, db: any, userId: string) {
return await db.query("SELECT * FROM users WHERE id = $1", [userId]);
}function* workflow(ctx: Context, email: string) {
// ✅ CORRECT - Wrap side effects in ctx.run
yield* ctx.run(sendEmail, email);
// ❌ WRONG - Side effect outside ctx.run
await sendEmailDirect(email); // Will re-send on replay!
}
async function sendEmail(_ctx: Context, email: string) {
// Side effect happens here, checkpointed result prevents replay
await emailService.send(email);
}Signature: ctx.beginRun(func, ...args) → yield* → Future
Use when:
function* workflow(ctx: Context, userId: string) {
// Fork: Start all operations
const userFuture = yield* ctx.beginRun(fetchUser, userId);
const ordersFuture = yield* ctx.beginRun(fetchOrders, userId);
const invoicesFuture = yield* ctx.beginRun(fetchInvoices, userId);
// Join: Await all results
const user = yield* userFuture;
const orders = yield* ordersFuture;
const invoices = yield* invoicesFuture;
return { user, orders, invoices };
}Pattern: Always fork first, then join. Don't interleave.
// ❌ WRONG - Hard to read, unclear concurrency
function* workflow(ctx: Context, id: string) {
const f1 = yield* ctx.beginRun(step1, id);
const r1 = yield* f1;
const f2 = yield* ctx.beginRun(step2, r1);
const r2 = yield* f2;
return r2;
}
// ✅ CORRECT - Sequential operations should use run
function* workflow(ctx: Context, id: string) {
const r1 = yield* ctx.run(step1, id);
const r2 = yield* ctx.run(step2, r1);
return r2;
}Signature: ctx.rpc(funcName, ...args, options?) → yield* → result
Behavior:
// Process A
function* workflow(ctx: Context, userId: string) {
// Call function in process B
const score = yield* ctx.rpc(
"computeScore",
userId,
{ model: "v2" },
ctx.options({ target: "poll://any@scorers" })
);
return score;
}
// Process B (different worker group "scorers")
function* computeScore(ctx: Context, userId: string, options: any) {
// Expensive computation
return 850;
}ctx.options({
target: "poll://any@workers" // Any worker in "workers" group
})
ctx.options({
target: "poll://any@gpu-workers" // Specific worker group
})function* workflow(ctx: Context) {
const db = ctx.getDependency("db");
// ❌ WRONG - DB connection can't serialize
yield* ctx.rpc("processData", db);
// ✅ CORRECT - Only pass serializable data
const data = yield* ctx.run(fetchData, db);
yield* ctx.rpc("processData", data);
}Serializable: strings, numbers, booleans, plain objects, arrays, null Not serializable: functions, class instances, DB connections, file handles
function* workflow(ctx: Context, userId: string) {
// Fork: Start remote operations
const scoreFuture = yield* ctx.beginRpc(
"computeScore",
userId,
ctx.options({ target: "poll://any@scorers" })
);
const riskFuture = yield* ctx.beginRpc(
"assessRisk",
userId,
ctx.options({ target: "poll://any@risk-workers" })
);
// Join: Await results
const score = yield* scoreFuture;
const risk = yield* riskFuture;
return { score, risk };
}function* workflow(ctx: Context, orderId: string) {
// Start analytics tracking but don't wait
yield* ctx.detached(
"trackOrder",
orderId,
ctx.options({ target: "poll://any@analytics" })
);
// Continue immediately
return processOrder(orderId);
}Important: Detached calls are NOT implicitly awaited, even with structured concurrency.
Signature: ctx.sleep(ms | options) → yield* → void
function* workflow(ctx: Context) {
yield* ctx.sleep(5000); // Sleep 5 seconds
yield* ctx.sleep({ for: 60_000 }); // Sleep 1 minute
}function* workflow(ctx: Context) {
const tomorrow8am = new Date();
tomorrow8am.setDate(tomorrow8am.getDate() + 1);
tomorrow8am.setHours(8, 0, 0, 0);
yield* ctx.sleep({ until: tomorrow8am });
// Resumes at exactly 8am tomorrow
}Key behaviors:
Signature: ctx.promise(options?) → yield* → Future
Use for: Human-in-the-loop, webhooks, external triggers
IMPORTANT: ctx.promise() does not take an id. Resonate generates a deterministic ID from the call tree, and that is the only supported mechanism — there is no way to pin a custom ID from inside a durable function.
The signature accepts timeout, data and tags and nothing else:
promise<T>(): RFI<T>;
promise<T>({ timeout, data, tags }: {
timeout?: number;
data?: any;
tags?: { [key: string]: string };
}): RFI<T>;Passing { id } is a TypeScript excess-property error, and if you force it through, the runtime destructures only { timeout, data, tags } and silently discards it — the promise still gets the sequence-generated ID.
So how does an external resolver learn the ID? Read it back off the promise and send it. That is the pattern below: create the promise first, then hand promise.id to whatever needs to resolve it.
const promise = yield* ctx.promise<T>();
const result = yield* promise;ctx.options() excludes id for the same reason — options(opts?: Partial<Omit<Options, "id">>).
function* approvalWorkflow(ctx: Context, orderId: string) {
// ID is auto-generated; read it back to give the external resolver
const approvalPromise = yield* ctx.promise<Decision>();
// Send notification with promise ID
yield* ctx.run(sendApprovalEmail, approvalPromise.id);
// Block until human approves (via external resolve)
const decision = yield* approvalPromise;
if (decision.approved) {
yield* ctx.run(processOrder, orderId);
}
return decision;
}// Webhook handler or UI callback
app.post("/approve/:promiseId", async (req, res) => {
// Base64 encode data before sending to Resonate server
const response = { approved: true, approver: req.body.userId };
const encodedData = Buffer.from(JSON.stringify(response)).toString('base64');
await resonate.promises.resolve(req.params.promiseId, {
data: encodedData,
});
res.json({ status: "approved" });
});Promise ID determinism is the SDK's job, not yours
Replay only works if a promise gets the same ID every time the function runs. Because ctx.promise() takes no id, you cannot get this wrong: the ID comes from a per-context sequence counter that advances in call order, so the same code path always produces the same IDs.
function* approvalLoop(ctx: Context, orderId: string) {
while (true) {
// Each iteration gets the next sequence ID — deterministic across replays
const promise = yield* ctx.promise<Decision>();
yield* ctx.run(sendApprovalEmail, orderId, promise.id);
const result = yield* promise;
if (result.approved) break;
}
}What you still have to get right: the sequence is positional, so the code path must be deterministic even though the IDs are. Branching on Date.now(), Math.random(), or anything else that varies between runs can change how many promises get created before a given one, which shifts every subsequent ID. Use ctx.date.now() and ctx.math.random() — they replay the recorded value — and keep non-deterministic input out of control flow.
// ❌ BAD — a wall-clock branch changes the call sequence between replays
if (Date.now() % 2) { yield* ctx.promise<Decision>(); }
// ✅ GOOD — recorded and replayed, so the branch is stable
const now = yield* ctx.date.now();
if (now % 2) { yield* ctx.promise<Decision>(); }Pro tip: the generated ID is deterministic and replay-safe by construction — there is no explicit-ID alternative to weigh it against. When something outside the workflow has to resolve the promise, read promise.id back and publish that.
Base64 Encoding: The Resonate server expects base64-encoded data. Encode at the API boundary (CLI/webhook), and the SDK automatically decodes it in your workflow.
function* workflow(ctx: Context) {
// ✅ CORRECT - Deterministic time
const timestamp = yield* ctx.date.now();
// ❌ WRONG - Non-deterministic
const timestamp = Date.now(); // Different on replay!
}function* workflow(ctx: Context) {
// ✅ CORRECT - Deterministic random
const rand = yield* ctx.math.random();
// ❌ WRONG - Non-deterministic
const rand = Math.random(); // Different on replay!
}Why: Replay must follow same code path. Non-deterministic values break recovery.
function* workflow(ctx: Context) {
const result = yield* ctx.run(
slowOperation,
"arg",
ctx.options({
id: "custom-promise-id",
timeout: 30_000, // 30 seconds
tags: { operation: "slow" }
})
);
}All started operations are implicitly awaited before return.
function* workflow(ctx: Context) {
yield* ctx.beginRun(task1); // Started
yield* ctx.beginRun(task2); // Started
return "done"; // Implicitly waits for task1 and task2
}Equivalent to:
function* workflow(ctx: Context) {
const f1 = yield* ctx.beginRun(task1);
const f2 = yield* ctx.beginRun(task2);
yield* f1; // Explicit wait
yield* f2; // Explicit wait
return "done";
}No orphaned work.
function* workflow(ctx: Context, userId: string) {
try {
const user = yield* ctx.run(fetchUser, userId);
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
// Retry logic or compensation
return null;
}
}
async function fetchUser(_ctx: Context, userId: string) {
if (!userId) {
throw new Error("User ID required");
}
// Fetch logic
}function* workflow(ctx: Context, amount: number) {
// Assert fails if condition is false
yield* ctx.assert(amount > 0, "Amount must be positive");
// Panic fails if condition is true
yield* ctx.panic(amount > 1000000, "Amount exceeds limit");
return amount;
}Use: assert for preconditions, panic for invariant violations
import { type Context } from "@resonatehq/sdk";
// Main workflow (durable)
function* processOrder(ctx: Context, orderId: string) {
// 1. Fetch order data
const order = yield* ctx.run(fetchOrder, orderId);
// 2. Parallel validation
const inventoryFuture = yield* ctx.beginRun(checkInventory, order);
const fraudFuture = yield* ctx.beginRpc(
"checkFraud",
order,
ctx.options({ target: "poll://any@fraud-workers" })
);
const inventory = yield* inventoryFuture;
const fraud = yield* fraudFuture;
if (!inventory.available || fraud.flagged) {
return { status: "rejected", reason: !inventory.available ? "out of stock" : "fraud" };
}
// 3. Charge customer
const payment = yield* ctx.run(chargeCard, order);
// 4. Create shipment
const shipment = yield* ctx.run(createShipment, order);
// 5. Send confirmation (fire and forget)
yield* ctx.detached(
"sendConfirmation",
orderId,
ctx.options({ target: "poll://any@email-workers" })
);
return { status: "complete", payment, shipment };
}
// Helper functions (async, not generators)
async function fetchOrder(_ctx: Context, orderId: string) {
// DB fetch
return { id: orderId, items: [...], total: 99.99 };
}
async function checkInventory(_ctx: Context, order: any) {
// Inventory check
return { available: true };
}
async function chargeCard(_ctx: Context, order: any) {
// Payment processing
return { transactionId: "txn_123", amount: order.total };
}
async function createShipment(_ctx: Context, order: any) {
// Shipping API
return { trackingNumber: "1Z999AA1", carrier: "UPS" };
}// ❌ WRONG
function* workflow(ctx: Context) {
const result = ctx.run(someFunc); // Returns LFC object, not result!
}
// ✅ CORRECT
function* workflow(ctx: Context) {
const result = yield* ctx.run(someFunc);
}// ❌ WRONG
function* workflow(ctx: Context) {
const result = await ctx.run(someFunc); // Breaks determinism!
}
// ✅ CORRECT
function* workflow(ctx: Context) {
const result = yield* ctx.run(someFunc);
}// ❌ WRONG - Replays will re-execute
function* workflow(ctx: Context) {
console.log("Processing..."); // Logs multiple times on replay!
await emailService.send("[email protected]"); // Sends multiple emails!
}
// ✅ CORRECT
function* workflow(ctx: Context) {
yield* ctx.run(logMessage, "Processing...");
yield* ctx.run(sendEmail, "[email protected]");
}// ❌ WRONG
function* workflow(ctx: Context) {
const timestamp = Date.now(); // Different on replay!
const rand = Math.random(); // Different on replay!
}
// ✅ CORRECT
function* workflow(ctx: Context) {
const timestamp = yield* ctx.date.now();
const rand = yield* ctx.math.random();
}function* workflow(ctx: Context) {
const db = ctx.getDependency("db");
// ❌ WRONG - DB connection can't serialize
yield* ctx.rpc("processData", db);
// ✅ CORRECT - Fetch data first, then pass it
const data = yield* ctx.run(fetchData, db);
yield* ctx.rpc("processData", data);
}What do I need?
ctx.run()ctx.beginRun()ctx.rpc()ctx.beginRpc()ctx.detached()ctx.sleep()ctx.promise()ctx.date.now()ctx.math.random()ctx.getDependency()The Durable World is your execution plane:
yield*Write sequential-looking code that runs distributed and durable.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.