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
bun add @haustle/notion-ormInitialize config from your project root (recommended):
bun notion init// 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 )
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:
bun notion setup-agents-sdk
bun notion syncbun 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.
bun notion syncWhere 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.
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.jsonFor 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:
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>forchat(),chatStream(), thread helpers, and history APIs (requiresnotion 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
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.
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
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
await notion.databases.books.update({
where: { id: "page-id" },
properties: { status: "Done", numberOfPages: 460 },
});Delete by filter
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).
// 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
// 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.dataAgents
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
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
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.
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.
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):
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.