Skip to main content

When to use this

  • v2 → v3 migration — v2 exposed a per-workflow CSV download; v3 doesn’t, so reproduce it in a few lines of SDK code.
  • Daily reporting jobs — grab yesterday’s runs on a cron, drop the CSV in a bucket / warehouse / email.
  • Ad-hoc pulls — analysts asking for “everything we processed on 2026-07-14 in Madrid time”.

What it does

  1. Convert the requested local day (Europe/Madrid) into a half-open UTC interval and hand it to GET /v3/workflows/{id}/runs/ via the document_packet_created_after / document_packet_created_before query params — server-side filtering, one round-trip per page.
  2. Fan out GET /v3/runs/{run_id}/ in parallel to fetch inline results.
  3. Emit one CSV row per run: run_id, packet_id, then one column per top-level scalar field. Nested list-of-object fields (e.g. line items) are skipped — v2’s CSV also flattened only scalars. Assumes a linear parse → extract workflow (no split / classify branches).
Concurrency + rate limiting, stdlib only:
  • asyncio.Semaphore(CONCURRENCY) — in-flight request cap.
  • A sliding-window token bucket at 55 req/min — one acquire() per outbound HTTP call, under the API’s 60 req/min ceiling with headroom for a 429 retry (which honours Retry-After).
Progress is logged to stderr at INFO — every request with its query params, the computed local↔UTC window, page sizes, and per-run status — so you can verify the timezone conversion at a glance.

End-to-end

Run it

Expected stderr (abridged):
Check that the Madrid-UTC offset for the day looks right: +02:00 in summer (CEST) with UTC bounds at 22:00, +01:00 in winter with 23:00 — Spain observes DST, so the exact UTC hours shift by month.

Adapting it

This is a starting point, not a production template. Common tweaks:
  • Nested rows — need line items? For each processed run, iterate the object list at extraction["fields"]["line_items"] (a list[dict[str, {"value": ...}]]) and emit one row per child, or write a second CSV.
  • Confidence / evidence columns — each cell dict carries confidence and evidence alongside value; append <field>_confidence columns or fold evidence text in as needed.
  • Windowing — swap parse_day for a --from / --to pair and pass the wider range as the same document_packet_created_{after,before} params.
  • Higher throughput — bump RATE_PER_MIN if you have a raised limit; keep it strictly under the ceiling so the retry has room.
  • Failure isolation — swap asyncio.gather(...) for asyncio.gather(..., return_exceptions=True) and log per-run failures instead of aborting the whole export.

Next steps

Runs & results

Run lifecycle, statuses, and the results envelope

Document packets

How packets group files and relate to runs