> ## Documentation Index
> Fetch the complete documentation index at: https://developers.notion.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data sources

> Declare the Notion data a custom block expects, then query and update it.

<Warning>
  Custom blocks are in private alpha:

  * Do not discuss publicly or share with anyone outside your workspace.
  * Access must be enabled per workspace. Installing the SDK does not grant access.
  * These APIs may experience breaking changes without notice.
</Warning>

Rows come from a data source the block declared in the worker, under `dataSources` on `worker.customBlock()`. Two consequences of that shape everything below:

* **The key you declared is the key you query by.** A block that declares `issues` calls `useDataSource("issues")`.
* When connected to a host, initialization validates all declared data source bindings before rendering children.

See [binding a block to Notion data](/custom-blocks/guides/data-sources) for how binding works, and the [worker capability reference](/custom-blocks/workers/capability) for every field of the declaration.

## Query rows

`useDataSource` takes the key you declared and returns the bound data source's rows.

```tsx theme={null}
import { useDataSource } from "@notionhq/custom-blocks/react"

export function IssueBoard() {
	const { items, isLoading, hasMore, error } = useDataSource("issues", {
		limit: 100,
		filter: { key: "title", title: { is_not_empty: true } },
		sorts: [{ key: "title", direction: "ascending" }],
	})

	if (error) return <p role="alert">Could not load issues: {error.message}</p>
	if (isLoading) return <p>Loading issues…</p>
	if (items.length === 0) return <p>No matching issues.</p>

	return (
		<div>
			<ul>
				{items.map((item) => {
					const title = item.propertiesByKey.title
					return <li key={item.id}>{typeof title === "string" ? title : "Untitled"}</li>
				})}
			</ul>
			{hasMore && <p>More issues match this query. These results are incomplete.</p>}
		</div>
	)
}
```

| Field | Description |
| - | - |
| `items` | The rows. Each has an `id`, its property values, and `update`, `archive`, and `unarchive` methods. |
| `isLoading` | True while a fetch is in flight. |
| `hasMore` | True when the data source has rows beyond those returned. |
| `error` | Present if the query failed. |
| `collectionSchema` | The bound data source's schema, when available. |
| `propertySchemasByKey` | Property schemas keyed by the keys you declared. |
| `propertyIdsByKey` | Maps your declared keys to the bound data source's real property IDs. |
| `propertySchemasById` | Property schemas keyed by real property ID. |

The `limit` option caps how many rows the query returns. It defaults to 20 and is capped at 999. `hasMore` means the results are incomplete. The hook does not provide cursor pagination. You can increase `limit` to request a larger result set, up to the cap.

### Filters and sorting

Use `key` to address a declared property, or `propertyId` to address a real Notion property ID. The SDK resolves declared keys through the block's property mappings.

`filter` accepts one property condition or a shallow `and` group with up to 25 conditions. Nested groups and `or` groups are unsupported.

| Property types | Filter operators |
| - | - |
| `title`, `rich_text`, `url`, `email`, `phone_number` | `equals`, `does_not_equal`, `contains`, `does_not_contain`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty` |
| `number` | `equals`, `does_not_equal`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, `is_not_empty` |
| `checkbox` | `equals`, `does_not_equal` |
| `select`, `status` | `equals`, `does_not_equal`, `is_empty`, `is_not_empty` |
| `multi_select` | `contains`, `contains_all`, `does_not_contain`, `is_empty`, `is_not_empty` |
| `date` | `equals`, `before`, `after`, `on_or_before`, `on_or_after`, `is_empty`, `is_not_empty` |
| `people` | `contains`, `does_not_contain`, `is_empty`, `is_not_empty` |
| `created_by` | `contains`, `does_not_contain`, `is_empty`, `is_not_empty` |

Empty checks require `true`. Option filters accept option names, either one string or an array. Date comparisons accept ISO 8601 dates or timestamps. Relative dates and filters on property types absent from the table are unsupported.

People filters accept one explicit reference to a human user or a group. Use `{ object: "user", id: "..." }` or `{ object: "group", id: "..." }`. Group filters match exact group mentions. They do not expand group membership. Using bot or organization bot pointers as filter references is not supported.

Creator filters require an actual **Created by** column in the bound database. The column can have any name. In this example, the declared property key `creator` maps to that column:

```typescript theme={null}
filter: {
	key: "creator",
	created_by: {
		contains: { object: "user", id: "12345678-1234-4234-8234-123456789abc" },
	},
}
```

Use `propertyId` instead of `key` to address the column by its actual property ID. The `created_by` key selects the filter type. It does not identify the column. The synthetic read entry `propertiesById.created_by` is available without a column, but `propertyId: "created_by"` does not address a Created by column.

Creator filters accept one explicit human user reference. Groups, bots, and organization bots are not supported as filter references. `does_not_contain` also matches rows with non-human or missing creators. Empty checks test whether creator metadata is missing.

`sorts` accepts property addresses with an `ascending` or `descending` direction. Notion accepts up to 10 unique properties and applies sorts in array order. The first sort has the highest priority.

See the [property support table](#property-support) for sortable property types.

### Query rows without React

Use the framework-neutral `customBlock.subscribeToDataSource` API. Initialize the SDK before you subscribe.

```typescript theme={null}
import { customBlock, initCustomBlock } from "@notionhq/custom-blocks"

await initCustomBlock()

const unsubscribe = customBlock.subscribeToDataSource({
	key: "issues",
	options: {
		limit: 100,
		filter: { key: "title", title: { is_not_empty: true } },
		sorts: [{ key: "title", direction: "ascending" }],
	},
	onSnapshot(snapshot) {
		if (snapshot.error) {
			console.error(snapshot.error.message)
			return
		}
		if (!snapshot.isLoading) console.log(snapshot.items)
	},
})
```

The callback receives the current snapshot and later updates. Call `unsubscribe()` when the view no longer needs the query:

```typescript theme={null}
unsubscribe()
```

This ends that subscription and its updates. Other subscriptions remain active.

### Keys versus IDs

Every row exposes its values twice: `propertiesByKey`, keyed by the keys you declared in the manifest, and `propertiesById`, keyed by the bound data source's real property IDs. Prefer `propertiesByKey` — see [binding to Notion data](/custom-blocks/guides/data-sources) for why.

## Update a row

Each row carries its own `update` method, so you do not need the pages API to edit a row in a bound data source.

```tsx theme={null}
const result = await item.update({
	properties: {
		status: { type: "status", status: { name: "Done" } },
	},
})

if (result.status === "error") {
	console.error("Could not update issue:", result.error.message)
}
```

`update` resolves to a result object rather than throwing, following the same `{ status: "success" }` or `{ status: "error", error }` pattern as the rest of the SDK.

Updates support `title`, `rich_text`, `number`, `url`, `email`, `phone_number`, `checkbox`, `select`, `status`, `multi_select`, `date`, `people`, `relation`, `files`, and `place`. Computed properties such as `formula`, `rollup`, and creation or edit timestamps are read-only. Declaring other property types does not make them writable.

To create rows, or to work with pages outside a bound data source, use the [pages API](/custom-blocks/sdk/pages).

## Archive and unarchive a row

`item.archive()` is shorthand for calling `item.update({ is_archived: true })`.
`item.unarchive()` is shorthand for calling `item.update({ is_archived: false })`.
Both methods return an `UpdatePageResult`.
Use `item.update` to change archive status and other fields in one request.

## Read the manifest at runtime

`useManifest()` returns the block's declared manifest, and `customBlock.getManifest()` does the same outside React. Both require the block to be initialized.

This is useful for rendering against your own declaration — for example, iterating declared properties to build columns — rather than hardcoding the same list in both the worker and the frontend.

The init payload also carries a `dataSources` array describing each bound data source, including its resolved `propertyIdsByKey` and `propertySchemasById`.

## Property support

This table shows property support for custom blocks running in Notion.

* **Query result** shows values in `propertiesById` and `propertiesByKey` from `useDataSource` and `customBlock.subscribeToDataSource`. Missing values can be `undefined`.

* **Update row** covers writes through `item.update({ properties: ... })`.

* **Filter** and **Sort** cover query options.

* 🟢 **Supported:** The SDK supports this operation in Notion.

* 🟡 **Limited:** The SDK supports this operation with the restrictions shown in the cell.

* 🔴 **Unavailable:** The SDK does not expose this operation.

* ⚫ **Not supported in Notion:** Notion itself does not support this operation in the stated case.

| Property type | Query result | Update row | Filter | Sort |
| - | - | - | - | - |
| `title` | 🟢 Plain text | 🟡 Plain text only | 🟢 Supported | 🟢 Supported |
| `rich_text` | 🟢 Plain text | 🟡 Plain text only | 🟢 Supported | 🟢 Supported |
| `number` | 🟢 Number | 🟢 Supported | 🟢 Supported | 🟢 Supported |
| `checkbox` | 🟢 Boolean | 🟢 Supported | 🟢 Supported | 🟢 Supported |
| `url` | 🟢 String | 🟢 Supported | 🟢 Supported | 🟢 Supported |
| `email` | 🟢 String | 🟢 Supported | 🟢 Supported | 🟢 Supported |
| `phone_number` | 🟢 String | 🟢 Supported | 🟢 Supported | 🟢 Supported |
| `select` | 🟢 Option name | 🟡 Set value only | 🟢 Supported | 🔴 Unavailable |
| `multi_select` | 🟢 Array of option names | 🟡 Set values only | 🟢 Supported | 🔴 Unavailable |
| `status` | 🟢 Option name | 🟡 Set value only | 🟡 Option names or empty checks, without groups | 🔴 Unavailable |
| `date` | 🟢 `NotionDateValue` | 🟡 Minute precision | 🟡 Fixed start-date comparisons or empty checks, at minute precision | 🟢 Supported |
| `people` | 🟢 Array of record pointers | 🟢 Supported | 🟢 Supported | 🔴 Unavailable |
| `files` | 🟡 Text fallback | 🟡 File URLs only | 🔴 Unavailable | 🔴 Unavailable |
| `unique_id` | 🟡 Text fallback | 🔴 Unavailable | 🔴 Unavailable | 🔴 Unavailable |
| `relation` | 🟢 Array of record pointers | 🟡 Replaces value. Rejects `has_more: true`. | 🔴 Unavailable | 🔴 Unavailable for standard relations. ⚫ Notion cannot sort XLDB targets. |
| `place` | 🟡 Text fallback | 🟢 Supported | 🔴 Unavailable | 🔴 Unavailable |
| `formula` | 🟡 Text fallback | 🔴 Unavailable | 🔴 Unavailable | 🔴 Unavailable |
| `rollup` | 🟡 Text fallback | 🔴 Unavailable | 🔴 Unavailable | 🔴 Unavailable for numeric targets or numeric aggregations. ⚫ Notion cannot sort other rollups. |
| `button` | 🟡 Text fallback | 🔴 Unavailable | ⚫ Not supported in Notion | ⚫ Not supported in Notion |
| `verification` | 🟡 Text fallback | 🔴 Unavailable | 🔴 Unavailable | 🔴 Unavailable |
| `last_visited_time` | 🟡 Text fallback | 🔴 Unavailable | 🔴 Unavailable | 🔴 Unavailable |
| `location` | 🟡 Text fallback | 🔴 Unavailable | 🔴 Unavailable elsewhere. ⚫ Notion disables filters in My Meetings and Library system collections. | 🔴 Unavailable elsewhere. ⚫ Notion disables sorts in Library system collections. |
| `created_time` | 🟢 `NotionDateTime` in UTC | 🔴 Unavailable | 🔴 Unavailable | 🟡 Schema property only |
| `last_edited_time` | 🟢 `NotionDateTime` in UTC | 🔴 Unavailable | 🔴 Unavailable | 🟡 Schema property only |
| `created_by` | 🟢 Array of record pointers | 🔴 Unavailable | 🟡 Schema property only | 🔴 Unavailable |
| `last_edited_by` | 🟢 Array of record pointers | 🔴 Unavailable | 🔴 Unavailable | 🔴 Unavailable |

### Notes

* Text values omit formatting, mention tokens, and annotations. A text fallback can be empty and does not preserve the property's structured value. It does not provide structured formula or rollup results.
* Record pointers contain a table name and a record ID.
* People properties return record pointers with table values of `notion_user`, `space_permission_group`, `bot`, or `organization_bot`. Group IDs use the composite form `groupId|workspaceId`. People filters support users and groups. Bot and organization bot filtering is not supported.
* Every row includes four built-in properties in `propertiesById`: `created_time`, `last_edited_time`, `created_by`, and `last_edited_by`. The data source does not need matching schema properties. These values are read-only. Creator filters require an actual Created by schema property even though metadata reads do not. Timestamps use `NotionDateTime` in UTC. Creator and editor values use arrays of record pointers.
* The SDK accepts timestamp sorts, but the current Notion host requires a property ID in the data source's actual schema. Sort using a configured created-time or last-edited-time property. The synthetic IDs `created_time` and `last_edited_time` alone are not sufficient.
* You can sort by up to 10 unique properties.
* Date-time filter values may include seconds or milliseconds, but the Notion host currently evaluates them at minute precision. Date filters do not expose relative dates or end-date targeting.
* Status filters accept option names and empty checks, but not status groups.
* Filters accept one condition or an `and` group of up to 25 conditions. The SDK does not support `or` or nested groups.
* Select, multi-select, and status query values contain option names. Their schemas provide option IDs and colors separately. Writes set the selected values. They do not edit option definitions or colors.

### Writing values

The value you read has a different shape from the value you write. For example, a query returns a status as a string:

```ts theme={null}
item.propertiesByKey.status // "Done"
```

To update it, wrap the string in a property object. Here, `status` is a declared property key:

```ts theme={null}
const result = await item.update({
  properties: {
    status: { type: "status", status: { name: "Done" } },
  },
});

if (result.status === "error") {
  console.error(result.error.message);
}
```

Other write restrictions:

* **Text:** Title and rich text writes preserve plain text, but not formatting or mentions.
* **Files:** Supply external or existing hosted file URLs. The host stores them as links and does not support upload references.
* **Relations:** Supply the complete replacement list of related pages. The host rejects `has_more: true`, which indicates an incomplete list.

See [Pages property support](/custom-blocks/sdk/pages#property-support) for the page-property read shapes.
