A lightweight TypeScript Notion API wrapper that aims to improve interactions with databases and custom agents, by leveraging static schema types
- Type inference when interacting with databases (e.g,
addandquery) - Sync remote schema changes in single command
- Quickly start/resume chat streams with your agents
- Access exported property values, schemas, and types
- Logs console warnings when local vs remote schema drift is detected
bun add @haustle/notion-ormAfter upgrading the package, run bun notion sync so generated build/src/index.* stays in sync with the version you installed (stale codegen can break at runtime when imports from the ORM package change).
Initialize config from your project root (recommended):
bun notion initGenerated config shape:
// 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;Add new database to track and generate static types (ex. how to find ID here )
bun notion add <database-id>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-sdkWhat 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 syncIf 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.
Fetch/refresh database schemas. If the agents SDK is installed, also syncs custom agents.
bun notion syncawait notion.databases.books.create({
icon: {
type: "emoji",
emoji: "📕",
},
// Expected <key,value> is constrained to `books` database schema
properties: {
bookName: "Creativity, Inc.",
genre: ["Non-fiction"],
publishDate: {
start: "2026-03-01",
},
},
});const books = await notion.databases.books.findMany({
where: {
and: [
{ genre: { contains: "Non-fiction" } },
{ publishDate: { on_or_after: "2026-01-01" } },
{
or: [
{ bookName: { contains: "Creativity" } },
{ bookName: { contains: "Innovation" } },
],
},
],
},
sortBy: [{ property: "bookName", direction: "ascending" }],
select: ["bookName", "genre"],
});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",
});const thread = await notion.agents.helpBot.chatStream({
message: "How can I update my shipping address?",
onMessage: ({content, role}) => (msg.content),
});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; // AgentClientGenerated database and agent names are camelCased and exposed on an instance of NotionORM.
- Use
notion.databases.<camelCaseDatabaseName>for typed CRUD + query operations (findMany,findFirst,findUnique,create,update,delete, and more). - Use
notion.agents.<camelCaseAgentName>forchat(),chatStream(), thread helpers, and history APIs. - For full method signatures and response shapes, see API Reference.
Only title is required by Notion for a minimal page.
await notion.databases.books.create({
properties: {
bookName: "Raphael, Painter in Rome: a Novel", // title
author: "Stephanie Storey", // rich_text
status: "In progress", // status
numberOfPages: 307, // number
genre: ["Historical Fiction"], // multi_select
startDate: {
start: "2023-01-01",
}, // date
phone: "0000000000", // phone_number
email: "tyrus@haustle.studio", // email
},
});Pass a markdown string alongside properties to create a page with body content in a single call. This uses Notion's enhanced markdown format, which supports headings, lists, code blocks, quotes, checklists, and more.
await notion.databases.books.create({
properties: {
bookName: "Hello World",
},
markdown: "# Hello World\n\nThis is a page created with **markdown**.",
});markdown is mutually exclusive with children / content — use one or the other. When properties.title is provided, the # h1 heading is treated as body content; when omitted, Notion extracts it as the page title.
Query filters are typed by your generated schema, including nested compound filters. Find Notion filter operators here.
Example single filter:
await notion.databases.books.findMany({
where: {
genre: {
contains: "Sci-Fi",
},
},
sortBy: [
{
property: "bookName",
direction: "ascending",
},
],
});Example compound filters:
await notion.databases.books.findMany({
where: {
and: [
{
or: [
{ genre: { contains: "Sci-Fi" } },
{ genre: { contains: "Biography" } },
],
},
{ numberOfPages: { greater_than: 250 } },
],
},
});Projection is available via select and omit string arrays:
const response = await notion.databases.books.findMany({
where: {
genre: { contains: "Sci-Fi" },
},
select: ["bookName", "genre"],
});Successful response shape:
[
{
bookName: "The Dream Machine",
genre: ["Non-fiction"],
},
]Agents are generated from those shared with your integration and exposed at notion.agents.*.
- 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",
});- 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.",
});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.
| runtime property | type | description | go deeper |
|---|---|---|---|
notion.databases |
Record<string, DatabaseClient> |
Generated database client map keyed by camelCase database name | Adding, Querying |
notion.agents |
Record<string, AgentClient> |
Generated agent client map keyed by camelCase agent name | Agents, Agent methods |
| member | kind | description | go deeper |
|---|---|---|---|
id |
property | Notion data source ID used by this client instance | - |
name |
property | Human-readable database name captured during generation | - |
findMany({ where?, sortBy?, size?, select?, omit?, stream?, after? }) |
method | Queries database pages with typed filters, projection, pagination, or streaming | Querying, Supported database properties |
findFirst({ where?, sortBy?, select?, omit? }) |
method | Returns the first matching row or null |
Querying |
findUnique({ where: { id }, select?, omit? }) |
method | Fetches a row by page ID with optional projection | Querying |
create({ properties, icon?, cover?, markdown? }) |
method | Creates a page with optional markdown body content | Adding, Markdown |
| member | kind | description | go deeper |
|---|---|---|---|
id |
property | Notion agent ID used by this client instance | - |
name |
property | Human-readable agent name | - |
icon |
property | Normalized agent icon metadata (or null) |
- |
listThreads() |
method | Lists recent threads with id, title, and status |
Thread response shapes |
getThreadInfo(threadId) |
method | Fetches a single thread record | Thread response shapes |
getThreadTitle(threadId) |
method | Convenience helper to fetch just the thread title | Thread response shapes |
chat({ message, threadId? }) |
method | Sends a message and creates/resumes a thread | Agents, Thread response shapes |
chatStream({ message, threadId?, onMessage? }) |
method | Streams messages and returns final ThreadInfo |
Agents, Thread response shapes |
getMessages(threadId, { role? }) |
method | Gets full (or role-filtered) message history | Thread response shapes |
pollThread(threadId, options?) |
method | Polls until thread processing completes | Thread response shapes |
AgentClient.getAgentResponse(threadInfo) |
method | Extract combined plain-text agent output from a streamed thread | Thread response shapes |
| import path | what you get | when to use |
|---|---|---|
@haustle/notion-orm/build/db/<databaseName> |
<databaseName>(auth) factory, DatabaseSchemaType, QuerySchemaType, generated Zod schema, generated option tuples (for select/status/multi-select), schema/type aliases |
Script-level direct DB usage without the NotionORM wrapper |
@haustle/notion-orm/build/agents/<agentName> |
<agentName>(auth) factory that returns an AgentClient |
Script-level direct agent usage |
@haustle/notion-orm/build/db |
databases barrel object (all database factories) |
Dynamic database selection or custom registry wiring |
@haustle/notion-orm/build/agents |
agents barrel object (all agent factories) |
Dynamic agent selection or custom registry wiring |
chatStream(...) returns ThreadInfo with the following properties:
| ThreadInfo property | type | description | example |
|---|---|---|---|
threadId |
string |
Stable thread identifier used to continue a conversation | "1f4e6f4a-5b58-4d91-a7fc-2f5f2a0f6bb1" |
agentId |
string |
Agent identifier that produced the response | "2c3c495da03c8078b95500927f02d213" |
messages |
`Array<{ role: "user" | "agent"; content: string }>` | Full message history currently available in the thread |
messages item shape:
| message property | type | description |
|---|---|---|
role |
user |
agent (string) |
content |
string |
Plain text message content |
| property_type | expected returned shape | example value |
|---|---|---|
title |
string |
"The Dream Machine" |
rich_text |
string |
"Long-form notes from the page" |
number |
number |
460 |
date |
{ start: string; end: string } |
{ start: "2026-03-01", end: "2026-03-02" } |
status |
string |
"In progress" |
select |
string |
"Non-fiction" |
multi_select |
string[] |
["Sci-Fi", "Biography"] |
checkbox |
boolean |
true |
email |
string |
"tyrus@haustle.studio" |
phone_number |
string |
"0000000000" |
url |
string |
"https://developers.notion.com/" |
files |
Array<{ name: string; url: string }> |
[{ name: "brief.pdf", url: "https://..." }] |
people |
string[] |
["1f4e6f4a-5b58-4d91-a7fc-2f5f2a0f6bb1"] |
relation |
string[] |
["6f7f9cbf-8d45-48f8-a194-661e73f7f5d9"] |
created_by |
string |
"Ada Lovelace" |
last_edited_by |
string |
"user_123" |
created_time |
string |
"2026-03-01T10:30:00.000Z" |
last_edited_time |
string |
"2026-03-01T13:15:00.000Z" |
unique_id |
string |
"TASK-42" |
rollup and formula are intentionally unsupported.
formula: Notion computes formula values at read time, and the actual output shape depends on the formula expression and its current result type. That makes it a poor fit for the generated static schema this client exposes. Because we cannot provide a stable contract for reads, writes, or filters, formula properties are skipped entirely during code generation.rollup: Rollup values are polymorphic and still need additional normalization before we can expose them as a predictable typed contract.
All supported properties can be used in typed filters. Formula properties are not surfaced in the generated client at all, so they are unavailable for selection, filtering, and normalized query results.
.
├── 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
├── plugins # lint/tooling helpers
└── build # generated output (after build/sync)
├── src
├── db
└── agents