Gates
How a supervised build asks you a question, and how you answer it.
A gate is the builder 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, deliberately: a
discriminator that can disagree with its payload is a way to be coerced onto an arm you did not fill in.
- 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
400from the core. This API never guesses a kind.
| Gate kind | What it asks | Body |
|---|---|---|
choice | Pick one of options.choices | {"choice_id": "opt-1"} |
confirm | Yes or no | {"confirm": true} |
schema_review | Approve or edit the proposed columns | {"schema": {"approve": true, "edits": [], "notes": ""}} |
text | Anything, in prose | {"text": "…"} |
source_picker | Which source URL to build from | {"source": {"chosen_url": "…"}} or {"source": {"re_discover": true}} |
publish_review | Approve publication of the finished feed | {"publish": {"approve": true, "cadence": "…", "delivery_target": []}} |
initial_query | Confirm the refined request | {"initial_query": "…"} |
questionnaire | Several 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, a body that omits approve is refused with 400 approve_required. It is
never read as false. null earns the same 400.
This is the single most expensive way for a client bug to be invisible. A silent false at a publish-review gate
is not "unanswered" — it is a considered refusal that withholds a finished feed and ends the run, with nothing
anywhere recording that nobody actually decided.
If your generated client models approve as a plain boolean defaulting to false, fix the client. An absent
decision must never reach this API as a decline.
Note the contrast with SchemaColumnEdit.drop, where omitted means keep the column. That default is
non-destructive, which is exactly why it is allowed to have one and approve is not.
{"text": "…"} answers every gate kind
The builder raises gate kinds 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.
A client that meets an unrecognised kind should show the gate's prompt and send the operator's typed reply as
text, rather than failing the build. That escape hatch is what keeps a new gate kind from being a breaking change.
// 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, because the builder has to be able to tell a skip from an answer. 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 is what opens the pull request and registers the delivery subscription. A build that
reaches a validated feed and never gets that approval produces nothing you can collect — which is also why
unattended modes are refused.
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.