Design HTTP services that use Resonate durable functions behind route handlers, including routing patterns, workflow boundaries, and RPC calls to other service workers (e.g., database service). Use when building or refactoring HTTP APIs that trigger durable workflows in TypeScript.
Use this skill to design HTTP services where route handlers start or await durable workflows. The HTTP server is the entrypoint; durable functions do the work and coordinate via Resonate. Downstream services (like a database service) expose their own durable functions and are invoked via RPC.
client -> HTTP routes -> Resonate Client (beginRpc/run)
-> worker group (durable workflow)
-> ctx.rpc -> db worker group (durable db functions)
function* with yield*.POST /jobs starts a workflow and returns jobId.GET /jobs/:id returns status or result.POST /jobs starts a workflow and returns jobId.POST /webhooks/service resolves a durable promise tied to a workflow.import express from "express";
import { Resonate } from "@resonatehq/sdk";
import crypto from "node:crypto";
const app = express();
app.use(express.json());
const resonate = new Resonate({
url: "http://localhost:8001",
group: "api",
});
// Start workflow
app.post("/jobs", async (req, res) => {
const jobId = `job/${crypto.randomUUID()}`;
await resonate.beginRpc(
jobId,
"process-job",
req.body,
resonate.options({ target: "poll://any@workers" })
);
res.status(202).json({ id: jobId });
});
// Poll status
app.get("/jobs/:id", async (req, res) => {
const handle = await resonate.get(req.params.id);
const result = await handle.result();
res.json({ id: req.params.id, result });
});
// Webhook: external system resolves promise
app.post("/webhooks/approval", async (req, res) => {
const { promiseId, approved } = req.body;
await resonate.promises.resolve(promiseId, approved);
res.status(204).end();
});
import { Resonate, type Context } from "@resonatehq/sdk";
const resonate = new Resonate({ url: "http://localhost:8001", group: "workers" });
function* processJob(ctx: Context, payload: { accountId: string }) {
const account = yield* ctx.rpc(
"db.getAccount",
payload.accountId,
ctx.options({ target: "poll://any@db" })
);
const result = yield* ctx.run(processAccount, account);
const approval = yield* ctx.promise({ id: `approve/${ctx.id}` });
const ok = yield* approval as boolean;
if (!ok) {
throw new Error("rejected");
}
yield* ctx.rpc(
"db.saveResult",
{ id: ctx.id, result },
ctx.options({ target: "poll://any@db" })
);
return { status: "done", result };
}
resonate.register("process-job", processJob);
import { Resonate, type Context } from "@resonatehq/sdk";
const resonate = new Resonate({ url: "http://localhost:8001", group: "db" });
function* getAccount(_: Context, id: string) {
return await db.loadAccount(id);
}
function* saveResult(_: Context, record: { id: string; result: unknown }) {
await db.save(record);
return { ok: true };
}
resonate.register("db.getAccount", getAccount);
resonate.register("db.saveResult", saveResult);
ctx.date.now() and ctx.math.random() inside durable functions.ctx.run, ctx.rpc).function* aggregate(ctx: Context, ids: string[]) {
const futures = ids.map((id) =>
ctx.beginRpc("db.getAccount", id, ctx.options({ target: "poll://any@db" }))
);
const results = [];
for (const f of futures) {
results.push(yield* f);
}
return results;
}
ctx.options({ timeout: ... }) to bound retries.job/<uuid>, approve/<jobId>, db/op/<jobId>.