Skip to main content
You have a folder of invoices, contracts or reports, and you want rows in a database. This page goes from a PDF on your disk to validated JSON, in one script you can run now. Here is the whole trip. This is a real invoice PDF, and real output from the run that produced this page:
Forty-three seconds end to end on a cold session, most of it the sandbox starting. One API call to begin, one to collect.

Before you start

Three things, and the quickstart covers all of them:
and a model connected under Settings › LLM models. Check all three at once:
If that lists a credential, you are ready. The model in it is what goes in agent.model below.

The whole thing

Run it against any PDF. Standard library only — no SDK, no dependencies, in either language. The TypeScript is ESM: save it as .mts, or put "type": "module" in your package.json, or the top-level await will not compile.
The rest of this page is what each part does, and what to change when your documents are not invoices.

Getting your files in

This is the step most pipelines get stuck on, so it is worth being exact.
Base64 from a shell, if you are not in Python:
There is no type: "url" and no file-store id to reference: Gobare does not fetch addresses on your behalf. For inputs past the ceilings, put them in a repository and set environment.repo — the session clones it. To add a file to a session that is already running, the same shape goes to POST /v1/sessions/{session_id}/files. See sessions.md.

Binary formats

Your documents are probably not text, and you do not need to convert them. The session is a computer, and the agent is a coding agent on it. Asked for a PDF it cannot read directly, it writes code and reads it anyway. Both of these are from real runs: Nothing in the request asked for that fallback and nothing had to be installed. The same applies to .xlsx, .docx and images: it is a sandbox with Python in it, so “can it read my format” is usually “yes, and it costs a few seconds”. Two things follow. Budget for the slower path — a scanned page needing OCR is minutes, not seconds. And say the format in your prompt when you know it; “the PDF at invoices/x.pdf” saves the agent a file call.

Shaping the output

agent.text.format composes your JSON Schema into the model’s instructions. What that is worth, measured on the same invoice:
A string with a thousands separator where your loader wants a number. It will parse in your tests and fail on the first invoice over a thousand. So: always set it. And never trust it. The schema is composed into a prompt; it is not parsed, enforced or retried on your behalf. The assert in the script above is not decoration — it is the only line in this pipeline that turns “usually correct” into “correct or loud”.

Why two waits

A turn reaching completed does not mean its files are fetchable. Artifacts are published after the turn settles, so for a moment the list is legitimately empty — and an empty list is indistinguishable from “this turn produced nothing”. The turn’s artifacts field is the one to poll: Wait for anything that is not pending. Treating partial as “not done yet” loops for ever; treating it as ready is how a dropped file becomes a file you believe the agent never wrote. Using webhooks instead? Then ignore all of this: turn.completed is sent after publishing has finished.

A batch, not one file

One session per document, each with its own idempotency key derived from the input:
Re-running after a crash re-sends the same keys and gets the original sessions back rather than doing the work twice. Keys live 24 hours. Two ceilings shape a batch. An organization runs a limited number of sessions at once, so a hundred documents is a queue you drain rather than a hundred simultaneous calls; and both 429s — rate_limit_exceeded and project_limit_exceeded — need telling apart, because only one is worth retrying. An agent behind your API has the loop. For many rows from one turn, take them in a single request:

When it goes wrong

You have it working when

  • One session per document, each reaching artifacts: ready
  • Every artifact parses as JSON and passes your own type check
  • Re-running the batch with the same keys creates no new sessions
  • metadata on each session ties the row back to the document

Next