Table sets through the API¶
The concept behind knowledge is explained in Knowledge.
A table set holds the tables in your documents -- every worksheet of a workbook, every CSV or
JSON file, every table inside a Word file, and with the tables/llama-parse template those of a
parsed PDF -- each with its columns and its rows,
so you can query them by field. An index answers "which passages resemble this
question"; a graph answers "what is connected to this"; a table set answers
"which rows meet these conditions, and what do they add up to": the invoices above 10,000 euro
last quarter, the revenue per supplier, the product with the lowest stock.
Your key needs the knowledge capability. A table set has no capability of its own. You do the
same with a mouse in Table sets in the console.
Tables, columns and sources¶
| What it is | |
|---|---|
| Table set | selects one or more collections and holds the tables found in their sources. Its id starts with tset_ |
| Table | one table from one source: a worksheet, a CSV or JSON body, or a table inside a document. Its id starts with tbl_ |
| Column | a field of a table, with a type Core deduced from the values: boolean, integer, number, date or string |
| Row | one line of a table, as a JSON object of column name and value |
| Source | the document a table was read from. A table belongs to exactly one source, and goes when the source goes |
Core writes the tables itself, from your documents. You cannot add, change or delete a row by hand; change the document and Core reads it again. For records your application writes itself, use a datastore.
1. Create a table set¶
curl -s -X POST "$CORE_URL/knowledge/table-sets" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{
"name": "finance-tables",
"description": "Invoices, budgets and supplier lists from the finance folder.",
"collection_ids": [{"collection_id": "col_2Pf7...",
"metadata_filter": {"department": "finance"}}],
"ingest_pipeline": {"template": "tables/documents"},
"labels": {"department": "finance"}
}'
The answer is 201 with the table set and a Location header. The examples below use
$TABLE_SET_ID for its id.
| Field | What it does |
|---|---|
name |
unique within the organisation: lower-case letters, digits, _ and -, starting with a lower-case letter or a digit, at most 64 characters. A policy rule names the table set by this name, so it never changes afterwards |
description |
what the table set holds, at most 1,024 characters |
collection_ids |
at most 16 collections, each with an optional metadata_filter. Only sources whose metadata contains the filter are read; without a filter the whole collection |
ingest_pipeline |
how tables are read: a built-in tables/... template or a version of a pipeline of your own (Choose the pipeline). Leave it out or send null to use tables/documents |
labels |
your organisation's own classification, as label and value; rules about Table sets read them |
metadata |
a JSON object of your own, at most 16 KiB. Core stores and returns it and never reads it |
GET /knowledge/table-sets lists the table sets your key reaches, GET /knowledge/table-sets/{id}
returns one. PATCH changes description, labels, metadata, collection_ids and
ingest_pipeline; a field you leave out stays as it is. The name cannot be changed. DELETE
removes the table set with all its tables and their rows and answers 204; the collections and
their sources stay.
| Answer | Meaning |
|---|---|
409 |
another table set in your organisation already has this name |
403 |
no rule in your key's policy permits a table set by this name |
422 on labels.<name> |
the label is not in your organisation's vocabulary |
422 on ingest_pipeline |
the pipeline cannot fill a table set (Choose the pipeline) |
2. Fill from sources¶
A table set reads the sources of the collections it selects. Creating it reads nothing yet: when
its collections already hold sources, the answer carries reindex_required: true, and you start
the reading yourself (Read again). A source added afterwards is read automatically.
Choose the pipeline¶
Three built-in templates fill a table set. None of them calls a model.
| Template | What it reads |
|---|---|
tables/structured |
a workbook's worksheets, a CSV or JSON file, and a records source |
tables/documents (the default) |
the same, plus the tables inside a Word file, a slide deck, a web page or Markdown |
tables/llama-parse |
the same, plus the tables of a PDF or a scan, read by the deployment's parsing service. Only available when your deployment has a parsing service |
A pipeline of your own may hold only the steps input, load, parse, llama_parse,
tabular_extract, table_store and output. It needs at least one tabular_extract and exactly
one table_store, which leads straight to output. Any other pipeline answers 422 on
ingest_pipeline. A pipeline that fills a table set cannot fill an index or a graph, and the other
way round. See Pipelines for editing steps; the tabular_extract step takes
sheets (which worksheets to read) and max_tables (at most this many tables per source, 50 by
default).
How a table is read¶
- A worksheet is one table, with its first row as the header. A CSV or a JSON file (an array of flat objects) is one table. Inside a Word file, a deck, a web page or Markdown, each table is one table, named after the heading above it.
- Each table gets its own columns. Core gives every column the narrowest type that fits all
of its values:
boolean,integer,number,date, otherwisestring. Empty cells do not count. A column with both whole and decimal numbers isnumber; any other mix isstring. - A cell holding only
n/a,na,-,null,noneor#n/ais read as empty, so one placeholder does not turn a column of amounts into text. - A CSV's separator is detected: comma, semicolon, tab or pipe.
- A number with a decimal comma, such as
1.234,56, stays text. In a CSV, write numbers with a decimal point. A workbook stores real numbers and is not affected. - A table holds at most 50,000 rows.
When a source is read¶
A new source in a selected collection is read automatically. When you change a source's metadata, Core decides again which table sets it belongs in: a table set that no longer selects it loses its tables at once.
GET .../sources gives per source its title, its status (queued, processing, ready or
failed), table_count and row_count, and on a failure a fixed error_code. Add ?status=failed
to see only the failures.
error_code |
Meaning | What you do |
|---|---|---|
parse_error |
a file that cannot be read: a damaged workbook, a CSV with duplicate or empty column names, a JSON file that is not an array of flat objects | correct the file, then upload it again |
too_many_tables |
the document holds more tables than max_tables |
split the document, or raise max_tables in a pipeline of your own |
sheet_not_found |
the pipeline's sheets names a worksheet the workbook does not have |
correct sheets |
too_large |
a table of more than 50,000 rows | split the file |
too_large_for_pipeline |
a file larger than the upload limit, 25 MiB by default; a table set reads a document whole | split the file |
table_set_full |
the table set would hold more than 10,000 tables or 5,000,000 rows | split the collections over more table sets, or narrow a metadata_filter |
no_pipeline |
the pipeline version the table set names has been deleted | choose another pipeline, then read again |
Other codes, such as needs_parse, mean what they mean for an index
(Why indexing failed).
A source with no table in it, such as a PDF of prose, is ready with table_count: 0.
A new version of a document replaces its tables. When a source changes, its tables are read
again and replaced whole: a column that disappeared is gone, and a column whose values changed type
gets the new type. A table at the same position with the same name keeps its id, so a stored
table_id stays valid until the worksheet is renamed or moved. A query naming a column that is
gone answers 422 with the column's name.
When a source is deleted, its tables and their rows are deleted with it, in every table set.
Read again¶
curl -s -X POST "$CORE_URL/knowledge/table-sets/$TABLE_SET_ID/reindex" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"mode": "full"}'
full (the default) reads every source in scope again and removes the tables of sources the table
set no longer selects. failed retries only the sources whose last reading failed. The answer is
202 with a run; GET .../runs and GET .../runs/{run_id} show its progress (status,
sources_total, sources_done, sources_failed). When a run for this table set is already
running or queued, the answer is 409.
Read again after you created the table set over collections that already held sources, changed its
pipeline, added or removed a collection, or changed a metadata_filter. Every answer that carries
the table set says reindex_required: true until a full run has completed.
3. Find a table and read its rows¶
curl -s "$CORE_URL/knowledge/table-sets/$TABLE_SET_ID/tables?q=invoices" \
-H "Authorization: Bearer $CORE_KEY"
Each table in the list carries its id, name (the worksheet's name, the heading above the table,
or table-<n>), origin (sheet, csv, json or markdown), position in its source,
columns with their types, row_count, and the source_id and source_title it was read from.
q keeps the tables whose name or whose source's title contains the text; source_id keeps the
tables of one source. GET .../tables/{table_id} returns one table.
curl -s "$CORE_URL/knowledge/table-sets/$TABLE_SET_ID/tables/tbl_4Hq9.../rows?limit=100" \
-H "Authorization: Bearer $CORE_KEY"
GET .../rows returns the rows in the order the document holds them, each as {"id", "data"}, at
most 100 per page; pass next_cursor as cursor for the next page.
GET /knowledge/collections/{id}/sources/{sid}/tables lists the tables of one document across
every table set your key reaches, each with its table_set_id, a page at a time: limit (1-100,
default 20), and the next_cursor of one page as cursor for the next.
4. Query a table¶
A query is a form, not query text. Core checks it against the table's columns and runs it with every value bound.
curl -s -X POST "$CORE_URL/knowledge/table-sets/$TABLE_SET_ID/tables/tbl_4Hq9.../query" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{
"filters": [
{"field": "country", "op": "in", "value": ["NL", "BE"]},
{"field": "invoice_date", "op": "gte", "value": "2026-07-01"}
],
"aggregate": {"fn": "sum", "field": "amount"},
"group_by": "supplier",
"sort": {"field": "$value", "direction": "desc"},
"limit": 10
}'
This reads as: the ten suppliers with the largest total amount invoiced in the Netherlands or Belgium since July.
| Part | What you fill in |
|---|---|
filters |
at most 20 conditions, all of which a row must meet. Each is {"field", "op", "value"} |
aggregate |
one of count, sum, avg, min and max, with a field. count may leave out the field and counts rows; with a field it counts the rows that hold a value for it. sum and avg need an integer or number column; min and max take any column but a boolean |
group_by |
one column to group the aggregate by. Needs an aggregate |
sort |
{"field", "direction"}, direction asc (the default) or desc. Without an aggregate, a column. With group_by, the group column or $value, the aggregate. An aggregate without group_by gives one row and takes no sort |
fields |
the columns each row returns, at most 50; without an aggregate only. Left out, every column |
limit |
rows, or groups, 1 to 500; default 50 |
cursor |
the next_cursor of an earlier answer to the same query, for the next page; without an aggregate only |
op |
Matches |
|---|---|
eq, ne, lt, lte, gt, gte |
a comparison, in the column's type. No ordering on a boolean |
contains, starts_with, ends_with |
text, literally and case-insensitively. Text columns only |
in |
any value of a list of at most 100 |
exists |
true: the row holds a value for the column; false: it does not |
Without a sort, rows come in the order the document holds them and groups in the order of the
group column. A sort puts rows without a value last in either direction.
The answer, abridged:
{
"columns": [{"name": "supplier", "type": "string"}, {"name": "sum", "type": "number"}],
"items": [{"supplier": "Acme", "sum": 48210.5}, {"supplier": "Widget BV", "sum": 30115.0}],
"next_cursor": null,
"truncated": false
}
items holds the rows, each with only the columns asked for, or the groups, each with the group
column and the aggregate named after its function. A query without an aggregate is paged by
next_cursor; you may change limit and fields between pages, and nothing else. A grouped answer
is not paged: when there are more groups than limit, you get the first limit and
truncated: true. Ask a narrower question rather than raising limit.
| Answer | Meaning |
|---|---|
422 with a field, such as filters[1].value or sort.field |
the table has no such column, the operator does not fit the column's type, the value cannot be read as that type, or the parts do not fit together |
422 on cursor |
the cursor belongs to another table, other filters or another sort |
422 asking for a narrower query |
the query ran past its time limit, five seconds by default; add a filter |
403 |
your key's policy requires an agent; reading rows and querying work only inside a turn |
404 |
the table set or the table does not exist, or your key or the person the request names does not reach it |
A query through this route is not metered as usage.
5. Let a model search¶
A model reaches a table set through a search tool. Copy the built-in template
retrieval/table-set, fill in its table_set block, and register the tool on the version you get
back:
curl -s -X POST "$CORE_URL/knowledge/pipelines/from-template" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"template": "retrieval/table-set", "name": "finance-tables-search",
"sources": {"table_set": "tset_6Rk2..."}}'
curl -s -X POST "$CORE_URL/knowledge/retrieval-tools" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "finance_tables",
"description": "Answers questions about invoices, budgets and suppliers from the finance spreadsheets.",
"pipeline": {"pipeline_id": "pip_...", "version": 1}}'
Offer the tool as {"type": "tool", "name": "knowledge__finance_tables"}. The template calls no
model of its own: the model in the turn writes the query.
The tool offers the model a table_query argument: a table_id, and the same query you send
Query a table -- filters with every operator, aggregate, group_by, sort,
fields and limit -- without cursor, because a tool call answers one page. A copy of the
template can set its tabular_query step to llm mode and name a model; that model then writes
the same query from the question.
- The tool's description names no table. A table's name, its document's title and its columns
are text the document's author wrote, so the description carries ids alone. A call without a
table_queryis answered with the tables the question most likely concerns, at most 24, each with its id, name, document title and columns, ranked by the words of the question that appear in them. The model reads that list and asks again with an id. - When the tool reaches at most 24 tables,
table_idis a choice between their ids; when it reaches more, it is free text, checked when the call runs. A table set of thousands of monthly exports stays usable this way.
What the model gets back is the table, its columns, the rows found and the query Core ran, as
structured data. Rows past 32,768 characters of JSON are left out and the answer says truncated,
so the model narrows its question instead of reading a partial sum as a whole one. A table the tool
does not reach and a query the columns do not fit come back to the model as "Unknown table." or
as invalid arguments, so it can correct itself; the turn does not fail because of it. A query that
runs past its time limit, five seconds by default, comes back the same way as too costly, so the
model can add a filter and ask again.
A table set can be narrowed by metadata. Unlike a graph, a table belongs to one source, so a
tool's metadata_filter and its filter_keys work on a table set: they are matched against the
metadata of the source each table was read from.
Combine prose and tables in one tool by making a pipeline of your own with an index block and a
table_set block, so one tool answers from both the text and the tables of the same documents
(Custom retrieval pipelines).
Every table query in a turn counts as one retrieval unit (retrieval).
Who reaches which tables¶
Which table sets a key reaches is decided by rules about Table sets (knowledge_table_set) in
the policy, by name or by label, with the actions see, use,
write and delete. The default rules of a key registered without a policy permit every table
set. A table set no rule permits is missing from lists and answers 404; creating one under a name
no rule permits answers 403. A search tool whose pipeline names a table set your key does not
reach cannot be written or pinned, and a request that offers it is refused as if the tool did not
exist.
A key's or an organisation's policy stored before table sets existed, including the default rules
written out when a key was registered, has no rule about Table sets and reaches none: listing
and reading answer 404 and creating answers 403, until you add a rule that permits Table
sets (Add or change a rule).
Inside a table set, a table is visible exactly when its source is. A request that names a
person with the X-Core-Subject header (Build for people) sees
the tables of that person's own sources, of shared sources, and of sources the rules about
Knowledge sources and Stored files permit by label. This holds for lists, rows, queries and
a model's tool calls alike. A table from another person's spreadsheet answers 404.
What a table set does not do¶
- A query reads one table. It does not join tables, and does not combine twelve monthly exports into one table, even when their columns are the same.
- The header is always the first row. A worksheet that starts with a title block, or has a header over two rows, is read with its first row as the column names.
- Numbers with a decimal comma are read as text.
- A model cannot write rows during a turn, and you cannot write them through the API.
- A table set cannot go into a publication.
- An organisation export carries the table set and its collections, not its tables: a full reindex in the new place reads them again.