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.

On this page