---
title: Read a list
url: /docs/developers/reference/data-view-source-query
---

# Read a list

`data.view-source.query@1`

Reads the rows of one table, a page at a time, with the fields, filters, search and sort order you ask for.

## Call it

Send `POST /invocations` with the `target` this operation's catalogue entry gives: replace `YOUR_FEATURE_ID` with its `featureId`, and `YOUR_TABLE_REF` with a table's `resourceRef` from [List data tables](data-tables-query.md). This one reads the first two quote requests whose email is at example.com, by name.

```curl tabs
# A new operationId for each new request; the same one to retry it.
curl https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations \
  -H "Authorization: Bearer gl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "target": {
    "featureId": "YOUR_FEATURE_ID",
    "export": "queryRows",
    "contractRef": "data.view-source.query@1"
  },
  "operationId": "read-quote-requests-1",
  "input": {
    "resourceRef": "YOUR_TABLE_REF",
    "query": {
      "columns": [
        "f_name",
        "f_email"
      ],
      "filters": [
        {
          "field": "f_email",
          "op": "contains",
          "value": "@example.com"
        }
      ],
      "sort": [
        {
          "field": "f_name",
          "direction": "asc"
        }
      ],
      "pageSize": 2
    }
  }
}'
```

```javascript tabs
// A new operationId for each new request; the same one to retry it.
const response = await fetch(
  "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer gl_YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "target": {
        "featureId": "YOUR_FEATURE_ID",
        "export": "queryRows",
        "contractRef": "data.view-source.query@1"
      },
      "operationId": "read-quote-requests-1",
      "input": {
        "resourceRef": "YOUR_TABLE_REF",
        "query": {
          "columns": [
            "f_name",
            "f_email"
          ],
          "filters": [
            {
              "field": "f_email",
              "op": "contains",
              "value": "@example.com"
            }
          ],
          "sort": [
            {
              "field": "f_name",
              "direction": "asc"
            }
          ],
          "pageSize": 2
        }
      }
    }),
  },
);
const receipt = await response.json();
console.log(response.status, receipt);
```

```python tabs
import requests

# A new operationId for each new request; the same one to retry it.
response = requests.post(
    "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations",
    headers={"Authorization": "Bearer gl_YOUR_API_KEY"},
    json={
        "target": {
            "featureId": "YOUR_FEATURE_ID",
            "export": "queryRows",
            "contractRef": "data.view-source.query@1",
        },
        "operationId": "read-quote-requests-1",
        "input": {
            "resourceRef": "YOUR_TABLE_REF",
            "query": {
                "columns": [
                    "f_name",
                    "f_email",
                ],
                "filters": [
                    {
                        "field": "f_email",
                        "op": "contains",
                        "value": "@example.com",
                    },
                ],
                "sort": [
                    {
                        "field": "f_name",
                        "direction": "asc",
                    },
                ],
                "pageSize": 2,
            },
        },
    },
    timeout=30,
)
print(response.status_code, response.json())
```

## Parameters

What goes in `input`:

| Name | Type | Required | Description |
|---|---|---|---|
| `resourceRef` | string | Yes | The table to read: its `resourceRef` from [List data tables](data-tables-query.md). |
| `query` | object | Yes | What to read, and how much of it. |
| `query.metadata` | object | No | Narrows the rows to those that came in a certain way or at a certain time. These are facts GrowLocal records about each row, never what a visitor typed. |
| `query.metadata.origin` | object | No | Only rows that came in through one form on one site. |
| `query.metadata.origin.siteId` | string (uuid) | Yes | The site the rows came in through. |
| `query.metadata.origin.featureId` | string | Yes | The form they came in through, as a row's `metadata.origin.featureId` names it. |
| `query.metadata.createdAt` | object | No | Only rows that came in during a period. |
| `query.metadata.createdAt.gte` | string (date-time) | No | From this moment on, inclusive. |
| `query.metadata.createdAt.lt` | string (date-time) | No | Before this moment. |
| `query.columns` | array of string | No | The fields each row carries, by `id`, at most 256. Leave it out for every field this key can read. |
| `query.filters` | array of object | No | Up to 16 filters, all of which a row must match. Which operators a field takes is its `query.filters` in List data tables. |
| `query.filters[].field` | string | Yes | The field to compare, by `id`. |
| `query.filters[].op` | one of `eq`, `in`, `not-in`, `contains`, `gt`, `gte`, `lt`, `lte`, `empty`, `present` | Yes | How to compare: `eq` equals `value`; `in` and `not-in` compare with `values` (`not-in` also matches an empty field); `contains` finds `value` as text; `gt`, `gte`, `lt` and `lte` compare numbers or dates; `empty` and `present` need no value. |
| `query.filters[].value` | string, number, boolean, object or array | Sometimes | What `eq`, `contains`, `gt`, `gte`, `lt` and `lte` compare with. Never `null`: match a missing value with `empty`. |
| `query.filters[].values` | array of string, number, boolean, object or array | Sometimes | What `in` and `not-in` compare with, at most 64 values. |
| `query.search` | object | No | Finds rows by text, ignoring case. |
| `query.search.text` | string | Yes | The text to find, 1 to 200 characters. |
| `query.search.columns` | array of string | No | The fields to search, by `id`. Leave it out to search every field this key can read that allows it. |
| `query.sort` | array of object | No | Up to 3 fields to sort by, in order. Rows with no value sort last either way. |
| `query.sort[].field` | string | Yes | The field to sort by, by `id`. |
| `query.sort[].direction` | one of `asc`, `desc` | Yes | `asc` or `desc`. |
| `query.tieBreak` | one of `oldest-first`, `newest-first` | No | The order of rows that sort the same, and of every row when there is no `sort`: by when they were added, `oldest-first` unless you ask for `newest-first`. |
| `query.pageSize` | integer | Yes | How many rows a page holds, 1 to 200. |
| `query.cursor` | string | No | The `nextCursor` of the page before, to read the next one ([Pagination](../pagination.md)). Leave it out for the first page. |
| `query.archived` | boolean | No | `true` reads the table's archived rows instead of its current ones. |
| `query.trashed` | boolean | No | `true` reads the rows in Trash instead. Not together with `archived`. |
| `recordRef` | object | No | Narrows the page to one row: its `recordRef` as a page of this table gives it. |
| `recordRef.businessProfileId` | string | Yes | Your business's id. |
| `recordRef.featureId` | string | Yes | The `featureId` of the row's `recordRef`. |
| `recordRef.collectionId` | string | Yes | The `collectionId` of the row's `recordRef`. |
| `recordRef.id` | string | Yes | The row's id. |

## Response

A call that worked answers `200` with an `applied` receipt; its `output` holds these fields, among others. `GET /catalog/data.view-source.query@1` answers the complete shape as JSON Schema.

| Name | Type | Description |
|---|---|---|
| `rows` | array of object | The page's rows, at most `pageSize`. |
| `rows[].metadata` | object | Facts GrowLocal records about the row. |
| `rows[].metadata.createdAt` | string (date-time) | When the row was added. |
| `rows[].metadata.origin` | object or null | The form and site the row came in through, or `null` when it was added another way. |
| `rows[].recordRef` | object | Names the row: send it as `row` to [Change items](data-collection-mutate.md). |
| `rows[].revision` | integer | The row's revision. Changing it needs this as `expectedRevision`. |
| `rows[].values` | object | The row's values, keyed by field `id`. |
| `rows[].fields` | array of string | The fields this row carries, by `id`. |
| `rows[].allowedTransitions` | array of string | For a table with a status, the statuses this row can move to next. |
| `rows[].trash` | object | On a `trashed: true` page: when the row was deleted and when it will be removed for good. |
| `nextCursor` | null or string | Send it as `query.cursor` for the next page; `null` when this is the last. |

```json title="Response"
{
  "status": "applied",
  "operationId": "read-quote-requests-1",
  "output": {
    "contract": "data.view-page/1",
    "schemaRef": "…",
    "rows": [
      {
        "key": "0b6f3c2a-5d1e-4f7a-9c8b-2e4d6f8a1c3e",
        "recordRef": {
          "businessProfileId": "YOUR_BUSINESS_ID",
          "featureId": "juniper-cakes.data",
          "collectionId": "juniper-cakes.quote-requests",
          "id": "0b6f3c2a-5d1e-4f7a-9c8b-2e4d6f8a1c3e"
        },
        "revision": 1,
        "metadata": {
          "createdAt": "2026-11-06T01:57:31.090547+00:00",
          "origin": null
        },
        "values": {
          "f_name": "Jordan Example",
          "f_email": "jordan@example.com"
        },
        "fields": [
          "f_email",
          "f_name"
        ],
        "allowedTransitions": [
          "quoted"
        ]
      },
      {
        "key": "7c1e9a4b-3f2d-4e6a-8b5c-9d0e1f2a3b4c",
        "recordRef": {
          "businessProfileId": "YOUR_BUSINESS_ID",
          "featureId": "juniper-cakes.data",
          "collectionId": "juniper-cakes.quote-requests",
          "id": "7c1e9a4b-3f2d-4e6a-8b5c-9d0e1f2a3b4c"
        },
        "revision": 1,
        "metadata": {
          "createdAt": "2026-10-07T20:44:46.675444+00:00",
          "origin": {
            "siteId": null,
            "featureId": "juniper-cakes.quote-requests-form"
          }
        },
        "values": {
          "f_name": "Omar Reyes",
          "f_email": "omar@example.com"
        },
        "fields": [
          "f_email",
          "f_name"
        ],
        "allowedTransitions": [
          "quoted"
        ]
      }
    ],
    "nextCursor": "eyJkIjoiNGYxNzA3NzA0YzU2NjNjZDgxNWI0ZTdlMmIyOGUyZmYiLCJrIjpbIk9tYXIgUmV5ZXMiXX0"
  }
}
```

## Errors

It reads and changes nothing. Besides the answers every request can get ([Errors](../errors.md)), its receipt can say:

| Status | HTTP | What it means |
|---|---|---|
| `rejected` | 400, or 403 when it's a matter of access | Not done. `diagnostics` says why: each has a `code`, a `path` into what you sent and a `message`. |

The codes in `diagnostics` you're most likely to see:

| Code | Status | What it means |
|---|---|---|
| `DATA.RESOURCE_NOT_FOUND` | `rejected` | No such table here for this key. |
| `DATA.CURSOR_INVALID` | `rejected` | The cursor isn't one, or belongs to another query ([Pagination](../pagination.md)). |
| `CONTRACT.INPUT_INVALID` | `rejected` | The query doesn't fit: `message` names the field, for example a `pageSize` over 200. |
