Walkthrough
One real build from POST to published feed, with the actual requests and responses.
Every request and response below is from a single real build — 680f1af1-367e-476a-9139-160e61645530,
which produced the feed job_postings.dwp_findajob_vacancies. Ids, timings and payloads are as they
came back, trimmed only where a field repeats. The one exception is the event-stream excerpt in step 2,
captured live from a second build — this one had already finished, and a finished build has no stream
left to watch.
1. Start the build
curl -X POST "https://feedbuilder.usestring.ai/v1/builds" \
-H "Authorization: Bearer $STRING_FEED_BUILDER_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "A feed of current job vacancies from the UK government Find a job service at https://findajob.dwp.gov.uk/ - capture job title, employer, location, salary, posting date, closing date, contract type and the listing URL."
}'{ "id": "680f1af1-367e-476a-9139-160e61645530", "object": "build" }Keep that id — it is the only handle you get, and every later step uses it.
2. Watch it
Two ways. Poll GET /v1/builds/{id}, or subscribe to the stream — same information, and the stream is
the right choice if a person is watching.
curl -N "https://feedbuilder.usestring.ai/v1/builds/{id}/events" \
-H "Authorization: Bearer $STRING_FEED_BUILDER_KEY" \
-H "Accept: text/event-stream"The opening of a real stream, verbatim, with the data: lines truncated where they run long:
id: 3
event: build.state
data: {"id":"2c9c0dc0-…","object":"build","version":3,"status":"running","query":"A feed of the current job openings …"}
id: 3
event: build.activity
data: {"at":"2026-08-28T15:28:21.481314356Z","type":"tool_call","tool_name":"Bash"}
id: 5
event: build.progress
data: {"id":"2c9c0dc0-…","version":5,"status":"running","progress":{"summary":"Working"}}
id: 5
event: build.activity
data: {"at":"2026-08-28T15:28:25.838602599Z","type":"narration","text":"The request clearly names its source …"}
id: 8
event: build.gate.raised
data: {"id":"3970a623-…","object":"gate","kind":"initial_query","prompt":"About to build this feed — confirm before I start","raised_at":"2026-08-28T15:28:28.583474626Z","expires_at":"2026-08-28T15:30:28.583478325Z","options":{…}}
: keep-alive| Frame | What it is |
|---|---|
build.state | A whole build object — the same one GET /v1/builds/{id} returns. Always the first frame. Render from it; everything after is an update. |
build.progress | status and a one-line progress.summary. Liveness, not history. |
build.activity | One step: a tool_call with its tool_name, or a narration Composer wrote. |
build.gate.raised | A gate opened. This is the signal to put a person on it. |
: keep-alive | An SSE comment every 15 seconds, so a silent build does not look like a dead connection. Ignorable by the protocol. |
id: carries the build's version, and it does not increment once per frame — several frames share a
version, and versions are skipped. Treat it as a checkpoint, not a sequence number.
Do not block waiting for `build.state`
On a reconnect, Last-Event-ID suppresses frames you already have rather than replaying ones you
missed — so if nothing changed while you were away, the stream opens silent and no snapshot arrives.
Render from what you have, or from GET /v1/builds/{id}, and let the frame update you when it comes.
3. Answer the first gate
Roughly a minute in, status became awaiting_input:
{
"status": "awaiting_input",
"version": 9,
"pending_gate": {
"id": "6776d1bc-8b32-4e40-acc2-95169ef2d580",
"kind": "initial_query",
"prompt": "About to build this feed - please confirm scope before I start.",
"options": {
"refined_request": "Build a job_postings feed that scrapes current job vacancies listed on the UK government's Find a Job service (findajob.dwp.gov.uk). Each row will capture: job title, employer/company name, location, salary (as displayed), posting date, closing date, contract type, and the listing URL. …"
}
}
}This is Composer restating your request before spending anything. Read refined_request — it is
your last cheap chance to correct the scope.
curl -X POST "https://feedbuilder.usestring.ai/v1/builds/680f1af1-…/gates/6776d1bc-…" \
-H "Authorization: Bearer $STRING_FEED_BUILDER_KEY" \
-H "Content-Type: application/json" \
-d '{"initial_query": "Confirmed. Build exactly that scope."}'204 No Content, and the build resumes.
Not every gate stops for you
This build raised three gates, but only two reached the caller. schema_review was answered
automatically — auto_proceed is on by default, and Composer answers the gates it can. It shows
up in gate_history as {"kind": "schema_review", "answer": {"schema": {"approve": true}}}.
Read gate_history to see what was decided on your behalf, and turn it off with
PATCH /v1/builds/{id} if you want every gate.
4. Approve publication
The crawl read 244 rows, and the build parked on its last gate at version 346:
{
"id": "3a1e9d90-8012-47b6-b90b-ff9ed135f940",
"kind": "publish_review",
"prompt": "Final review: feed job_postings.dwp_findajob_vacancies validated with 5 rows (sample below, verified server-side). Approve to publish — the feeds PR opens and the feed is registered — or reject with notes to withhold it.",
"options": {
"feed_name": "job_postings.dwp_findajob_vacancies",
"source_url": "https://findajob.dwp.gov.uk/",
"rows_written": 5,
"fields": [
{ "key": "job_title", "type": "string", "required": true, "repeated": false, "description": "Job title as listed" },
{ "key": "employer", "type": "string", "required": true, "repeated": false },
{ "key": "listing_url", "type": "string", "required": true, "repeated": false }
],
"sample_rows": [
["Locum SHO - (ENT, Surgery, Medicine, Psychiatry & T&O)", "Medecho LTD", "Chichester", "£35 - £45", "27 Aug 2026", "26 Sept 2026", "Contract", "https://www.jobs.service.gov.uk/jobs/6a9077df012f773fd8f48738"]
]
}
}sample_rows are positionally aligned with fields — real rows from a live run, not a schema preview.
curl -X POST "https://feedbuilder.usestring.ai/v1/builds/680f1af1-…/gates/3a1e9d90-…" \
-H "Authorization: Bearer $STRING_FEED_BUILDER_KEY" \
-H "Content-Type: application/json" \
-d '{"publish": {"approve": true, "notes": "Approved."}}'204. approve must be present — omitting it is 400 approve_required, never a decline.
5. Confirm it landed
{
"status": "completed",
"version": 349,
"report": {
"feed_name": "job_postings.dwp_findajob_vacancies",
"source_url": "https://findajob.dwp.gov.uk/",
"rows_written": 5
}
}completed with no error means the feed is registered and its delivery subscription exists. A build
that reaches a validated dataset but fails to register reports errored with the reason in error.
`version` keeps moving after `completed`
This build finished at version 349 and is now at 351: its feeds pull request was squash-merged
afterwards, and the report was amended to say so — merged flipped to true and notes gained
"required checks passed and the approved revision was squash-merged automatically".
completed_at does not move, and status does not change. So do not treat a terminal status as
"nothing will change again" — poll or stay subscribed if you care about the merge landing.
6. Read the schema back
curl "https://feedbuilder.usestring.ai/v1/builds/680f1af1-…/sample" \
-H "Authorization: Bearer $STRING_FEED_BUILDER_KEY"{
"object": "sample",
"build_id": "680f1af1-367e-476a-9139-160e61645530",
"feed_name": "job_postings.dwp_findajob_vacancies",
"fields": [ … 8 fields … ],
"sample_rows": [ … 5 rows … ],
"rows_written": 5
}This is a sample, not an export. The dataset itself arrives through the delivery subscription the publish-review gate registered — see Limits.
What to build on your side
The two things this walkthrough shows that a client has to handle:
- A gate can appear at any time and blocks until answered. Watch for
status == "awaiting_input"and route it to a person. Nothing calls you back — see Noticing a gate. - A build runs for tens of minutes. Treat
POST /v1/buildsas the start of a long job, keep the id, and poll or stream. Do not hold a request open waiting for it.