---
title: "Query table rows"
method: POST
path: "/tables/{table_id}/rows/query/"
tags: ["table-rows"]
---

# Query table rows

`POST /tables/{table_id}/rows/query/`

## Request

- Base URL: `https://api.getoneprofile.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.getoneprofile.ai/tables/{table_id}/rows/query/`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

List a table's rows with keyset pagination on an opaque cursor. Without a sort, rows come in DISPLAY order: the user-controlled row position, with the time-ordered record ID as a tiebreak. With one, they come in that column's value order, and rows with no value for it sort LAST in both directions. Filters are predicates on cell values and compose with either order.

`column_ids` projects each row's cells: send the columns the grid will render and the response carries only those. It echoes the projection back, so a column outside it reads as 'not fetched' rather than 'blank'.

A POST because a filter list has no honest query-string encoding. Each cell's computing flag is derived: stale AND an active run covers that row.

Performance note: an unfiltered sort streams the column's index. Add a filter and Postgres sorts the matched set instead, so a filter that matches most of a very large table is correspondingly slower.

## Path parameters

- `table_id` string, uuid, required

## Request body

- TableRowsQueryRequestSchema: Request body for the rows query. A POST rather than a GET because a filter list has no honest query-string encoding. ``column_ids`` is the projection: send the columns the grid will actually render and the response carries only those cells.
  - `column_ids` string[], nullable: Project each row's cells to these columns. Omit for every column. The response echoes the projection in `column_ids` so a client can tell a blank cell from one it did not ask for.
  - `cursor` string, nullable: Opaque cursor from a previous page's next_cursor.
  - `limit` integer: Maximum rows per page.
  - `offset` integer, nullable: Start the listing this many rows in, rather than at the top: a jump-to-row seek. Mutually exclusive with `cursor`, which resumes a listing rather than starting one. Postgres walks the skipped rows, so use this for a deliberate jump. Keep paging with `next_cursor`.
  - `record_ids` string[], nullable: Hydrate exactly these rows, instead of describing a listing. Mutually exclusive with `sorts`, `where`, `row_status`, `offset` and `cursor`: those select rows, and this names them. Pair with `/rows/ids/` to fetch an ordering once and then fill in row contents a window at a time, which lets re-sorting or re-filtering reuse cells the client already holds.
  - `row_status` 'errored' | 'fully_computed': A row-level state, derived from its cells rather than stored. ``ERRORED`` is "any cell of this row failed to compute"; ``FULLY_COMPUTED`` is its exact complement, so the two partition the table. Serves the built-in smart views, which are questions about computation rather than about values. The ordinary filter vocabulary cannot express either.
  - `sorts` TableRowSortSchema[]: Order by these columns, in priority order, instead of the display order. Blanks sort last for every key, whichever way that key runs.
    - `column_id` union, required: A column ID, or `__created_at__` / `__updated_at__` to order by when the row was added or last edited.
      - string, uuid
      - '__created_at__' | '__updated_at__': The sort keys that order by the ROW rather than by one column's cells. ``CREATED_AT`` is when the row was added; ``UPDATED_AT`` is when any of its cells last changed (the row was added, for one with no cells). They share the sort slot with column IDs, so a query or a saved view names either the same way; the dunder spelling keeps them apart from any UUID.
    - `direction` 'asc' | 'desc': Which way a sorted row listing runs.
  - `where` TableRowsWhereSchemaInput: The whole predicate for a rows query. ``items`` mixes conditions with groups under one joiner, so ``A and (B or C)`` is an AND over ``[A, group(OR, [B, C])]``. Send no ``where`` at all rather than an empty one, so "no filter" stays a single state. The cap is on LEAVES, not on items: eight conditions cost eight correlated EXISTS clauses whether they are grouped or not, and it is the clause count that the query planner pays for.
    - `items` TableRowWhereItemSchemaInput[], required: Conditions and groups.
      - union
        - TableRowFilterSchemaInput: One predicate a row must satisfy. Which operators apply depends on the column's type: a range comparison on a checkbox is rejected rather than silently matching nothing. The valueless operators (``is_empty``, ``is_not_empty``, ``is_true``, ``is_false``) must not carry a value.
          - `column_id` union, required: A column ID, or `__any_column__` to ask about every cell in the row at once. `__any_column__` takes only `has_error` and `has_no_error`.
            - string, uuid
            - '__any_column__' | '__record_id__': The filter targets that name the whole ROW rather than one column. ``ANY_COLUMN`` asks a question of every cell in the row at once: "does any column have an error?" is the table-wide half of the per-column ``has_error``. It stands in the ``column_id`` slot of an ordinary condition (the same trick the system sort keys use), so a row-wide question travels through every surface that already carries a predicate tree (a saved view, a run's scope, an export, a count) with nothing to add to any of them. The dunder spelling keeps it apart from any UUID.
          - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'contains_any_of' | 'not_contains_any_of' | 'starts_with' | 'is_true' | 'is_false' | 'is_empty' | 'is_not_empty' | 'has_error' | 'has_no_error' | 'has_results' | 'has_no_results' | 'has_not_run' | 'is_stale' | 'is_not_stale' | 'run_condition_not_met' | 'run_stopped', required: A row filter predicate. Which operators are legal depends on the column's data type; the service validates the pairing and rejects a mismatch, so an operator here is a vocabulary entry rather than a promise it applies everywhere. Two families live here. The VALUE operators ask about what a cell holds and are keyed off the column's storage slot. The STATE operators ask about what the last run DID to the cell (errored, skipped, never ran, went stale) and are keyed off the column's kind instead: they read the compute metadata that only a computed column ever writes, and mean nothing on a hand-typed one.
          - `type` 'condition'
          - `value` union
            - string
            - integer
            - number
            - boolean
            - string, date-time
            - string, date
            - CellValueInput[]
            - object
        - TableRowFilterGroupSchemaInput: A parenthesised run of conditions, joined among themselves. Holds conditions and never groups: one level of nesting is expressible and a second is not, structurally. That covers ``A and (B or C)`` (what a filter panel offers) while keeping the SQL a flat walk.
          - `conditions` TableRowFilterSchemaInput[], required: The conditions inside the parentheses.
            - `column_id` union, required: A column ID, or `__any_column__` to ask about every cell in the row at once. `__any_column__` takes only `has_error` and `has_no_error`.
              - …
            - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'contains_any_of' | 'not_contains_any_of' | 'starts_with' | 'is_true' | 'is_false' | 'is_empty' | 'is_not_empty' | 'has_error' | 'has_no_error' | 'has_results' | 'has_no_results' | 'has_not_run' | 'is_stale' | 'is_not_stale' | 'run_condition_not_met' | 'run_stopped', required: A row filter predicate. Which operators are legal depends on the column's data type; the service validates the pairing and rejects a mismatch, so an operator here is a vocabulary entry rather than a promise it applies everywhere. Two families live here. The VALUE operators ask about what a cell holds and are keyed off the column's storage slot. The STATE operators ask about what the last run DID to the cell (errored, skipped, never ran, went stale) and are keyed off the column's kind instead: they read the compute metadata that only a computed column ever writes, and mean nothing on a hand-typed one.
            - `type` 'condition'
            - `value` union
              - …
          - `joiner` 'and' | 'or': How the members of one filter level combine.
          - `type` 'group'
    - `joiner` 'and' | 'or': How the members of one filter level combine.

## Response `200`

Successful Response

- TableRowsPageSchema: One keyset page of table rows.
  - `column_ids` string[], nullable, required: The columns whose cells this page carries, or null for all of them. A column outside this list is NOT blank. It was simply not fetched.
  - `has_more` boolean, required
  - `limit` integer, required
  - `next_cursor` string, nullable, required: Cursor for the next page; None when this is the last page.
  - `rows` TableRowSchema[], required
    - `cells` object, required
    - `created_at` string, date-time, required: When the row was created.
    - `record_id` string, uuid, required

## Other responses

- `401`: Unauthorized
- `403`: Forbidden
- `404`: Table not found
- `422`: Invalid request made to the API.
- `500`: Internal server error.
- `502`: Bad gateway error.
- `503`: Service unavailable.

---

[API](https://skmtc.dev/getoneprofile/apis/api-reference-oneprofile.md) · [All operations](https://skmtc.dev/getoneprofile/apis/api-reference-oneprofile/llms.txt) · [OpenAPI document](https://skmtc.dev/getoneprofile/apis/api-reference-oneprofile/revisions/4a6bd71e3f02?raw)
