Notion ORMGitHub

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.

Try playground

Runtime access

index.ts
import { NotionORM } from "./notion";

const notion = new NotionORM({
  auth: process.env.NOTION_KEY,
});

// DatabaseClient — e.g. notion.databases.books
// AgentClient — e.g. notion.agents.helpBot
notion.databasesRecord<string, DatabaseClient>
Generated database client map keyed by camelCase database name
notion.agentsRecord<string, AgentClient>
Generated agent client map keyed by camelCase agent name

Database client

Properties

id
Notion data source ID used by this client instance
name
Human-readable database name captured during generation

Methods

findUnique({ where: { id } })
Retrieves a single page by its Notion ID (source)
findFirst(args?)
Returns the first matching row or null (source)
findMany(args?)
Query rows. Returns Partial<Schema>[] by default, PaginateResult with after, or AsyncIterable with stream (source)
count(args?)
Returns the total number of matching rows (source)
create({ properties, icon?, cover?, markdown? })
Creates a page with typed properties and optional markdown body content (source)
createMany([{ properties, icon?, cover?, markdown? }])
Creates multiple pages sequentially (source)
update({ where: { id }, properties })
Updates a single page by ID (source)
updateMany({ where, properties })
Updates all pages matching a filter (source)
upsert({ where, create, update })
Creates or updates depending on whether a match exists (source)
delete({ where: { id } })
Archives a single page by ID (source)
deleteMany({ where })
Archives all pages matching a filter (source)

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.

index.ts
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.

index.ts
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:

No stream or after
Promise<Partial<Schema>[]>
after: string | null
Promise<PaginateResult<Schema>>
stream: number
AsyncIterable<Partial<Schema>>

Args: All properties are optional.

whereQueryFilter
Limits rows to those matching the filter (per-column conditions and nested and / or). Omit to consider every page in this data source (still bounded by size and by list vs pagination vs streaming mode).
sortByQuerySort<ColumnTypes>
Sort order: one or more column sorts and/or created_time / last_edited_time. Omit for the API’s default ordering.
sizenumber
Notion page_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)[]
Return only these properties on each row (narrows the partial row type).
omitreadonly (keyof Schema & string)[]
Return all properties except those listed.
streamnumber
Enables async iteration: each underlying query requests up to this many rows until the cursor is exhausted. Omit stream for promise-based list or pagination. Cannot be used together with after.
afterstring | null
Pagination cursor: pass null 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.

index.ts
// 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>[]
Current page of results
nextCursorstring | null
Pass to after for the next page, null when done
hasMoreboolean
Whether more pages remain
index.ts
// 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:

index.ts
await notion.booksDb.findMany({
  where: { genre: { contains: "Sci-Fi" } },
});

Compound filters:

index.ts
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.

index.ts
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.

index.ts
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 ID

Pass 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.

index.ts
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.

index.ts
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.

index.ts
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.

index.ts
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.

index.ts
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).

index.ts
await notion.booksDb.delete({ where: { id: "page-id" } });

deleteMany

Archives all pages matching a filter.

index.ts
await notion.booksDb.deleteMany({
  where: { status: { equals: "Archived" } },
});

Supported database properties

titlestring
"The Dream Machine"
rich_textstring
"Long-form notes from the page"
numbernumber
460
date{ start: string; end: string }
{ start: "2026-03-01", end: "2026-03-02" }
statusstring
"In progress"
selectstring
"Non-fiction"
multi_selectstring[]
["Sci-Fi", "Biography"]
checkboxboolean
true
emailstring
"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:

formula
Formula values are computed by Notion at read time, and the result shape depends on the expression plus the current value type. Because that contract is not stable enough for generated schema types, formula properties are skipped entirely instead of being exposed as readable-but-limited fields.
rollup
Rollup values are polymorphic and require additional normalization before we can provide safe, predictable filter contracts.

Formula properties are therefore omitted from generated database modules and are unavailable in typed reads, projections, or filters.

Agent client

Properties

id
Notion agent ID used by this client instance
name
Human-readable agent name
icon
Normalized agent icon metadata (or null)

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.

index.ts
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:

threadIdstring
Thread to pass to chat, getMessages, etc.
statusThreadStatus
Processing state from the Agents API
isNewChatboolean
true when threadId was omitted in the request

chatStream

Streams message chunks via onMessage and resolves to ThreadInfo with the full transcript when the stream completes.

index.ts
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:

index.ts
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.

threadIdstring
Stable thread identifier used to continue a conversation
agentIdstring
Agent identifier that produced the response
messagesArray<{ role: "user" | "agent"; content: string }>
Full message history available after the stream

Message items:

roleuser | agent
Message author
contentstring
Plain text content

AgentClient.getAgentResponse

Static helper: joins every agent message in a ThreadInfo into a single string (useful with chatStream results).

index.ts
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.

index.ts
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.

index.ts
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.

index.ts
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).

index.ts
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.

index.ts
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 get as const option lists.
  • TypesPageSchema, CreateSchema, QuerySchema.
  • Factory — call it with your token to initialize a standalone typed DatabaseClient for that database (you do not need NotionORM if you only want this DB).

The sample below is abridged (ellipsis marks omitted properties).

index.ts
// 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

PageSchema
Row shape from reads (findMany, findFirst, findUnique, streams, …).
CreateSchema
Fields allowed on create/update/upsert (read-only and formula columns omitted).
QuerySchema
Typed filters, sort, field selection, cursors, and stream options.
InventoryItems
(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)
Typical app code
./notion/databases/<Database><Database>(auth) factory, PageSchema, CreateSchema, QuerySchema, columns metadata, option tuples for select/status/multi-select, related type aliases
Direct database usage without the NotionORM wrapper
./notion/agents/<Agent><Agent>(auth) factory → AgentClient
Direct agent usage without the wrapper
./notion/databasesdatabases barrel (all database factories)
Dynamic DB selection or custom wiring
./notion/agentsagents barrel (all agent factories)
Dynamic agent selection or custom wiring

Directory tree

generated_tree.txt
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 agents

Sync writes metadata.json cache files that store Notion ids and display names so incremental syncs stay fast and stable across runs.