invalid_request, naming
the uploaded file to use instead.
The exploration loop is progressive — each step narrows the previous one
instead of dumping the corpus:
folder-metadata— orient: call it non-recursively (the default) on/; the per-subdirectory summaries say where to descend. Descend bydirectory, or narrow bycategory/ingestion_status.file-metadata— decide: what is inside one file, how big is each part, which tool applies?read-file/qa-file— act: read the text, or ask a question.run-sql— run your own read-only SELECT over the workspace’s tables (names fromfile-metadata/ the folder census);query-tables— ask one natural-language question and let the engine plan, query, and answer.
file-metadata names: a PDF’s
sections, a spreadsheet’s tabs, a recording, an image, or the raw upload itself.
read-file and qa-file accept a component argument to scope a read or a
question to one of them.
read-file and qa-file require the file to be ingested
(POST .../ingestions); a file that is not returns file_not_ingested.
folder-metadata and file-metadata serve un-ingested files too, reporting
their ingestion_status so you can decide what to do next. For raw source
bytes, use POST /v1/parse or the artifact content route.
Folder metadata
directory(optional, default"/") — Folder path. Use"/"for the workspace root.recursive(optional, defaultfalse) — Count and list the whole subtree instead of the directory’s direct children. Use only when you genuinely need the full census.category(optional) — Narrow both the counts and the listing to files whose ledger category exactly matches one of the reported names. This is the descent step for a flat directory that has no subdirectories to descend into.ingestion_status(optional) — Narrow both the counts and the listing to files in exactly this status (one of theingestion_summarykeys, e.g."failed"). An unknown status is refused with a validation error.kind(optional) — Narrow to one ingestion lane:"table"(every spreadsheet/CSV in scope),"document"(paged and prose files), or"media"(recordings). An unknown lane is refused naming the valid ones.name_contains(optional) — Case-insensitive substring match against the uploaded path. Finds files by what they are called — the search tools find them by what they contain, and only a listing can find a file whose ingestion failed, since it was never indexed.%and_match literally.
recursive
unless you send recursive explicitly: the names those filters take come
from subtree censuses (root categories, subdirectory top_categories /
failed_files, ingestion_summary), so the follow-up call scopes the same
subtree. An explicit recursive: false alongside a narrow is honored as sent.
Two mistakes self-recover instead of dead-ending: a directory that names a
file is refused with invalid_request pointing at file-metadata, and an
unfiltered call that matches nothing (a typo’d path) answers 200 with a
hint naming the nearest existing ancestor and its subdirectories — e.g.
"No files under 'planing'. '/' contains: execution/, planning/." The hint
never names anything your access labels hide.
page_size(optional, default 100, max 500) — File entries per page — your context budget knob.start_after(optional) — Resume the path-sorted listing strictly after this path (pass the last entry of the previous page). Counts are unaffected.include_tables(optional, default false) — Add the table census to the response: onetablesrow per queryable tab in scope (path,component,row_count,column_count, the owning file’ssize_bytes) plusshared_columns— column names appearing in several tables, each occurrence carrying its profileddistinct_count/row_count/min_value/max_value. Each tab row also carriestable_name— the SQL view namerun-sqlregisters it under.shared_columnsare unverified join candidates: same name and compatible stats suggest a relation (equal distinct counts, a contained id range) — verify with onerun-sqljoin before relying on one. One census call replaces afile-metadatacall per file; it reads the ingestion profiles, so it is slower than a bare orientation call — use it once, then work per-file. Impliesrecursivethe way the narrows do. Capped (tablesat 500,shared_columnsat 40 ranked most-shared then most key-like, 5 occurrences each) withtables_truncated/shared_columns_truncatedreporting cuts — narrow thedirectoryto recover.
file_entries— The listing: one page of visible source files, path-sorted. Each entry carriesingestion_status,size_bytes,uploaded_at(when the file was first registered — “what arrived recently” triage for PBC/legal delivery waves), and — when known —categoryandkind(the ingestion lane:document/table/media), so a folder can be triaged without afile-metadatacall per file.files_truncatedreports a cut page; resume withstart_after, or better, narrow with a filter or a subdirectory.subdirectories— One summary per immediate subdirectory, always covering its whole subtree:overall_files,top_categories(most populous first, capped at 5), andfailed_fileswhen any ingestion failed there.ingestion_summary— File counts by ingestion status.coverage— Labels required to see withheld files.
null (an unclassified entry’s category, a
subdirectory’s zero failed_files) are omitted rather than sent as nulls.
The progressive loop in practice: the root call above says execution/ holds
177 files, mostly invoices, 2 failed — so the next call is
{"directory": "execution", "category": "source_vouchers/invoice"}, and the
one after that is file-metadata on a candidate from its listing.
Three more one-call narrows that each replace a client-side scan of the
whole listing:
- See what failed — the counts advertise failures (
ingestion_summary.failed, each subdirectory’sfailed_files);{"directory": "/", "ingestion_status": "failed"}lists exactly those files, ready forfile-metadata, whoseingestion_errorsays why each one failed. - List every workbook —
{"directory": "/", "kind": "table"}returns each spreadsheet/CSV in the corpus (the filesqa-filecan query as tables). - Find files by name —
{"directory": "/", "name_contains": "fabrinet"}matches paths case-insensitively — including files whose ingestion failed, which no content search can retrieve. - Census the tables —
{"directory": "/", "include_tables": true}returns every queryable tab with its extent plus shared-column join hints — the one orientation call for a tabular corpus.
File metadata
read-file and qa-file, and to learn
the component names that scope those calls.
Request body:
path(required) — The uploaded file’s path.
kind— Ingestion lane (document/table/media), when known.summary— The primary segment’s (the first logical document’s, or the first summarized sheet’s), capped — never a concatenation across components. Each component carries its own. (Extraction keywords are deliberately not served: generic phrases that cost tokens without aiming any tool.)ingestion_error— Present on a failed file when the ledger recorded a reason, so a"failed"status is not a dead end.page_count/tab_count— Total pages (paged documents) / total tabs (spreadsheets).total_chars— Real character count of the file’s text, present when ingestion measured one (new ingests).total_approx_bytesis the honest byte-based estimate that is always available; the two are never conflated.components— The file’s public units, in the file’s own order. Each carries:kind—"section"(a PDF’s classified segment),"tab"(a spreadsheet sheet),"image","raw"(a directly readable upload like CSV or markdown), or"media"(a recording — a separate shape, below).name— The public component name (section name or tab name) to pass ascomponentonread-file/qa-file. Empty when the whole file is the unit.table_name— The SQL view name a queryable tab is registered under forrun-sql; absent otherwise.page_start/page_end— 1-based source page extent (sections);row_count/column_countfor tabs.char_count/approx_bytes— Real count when measured, byte estimate otherwise.readable/queryable— Which content tool applies:read-fileworks on readable components,qa-file’s table engine on queryable ones (qa-fileitself handles every kind, including images).columns— A queryable tab’s column schema with the statistics ingestion profiled: per column,null_count,distinct_count(APPROXIMATE — compare torow_countfor key candidates; verify exact uniqueness with a query),min_value/max_value(clipped; id-format exemplars),sum_value(numeric columns), and eithertop_values(a frequency census — values WITH row counts, plustop_values_coverage) orsample_values(the complete value list, on profiles that predate censuses). Every rendered value is clipped server-side. This is the same briefing the platform’s own SQL engine reads before writing a query — grain, keys, id formats, date ranges, and categorical vocabularies without probe queries. Fields are absent when the profile predates them;columnsitself is absent when the component is not queryable or the profile is unavailable;columns_truncated: truemarks a schema cut at the column cap. The access gate is the same oneread-file/query-tablespass, so nothing here exceeds what those already serve.error— A per-tab ingestion degradation, passed through. Such a component stays visible but has no readable or queryable content.
- A media component (
kind: "media") instead carriesmodality(audio/video),duration_ms,speaker_count,speech_segment_count,frame_count, andsummary. Ask about a recording withqa-file.
page_start, a section never carries row_count, and you will not see either
as null noise.
An un-ingested file answers 200 with its ingestion_status and no
components — exactly what a caller needs to decide what to do next. A failed
one additionally carries ingestion_error with the recorded reason.
Read file
=== Tab: Revenue ===).
Request body:
path(required) — The uploaded file’s path. A workspace-internal path is refused withinvalid_requestnaming the uploaded file to use.pages(optional) — 1-based page numbers, on the file’s own page numbering. Out-of-range pages are ignored (absent frompages_returned); selected pages come back de-duplicated in document order, whatever order they were asked in. Spreadsheets have no pages — scope those withcomponentinstead (invalid_requestsays so).component(optional) — Scope the read to one component by the namefile-metadatalists (one tab, one section).max_chars(optional, default 200000, max 1000000) — Character budget for the returned content. On a multi-part read the budget is shared fairly across the selected parts, headers included.
file_id— The uploaded file’s ID.pages_returned— Pages actually included (may differ from requested if the document has fewer pages).truncated—truewhen the budget was hit before the end.components_returned/components_omitted— Which components contributed tocontentand which were cut for size (the budget ran out, or a part was too large to serve). Truncation is always visible — never a silently missing tab. Unnamed components (a recording, a raw upload) are listed by kind.
unsupported_file_type) — ask about them with qa-file,
which routes them to vision.
QA file
Request body:
path(required) — The uploaded file’s path. A workspace-internal path is refused withinvalid_request.query(required, max 4096 chars) — Natural-language question.include_citations(optional, defaulttrue) — Attach source evidence.component(optional) — Scope the question to one component by the namefile-metadatalists. Scoping a multi-tab workbook down to one tab routes the question straight to that tab’s engine.
answer— The direct answer. It refers to your file, its tab/section names, and page numbers — never to anything workspace-internal.citations— Source evidence;pathis always the uploaded file, andcomponentnames the tab/section the citation came from (reusable ascomponent=on a follow-upqa-file/read-file;nullfor single-component files or when attribution is unavailable).engine— Which pipeline answered:"recursive_llm","duckdb_agent","vision", or"agent".unavailable— Tables that could not be queried (a tab that failed to ingest), witherroranderror_kind. A failed tab does not fail the whole call.
Table files and the component escape hatch
A whole table file (spreadsheet or record upload) is refused with a typed redirect — its rows belong torun-sql / query-tables. The exception is a
named component: prose that lives in a workbook (a policy tab, a notes
sheet) is askable per tab — qa-file with component=<tab> runs the document
engine over that tab’s rendered text (queryable row extractions stay with
the table engines). A component with no rendered text is refused, naming
run-sql.
Query tables
qa-file, runs the table query, and retains generated result tables
as downloadable Parquet files.
Request body:
paths(required, 1–32) — Workspace-relative paths of uploaded tabular source files.query(required, max 4096 chars) — Natural-language question over all selected tables.include_citations(optional, defaulttrue) — Include source evidence.components(optional) — Tab names scoping every workbook path to just those tabs instead of every sheet. The names are the same stringsfile-metadataand filtered-search publish ascomponent(andqa-fileaccepts ascomponent=), matched per path: exact tab name first, then the unambiguous sanitized alias. A matched tab with no queryable derivative is reported inunavailablerather than silently dropped, as is a workbook none of the names match; a name matching no requested path at all — or a filter over a single-table upload (csv/jsonl/parquet), which has no named components — isinvalid_request.
answer— Natural-language answer across the selected sources.error/timed_out— Query failure details without discarding any partial result.citations— Evidence from the uploaded source files.result_citations— Generated result files, the source paths behind them, and the SQL used.result_materialization_error— Set when a result could not be retained for download.unavailable— Inputs that could not be queried, witherroranderror_kind.tables— Bounded inline previews; useresult_citations[].result_file.download_urlfor the full Parquet result.
Run SQL
query-tables: no planner and no answering agent — your
statement runs directly in the same guarded SQL engine, against every
queryable tab in the workspace registered as a view under the table_name
that file-metadata and the folder-metadata table census publish. DuckDB
dialect, read-only, any number of tables per statement; joins across files
are ordinary SQL joins.
Request body:
sql— The normalized statement that ran.result— The preview:handle,row_count,columns, boundedrows, andpreview_truncatedwhen the stored result holds more.duplicate—truewhen an identical statement’s stored result answered without re-running anything.result_citation— The retained result:result_file.download_urlfetches it whole as Parquet,source_pathsname the files the statement read.nullwhen retention failed — see the next field.result_materialization_error— Set when the result could not be retained for download; the inline preview still stands, and paging withLIMIT/OFFSETstill works.
- Results are content-addressed and chainable. Every result’s
res_…handle is itself a registered table on your next statement — join it, aggregate it, or page it withLIMIT/OFFSET. Repeating an identical statement is answered from the stored result (duplicate: true) without re-scanning anything. - The guard is the error message. A non-SELECT statement, or one reading
a table outside the registered set, is refused with
invalid_requestnaming the rule broken and the registered table names. When two files’ tabs settled on the sametable_name, each is registered under a per-file suffixed name (gl_1,gl_2, …) and the bare name is refused with the file-to-name mapping — ambiguity never resolves silently.
Choosing between tools
For questions spanning many files, use
fact-search or deep-search instead;
for ranked passages without synthesis, use
hybrid-search.