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

# Workflows

> Define a workflow, group it into an app, and trigger it with an event.

A workflow is a durable function started by an event or a schedule. You define it with
`workflow`, then connect a runner so Duraton can drive it.

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

interface TicketData {
  ticketId: string;
}

const ticketCreated = workflow<TicketData>({
  name: "ticket.created",
  retry: { maxAttempts: 3 },
  handler: async (ctx) => {
    return await ctx.step.run("triage", () => triageTicket(ctx.event.data));
  },
});
```

| Property    | Type                     | Default                 | Description                                                                                                                     |
| ----------- | ------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | `string`                 | required                | Identifies the workflow. With no `triggers`, an event of the same name starts it.                                               |
| `handler`   | `(ctx) => Promise<T>`    | required                | The workflow body. It receives the handler context and does durable work through `ctx.step`. See [Steps](/reference/sdk/steps). |
| `triggers`  | `Trigger[]`              | the workflow's own name | What starts the workflow: event triggers (with filters and wildcards) or cron schedules. See [Triggers](/core/triggers).        |
| `retry`     | `{ maxAttempts }`        | `{ maxAttempts: 1 }`    | How a failing step retries. See [Retries](/core/retries).                                                                       |
| `onFailure` | `(ctx) => Promise<void>` | none                    | Compensation or notification that runs once the run has failed. See [Retries](/core/retries#onfailure).                         |

The type parameter (`<TicketData>`) types `ctx.event.data`, so your event payload is checked. The full
handler context - `ctx.event`, `ctx.step`, `ctx.log`, `ctx.runId`, `ctx.attempt`, and the rest - is
documented in [SDK: Steps](/reference/sdk/steps). `workflow` also takes flow-control options
(`concurrency`, `throttle`, `rateLimit`, `debounce`, `batch`, `priority`, `singleton`, `idempotency`)
and AI spend options (`cap`, `tokenThrottle`); see
[Defining workflows](/reference/sdk/defining-workflows).

## Apps and runners

An **app** is a named set of workflows that run together in one process. A **runner** is a process
hosting one app. `connect()` dials Duraton over an outbound WebSocket, registers the app's workflows,
and receives invokes on that socket - so the runner needs no inbound URL and no separate registration
call.

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

const handle = connect({
  apiKey: process.env.DURATON_API_KEY,
  app: "support-app",
  workflows: [ticketCreated],
});

process.on("SIGTERM", () => handle.close());
```

Re-connecting re-registers the app's workflows, so restarts and deploys are safe.

## Triggering a run

Send an event whose `name` matches a workflow. Duraton creates a run and drives it to completion.

<Tabs>
  <Tab title="TypeScript">
    ```ts theme={null}
    import { createClient } from "@duraton/sdk";

    const duraton = createClient({ url: process.env.DURATON_URL });

    await duraton.events.send({
      name: "ticket.created",
      app: "support-app",
      data: { ticketId: "T-421" },
    });
    ```
  </Tab>

  <Tab title="REST API">
    ```sh theme={null}
    curl -X POST $DURATON_URL/events \
      -H "Authorization: Bearer $DURATON_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name":"ticket.created","app":"support-app","data":{"ticketId":"T-421"}}'
    ```
  </Tab>
</Tabs>

The response carries the run id. Inspect the run with [`GET /runs/{id}`](/reference/api/runs) and its steps
with [`GET /runs/{id}/steps`](/reference/api/runs).

The console lists every workflow an app has registered; select one to open its detail drawer, with the
definition, live stats, charts, and that workflow's runs.

<Frame>
  <img src="https://mintcdn.com/duraton-950ff1ca/C6sntRFXqHtJQroE/images/workflows.png?fit=max&auto=format&n=C6sntRFXqHtJQroE&q=85&s=54db1cfa6dfaae33fb2d789646a7f11e" alt="Registered workflows in the console" width="3200" height="2000" data-path="images/workflows.png" />
</Frame>

## The event log

Duraton keeps a durable **event log**: every event it ingests is recorded together with what it
triggered. [Triggers](/core/triggers) are the other half - what starts a workflow.

### What's recorded

An event reaches the log two ways: an external `POST /events` (`source: "api"`) or a workflow's
`step.emit` (`source: "emit"`). Each record carries the event (`name`, `app`, `data`), when it arrived,
and the outcome - the waiters it resumed and the per-workflow fan-out:

```json theme={null}
{
  "id": "9f2b…",
  "name": "ticket.created",
  "app": "support",
  "source": "api",
  "data": { "ticketId": "T-421", "subject": "Charged twice" },
  "receivedAt": "2026-06-15T09:00:00Z",
  "woke": 0,
  "triggered": [
    { "workflow": "fulfillment", "runId": "01H…" },
    { "workflow": "audit", "runId": "01H…" }
  ]
}
```

An event that matched nothing is still recorded with an empty `triggered` - so a fire-and-forget event
that hit no workflow is visible, not lost. A **cron** firing is not an event: it starts a run directly,
so it shows up in runs, not here.

### Reading it

```
GET /events                 # newest first; filter with ?app= ?name= ?limit=
GET /events/{id}            # one event
GET /events/stream          # live tail (Server-Sent Events)
```

The console's **Events** view lists the log and live-tails the stream. The stream is a best-effort
live view - a slow or reconnecting client can miss events; `GET /events` is the complete record.

<Note>
  Recording is best-effort on the ingest path: if the log write fails, event delivery still succeeds
  (the runs are already durable). The log is not auto-pruned, and listings return a bounded page.
</Note>

<Note>
  See the **events** example running end to end in [Examples](/start/recipes#fan-one-event-out-to-many-workflows).
</Note>
