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 NotionDeliverable 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):
| Property | Type |
|---|---|
| Name | Title |
| Status | Select (In Progress, Ready for Review, Approved, Delivered) |
| Period Start | Date |
| Period End | Date |
| Blog Posts | Number |
| Social Posts | Number |
| Review Link | URL |
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)
| Test | Approach |
|---|---|
createPage() chunks blocks when > 100 | Mock markdownToNotionBlocks returning 150 items; assert appendBlocks called with remaining 50 |
createPage() returns pageId and url | Mock pages.create; assert mapped return |
createDatabaseEntry() maps all properties correctly | Assert properties object structure in pages.create call |
updateDatabaseEntryStatus() updates only Status | Assert only Status property in pages.update |
appendBlocks() batches into chunks of 100 | Mock 250 blocks; assert blocks.children.append called 3 times |
verify() throws when API token is invalid | Mock users.me throws 401; assert propagated |
Related
- Slack Provider — alternative notification and delivery channel
- Google Drive Provider — document storage alternative
- Strategy Writer Agent — generates multi-month strategy documents
- Report Writer Agent — generates monthly performance reports
- Tool Integration Layer — integration management