Run lifecycle
Start a run safely, follow it as events or a light poll, steer, stop or cancel it, rename it, and undo a change that went live.
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, 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.
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 hasexisting_run_id, andLocationpoints 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 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
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 anevent_id. - New events carry an SSE
id. Past events do not. stream_heartbeatmessages carry no news. They carry the currentid.- To pick up where you left off, send the last
idasLast-Event-ID. - An event can come twice across a reconnect. Drop repeats by
event_id. stream_resetmeans live events are down for now. ReadGET /runs/{id}, then connect again.
The events are the same ones webhooks send. See Stream a run's 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.
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.
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 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.
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:
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
202right 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
409on 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.
6. Read what it made
run.result_kindispull_requestorno_changesonce the run completes.run.pr_urllinks to the pull request.final_response.contentis the run's own summary, oncefinal_response.statusisready.GET /runs/{id}/proposed-diffreturns the diff it delivered.run.landsays whether the change went live. See Review and 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.
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}'
409if the change never landed.409if newer changes landed on top of it. Send"force": trueto revert it anyway.404if 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.