String API
Composer API

Gates

How a build asks you a question, and how you answer it.

A gate is Composer stopping to ask you something. While one is open the build's status is awaiting_input and pending_gate carries the question. Nothing moves until you answer it.

GET  /v1/builds/{id}            → status: "awaiting_input", pending_gate: { id, kind, prompt, options }
POST /v1/builds/{id}/gates/{gate_id}

The gate_id comes from pending_gate.id on the build, or from a build.gate.raised event on the stream.

The body's shape is the discriminator

Set exactly one member. Which one you set picks the answer kind; there is no kind field.

  • Setting none is a 400 (gate_answer_required).
  • Setting more than one is a 400 (gate_answer_ambiguous).
  • An answer whose member does not match the gate's kind is a 400 from the core. This API never guesses a kind.
Gate kindWhat it asksBody
choicePick one of options.choices{"choice_id": "opt-1"}
confirmYes or no{"confirm": true}
schema_reviewApprove or edit the proposed columns{"schema": {"approve": true, "edits": [], "notes": ""}}
textAnything, in prose{"text": "…"}
source_pickerWhich source URL to build from{"source": {"chosen_url": "…"}} or {"source": {"re_discover": true}}
publish_reviewApprove publication of the finished feed{"publish": {"approve": true, "cadence": "…", "delivery_target": []}}
initial_queryConfirm the refined request{"initial_query": "…"}
questionnaireSeveral questions at once{"questionnaire": {"answers": {"q1": "…"}}}

204 on success.

approve must be sent, and it is never inferred

An omitted `approve` is a `400`, not a decline

On schema_review and publish_review, omitting approve is 400 approve_required; null is the same. It is never read as false — a silent false at a publish-review gate withholds a finished feed and ends the run.

If your generated client serializes an unset boolean as false, fix the client.

SchemaColumnEdit.drop does have a default: omitted means keep the column.

{"text": "…"} answers every gate kind

Gate kinds are raised dynamically, so your client will meet a kind that did not exist when it was written. A plain text answer is valid against every kind, including unknown ones — show the gate's prompt and send the typed reply as text rather than failing the build.

// Correct response to a kind you have no renderer for.
{ "text": "yes, use the second source and drop the SKU column" }

Answering a questionnaire

answers is keyed by Question.id. A question you are skipping must be absent, never present with an empty string. At least one entry is required.

{ "questionnaire": { "answers": { "q_region": "EU only", "q_currency": "EUR" } } }

An open question is one whose choices array is empty.

Answering a source picker

Set exactly one of chosen_url or re_discover. Both, or neither, is a 400 (source_answer_ambiguous).

The publish-review gate is the one that ships the feed

Approving publish_review opens the pull request and registers the delivery subscription. That approval is the only route to the rows. See Limits.

A gate can expire, and expiring means answered

pending_gate.expires_at is the deadline. If it is present, not answering is a decision: at that moment Composer answers the gate for you with its own suggested answer and the build carries on. The gate does not fail, and the build does not stop — you simply were not the one who decided.

gate_history records it, with origin set to timeout rather than user. That is how you tell afterwards which decisions were yours.

Windows differ per gate — a scope confirmation gets minutes, a schema review longer — so read expires_at rather than assuming a fixed budget.

No `expires_at` means it waits for you

The field is absent when auto-proceed is off for that gate, and then the build waits indefinitely. Turn auto-proceed off with PATCH /v1/builds/{id} if you would rather a build stall than have a decision made for it.

Noticing a gate in the first place

Nothing calls you back — there are no webhooks on this API, and nothing in it will ever make an outbound request to you. A build stops at a gate and stays stopped indefinitely, so watching for that is your side's job, and an unanswered gate is the most common reason a build appears to have stalled.

You are building…Watch
A UI where someone watches the buildGET /v1/builds/{id}/eventsbuild.gate.raised
A backend job that checks in periodicallyGET /v1/builds/{id}, comparing version
A dashboard over many buildsGET /v1/builds — the summary rows carry pending_gate_id and pending_gate_kind

GET /v1/builds answers "is anything of mine waiting on me?" across every build at once, without fetching each one. If you poll, poll often enough that a person is not waiting on your cron.

Answering a gate that is no longer pending

409, not 400. Your JSON is fine; the build's state moved. Re-read GET /v1/builds/{id} and answer whatever is pending now.