String API
Composer API

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."
  }'
201 Created
{ "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
FrameWhat it is
build.stateA whole build object — the same one GET /v1/builds/{id} returns. Always the first frame. Render from it; everything after is an update.
build.progressstatus and a one-line progress.summary. Liveness, not history.
build.activityOne step: a tool_call with its tool_name, or a narration Composer wrote.
build.gate.raisedA gate opened. This is the signal to put a person on it.
: keep-aliveAn 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:

GET /v1/builds/680f1af1-…
{
  "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:

pending_gate
{
  "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

GET /v1/builds/680f1af1-… after approval
{
  "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/builds as the start of a long job, keep the id, and poll or stream. Do not hold a request open waiting for it.