Skip to content

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, otherwise string. Empty cells do not count. A column with both whole and decimal numbers is number; any other mix is string.
  • A cell holding only n/a, na, -, null, none or #n/a is 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.

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_query is 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_id is 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.