# Table Workspace

> Applies to: ChengOS v0.1.0+ ｜ Last verified: 2026-08-12 ｜ Source: `crates/cheng-core/src/domain/table`, `crates/cheng-nodes/src/nodes/builtin/table`, `crates/cheng-api/src/rest/handlers/table/mod.rs`, `chengflow-ui/src/features/table`

Tables are ChengOS's structured-data counterpart to [documents](/docs/data-document-workspace):
a multi-dimensional table in the Airtable sense — typed columns, ordered records, and saved
views — rather than a spreadsheet of loose cells.

The difference matters for workflows. Because every column has a declared type and every record
is addressed by a stable id, a model can be handed a table and asked to modify it, and the
result is still validated data rather than free text.

## The domain model

```text
Table (aggregate root)
  ├── Column
  │     └── ColumnConfig   (e.g. select options)
  ├── Record
  │     └── CellValue      (tagged union)
  └── View
        └── ViewConfig
```

**Column types** — `text`, `number`, `select`, `date`, `checkbox`.

**Cell values** are a tagged union, not bare JSON: `null`, `text`, `number`, `checkbox`,
`select`, `date`. The tag is carried in the wire format, so a cell is written as
`{"type": "text", "value": "hello"}`. Validation happens on the way in — numbers reject NaN and
infinity and are capped at 10 decimal places, dates must be `YYYY-MM-DD`, and a value whose type
does not match its column is rejected rather than coerced.

`null` is the universal clear: writing it empties a cell of any type.

**Ordering** uses a fractional `order_index` on both columns and records, which is why inserting
a record between two others does not renumber the whole table.

## Views

A view is a saved configuration over the same records, not a copy of them. `ViewType` declares
`grid`, `kanban`, `calendar`, `gallery`, and `timeline`; **only `grid` is implemented today** —
the rest are placeholders in the enum, and choosing one gets you nothing yet.

The grid view supports merged cells in both the header and the data region. Data-region merges
are anchored to **record ids**, not row indices, so a merge survives sorting and filtering
instead of smearing across whatever rows happen to occupy those positions.

## Querying

Records are queried with a filter tree, not a flat condition list:

```text
FilterNode
  ├── Condition { column_id, operator, value }
  ├── And([FilterNode, ...])
  └── Or([FilterNode, ...])
```

**Operators** — `equals`, `not_equals`, `contains` (text only), `gt`, `gte`, `lt`, `lte`
(numbers and dates), `is_empty`, `is_not_empty`. The last two take no value.

Sorting is a `SortSpec` of column plus `asc`/`desc`.

## The table nodes

Three nodes, each with a distinct job.

| Node | What it does |
|---|---|
| `table/query` | Reads columns and records, paginated |
| `table/apply_record_ops` | Applies a batch of insert / update / delete operations |
| `table/build_llm_context` | Packages a table plus an instruction into a model-ready prompt |

`table/query` returns column definitions (`include_schema`, default true) alongside the
records, because a downstream model cannot interpret `data` without knowing what each column id
means. Pagination defaults to 100 records per page with a hard cap of 1000; pass `record_ids` to
fetch a specific set instead. `workspace_id` is injected from the ambient execution context and
used to validate that the table actually belongs to the caller's workspace.

## Writing records

`table/apply_record_ops` takes an ordered `operations` array, each tagged by `op`:

```json
{
  "table_id": "...",
  "operations": [
    { "op": "insert", "data": { "col_1": { "type": "text", "value": "Acme" } } },
    { "op": "insert", "data": { "col_1": { "type": "text", "value": "Globex" } },
      "position": { "after": { "reference_id": "<record-id>" } } },
    { "op": "update", "id": "<record-id>", "data": { "col_2": { "type": "number", "value": 42 } } },
    { "op": "delete", "id": "<record-id>" }
  ],
  "summary": "import from CRM"
}
```

Two details that change how you use it:

- **`update` is PATCH, not PUT.** Only the columns present in `data` are touched; omitted
  columns keep their values. You never have to read a record before updating one field of it.
- **`insert` defaults to append.** Pass `position` with an `after` reference id to place a
  record precisely.

The output reports per-operation results plus `records_inserted` / `records_updated` /
`records_deleted` / `records_affected`, so a partially successful batch is diagnosable without
re-reading the table.

`summary` is optional and exists for logging and audit — use it when an agent is the caller.

## Handing a table to an agent

`table/build_llm_context` does the packaging that would otherwise be prompt-engineering
scattered across your workflow. Given a `table_id` and an `instruction`, it emits:

- `system_prompt` — the rules for editing this table
- `user_prompt` — the instruction plus the rendered table
- `tools_json` — function-calling tool definitions for the record operations
- `table_id` — carried through for the downstream node

Options: `format` (`json`, `markdown`, or `csv`; default `json`), `max_records` (default 100,
0 = unlimited), and `selected_record_ids` to restrict the context to a subset — the natural
choice when the user has selected rows in the grid.

The canonical shape is a three-node chain:

```text
table/build_llm_context ──► agent or ai/llm ──► table/apply_record_ops
```

The model returns operations; the apply node validates and executes them. Because cell values
are typed and records are addressed by id, a malformed edit fails at the boundary instead of
corrupting the table.

## REST access

Tables are fully addressable over the API: `/tables` for CRUD, `/tables/:id/columns` (with
`/reorder`), `/tables/:id/records` (with `/query`, `/reorder`, `/batch-delete`), and
`/tables/:id/views`. See [the REST API overview](/docs/api-rest-overview).

## Next steps

- [Document Workspace](/docs/data-document-workspace) — the unstructured counterpart.
- [Knowledge Bases & Workspace Context](/docs/data-knowledge-base) — how workspace data reaches an agent.
- [ReAct Agent Guide](/docs/agent-react-guide) — the agent that consumes the context these nodes build.
