> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

```ts theme={null}
import { createClient } from "@duraton/sdk/client";

const duraton = createClient({
  apiKey: process.env.DURATON_API_KEY,
});
```

<ResponseField name="url" type="string" default="DURATON_URL, then https://run.duraton.dev">
  Your Duraton base URL. A trailing slash is fine.
</ResponseField>

<ResponseField name="apiKey" type="string" default="DURATON_API_KEY">
  The project API key, sent as a Bearer token.
</ResponseField>

<ResponseField name="fetch" type="typeof fetch" default="the global fetch">
  Override the fetch implementation - a custom agent, a test double.
</ResponseField>

A **public** key can call the `GET`-backed methods. Writes - `events.send`, `cancel`, `pause`,
`resume`, `replay`, `retryFromStep`, and `bulkReplay` - need a
**secret** key.

## events

```ts theme={null}
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

```ts theme={null}
// 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](/reference/api/runs): `app`, `workflow`, `status`,
`runType`, `eventId`, `session`, `replayOf`, `parentRunId`, `scoreName`, `minScore`, `maxScore`, `q`,
`deep`, `since` (a `string` or `Date`), `sort`, `dir`, `limit`, `cursor`.

`parentRunId` is the child-run filter: `runs.list({ parentRunId })` returns the runs one run's
`step.runWorkflow` calls spawned, the reverse of the `parentRunId` each child carries.

`find(opts)` is the single-run [lookup](/reference/api/runs#look-up-a-single-run): the same filters as `list`
minus pagination, capped at one row, returning `Run | null` - `parentRunId` included, so
`find({ parentRunId })` picks one child rather than a page of them. 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](/reference/api/runs) by default; pass `until` to also
stop at a resting state like `waiting` or `paused`.

```ts theme={null}
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);
```

<ResponseField name="until" type="RunStatus[]" default="the terminal statuses only">
  Extra non-terminal statuses to also stop at, e.g. \["waiting"]. The terminal set is always honoured on top, so the wait still resolves if the run finishes first.
</ResponseField>

<ResponseField name="timeoutMs" type="number" default="30000">
  Reject if the run has not reached a stop status within this many ms.
</ResponseField>

<ResponseField name="intervalMs" type="number" default="150">
  Poll cadence in ms.
</ResponseField>

<ResponseField name="signal" type="AbortSignal">
  Aborting it rejects the pending wait.
</ResponseField>

`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

```ts theme={null}
// A run's timeline: status transitions and ctx.log lines, in order. 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

```ts theme={null}
// Each returns the affected run; replay and retryFromStep 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..." });
```

### timeseries

Run counts and latency bucketed over time - the numbers behind the console's Overview charts.

```ts theme={null}
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);
}
```

<ResponseField name="app" type="string">
  Narrow the series to one app.
</ResponseField>

<ResponseField name="workflow" type="string">
  Narrow the series to one workflow, by exact name.
</ResponseField>

<ResponseField name="since" type="string">
  Only runs started at or after this timestamp.
</ResponseField>

<ResponseField name="bucket" type="number" default="3600 (one hour)">
  Bucket width in seconds.
</ResponseField>

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](/core/flow-control): how many events are currently
coalescing in a debounce window, and how many are buffered toward a batch flush.

```ts theme={null}
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

```ts theme={null}
// 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. The decider is
// recorded from the authenticated actor, so the decision cannot be attributed to anyone else.
const open = await duraton.approvals.list({ status: "pending" });
await duraton.approvals.decide(open[0].id, { status: "approved" });
```

## workflows, apps, runners, health

```ts theme={null}
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.

```ts theme={null}
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](/reference/api#conventions).

## 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.

```ts theme={null}
const duraton = createClient({
  url: process.env.DURATON_URL!,
  apiKey: process.env.DURATON_API_KEY,
  fetch: myInstrumentedFetch,   // any fetch-compatible implementation
});
```
