API Reference
Complete reference for the Notion ORM API. The sections follow a learning progression: start with runtime access, then databases, then agents, then advanced usage.
Runtime access
import { NotionORM } from "./notion";
const notion = new NotionORM({
auth: process.env.NOTION_KEY,
});
// DatabaseClient — e.g. notion.databases.books
// AgentClient — e.g. notion.agents.helpBotnotion.databasesRecord<string, DatabaseClient>notion.agentsRecord<string, AgentClient>Database client
Properties
idnameMethods
findUnique({ where: { id } })findFirst(args?)null (source)findMany(args?)Partial<Schema>[] by default, PaginateResult with after, or AsyncIterable with stream (source)count(args?)create({ properties, icon?, cover?, markdown? })createMany([{ properties, icon?, cover?, markdown? }])update({ where: { id }, properties })updateMany({ where, properties })upsert({ where, create, update })delete({ where: { id } })deleteMany({ where })findUnique
Retrieves a single page by its Notion page ID, scoped to the current database client's data source. Returns null for missing, partial, or cross-data-source pages. Supports select / omit projection with the same semantics as findMany.
const page = await notion.booksDb.findUnique({
where: { id: "page-id-here" },
select: ["bookName", "status"],
});findFirst
Returns the first row matching the filter, or null if none match.
const book = await notion.booksDb.findFirst({
where: { bookName: { equals: "Creativity, Inc." } },
});Args: Same as findMany but without stream, after, or size.
findMany
Queries the database with optional filtering, sorting, projection, pagination, and streaming.
Overloads:
stream or afterPromise<Partial<Schema>[]>after: string | nullPromise<PaginateResult<Schema>>stream: numberAsyncIterable<Partial<Schema>>Args: All properties are optional.
whereQueryFilterand / or). Omit to consider every page in this data source (still bounded by size and by list vs pagination vs streaming mode).sortByQuerySort<ColumnTypes>created_time / last_edited_time. Omit for the API’s default ordering.sizenumberpage_size for this request. In default list mode, caps how many rows that single response returns. With after, caps each page. Omit to use the API default page size.selectreadonly (keyof Schema & string)[]omitreadonly (keyof Schema & string)[]streamnumberstream for promise-based list or pagination. Cannot be used together with after.afterstring | nullnull for the first page, then the previous page’s nextCursor. Cannot be used together with stream.select and omit are mutually exclusive — providing both throws at runtime.
// Simple query — returns all rows
const allBooks = await notion.booksDb.findMany();
// Filtered + sorted + projected
const recent = await notion.booksDb.findMany({
where: { publishDate: { on_or_after: "2025-01-01" } },
sortBy: [{ property: "publishDate", direction: "descending" }],
select: ["bookName", "publishDate"],
size: 20,
});
// Cursor pagination
const firstPage = await notion.booksDb.findMany({ after: null, size: 10 });
const secondPage = await notion.booksDb.findMany({ after: firstPage.nextCursor, size: 10 });
// firstPage.data, firstPage.hasMore, firstPage.nextCursor
// Streaming large datasets
for await (const row of notion.booksDb.findMany({ stream: 100 })) {
console.log(row.bookName);
}PaginateResult
Returned by findMany when after is provided.
dataPartial<Schema>[]nextCursorstring | nullafter for the next page, null when donehasMoreboolean// Walk every page: start with after: null, then pass nextCursor until it is null
let after: string | null = null;
do {
const page = await notion.booksDb.findMany({ after, size: 10 });
for (const row of page.data) {
/* use row */
}
after = page.nextCursor;
} while (after !== null);Filtering
Filters are typed by your generated schema. Single-property filters and compound and/or are supported.
Single filter:
await notion.booksDb.findMany({
where: { genre: { contains: "Sci-Fi" } },
});Compound filters:
await notion.booksDb.findMany({
where: {
and: [
{ genre: { contains: "Sci-Fi" } },
{ numberOfPages: { greater_than: 200 } },
{
or: [
{ status: { equals: "In progress" } },
{ status: { equals: "Not started" } },
],
},
],
},
});Filter operators vary by property type — see the Notion filter reference for the full list.
count
Returns the total number of rows matching an optional filter. Paginates internally to count all results.
const total = await notion.booksDb.count();
const filtered = await notion.booksDb.count({
where: { status: { equals: "Done" } },
});create
Creates a single page. Properties are typed to your generated schema.
const result = await notion.booksDb.create({
properties: {
bookName: "The Dream Machine",
genre: ["Non-fiction"],
numberOfPages: 460,
},
icon: { type: "emoji", emoji: "📗" },
});
// result.id — the new page IDPass markdown to add body content to the page in a single call. This uses Notion's enhanced markdown format — headings, lists, code blocks, quotes, checklists, and more are all supported.
await notion.booksDb.create({
properties: {
bookName: "Reading Notes",
},
markdown: "# Key Takeaways\n\n- **Creativity requires candor** — honest feedback loops matter\n- Protect the new — early ideas are fragile\n\n> \"Quality is the best business plan.\"",
});markdown is mutually exclusive with children / content.
createMany
Creates multiple pages sequentially and returns all responses.
Accepts an array of create(...) payloads (Array<{ properties; icon?; cover?; markdown? }>), so every row can carry its own icon, cover, and markdown body in addition to properties.
const results = await notion.booksDb.createMany([
{ properties: { bookName: "Book A", genre: ["Sci-Fi"], numberOfPages: 300 } },
{ properties: { bookName: "Book B", genre: ["Biography"], numberOfPages: 250 } },
{
properties: { bookName: "Book C", genre: ["Fantasy"], numberOfPages: 420 },
icon: { type: "emoji", emoji: "📘" },
},
{
properties: { bookName: "Book D", genre: ["History"], numberOfPages: 380 },
markdown: "# Notes\n\n- Imported from archive",
},
]);update
Updates a single page by ID. Requires at least one property.
await notion.booksDb.update({
where: { id: "page-id" },
properties: { status: "Done", numberOfPages: 512 },
});updateMany
Finds all pages matching a filter and applies the same property updates to each.
await notion.booksDb.updateMany({
where: { status: { equals: "Draft" } },
properties: { status: "In progress" },
});upsert
Finds the first row matching where. If found, applies update; otherwise runs create.
await notion.booksDb.upsert({
where: { bookName: { equals: "The Dream Machine" } },
create: {
bookName: "The Dream Machine",
genre: ["Non-fiction"],
numberOfPages: 460,
},
update: { numberOfPages: 500 },
});delete
Archives a single page by ID (Notion does not hard-delete pages).
await notion.booksDb.delete({ where: { id: "page-id" } });deleteMany
Archives all pages matching a filter.
await notion.booksDb.deleteMany({
where: { status: { equals: "Archived" } },
});Supported database properties
titlestring"The Dream Machine"rich_textstring"Long-form notes from the page"numbernumber460date{ start: string; end: string }{ start: "2026-03-01", end: "2026-03-02" }statusstring"In progress"selectstring"Non-fiction"multi_selectstring[]["Sci-Fi", "Biography"]checkboxbooleantrueemailstring"tyrus@haustle.studio"phone_numberstring"0000000000"urlstring"https://developers.notion.com/"filesArray<{ name: string; url: string }>[{ name: "brief.pdf", url: "https://..." }]peoplestring[]["1f4e6f4a-5b58-4d91-a7fc-2f5f2a0f6bb1"]relationstring[]["6f7f9cbf-8d45-48f8-a194-661e73f7f5d9"]created_bystring"Ada Lovelace"last_edited_bystring"user_123"created_timestring"2026-03-01T10:30:00.000Z"last_edited_timestring"2026-03-01T13:15:00.000Z"unique_idstring"TASK-42"Unsupported properties
These property types are intentionally excluded from the generated schema and client surface:
formularollupFormula properties are therefore omitted from generated database modules and are unavailable in typed reads, projections, or filters.
Agent client
Properties
idnameiconnull)Methods
chat({ message, threadId? })Sends a message and creates/resumes a thread (source)await notion.agents.helpBot.chat({ message: "Hello" })chatStream({ message, threadId?, onMessage? })Streams messages and returns final ThreadInfo (source)await notion.agents.helpBot.chatStream({ message: "Hi", onMessage: (m) => {} })pollThread(threadId, options?)Polls until thread processing completes (source)await notion.agents.helpBot.pollThread(threadId)getMessages(threadId, { role? })Gets full (or role-filtered) message history (source)await notion.agents.helpBot.getMessages(threadId, { role: "agent" })AgentClient.getAgentResponse(threadInfo)Joins every agent message in ThreadInfo into one string (source)AgentClient.getAgentResponse(thread)listThreads()Lists recent threads with id, title, and status (source)await notion.agents.helpBot.listThreads()getThreadInfo(threadId)Fetches a single thread record (source)await notion.agents.helpBot.getThreadInfo(threadId)getThreadTitle(threadId)Convenience helper to fetch just the thread title (source)await notion.agents.helpBot.getThreadTitle(threadId)chat
Sends a user message. Omit threadId to start a new thread; pass a prior threadId to continue. The response includes the thread id and status — it does not embed the assistant’s reply text; call getMessages or use chatStream when you need message bodies.
const first = await notion.agents.helpBot.chat({
message: "Give me a high-protein dinner idea under 30 minutes.",
});
// first.threadId, first.status, first.isNewChat
const followUp = await notion.agents.helpBot.chat({
threadId: first.threadId,
message: "Now make it vegetarian.",
});chat return shape:
threadIdstringchat, getMessages, etc.statusThreadStatusisNewChatbooleantrue when threadId was omitted in the requestchatStream
Streams message chunks via onMessage and resolves to ThreadInfo with the full transcript when the stream completes.
import { AgentClient } from "@haustle/notion-orm";
const thread = await notion.agents.helpBot.chatStream({
message: "How do I reset my password?",
onMessage: (msg) => {
if (msg.role === "agent") process.stdout.write(msg.content);
},
});
const plainText = AgentClient.getAgentResponse(thread);Resume an existing thread by passing threadId:
const continued = await notion.agents.helpBot.chatStream({
threadId: thread.threadId,
message: "Shorter version, bullet points only.",
onMessage: (msg) => {
/* … */
},
});ThreadInfo
Returned by chatStream. Use messages for the transcript, or AgentClient.getAgentResponse for a single concatenated agent reply.
threadIdstringagentIdstringmessagesArray<{ role: "user" | "agent"; content: string }>Message items:
roleuser | agentcontentstringAgentClient.getAgentResponse
Static helper: joins every agent message in a ThreadInfo into a single string (useful with chatStream results).
import { AgentClient } from "@haustle/notion-orm";
const thread = await notion.agents.helpBot.chatStream({
message: "One paragraph about our refund policy.",
});
const answer = AgentClient.getAgentResponse(thread);pollThread
Waits until the thread finishes processing (configurable backoff). Often used after chat before reading messages.
const chat = await notion.agents.helpBot.chat({
message: "Summarize yesterday’s standup notes.",
});
await notion.agents.helpBot.pollThread(chat.threadId);
// Optional: tune polling (defaults: maxAttempts 60, baseDelayMs 1000, …)
await notion.agents.helpBot.pollThread(chat.threadId, {
maxAttempts: 30,
initialDelayMs: 500,
});getMessages
Loads the full history for a thread. Pass role to return only user or only agent messages.
await notion.agents.helpBot.pollThread(threadId);
const all = await notion.agents.helpBot.getMessages(threadId);
const agentOnly = await notion.agents.helpBot.getMessages(threadId, {
role: "agent",
});listThreads
Returns recent threads for this agent, each with id, title, and status.
const threads = await notion.agents.helpBot.listThreads();
for (const t of threads) {
console.log(t.id, t.title, t.status);
}getThreadInfo
Fetches a single thread by ID. Returns the Agents SDK ThreadListItem (includes id, title, status, and related fields).
const info = await notion.agents.helpBot.getThreadInfo("thread-id-here");
// info.id, info.title, info.status, …getThreadTitle
Shortcut when you only need the title string.
const title = await notion.agents.helpBot.getThreadTitle("thread-id-here");Generated code layout
After bun notion sync:
notion/is created at the project root.- A full sync replaces the whole
notion/tree.
Example: generated database module
Each database gets one generated module. In outline:
columns— column metadata; select, status, and multi-select columns also getas constoption lists.- Types —
PageSchema,CreateSchema,QuerySchema. - Factory — call it with your token to initialize a standalone typed
DatabaseClientfor that database (you do not needNotionORMif you only want this DB).
The sample below is abridged (ellipsis marks omitted properties).
// Generated by @haustle/notion-orm — do not edit manually.
// Regenerate with `notion sync` (or your package script).
import type {
DatabaseColumns,
DatabaseDefinition,
InferCreateSchema,
InferDatabaseSchema,
Query,
} from "@haustle/notion-orm";
import { DatabaseClient } from "@haustle/notion-orm";
// Option literals for select / status / multi_select columns
export const CategoryPropertyValues = ["Electronics", "Clothing", "Food"] as const;
export const AvailabilityPropertyValues = [
"In Stock",
"Out of Stock",
"Backordered",
] as const;
export const TagsPropertyValues = ["New", "Sale", "Featured"] as const;
const columns = {
"itemName": { columnName: "Item Name", type: "title" },
"category": {
columnName: "Category",
type: "select",
options: CategoryPropertyValues,
},
"availability": {
columnName: "Availability",
type: "status",
options: AvailabilityPropertyValues,
},
"tags": {
columnName: "Tags",
type: "multi_select",
options: TagsPropertyValues,
},
"price": { columnName: "Price", type: "number" },
// …plus every other supported column type (e.g. relations, rich_text, files, …)
} as const satisfies DatabaseColumns;
export type PageSchema = InferDatabaseSchema<typeof columns>;
export type CreateSchema = InferCreateSchema<typeof columns>;
export type QuerySchema = Query<DatabaseDefinition<typeof columns>>;
export const InventoryItems = (auth: string) => {
return new DatabaseClient<DatabaseDefinition<typeof columns>>({
id: "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5",
columns,
name: "Inventory Items",
auth,
});
};Exports
PageSchemafindMany, findFirst, findUnique, streams, …).CreateSchemaQuerySchemaInventoryItems(auth) => DatabaseClient for this database (name matches the module).Select, status, and multi-select options drive typed filters and writes from the same option literals in generated modules. At runtime, DatabaseClient uses that column metadata to validate normalized query rows (Zod is internal to the ORM, not part of generated files). Relation columns point at other databases instead of option lists. A .js file is emitted next to .ts for plain JS imports.
Import paths
Use project-relative imports into ./notion/. After sync, that directory holds NotionORM, per-database and per-agent modules, and barrels—import from these generated files rather than from elsewhere.
./notion/NotionORM and base re-exports (import { NotionORM } from "./notion/"; directory import resolves to index.ts)./notion/databases/<Database><Database>(auth) factory, PageSchema, CreateSchema, QuerySchema, columns metadata, option tuples for select/status/multi-select, related type aliasesNotionORM wrapper./notion/agents/<Agent><Agent>(auth) factory → AgentClient./notion/databasesdatabases barrel (all database factories)./notion/agentsagents barrel (all agent factories)Directory tree
notion/
├── index.ts # NotionORM entry + re-exports
├── databases/
│ ├── index.ts # `databases` registry barrel (all DB factories)
│ ├── <Database>.ts # one file per tracked database (e.g. CustomerOrders.ts)
│ └── metadata.json # sync cache: ids + display names for incremental runs
└── agents/
├── index.ts # `agents` registry barrel (all agent factories)
├── <Agent>.ts # one file per agent (e.g. MealAgent.ts)
└── metadata.json # sync cache for agentsSync writes metadata.json cache files that store Notion ids and display names so incremental syncs stay fast and stable across runs.