---
title: "Workflow"
url: /workflow
---

# Workflow

> Work that runs for days and never loses its place: durable code, a state machine, or both.

Work that runs for days and never loses its place: a Workflow is durable code, a state machine, or both. It is the body of a long Verb, and each moment your Customer sees is a state.

```ts title="kickoff: durable code"
export default define({
  …
  Deal: {
    …
    kickoff: workflow(async ($, deal) => {
      const { plan } = await $.generative('Draft the kickoff plan for {company.name}', deal,
        { returns: { plan: 'string' } })
      await $.human('Approve the plan', { plan }, { role: 'manager' })
      const at = await $.step('stamp it', () => new Date().toISOString())
      return { plan, at }
    }),
  },
  on: { 'Deal.won': 'Deal.kickoff' },           // Trigger: what reacts to a change
  …
})
```

```mermaid title="kickoff: the machine"
stateDiagram-v2
  [*] --> reviewing: Deal.won
  reviewing --> approved: approve
  reviewing --> escalated: after 2d
  escalated --> approved: approve
```

## Durable code
A won Deal starts the kickoff: a model drafts the plan, a manager approves it, and the run keeps the time it was approved. The `on` line is its [Trigger](/patterns.md#the-blocks).

A Verb the `on` line names starts when its Event happens, once per Event, with no call.

Each `$` call is a step: it runs once, and its result is kept. A failed step retries until its limit runs out, and a run survives restarts and waits of weeks.

*What $ gives a run*

| Call | What it does |
| --- | --- |
| `$.step(name, fn)` | Runs `fn` once and keeps its result: a replay reads the result and never runs it again. |
| `$.code(fn, input)` | Runs a code Function as a step. |
| `$.generative(prompt, input, { returns })` | Asks a model, and answers the fields `returns` names. |
| `$.agentic(prompt, { tools, returns })` | Gives an agent the job and the Sources it may use; it answers when the job is done. |
| `$.human(task, input, { role })` | Waits for a member who holds the Role to complete the task at `/actions/{id}/complete`. |
| `$.wait('14d'), $.wait({ event, where })` | Waits for a duration, or for the first Event that matches `where`. |
| `$.call('Company.get', input)` | Calls a Verb of this API, or of a Source that is an API product, and answers its reply. |
| `return output` | Ends the run: the value is the Action's `output`. |

Durations read `'2d'` or `'30m'`, and a schedule reads `'every Mon, Wed, Fri 09:00'`, run once per Customer in that Customer's timezone. While the manager has not approved, the run is Pending, and its reason says who holds the ball.

**get**

```sh title="Request"
curl https://pipeline.apis.dev/actions/act_8 \
  -H "Authorization: Bearer $PIPELINE_KEY"
```

```http title="Response"
HTTP/1.1 200 OK
X-Cost: price=0, currency="USD"
X-Request-Id: req_1154ca02bc01e341ac7b

{
  "id": "act_8",
  "verb": "Deal.kickoff",
  "status": "pending",
  "reason": "AwaitingWorker",
  "poll": "/actions/act_8"
}
```

**complete**

```sh title="Request"
curl https://pipeline.apis.dev/actions/act_8/complete \
  -H "Authorization: Bearer $MANAGER_KEY" \
  --json '{"output":{"approved":true}}'
```

```http title="Response"
HTTP/1.1 200 OK
X-Cost: price=0, currency="USD"
X-Request-Id: req_5881a5df799b8e82a74b

{
  "id": "act_8",
  "verb": "Deal.kickoff",
  "status": "done",
  "output": {
    "plan": "Week 1: import. Week 2: first review.",
    "at": "2026-10-26T15:00:00Z"
  }
}
```

*Why an Action waits*

| Reason | What causes it |
| --- | --- |
| `AwaitingWorker` | A `$.human` step waits for a member who holds its Role. |
| `AwaitingInput` | A machine waits in a state for an event it `accepts`. |
| `AwaitingTime` | A `$.wait` duration or an `after` timer is running. |
| `AwaitingSource` | A Source has not answered yet, or a `$.wait` waits for its Event. |
| `AwaitingAction` | A `$.call` waits for another Action to finish. |
| `Retrying` | A step failed and runs again after its backoff, until its limit runs out. |
| `Running` | A step or a Function is working now. |

## State machine
Same job, other style: write code when the work is a list of steps, and a machine when it waits on people and the clock. A machine names its states, the events each one takes and its timers.

```ts title="kickoff: a state machine"
export default define({
  …
  Deal: {
    …
    kickoff: machine({
      initial: 'reviewing',
      states: {
        reviewing: {                      // the draft runs on the way in
          entry: generative('Draft the kickoff plan for {company.name}',
            { returns: { plan: 'string' } }),
          on: { approve: { target: 'approved', role: 'manager' } },
          after: { '2d': 'escalated' },
        },
        escalated: { on: { approve: { target: 'approved', role: 'manager' } } },
        approved:  { type: 'final' },
      },
    }),
  },
  on: { 'Deal.won': 'Deal.kickoff' },           // Trigger: what reacts to a change
  …
})
```

A machine's events are Verbs on its Noun, each sent by the Role it names; it fires `after` on its timer, and resumes where it left off.

The machine's state is the Action's `state`, and `accepts` lists the events it takes. `approve` is a Verb on Deal: `POST /deals/deal_1/approve`, `approveDeal` and `pipeline deals approve`.

**get**

```sh title="Request"
curl https://pipeline.apis.dev/actions/act_9 \
  -H "Authorization: Bearer $PIPELINE_KEY"
```

```http title="Response"
HTTP/1.1 200 OK
X-Cost: price=0, currency="USD"
X-Request-Id: req_e51d811890c4fe07c5e4

{
  "id": "act_9",
  "verb": "Deal.kickoff",
  "status": "pending",
  "reason": "AwaitingInput",
  "state": "reviewing",
  "accepts": ["approve"],
  "poll": "/actions/act_9"
}
```

**approve**

```sh title="Request"
curl -X POST https://pipeline.apis.dev/deals/deal_1/approve \
  -H "Authorization: Bearer $MANAGER_KEY"
```

```http title="Response"
HTTP/1.1 200 OK
X-Action-Id: act_9
X-Cost: price=0, currency="USD"
X-Request-Id: req_f157fac4bfe56e473ecf

{
  "id": "act_9",
  "verb": "Deal.kickoff",
  "status": "done",
  "state": "approved",
  "output": {
    "plan": "Week 1: import. Week 2: first review.",
    "at": "2026-10-26T15:00:00Z"
  }
}
```

An event the current state does not take answers 409, and a member without its Role answers 403.

## When a Verb is internal
A Verb that an `on` line starts and that names no Role is internal: it serves no path, tool or command. While it runs, its Thing is in its activity state, such as `drafting`.

**get**

```sh title="Request"
curl https://filings.apis.dev/filings/filing_1 \
  -H "Authorization: Bearer $FILINGS_KEY"
```

```http title="Response"
HTTP/1.1 200 OK
X-Cost: price=0, currency="USD"
X-Request-Id: req_f781a6ad9678c1e5146d

{
  "id": "filing_1",
  "company": "acme.com",
  "year": 2026,
  "status": "drafting",
  "createdAt": "2026-10-24T09:00:00Z",
  "updatedAt": "2026-10-24T09:00:01Z"
}
```

**draft**

```sh title="Request"
curl -X POST https://filings.apis.dev/filings/filing_1/draft \
  -H "Authorization: Bearer $FILINGS_KEY"
```

```http title="Response"
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
X-Cost: price=0, currency="USD"
X-Request-Id: req_537212d0f3c7ec2b2bb7

{
  "type": "https://filings.apis.dev/docs/errors#no_route",
  "title": "Not Found",
  "status": 404,
  "detail": "draft is internal; Filing.ordered starts it.",
  "code": "no_route",
  "instance": "req_537212d0f3c7ec2b2bb7"
}
```

## What you never write
A read, a create, an update, a delete and a lifecycle move answer the record, and a [Function](/patterns.md#function-api) or a Workflow answers its [Action](/reference.md#action). Durable runs survive restarts and long waits, and each run stays on the [Version](/reference.md#version) it started on.

*What you never write*

| What you never write | What does it instead |
| --- | --- |
| Code that starts a run | The `on` line, or a call to the Verb |
| A route that takes an event | A Verb on the Noun, which the machine accepts |
| A check for a wrong move | The lifecycle, which answers 409 before anything runs |
| A status route | The Action at `/actions/{id}`, with its reason while it waits |
| A history table | The change feed: every change as an Event, in order |

---

Seen is a state and unseen is a step: the drafted plan is a step, and `reviewing`, `escalated` and `approved` are states.