Notion ORM
A 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 - Manage databases and agents in Notion
- Sync remote schema changes in a single command
- Quickly start/resume chat streams with your agents
- Access exported property values, schemas, and types (ex.
zodvalidators) - Validate config and other untrusted input with
zod
Setup
bun add @haustle/notion-orm
Initialize 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;
Generated config from `notion init`
Adding databases
Add new database to track and generate static types (ex. how to find ID here )
bun notion add <database-id>
Adding agents (paid feature)
Agent support requires the Notion Agents SDK, which is currently in alpha and not published to npm. Because of this, a one-command setup handles the entire download-and-install flow for you:
bun notion setup-agents-sdk
What this does:
- Clones the SDK repository into a local cache (
node_modules/.cache/.notion-agents-sdk) - Installs the SDK's dependencies and builds it
- Adds the built
@notionhq/agents-clientpackage to your project
After setup, run notion sync to generate agent types. Agents linked to your integration are automatically discovered.
Updating: When the upstream SDK receives changes, rerun the same command. It pulls the latest from the cached clone, rebuilds, and reinstalls:
bun notion setup-agents-sdk
bun notion sync
If you have not run the setup command, notion sync will skip agent generation and only produce database types. Once the SDK is published to npm, this step will no longer be necessary.
Learn more about Custom Agents in the Notion documentation.
Full sync command (notion sync)
- Fetch/refresh database schemas. If the agents SDK is installed, also syncs custom agents.
bun notion sync
Initialization
Create a single ORM instance with your Notion integration key:
import NotionORM from "@haustle/notion-orm";
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
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: "š" },
});
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
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.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
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.
Size & dependencies
425.2 kB is unpacked size (npm pack --dry-run).
Runtime dependencies:
@notionhq/clientā Notion API client used by runtimeDatabaseClientandAgentClientto call official endpoints.@babel/parserā Parses generated TypeScript/JavaScript source into AST nodes during codegen and sync flows.@babel/generatorā Emits formatted source code from AST transforms used by generated config/registry/type files.zodā Validates config and untrusted boundary input with typed schemas and clear parse errors.
Project Structure
.
āāā src
ā āāā cli # notion init / add / sync / setup-agents-sdk
ā āāā config # config discovery, loading, and validation
ā āāā client # runtime DatabaseClient + AgentClient
ā ā āāā query # typed filters + response simplification
ā āāā ast # code generation internals
ā ā āāā database
ā ā āāā agents
ā ā āāā shared
ā āāā types # local type bridges
āāā tests # runtime, codegen, types, and CLI coverage
āāā plugins # lint/tooling helpers
āāā build # generated output (after build/sync)
āāā src
āāā db
āāā agents