# Workflow runs

> Start workflow runs, send them signals, read and follow a run's tree, and cancel a run with everything it started.

A [workflow](/docs/concepts/workflows) run is a job, so the [Jobs](/docs/api/jobs) endpoints work on runs too. The code of a workflow lives in your workers, written with an SDK: these endpoints start runs, signal them and watch them from anywhere.

## Start a run

`POST /v1/spaces/{space}/jobs` · scope `create`

Create a job with the workflow's name, its input as the payload, and `"workflow": true`. A worker that serves the workflow picks it up.

```bash
curl -s "$EMIT_URL/v1/spaces/accounts/jobs" \
  -H "Authorization: Bearer $EMIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "trial", "payload": {"userId": "usr_42", "email": "ada@example.com"}, "key": "trial-usr_42", "workflow": true}'
# {"created":[{"id":"01M3QSR386017JCY0DC11GTG7D","outcome":"NEW","attempt":0}]}
```

Use `key` against double starts, or `singleton` for one open run at a time: see [Starting, keys and cancelling](/docs/concepts/workflows#starting-keys-and-cancelling). Wait for the result by [following the run](/docs/api/jobs#follow-a-job), or by getting it once it's done.

## Send signals

`POST /v1/spaces/{space}/jobs:signal` · scope `create`

Send one signal, an array, or `{"signals": [...]}`. Name the run by exactly one of `id`, `key` and `singleton`.

:::fields
:::field name {type="string" required}
The signal's name: what the run waits for. At most 256 bytes.
:::
:::field id {type="string"}
The run's id.
:::
:::field key {type="string"}
The `key` the run was started with.
:::
:::field singleton {type="string"}
The `singleton` the run was started with.
:::
:::field data {type="JSON"}
What the run receives. At most 4 MiB.
:::
:::

:::code-group
```bash curl
curl -s "$EMIT_URL/v1/spaces/accounts/jobs:signal" \
  -H "Authorization: Bearer $EMIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "trial-usr_42", "name": "upgraded", "data": {"plan": "pro"}}'
```
```ts TypeScript
const took = await trial.signal({ key: "trial-usr_42" }, "upgraded", { plan: "pro" });
```
```go Go
took, err := trial.Signal(ctx, emit.ByKey("trial-usr_42"), "upgraded", map[string]string{"plan": "pro"})
```
:::

```json
{ "delivered": [true], "ids": ["01M3QSR386017JCY0DC11GTG7D"] }
```

One entry per signal: `delivered` is `true` if a run took it, and `ids` names that run. `false`, with an empty id, means there's no unfinished run to take it. A signal is never refused for that.

A signal sent before the run waits for it is kept. A signal repeated after a lost answer is delivered again, so send data that can arrive twice safely.

## Signal, or start

`POST /v1/spaces/{space}/jobs:signalOrStart` · scope `create`

Delivers a signal to the open run under a `singleton`, or, if there's none, starts a run with `job` instead. Two calls at once never start two runs.

:::fields
:::field singleton {type="string" required}
The run to find, or to start under.
:::
:::field name {type="string"}
The signal to send. Leave it out to only start a run if none is open.
:::
:::field data {type="JSON"}
The signal's data.
:::
:::field job {type="object" required}
The run to start if none is open, as in [Create jobs](/docs/api/jobs#create-jobs): its `name`, `payload` and `"workflow": true`. No `key`. The run it starts doesn't get the signal: its input is how it begins.
:::
:::

```bash
curl -s "$EMIT_URL/v1/spaces/accounts/jobs:signalOrStart" \
  -H "Authorization: Bearer $EMIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "singleton": "trial-usr_88",
    "name": "upgraded",
    "data": {"plan": "pro"},
    "job": {"name": "trial", "payload": {"userId": "usr_88", "email": "di@example.com"}, "workflow": true}
  }'
# {"id":"01M3QSRDB002NRZ5VBGNQXKT8Q","started":true}
```

`started` is `true` if this call started the run, and `false` if a run was open and got the signal.

## Read a run's tree

`GET /v1/spaces/{space}/jobs/{id}/tree` · scope `read`, or a follow token

The run and everything under it: its steps, sleeps and signals, in the order they were made. A step that's itself a workflow run brings its own children. This is what the dashboard's run timeline shows.

```bash
curl -s "$EMIT_URL/v1/spaces/accounts/jobs/01M3QSR386017JCY0DC11GTG7D/tree" -H "Authorization: Bearer $EMIT_TOKEN"
```

```json
{
  "id": "01M3QSR386017JCY0DC11GTG7D",
  "name": "trial",
  "status": "COMPLETED",
  "attempt": 3,
  "children": [
    { "name": "send-email", "lane": 0, "status": "COMPLETED", "label": "", "children": [] },
    { "name": "_timeout", "lane": 2, "status": "CANCELLED", "untilMs": "0", "children": [] },
    { "name": "upgraded", "lane": 1, "status": "COMPLETED", "children": [] }
  ]
}
```

Each node has the job's `id`, `name`, `status`, `attempt`, `maxAttempts`, `createdAtMs`, `finishedAtMs`, `progress` and `children`, plus:

:::fields
:::field lane {type="integer"}
What the node is in its run: `0` a step, `1` a signal (its `name` is the signal's), `2` a sleep (`_sleep`) or a wait's timeout (`_timeout`).
:::
:::field untilMs {type="string"}
For a sleep or a timeout, when it ends. For a running step, when its lease ends.
:::
:::field error {type="string"}
For a failed step, or one waiting to be retried, its last error.
:::
:::field notes {type="object[]"}
The lines written for people to read (`wf.note`, `job.note`), in order.
:::
:::field label {type="string"}
The few words the step was started with.
:::
:::field detached {type="boolean"}
The step is waiting on a [link](/docs/concepts/workflows#people-and-outside-services).
:::
:::field flow {type="object"}
For a run: `parked` is `true` while it waits, with no worker holding it, and `awaiting` names the signals it waits for.
:::
:::field nextN {type="integer"}
Set when this node's children were cut to fit: ask for `?from_n=` this number to read the rest.
:::
:::field more {type="integer"}
With `nextN`: how many of its children were cut.
:::
:::

A tree holds at most 20,000 nodes, 14 MiB, and 16 levels of runs. From emit 0.6, `?limit=` reads a page of at most that many nodes (up to 500): the run's `nextN` says where the next page starts, so ask again with `?from_n=` that number until there's no `nextN`. A child run cut by the page has its own `nextN`: page it by its own id. If the run starts over (`flow.gen` changes) while you page, start again from the beginning.

To follow a run as it changes, add `?tree=true` to [Follow a job](/docs/api/jobs#follow-a-job): each message is then the whole tree, sent again whenever any node in it changes. Someone following with a follow token sees names, statuses, progress and notes, but no inputs, results, errors or labels.

## Cancel a run

`POST /v1/spaces/{space}/jobs:cancel` · scope `admin`

With `"tree": true`, cancels each run and everything it started, all the way down: steps, child workflows and their steps, and steps it enqueued.

```bash
curl -s "$EMIT_URL/v1/spaces/accounts/jobs:cancel" \
  -H "Authorization: Bearer $EMIT_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["01M3QSR386017JCY0DC11GTG7D"], "tree": true}'
```

Without `tree`, the run and the steps it started are cancelled, and steps it enqueued go on.
