---
title: Spreadsheets with SQL
description: Query the cells of every spreadsheet with one SQL statement and fill reports without the data passing through the agent.
sidebar:
  order: 3
---

Every uploaded `.xlsx`, `.xlsm` or `.csv` spreadsheet becomes facts the agent queries with SQL. A question about ten spreadsheets fits in one `sql` call instead of ten file reads. `sql` and `sql_schema` run inside `query` scripts; `sql_write` and `sheet_write` run inside `execute` scripts ([MCP](/docs/en-US/agents/mcp)).

## When the facts are ready [#readiness]

Every file has a `status`, which `get`, `files_list`, `file_read` and `document_search` report; one that is not `ready` says what to do in `statusMessage`:

| Status | Meaning |
| :-- | :-- |
| `processing` | The file is still being read. |
| `ready` | Its cells or text can be queried. |
| `too_large` | The file exceeds the plan's per-spreadsheet cell or size limit. It stays downloadable; split it into smaller parts and upload them again, or change plans. |
| `limit_reached` | The organization's processed data reached the plan's limit. The file stays stored and is read on its own once there is room: after a plan change, or deleting files or SQL tables. |
| `failed` | The file could not be read. |
| `stored` | A type Siglata keeps but does not read, such as a video. |

Siglata stores deterministic facts only: it never recalculates formulas and never guesses versions or locales. Interpretation is up to the agent.

## Tables and functions [#facts]

Call `sql_schema` for each table's columns, the functions and the objects you can read.

| Table | Contents |
| :-- | :-- |
| `sheet_files` | One row per spreadsheet, with the state of its facts. |
| `cells` | One row per cell: file, sheet, address, row, column, raw text, typed value, formula and number format. |
| `styles` | Each style's font, fill, border and alignment. |
| `sheets` | Sheets and their used ranges. |
| `excel_tables` | Excel Tables. |
| `defined_names` | Defined names. |
| `merges` | Merged cells. |

| Function | Use |
| :-- | :-- |
| `sheet_rows(file, sheet)` | A sheet's wide rows, with columns by letter (`A`, `B`, `C`…). |
| `parse_number(text, locale)` | Turns text such as `1.234,56` (`pt-BR`) or `1,234.56` (`en-US`) into a number. |
| `parse_date(text, format)` | Turns text into a date with a pattern such as `DD/MM/YYYY`. |
| `column_letters(col)` | Turns a column number into letters. |

CSV values stay text; use `parse_number` and `parse_date` to interpret them.

## Query [#query]

`sql` (`files:read`) runs one `SELECT` in Postgres syntax, in a `query` script:

```js
return await sql({
  query: `SELECT raw AS filial, count(*) AS celulas
FROM cells
WHERE sheet = 'Vendas' AND col = 2 AND row >= 2
GROUP BY raw`,
});
```

- It returns the rows that fit in 100,000 bytes of JSON, with no row limit, and runs for at most 10 s. When something is left out, the result carries `truncated: true` and `next`, which says how to page with `ORDER BY` and `LIMIT`/`OFFSET` or narrow the `SELECT`.
- Name tables without a schema. Only the listed functions and types are accepted; system catalogs and statements that change data are refused.
- Before a statement runs, Siglata bounds the database memory it could take at worst, from the longest values the tables it reads hold and a size rule for each function, operator and cast, through every CTE, subquery and view. A statement over 128 MiB is refused with `too_large`, before it runs, in `SELECT`, `CREATE VIEW` and `CREATE TABLE AS`: build smaller values (fewer nested `concat`, `replace`, casts or JSON and array constructors), select fewer columns, or split the work. `WITH RECURSIVE` is refused (use `generate_series`), and a regular-expression or `SIMILAR TO` pattern must be a string constant of at most 2,000 compiled states. At most 4 statements run at once; a statement that finds them all busy fails with `conflict`, so retry it.
- Every refusal has a code, such as `relation_not_allowed`, `function_not_allowed`, `timeout` or `conflict` (retry the call).

## Views and snapshots [#views-and-snapshots]

`sql_write` (`files:write`) creates and drops your own objects, in an `execute` script:

| Statement | Result |
| :-- | :-- |
| `CREATE VIEW name AS SELECT …` | A view shared with the organization. Each member reads it with their own access. |
| `CREATE TABLE name AS SELECT …` | A snapshot only you read, over `sources` (the ids of the files it may read; every file you can read when omitted). It counts toward the plan's limit for spreadsheets the agent queries. |
| `DROP VIEW name` or `DROP TABLE name` | Drops an object you created. |

```js
return await sql_write({
  query:
    "CREATE TABLE vendas_agosto AS SELECT raw FROM cells WHERE sheet = 'Agosto'",
  sources: ["<spreadsheet id>"],
});
```

A snapshot follows its source files: it is hidden while a source is in the trash, returns when the source is restored, and is dropped when the source is purged.

## Fill a report [#fill]

`sheet_write`, in an `execute` script, accepts a query patch `{ sheet, anchor, query }` (it also needs `files:read`). It runs the `SELECT` and writes every row from the `anchor` cell of a new edition of the template, keeping its formatting:

```js
return await sheet_write({
  fileId: "<template id>",
  requestId: "vendas-setembro-2026",
  patches: [
    {
      sheet: "Relatório",
      anchor: "A2",
      query: "SELECT filial, receita FROM vendas_mes",
    },
  ],
});
```

The rows never pass through the agent's context; the response carries only the new file and the written range. A result over 5,000 rows, 8 MiB or 1,000,000 cells fails with `too_large` and creates no file.

## Permissions [#permissions]

SQL sees only the cells of active files the connection's user can read. A restricted file needs a grant; another organization never appears. Trashed files stay hidden until they are restored.
