Skip to content

Notion

Overview#

The Notion integration gives your Cognest assistant full read/write access to your Notion workspace through an internal integration. It can create and update pages, query and filter databases, append block content, and search across the workspace. Combined with webhooks, your assistant can react to page and database changes in real time to build powerful automation workflows.

Prerequisites#

  1. A Notion workspace on a Plus, Business, or Enterprise plan (for API access)
  2. An internal integration created at notion.so/my-integrations
  3. The integration connected to target pages/databases via the "Connect to" menu in Notion (three-dot menu on each page)

Integration Permissions

A Notion internal integration can only access pages and databases that have been explicitly shared with it. After creating your integration, you must open each page or database you want the assistant to interact with and select "Connect to" > your integration name from the three-dot menu.

Configuration#

Add the Notion integration to your cognest.config.yaml:

cognest.config.yaml
integrations:
  notion:
    enabled: true
    credentials:
      api_key: ${NOTION_API_KEY}
    settings:
      api_version: "2022-06-28"           # Notion API version
      polling_interval: 60                 # Seconds between change-detection polls
      watched_databases:                   # Databases to monitor for changes
        - id: "a1b2c3d4e5f6..."
          events: ["page:created", "page:updated"]
        - id: "f6e5d4c3b2a1..."
          events: ["page:updated"]
      default_parent_page: "your-page-id"  # Default parent for new pages
      rich_text_max_length: 2000           # Truncate text blocks at this length

Set the environment variable in your .env file:

.env
NOTION_API_KEY=ntn_your_internal_integration_token

SDK Usage#

Access the Notion integration through the @cognest/sdk and @cognest/integrations/notion:

skills/notion-tasks.ts
import { Cognest } from "@cognest/sdk";
import { NotionIntegration } from "@cognest/integrations/notion";

const cognest = new Cognest();
const notion = cognest.integration<NotionIntegration>("notion");

// Create a new page in a database
await notion.createPage({
  parent: { database_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" },
  properties: {
    Name: { title: [{ text: { content: "Weekly Standup Notes" } }] },
    Status: { select: { name: "In Progress" } },
    Assignee: { people: [{ id: "user-id-here" }] },
    "Due Date": { date: { start: "2025-02-15" } },
  },
});

// Query a database with filters
const results = await notion.queryDatabase({
  database_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  filter: {
    and: [
      { property: "Status", select: { equals: "In Progress" } },
      { property: "Assignee", people: { contains: "user-id-here" } },
    ],
  },
  sorts: [{ property: "Due Date", direction: "ascending" }],
  page_size: 20,
});

// Search across the workspace
const searchResults = await notion.search({
  query: "Q4 planning",
  filter: { property: "object", value: "page" },
  sort: { direction: "descending", timestamp: "last_edited_time" },
});

Available Methods#

MethodDescriptionReturns
`createPage(options)`Create a new page in a database or as a child of another page`Promise<NotionPage>`
`updatePage(options)`Update properties on an existing page`Promise<NotionPage>`
`queryDatabase(options)`Query a database with filters, sorts, and pagination`Promise<NotionQueryResult>`
`getPage(pageId)`Retrieve a page and its properties by ID`Promise<NotionPage>`
`appendBlocks(options)`Append block children (text, headings, lists, etc.) to a page`Promise<NotionBlock[]>`
`search(options)`Search pages and databases by title across the workspace`Promise<NotionSearchResult>`
`createDatabase(options)`Create a new inline database inside a parent page`Promise<NotionDatabase>`

Events#

Cognest monitors your watched databases for changes using a polling mechanism. The following events are emitted when changes are detected:

EventTriggerPayload
`page:created`A new page is added to a watched database`{ pageId, databaseId, properties, createdBy, createdTime }`
`page:updated`A page's properties are modified in a watched database`{ pageId, databaseId, properties, editedBy, lastEditedTime, changedProperties }`
`database:updated`A watched database schema is modified (new columns, etc.)`{ databaseId, title, properties, lastEditedTime }`

Polling Frequency

Change detection uses the `polling_interval` setting (default: 60 seconds). For near-real-time reactions, you can lower this to 10 seconds, but be mindful of Notion API rate limits (3 requests per second per integration). Cognest batches and deduplicates requests to stay within limits.

Example#

This example builds a meeting notes skill that watches a Notion database for new entries, generates an AI summary, and appends structured content blocks to the page:

skills/meeting-notes.ts
import { Cognest, Skill, IncomingEvent } from "@cognest/sdk";
import {
  NotionIntegration,
  NotionPageCreatedEvent,
} from "@cognest/integrations/notion";

const cognest = new Cognest();
const notion = cognest.integration<NotionIntegration>("notion");

const meetingNotes = new Skill({
  name: "meeting-notes",
  description: "Auto-generate structured meeting notes from raw transcripts",
});

meetingNotes.on(
  "notion:page:created",
  async (event: IncomingEvent<NotionPageCreatedEvent>) => {
    const { pageId, databaseId, properties } = event.payload;

    // Only process pages in the meetings database
    const MEETINGS_DB = process.env.NOTION_MEETINGS_DB!;
    if (databaseId !== MEETINGS_DB) return;

    // Get the full page content (raw transcript)
    const page = await notion.getPage(pageId);
    const titleProp = properties["Name"];
    const meetingTitle =
      titleProp?.type === "title"
        ? titleProp.title.map((t) => t.plain_text).join("")
        : "Untitled Meeting";

    // Use the Think Engine to structure the transcript
    const structured = await cognest.think({
      prompt: `You are a meeting notes assistant. Given the meeting titled "${meetingTitle}", generate structured notes.

Return JSON with:
{
  "summary": "2-3 sentence executive summary",
  "key_decisions": ["decision 1", "decision 2"],
  "action_items": [
    { "task": "description", "owner": "name", "due": "YYYY-MM-DD" }
  ],
  "open_questions": ["question 1"]
}`,
      responseFormat: "json",
    });

    const { summary, key_decisions, action_items, open_questions } =
      structured.parsed;

    // Update the page status property
    await notion.updatePage({
      page_id: pageId,
      properties: {
        Status: { select: { name: "Processed" } },
      },
    });

    // Append structured blocks to the page
    await notion.appendBlocks({
      block_id: pageId,
      children: [
        {
          type: "heading_2",
          heading_2: {
            rich_text: [{ type: "text", text: { content: "Summary" } }],
          },
        },
        {
          type: "paragraph",
          paragraph: {
            rich_text: [{ type: "text", text: { content: summary } }],
          },
        },
        {
          type: "heading_2",
          heading_2: {
            rich_text: [
              { type: "text", text: { content: "Key Decisions" } },
            ],
          },
        },
        ...key_decisions.map((decision: string) => ({
          type: "bulleted_list_item" as const,
          bulleted_list_item: {
            rich_text: [{ type: "text" as const, text: { content: decision } }],
          },
        })),
        {
          type: "heading_2",
          heading_2: {
            rich_text: [
              { type: "text", text: { content: "Action Items" } },
            ],
          },
        },
        ...action_items.map(
          (item: { task: string; owner: string; due: string }) => ({
            type: "to_do" as const,
            to_do: {
              rich_text: [
                {
                  type: "text" as const,
                  text: {
                    content: `${item.task} — @${item.owner} (due ${item.due})`,
                  },
                },
              ],
              checked: false,
            },
          })
        ),
        {
          type: "heading_2",
          heading_2: {
            rich_text: [
              { type: "text", text: { content: "Open Questions" } },
            ],
          },
        },
        ...open_questions.map((q: string) => ({
          type: "bulleted_list_item" as const,
          bulleted_list_item: {
            rich_text: [{ type: "text" as const, text: { content: q } }],
          },
        })),
      ],
    });

    cognest.log.info(
      `Processed meeting notes for "${meetingTitle}" (${pageId})`
    );
  }
);

cognest.register(meetingNotes);
cognest.start();

Troubleshooting#

Common Issues

**"Could not find page" or 404 errors** — The integration has not been connected to the target page. Open the page in Notion, click the three-dot menu, select "Connect to", and choose your integration. **"Unauthorized" or 401 errors** — Your `NOTION_API_KEY` is invalid or expired. Internal integration tokens start with `ntn_`. Regenerate the token in your integration settings at notion.so/my-integrations. **Properties not appearing in query results** — Ensure you are using the exact property name (case-sensitive) from the database schema. Use `notion.getPage(id)` to inspect the available properties on a page. **Rate limiting (429 responses)** — Notion allows 3 requests per second per integration. Cognest queues and retries automatically, but if you are processing large batches, increase `polling_interval` and use pagination with `start_cursor` to spread the load.