A run is one task on one project. This page walks through a run from the first call to the last. Each step links to its page in the [API reference](/docs/api), which lists every field.

Every path below starts with `https://api.syndai.ai/api/v1/external/coding`. Every call sends `Authorization: Bearer $SYNDAI_API_KEY`.

## 1. Start it

`POST /runs` starts the work. It costs credits, so it is safe to retry only with the same key.

```bash
curl -s -X POST https://api.syndai.ai/api/v1/external/coding/runs \
  -H "Authorization: Bearer $SYNDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Add a contact form to the home page",
    "coding_repository_id": "<REPO_ID>",
    "external_request_id": "contact-form-001",
    "title": "Contact form"
  }'
```

`external_request_id` is your idempotency key. Pick one per piece of work, and keep it.

- The same key and body again: `409 IDEMPOTENCY_REPLAY`. The body has `existing_run_id`, and `Location` points at the run. No second run starts.
- The same key with a new body: `409 CONFLICT`.
- A network timeout: send the same call again and follow `existing_run_id`.

A follow-up is a new run. Send `parent_coding_run_id` and a new key. See [Create a run](/docs/api/runs/create-run) for every field.

## 2. Follow it

There are four ways to watch a run. Pick the one that fits your client.

| route | use it for |
|---|---|
| `GET /runs/{id}/events` | a live stream of events (server-sent events) |
| `GET /runs/{id}/change` | one short state, and a wait of up to 45 s for the next change |
| `GET /runs/{id}/live` | a light poll: goal, activity, questions, steer messages |
| `GET /runs/{id}` | the full state, when you need to decide what to do |

Phases are open-ended. A run can stop to ask you something at any time. So act on the state you read, not on the order you expect.

### The event stream

```bash
curl -s --no-buffer \
  -H "Authorization: Bearer $SYNDAI_API_KEY" \
  -H "Accept: text/event-stream" \
  "https://api.syndai.ai/api/v1/external/coding/runs/$RUN_ID/events"
```

The stream sends the run's past events first, then new ones as they happen.

- Each event has a name, such as `coding.run.phase_changed`, and an `event_id`.
- New events carry an SSE `id`. Past events do not.
- `stream_heartbeat` messages carry no news. They carry the current `id`.
- To pick up where you left off, send the last `id` as `Last-Event-ID`.
- An event can come twice across a reconnect. Drop repeats by `event_id`.
- `stream_reset` means live events are down for now. Read `GET /runs/{id}`, then connect again.

The events are the same ones [webhooks](/docs/api/webhooks#events) send. See [Stream a run's events](/docs/api/runs/stream-run-events).

### The change state

`GET /runs/{id}/change` returns a short **ChangeState**: `state`, a `headline`, open questions in `needs_input`, a `held` block, `live_url` and `done`. Pass the last `cursor` with `wait` (at most 45) and the call waits until something that matters moves. It can come back early with no change. Then call again. This is the loop the CLI and the MCP server use. See [Get a run's change state](/docs/api/runs/get-change-state).

### The timeline

`GET /runs/{run_id}/timeline` lists every event the run recorded, each with its time and sequence. Use it to look back over a run that has ended. See [Get a run's timeline](/docs/api/runs/get-run-timeline).

## 3. Answer it

When a run needs you, it sends `coding.run.question_required`. Read `pending_hitl_requests` from `GET /runs/{id}`. Answer each one with `POST /runs/{id}/responses`, as the [quickstart](/docs/api/quickstart#6-answer-a-question) shows. Send an `idempotency_key`, so a retry is safe.

A run can also wait for a person to review its spec. That is in [Review and land](/docs/guides/review-and-land).

## 4. Steer, stop or cancel it

| call | what happens to the work | scope |
|---|---|---|
| `POST /runs/{id}/steer` | The agent reads your message at its next turn. The run goes on. | `coding:runs:respond` |
| `POST /runs/{id}/stop` | The run stops, most often within a minute. It saves its work so far as a draft pull request. | `coding:runs:cancel` |
| `POST /runs/{id}/cancel` | The run stops and drops its work. Nothing is delivered. | `coding:runs:cancel` |

Steer a run in progress:

```bash
curl -s -X POST \
  "https://api.syndai.ai/api/v1/external/coding/runs/$RUN_ID/steer" \
  -H "Authorization: Bearer $SYNDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Keep the form to name, email and message."}'
```

A few rules:

- Steering a run that has ended is `409`. Start a follow-up run.
- Stop answers `202` right away and ends a moment later. Follow the run until it ends.
- A second stop, or a stop or cancel on a run that ended, changes nothing.
- A `409` on cancel means the run is busy. Wait a moment and send it again.

## 5. Rename it

`POST /runs/{id}/rename` with `{"title": "…"}` changes the name people see. It never changes the work or the `external_request_id`. See [Rename a run](/docs/api/runs/rename-run).

## 6. Read what it made

- `run.result_kind` is `pull_request` or `no_changes` once the run completes. `run.pr_url` links to the pull request.
- `final_response.content` is the run's own summary, once `final_response.status` is `ready`.
- `GET /runs/{id}/proposed-diff` returns the diff it delivered.
- `run.land` says whether the change went live. See [Review and land](/docs/guides/review-and-land#4-land).

## 7. Undo a change that went live

`POST /runs/{id}/revert` starts a new run that undoes the change. It is work like any run, so it costs credits. Call it again while that revert is open and you get the same one.

```bash
curl -s -X POST \
  "https://api.syndai.ai/api/v1/external/coding/runs/$RUN_ID/revert" \
  -H "Authorization: Bearer $SYNDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"force": false}'
```

- `409` if the change never landed.
- `409` if newer changes landed on top of it. Send `"force": true` to revert it anyway.
- `404` if the run did not land the change itself.

The answer has the new `run` and its `land`. Follow that run like any other. See [Revert a landed change](/docs/api/landing/revert-run).
