REST client
Drive Duraton from your app code: createClient is a typed wrapper over the HTTP API to trigger events and read or control runs.
createClient is how code outside a runner talks to Duraton: trigger events, read and control runs,
tail the event log, and read the numbers behind the console's charts - all typed, over plain HTTP.
import { createClient } from "@duraton/sdk/client";
const duraton = createClient({
url: process.env.DURATON_URL!, // your Duraton base URL, shown in the console
apiKey: process.env.DURATON_API_KEY,
});Prop
Type
A public key can call the GET-backed methods. Writes - events.send, cancel, pause,
resume, replay, retryFromStep, fork, bulkReplay, score, and the dataset methods - need a
secret key.
events
const res = await duraton.events.send({
name: "ticket.created",
app: "support-app",
data: { ticketId: "T-421" },
});
res.runId; // the run started, when exactly one workflow matched
res.triggered; // one entry per matched workflow
const events = await duraton.events.list({ app: "support-app", limit: 20 });
const one = await duraton.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 duraton.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 duraton.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 duraton.runs.listAll({ app: "support-app" })) {
console.log(run.id, run.status);
}
const run = await duraton.runs.get("01HXYZ...");
const steps = await duraton.runs.steps("01HXYZ...");
const stats = await duraton.runs.stats({ app: "support-app" });
// Look one run up by a business tag instead of its id - returns the match, or null.
const ticket = await duraton.runs.find({ tags: { ticketId: "123" } });list accepts the same filters as the runs API: app, workflow, status,
runType, eventId, session, replayOf, scoreName, minScore, maxScore, q, deep, since
(a string or Date), sort, dir, limit, cursor.
find(opts) is the single-run lookup: the same filters as list
minus pagination, capped at one row, returning Run | null. When several runs match, sort/dir
pick which one - the default is the most recent.
Trigger and await
runs.wait polls a run until it reaches a stop status and returns it - the "fire an event, get the
result" primitive. It stops at any terminal status by default; pass until to also
stop at a resting state like waiting or paused.
const { runId } = await duraton.events.send({ name: "ticket.created", data: { ticketId: "T-421" } });
const run = await duraton.runs.wait(runId!, { timeoutMs: 60_000 });
if (run.status === "succeeded") console.log(run.result);
else console.error(run.error);Prop
Type
wait rejects on timeout or an aborted signal - not on an unhappy outcome. A run that failed or
was cancelled resolves normally, so branch on run.status.
Live tails
// A run's timeline: status transitions and ctx.log lines, in ticket. Ends when the run is
// terminal; { from } resumes past a seq.
for await (const frame of duraton.runs.watch("01HXYZ...")) {
if (frame.kind === "log") console.log(frame.level, frame.message);
else console.log(frame.kind, frame.status);
}
// A run's persisted ctx.log history, oldest first; page forward with { from }.
const lines = await duraton.runs.logs("01HXYZ...", { from: 0, limit: 100 });
// Run status changes across the whole project. Only run_status frames flow and there is no
// per-run cursor - treat each frame as "refetch", not as a lossless log.
for await (const frame of duraton.runs.watchAll()) {
console.log(frame.runId, "->", frame.status);
}Control
// Each returns the affected run; replay, retryFromStep, and fork return the NEW run.
await duraton.runs.cancel("01HXYZ...");
await duraton.runs.pause("01HXYZ...");
await duraton.runs.resume("01HXYZ...");
const replayed = await duraton.runs.replay("01HXYZ..."); // or replay(id, editedInput)
const resumed = await duraton.runs.retryFromStep("01HXYZ...", "triage"); // fork from a step
const bulk = await duraton.runs.bulkReplay({ status: "failed", since: "2026-06-01T00:00:00Z" });
// The audit log of control actions (newest first), optionally filtered.
const history = await duraton.runs.controlActions({ runId: "01HXYZ..." });runs.fork, runs.score, and runs.scores are the eval surface - see Evals.
timeseries
Run counts and latency bucketed over time - the numbers behind the console's Overview charts.
const series = await duraton.runs.timeseries({
app: "support-app",
since: "2026-07-01T00:00:00Z",
bucket: 3600, // bucket width in seconds
});
series.bucketSeconds;
for (const b of series.buckets) {
console.log(b.ts, b.total, b.counts.failed, b.avgMs, b.maxMs);
}Prop
Type
Each bucket carries ts, per-status counts, a total, and avgMs / maxMs over the bucket's
terminal runs - both absent when the bucket has none.
flowState
The live buffer state of the flow controls: how many events are currently coalescing in a debounce window, and how many are buffered toward a batch flush.
const state = await duraton.flowState({ app: "support-app" });
for (const d of state.debounce) console.log(d.workflow, d.pending, d.nextFireAt);
for (const b of state.batch) console.log(b.workflow, b.buffered, b.oldestAt);In-flight and queued counts are not here - those come from runs.stats.
sessions, ai, approvals, datasets
// AI conversations: runs grouped by session id, most recent first.
const sessions = await duraton.sessions.list({ app: "support", limit: 20 });
// AI spend rolled up by hour, model, and workflow.
const spend = await duraton.ai.spend({ app: "support", since: "2026-07-01T00:00:00Z" });
// Decide an open human-in-the-loop gate, which resumes its parked run.
const open = await duraton.approvals.list({ status: "pending" });
await duraton.approvals.decide(open[0].id, { status: "approved", decidedBy: "[email protected]" });
// Datasets and eval run-sets - see Evals.
const ds = await duraton.datasets.create("qa-cases");workflows, apps, runners, health
await duraton.workflows.list(); // registered workflows, with their triggers and flow control
// Start one off-schedule run of a workflow, by identity - works even for a cron-only
// workflow. See "Trigger a run manually" in the triggers guide.
const res = await duraton.workflows.trigger("support-app", "nightly-report", { input: { force: true } });
res.runId; // absent if a flow-control gate short-circuited it
await duraton.apps.list(); // registered apps
await duraton.runners.list(); // registered runners and their liveness
await duraton.health(); // { status: "ok" }
await duraton.ready(); // { status, checks, draining }Errors
Any non-2xx response throws a DuratonApiError carrying the status and the raw body, with helpers
so you branch on intent instead of on status numbers.
import { DuratonApiError } from "@duraton/sdk/client";
try {
await duraton.runs.cancel(id);
} catch (err) {
if (err instanceof DuratonApiError && err.isConflict()) {
// 409: the run is already terminal
} else if (err instanceof DuratonApiError && err.isNotFound()) {
// 404: no such run
} else {
throw err;
}
}isBadRequest() (400), isUnauthorized() (401), isForbidden() (403), isNotFound() (404), and
isConflict() (409) map to Duraton's status codes.
Runtime
The client is built on the global fetch and runs on any fetch-native runtime (Node 18+, Bun, Deno,
Cloudflare Workers, browsers). The streaming methods - events.stream, runs.watch, runs.watchAll -
additionally need a streaming fetch body, which all of those provide. No dependencies.
const duraton = createClient({
url: process.env.DURATON_URL!,
apiKey: process.env.DURATON_API_KEY,
fetch: myInstrumentedFetch, // any fetch-compatible implementation
});Serving runners
Host workflows in an HTTP service you already deploy: serve, register, the framework adapters, invoke signature verification, and the SDK error types.
AI steps
Make every model call run once: the step.ai reference for generate, wrap, embed, and the durable agent loop, with providers, cost, and cache options.