# Runs

> A run executes a graph, from a saved project or from an inline graph you send with the request. Runs are asynchronous, observable while they happen, and report what they cost.

Source: https://kaitoi.io/docs/concepts/runs/
Section: Concepts
Written for: Anyone executing Kaitoi work from code rather than from the canvas.
Prerequisites: A Kaitoi API key with the runs:execute scope; A node type or saved project to execute
Last reviewed: 2026-09-16

---

A run is one execution of a graph. You create it, it goes away and works, and
you find out what happened by polling it or by streaming its events.

Creating a run returns `202 Accepted`, not the result. Anything that generates
media takes longer than a request should stay open for, so the API does not
pretend otherwise.

## Two sources, one lifecycle

A run executes either a saved project or an inline graph:

```json
{ "projectId": "proj_..." }
```

```json
{ "graph": { "nodes": [ ... ] }, "targetNodeIds": ["a"] }
```

Send one or the other. An inline graph run must name at least one target node,
because there is no saved default to fall back on. A saved-project run may omit
`targetNodeIds` and let the service pick the project's default target.

Either way the server snapshots the graph it is about to execute before it
starts. A run is not affected by edits made to the project while it is in
flight.

## Status

A run moves through a small set of states:

| Status | Meaning |
|---|---|
| `accepted` | The request was validated and the run exists. |
| `queued` | Waiting for capacity. |
| `running` | Executing. |
| `succeeded` | Finished, outputs available. |
| `failed` | Finished, `error` populated. |
| `canceled` | Stopped before finishing. |

`succeeded`, `failed` and `canceled` are terminal. Nothing leaves them.

Cancellation is a request rather than a guarantee of immediacy. A run in
`accepted` or `queued` becomes terminal straight away, because nothing has
started. A run already `running` is marked `cancelRequested` and stops at the
next safe point, which avoids leaving a half-written output behind.

## Watching a run

Two ways, same events underneath.

**Poll** `GET /runs/{id}/events` when you want a simple loop and do not need to
react within a second. **Stream** `GET /runs/{id}/events/stream` for Server-Sent
Events when you are driving a UI.

Events carry a stable `type` such as `run.accepted`, `node.progress` or
`node.log`, an optional `nodeId`, an optional `progress` number, and a
`createdAt`. The SSE `id` is an opaque cursor: send it back as `Last-Event-ID`
to resume a dropped stream without replaying everything or missing anything.

Cursors are only valid for the same run and the same key that issued them.

## Inputs without editing the project

`inputOverrides` sets input values for a single run without changing the saved
project. It is keyed by node id, then by input name, and each value declares its
own type:

```json
{
  "projectId": "proj_...",
  "inputOverrides": {
    "prompt_node": {
      "text": { "type": "string", "value": "a lighthouse at dusk" }
    }
  }
}
```

The available types are `string`, `number`, `boolean`, `json`, `file` and
`null`. A `file` override references a file id from the Files API rather than
carrying bytes.

This is what makes one saved project serve many end users. The project is the
template; the overrides are the request.

## Cost

When a run reaches a terminal state it reports what it actually cost, in
credits, as `creditsUsed`, with `creditsUsedMicrocredits` carrying the exact
figure in millionths of a credit for anything that needs to reconcile precisely.

`creditsUsed` is null while a run is active. It can also be null on a terminal
run where a reliable figure could not be produced, so treat null as "unknown"
rather than as zero.

## Keeping the result

A succeeded run's outputs are on the run. If it was a saved-project run and you
want those outputs written back into the project, call
`POST /runs/{id}/apply-to-project`. The server persists its own outputs; the
endpoint does not accept output values from the client.

## Retries

`POST /runs` accepts an `Idempotency-Key` header. Send one. A network timeout on
run creation is otherwise indistinguishable from a success you did not hear
about, and retrying without a key starts a second run that costs real money.

## Current limitations

- Runs are per-account. A run is only readable by the key owner that created it.
- There is no run-level webhook yet. Observation is polling or SSE.
