Notion Core Workflow A — Databases & Pages
Overview
Primary workflow for Notion integrations: querying databases with filters/sorts, creating pages with typed properties, updating page properties, and retrieving page content.
Prerequisites
- Completed
notion-install-auth setup
- A Notion database shared with your integration
- Understanding of your database's property schema
Authentication
Every call below uses a Client authenticated with an integration token
(process.env.NOTION_TOKEN). Token creation, secret storage, and sharing a
database with the integration are covered end-to-end in the notion-install-auth
skill — complete it first. Never hardcode the token; read it from the environment.
Instructions
The workflow is six steps. Steps 1–2 (schema + filtered query) are the skeleton
you almost always start with, shown here in full. Steps 3–6 (filter syntax by
type, page creation, updates/archive, pagination) live in
the full walkthrough so this file stays scannable.
Step 1: Retrieve Database Schema
Always inspect the schema first — property names and types drive every filter
and write. databases.retrieve returns db.properties keyed by property name.
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
async function getDatabaseSchema(databaseId: string) {
const db = await notion.databases.retrieve({ database_id: databaseId });
for (const [name, prop] of Object.entries(db.properties)) {
console.log(`${name}: ${prop.type}`);
if (prop.type === 'select') {
console.log(' Options:', prop.select.options.map(o => o.name));
}
}
return db.properties;
}
Step 2: Query with Filters
Notion filters use a nested structure keyed by property type, and combine with
and / or. sorts and page_size (max 100) tune the result set.
async function queryWithFilters(databaseId: string) {
const response = await notion.databases.query({
database_id: databaseId,
filter: {
and: [
{ property: 'Status', select: { equals: 'In Progress' } },
{ property: 'Priority', select: { does_not_equal: 'Low' } },
],
},
sorts: [{ property: 'Priority', direction: 'ascending' }],
page_size: 50,
});
return response.results;
}
Steps 3–6: Filter syntax, create, update, paginate
See the full walkthrough for copy-paste code:
- Step 3 — Filter syntax by property type. Every property type (text,
number, select, date, checkbox, people, relation, formula, rollup, timestamp)
has its own filter shape.
- Step 4 — Create a page with all property types. One
pages.create call
showing the correct payload for each typed property.
- Step 5 — Update & archive.
pages.update to change properties, or set
archived: true to soft-delete.
- Step 6 — Paginate all results. Loop on
has_more / next_cursor to pull
a full database beyond the 100-row page limit.
Output
- Database schema retrieved with property types and options
- Filtered and sorted query results
- Pages created with typed properties
- Pages updated and archived
Error Handling
| Error | Cause | Solution |
|---|
validation_error | Property name mismatch or wrong type | Use databases.retrieve to check schema |
object_not_found | Database not shared with integration | Add integration via Connections |
rate_limited (429) | >3 requests/second average | Respect Retry-After header |
Empty results | Filter too restrictive or no data | Test with no filter first |
Examples
Reading queried pages back into plain values requires switching on each
property's type. A reusable getPropertyValue helper plus a full
"flatten a database into an array of objects" example live in
examples & helpers:
function getPropertyValue(property: any) {
switch (property.type) {
case 'title': return property.title.map((t: any) => t.plain_text).join('');
case 'number': return property.number;
case 'select': return property.select?.name ?? null;
default: return null;
}
}
Resources