Notion ORMGitHub

Notion ORM

An unofficial Notion API TypeScript wrapper that leverages static types to deliver a better database (and custom agents) experience

Key Features

  • Full type inference when interacting with databases, including findMany, create, update, delete, and more
  • Sync remote schema changes in a single command
  • Quickly start/resume chat streams with your agents
  • Access exported property values, schemas, and types from generated modules

Setup

Terminal
bun add @haustle/notion-orm

Initialize config from your project root (recommended):

Terminal
bun notion init
notion.config.ts
// If you don't have an API key, sign up for free
// [here](https://developers.notion.com)

const auth = process.env.NOTION_KEY || "your-notion-api-key-here";
const NotionConfig = {
  auth,
  databases: [
    // Use: notion add <database-id>
  ],
  agents: [
    // Auto-populated by: notion sync
  ],
};

export default NotionConfig;

Optional NOTION_BASE_URL overrides the Notion REST host for @notionhq/client and matching CLI usage; use an origin such as https://api.notion.com and omit /v1 (the client appends it). @notionhq/agents-client does not read this variable.

Adding databases

Add new database to track and generate static types (ex. how to find ID here )

Terminal
bun notion add <database-id>

Adding Custom Agents

Agent support requires the Notion Agents SDK, which is currently in alpha and not published to npm. Because of this, use the commands below to download, build, and link the SDK, then sync:

Terminal
bun notion setup-agents-sdk
bun notion sync

bun notion setup-agents-sdk clones the Notion Agents SDK, builds it from a local cache, and links @notionhq/agents-client into your project. Run it with bun notion sync to generate agent types and pick up agents on your integration. When the upstream SDK changes, re-run setup, then sync again.

If you have not run setup, notion sync only generates database types. This extra step will go away when the SDK is published to npm.

Learn more about Custom Agents in the Notion documentation.

Full sync command

Fetch/refresh database schemas. If the agents SDK is installed, also syncs custom agents.

Terminal
bun notion sync

Where sync writes files

notion sync writes generated modules under notion/ at your project root—your synced database schemas, registries, and agent factories. A full sync replaces the entire notion/ tree so removed databases or agents do not linger.

generated_tree.txt
notion/
├── index.ts              # NotionORM entry + re-exports
├── index.js
├── index.d.ts
├── databases/
│   ├── index.ts          # `databases` registry barrel
│   ├── <Database>.ts     # one factory module per tracked database (PascalCase stem)
│   └── metadata.json     # sync cache (ids + display names)
└── agents/
    ├── index.ts          # `agents` registry barrel
    ├── <Agent>.ts        # one factory module per agent (PascalCase stem)
    └── metadata.json

For the full tree (including declaration maps), how camelCase registry keys map to PascalCase files, and project-relative import paths, see Generated code layout in the API Reference.

Initialization

Create a single ORM instance with your Notion integration key:

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

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

const db = notion.databases.yourDatabaseName; // DatabaseClient
const agent = notion.agents.yourAgentName; // AgentClient (after setup-agents-sdk)

Generated database and agent names are camelCased and exposed on an instance of NotionORM.

  • Use notion.databases.<camelCaseDatabaseName> for database operations (findMany, create, update, delete, …).
  • Use notion.agents.<camelCaseAgentName> for chat(), chatStream(), thread helpers, and history APIs (requires notion setup-agents-sdk).

Databases

Every generated database exposes a Prisma-style API with full type inference from your schema. Here are a few highlights — see the full API reference for findFirst, findUnique, count, createMany, updateMany, upsert, deleteMany, and more.

Create a page

index.ts
const page = await notion.databases.books.create({
  properties: {
    bookName: "Creativity, Inc.",
    author: "Ed Catmull",
    genre: ["Non-fiction"],
    numberOfPages: 368,
    publishDate: { start: "2014-04-08" },
  },
  icon: { type: "emoji", emoji: "📕" },
});
// `page` from `create` includes:
// - `id` — new page id
// - `object` — Notion’s resource-kind tag (`"page"` here; other APIs use the same field for blocks, databases, etc.)
// Use `findUnique` when you need the full row (properties, url, …).

Create a page with markdown content

Add body content to a page using Notion's enhanced markdown format. Headings, lists, code blocks, quotes, and checklists are all supported.

index.ts
await notion.databases.books.create({
  properties: {
    bookName: "Reading Notes",
  },
  markdown: "# Key Takeaways\n\n- Creativity requires candor\n- Protect the new\n\n> \"Quality is the best business plan.\"",
});

Find many with filters

index.ts
const books = await notion.databases.books.findMany({
  where: {
    and: [
      { genre: { contains: "Non-fiction" } },
      { publishDate: { on_or_after: "2024-01-01" } },
    ],
  },
  sortBy: [{ property: "bookName", direction: "ascending" }],
  select: ["bookName", "genre"],
});

Update by ID

index.ts
await notion.databases.books.update({
  where: { id: "page-id" },
  properties: { status: "Done", numberOfPages: 460 },
});

Delete by filter

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

Stream large result sets

size limits a single request. With stream: n, each Notion call returns up to n rows, cursors advance automatically, and for await walks the full result one row at a time without buffering everything or hand-rolling nextCursor (cursor pagination; findMany).

index.ts
// Notion is queried in chunks of 50; the loop runs once per matching row, not just the first chunk
for await (const book of notion.databases.books.findMany({ stream: 50 })) {
  console.log(book.bookName);
}

Cursor pagination

index.ts
// First page (after: null); next pages use prev.nextCursor
const first = await notion.databases.books.findMany({ after: null, size: 10 });
const next = await notion.databases.books.findMany({
  after: first.nextCursor,
  size: 10,
});
// first.data, first.hasMore, next.data

Agents

Agents require the Notion Agents SDK. Run notion setup-agents-sdk first, then notion sync to generate agent types.

Once set up, agents shared with your integration are exposed at notion.agents.*.

See the Agent client API reference for full method signatures, thread helpers, and message APIs.

Chat and read messages

index.ts
const chat = await notion.agents.helpBot.chat({
  message: "Is the company closed today",
});
await notion.agents.helpBot.pollThread(chat.threadId);
const messages = await notion.agents.helpBot.getMessages(chat.threadId, {
  role: "agent",
});

Stream chat

index.ts
const thread = await notion.agents.helpBot.chatStream({
  message: "How can I update my shipping address?",
  onMessage: (msg) => {
    if (msg.role === "agent") process.stdout.write(msg.content);
  },
});

Basic chat (non-streaming)

  • Useful when you want a straightforward request/response flow.
  • Helpful when you plan to fetch message history after completion.
index.ts
const chat = await notion.agents.yourAgentName.chat({
  message: "Give me a summary of this month",
});

await notion.agents.yourAgentName.pollThread(chat.threadId);

const messages = await notion.agents.yourAgentName.getMessages(chat.threadId, {
  role: "agent",
});

Continue an existing thread

  • Useful when you want to preserve context across follow-up prompts.
  • Helpful for chat UIs where users continue the same conversation.
index.ts
const nextTurn = await notion.agents.yourAgentName.chat({
  threadId: chat.threadId,
  message: "Now turn that into a grocery list.",
});

Streaming patterns

How to start a new chat stream (pass threadId to resume):

index.ts
import { AgentClient } from "@haustle/notion-orm";

const thread = await notion.agents.yourAgentName.chatStream({
  message: "Generate a prep list for that plan.",

  onMessage: (msg) => {
    if (msg.role === "agent") process.stdout.write(msg.content);
  },
});


const finalResponse = AgentClient.getAgentResponse(thread);
console.log("Thread ID:", thread.threadId);
console.log("Final:", finalResponse);

See API Reference for full method signatures, ThreadInfo shape, and message schemas.