REST client
createClient - a typed wrapper over the engine HTTP API to trigger events and read or control runs from app code.
The SDK has two halves. The authoring + runner half (defineWorkflow, serve,
register, connect) is how a runner runs workflows. The client is how the rest of your code
talks to the engine: trigger events, read and control runs, tail the event log, and list workflows
or apps - all typed, over plain HTTP.
import { createClient } from "@durablex/sdk/client";
const dx = createClient({
engineUrl: process.env.DURABLEX_ENGINE_URL!,
apiKey: process.env.DURABLEX_API_KEY, // omit when the engine runs auth-off
});createClient is also re-exported from the package root (@durablex/sdk) so you can trigger events
from a runner without a second import. The dedicated @durablex/sdk/client entry is runner-free - use it
from an admin tool, a frontend, or a script that never authors workflows.
apiKey falls back to DURABLEX_API_KEY. A public (read-only) key can call the GET-backed methods; writes
(send, cancel, pause, resume, replay, retryFromStep, bulkReplay) need a secret key.
events
// Trigger workflows by sending an event.
const res = await dx.events.send({
name: "order.created",
app: "order-app",
data: { orderId: "A1" },
});
res.runId; // the run started, when exactly one workflow matched
res.triggered; // one entry per matched workflow
// Read the event log (newest first).
const events = await dx.events.list({ app: "order-app", limit: 20 });
const one = await dx.events.get(events[0].id);
// Live tail (Server-Sent Events). Iterate to consume; abort to stop.
const ac = new AbortController();
for await (const ev of dx.events.stream({ signal: ac.signal })) {
console.log(ev.name, ev.triggered);
}runs
// One page, newest first, plus the keyset cursor for the next page.
const page = await dx.runs.list({ status: "failed", sort: "duration", dir: "desc", limit: 20 });
page.runs;
page.nextCursor; // pass back as { cursor } for the next page, or null on the last
// Walk every run across pages - the cursor is managed for you.
for await (const run of dx.runs.listAll({ app: "order-app" })) {
console.log(run.id, run.status);
}
const stats = await dx.runs.stats({ app: "order-app" }); // { total, active, succeeded, failed, successRate }
const run = await dx.runs.get("01HXYZ...");
const steps = await dx.runs.steps("01HXYZ...");
// Tail a run's timeline live - status transitions and ctx.log lines, in order.
// The stream ends when the run is terminal; { from } resumes past a seq.
for await (const frame of dx.runs.watch("01HXYZ...")) {
if (frame.kind === "log") console.log(frame.level, frame.message);
else console.log(frame.kind, frame.status);
}
// Read a run's persisted ctx.log history, oldest first; page forward with { from }.
const lines = await dx.runs.logs("01HXYZ...", { from: 0, limit: 100 });
// Tail run status changes across the whole namespace - a best-effort signal to refresh a list
// or overview. Only run_status frames flow; treat each as "refetch", not a lossless log.
for await (const frame of dx.runs.watchAll()) {
console.log(frame.runId, "->", frame.status);
}
// Control a run. Each returns the affected run (replay and retryFromStep return the new run).
await dx.runs.cancel("01HXYZ...");
await dx.runs.pause("01HXYZ...");
await dx.runs.resume("01HXYZ...");
const replayed = await dx.runs.replay("01HXYZ...");
const resumed = await dx.runs.retryFromStep("01HXYZ...", "charge"); // fork from a step
const bulk = await dx.runs.bulkReplay({ status: "failed", since: "2026-06-01T00:00:00Z" });
// The audit log of control actions (newest first), optionally filtered.
const history = await dx.runs.controlActions({ runId: "01HXYZ..." });list accepts the same filters as the runs API: app, workflow, status,
runType, eventId, replayOf, q, deep, since (a string or Date), sort, dir,
limit, cursor.
workflows, apps, health
await dx.workflows.list(); // registered workflows
await dx.apps.list(); // registered apps
await dx.health(); // { status: "ok" }
await dx.ready(); // { status, checks, draining }Errors
Any non-2xx response throws a DurablexApiError carrying the status and raw body, with helpers so
you branch on intent instead of status numbers:
import { DurablexApiError } from "@durablex/sdk/client";
try {
await dx.runs.cancel(id);
} catch (err) {
if (err instanceof DurablexApiError && err.isConflict()) {
// 409: the run is already terminal
} else if (err instanceof DurablexApiError && err.isNotFound()) {
// 404: no such run
} else {
throw err;
}
}isBadRequest() (400), isUnauthorized() (401), isForbidden() (403), isNotFound() (404), and
isConflict() (409) map to the engine's status codes.
Runtime
The client is built on the global fetch and works on any fetch-native runtime (Node 18+, Bun, Deno,
Cloudflare Workers, browsers). events.stream additionally needs a streaming fetch body, available on
all of those. No dependencies.