String API
Feed Builder API

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 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, 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.