Skip to Content
ProvidersNotion

Notion

Category: Collaboration / Documents
Integration type: Tenant internal integration token or OAuth (stored in integrations table)
External API: Notion API v1 (api.notion.com/v1)


Purpose

The Notion integration pushes platform-generated content — strategy documents, monthly reports, and deliverable summaries — into a client’s shared Notion workspace. It acts as a client-facing deliverable layer for agencies whose clients use Notion as their primary project management or knowledge tool.

Use cases:

  • Strategy delivery — Strategy Writer output is mirrored into a dedicated Notion page under the client’s workspace
  • Monthly report delivery — Report Writer output is appended as a new page in a Notion database each month
  • Deliverable tracking database — A Notion database row is created per deliverable period so clients can track status without logging into the platform

The platform writes Notion pages as read-only references — the source of truth remains in Leadmetrics. Edits made in Notion are not synced back.


Config Structure

Tenant integration

Notion supports two integration modes:

Option A: Internal integration token (simpler — single workspace)

interface NotionInternalConfig { token: string; // Notion internal integration secret (secret_...) deliverablesDatabaseId: string; // Notion database ID for deliverable period entries strategyParentPageId?: string; // Page under which strategy pages are created reportParentPageId?: string; // Page under which monthly report pages are created }

Option B: OAuth (for agencies managing multiple clients on different Notion workspaces)

scope: Notion scopes are workspace-level — the OAuth token grants access to any pages/databases the user shares with the integration. Stored in integrations: provider: 'notion' api_key: encrypt(token) metadata: { connectionType: 'internal' | 'oauth', workspaceId: string, workspaceName: string, deliverablesDatabaseId: string, strategyParentPageId: string | null, reportParentPageId: string | null, }

Important: Page sharing

For internal integrations, the client must manually share the target pages/databases with the Leadmetrics Notion integration. The setup wizard guides through this step.


Integration Pattern

Tool layer (packages/tools/src/notion.ts)

import { Client } from '@notionhq/client'; class NotionTool { private client: Client; constructor(private token: string) { this.client = new Client({ auth: token }); } /** * Create a new page under a parent page. * Used for strategy documents and monthly reports. */ async createPage(options: { parentPageId: string; title: string; markdownBody: string; // Converted to Notion blocks via markdown-to-blocks }): Promise<{ pageId: string; url: string }> { const blocks = markdownToNotionBlocks(options.markdownBody); const response = await this.client.pages.create({ parent: { type: 'page_id', page_id: options.parentPageId }, properties: { title: { title: [{ type: 'text', text: { content: options.title } }], }, }, children: blocks.slice(0, 100), // Notion API: max 100 blocks per request }); // Append remaining blocks in batches if content > 100 blocks if (blocks.length > 100) { await this.appendBlocks({ pageId: response.id, blocks: blocks.slice(100) }); } return { pageId: response.id, url: (response as any).url, }; } /** * Append additional blocks to an existing page. * Used when content exceeds 100 blocks. */ async appendBlocks(options: { pageId: string; blocks: object[]; }): Promise<void> { const CHUNK_SIZE = 100; for (let i = 0; i < options.blocks.length; i += CHUNK_SIZE) { await this.client.blocks.children.append({ block_id: options.pageId, children: options.blocks.slice(i, i + CHUNK_SIZE) as any, }); } } /** * Create a row in a Notion database. * Used for deliverable period tracking. */ async createDatabaseEntry(options: { databaseId: string; properties: { name: string; // "April 2026 — Acme Corp" status: 'In Progress' | 'Ready for Review' | 'Approved' | 'Delivered'; periodStart: string; // ISO date periodEnd: string; // ISO date blogCount: number; socialCount: number; portalUrl: string; }; }): Promise<{ pageId: string; url: string }> { const response = await this.client.pages.create({ parent: { type: 'database_id', database_id: options.databaseId }, properties: { Name: { title: [{ type: 'text', text: { content: options.properties.name } }], }, Status: { select: { name: options.properties.status }, }, 'Period Start': { date: { start: options.properties.periodStart }, }, 'Period End': { date: { start: options.properties.periodEnd }, }, 'Blog Posts': { number: options.properties.blogCount, }, 'Social Posts': { number: options.properties.socialCount, }, 'Review Link': { url: options.properties.portalUrl, }, }, }); return { pageId: response.id, url: (response as any).url, }; } /** * Update the Status property of an existing database entry. * Called when a deliverable period is approved by the client. */ async updateDatabaseEntryStatus(options: { pageId: string; status: 'In Progress' | 'Ready for Review' | 'Approved' | 'Delivered'; }): Promise<void> { await this.client.pages.update({ page_id: options.pageId, properties: { Status: { select: { name: options.status } }, }, }); } async verify(): Promise<void> { await this.client.users.me({}); } }

Markdown → Notion blocks helper

The Notion API requires structured block objects, not raw Markdown. A helper (markdownToNotionBlocks) converts generated Markdown to Notion block format. Recommended library: notion-to-md in reverse, or @tryfabric/martian.

import { markdownToBlocks } from '@tryfabric/martian'; function markdownToNotionBlocks(markdown: string): object[] { return markdownToBlocks(markdown); }

Agent Workflow

Strategy delivery

Strategy Writer agent generates strategy document (Markdown) ▼ (HITL review by DM team + client approval) Strategy Publisher worker ├── Resolve tenant's Notion integration ├── Convert Markdown to Notion blocks ├── Call createPage() under strategyParentPageId └── Store notion_page_id + url in strategy_versions table Client can view/comment directly in Notion

Deliverable period summary

Deliverable period marked "Ready for Review" Deliverable Publisher worker ├── Resolve tenant's Notion integration ├── Call createDatabaseEntry() with period stats and portal URL └── Store notion_page_id in deliverable_periods table When client approves in platform → updateDatabaseEntryStatus('Approved')

Deliverables Database Schema

The Notion database expected by the integration must have these properties (column names are exact):

PropertyType
NameTitle
StatusSelect (In Progress, Ready for Review, Approved, Delivered)
Period StartDate
Period EndDate
Blog PostsNumber
Social PostsNumber
Review LinkURL

A pre-built Notion database template is provided in the setup wizard via a “Duplicate Template” link.


Rate Limits

Notion API: 3 requests/second average, 90 requests/minute. For large strategy documents (many blocks), block appending is chunked to 100 blocks per request with a 350ms delay between chunks.


Test Cases

Unit tests (packages/tools/src/notion.test.ts)

TestApproach
createPage() chunks blocks when > 100Mock markdownToNotionBlocks returning 150 items; assert appendBlocks called with remaining 50
createPage() returns pageId and urlMock pages.create; assert mapped return
createDatabaseEntry() maps all properties correctlyAssert properties object structure in pages.create call
updateDatabaseEntryStatus() updates only StatusAssert only Status property in pages.update
appendBlocks() batches into chunks of 100Mock 250 blocks; assert blocks.children.append called 3 times
verify() throws when API token is invalidMock users.me throws 401; assert propagated

© 2026 Leadmetrics — Internal use only