Notion Workers Documentation

repository·main·Indexed 16 days ago

https://github.com/makenotion/workers-template

A beta platform for hosting Node/TypeScript programs to extend Notion's capabilities. It supports three primary capability types: Tools for custom agents, Syncs for pulling external data into Notion databases, and Webhooks for handling external service events. Includes documentation on the ntn CLI, OAuth implementation, environment secret management, and the @notionhq/workers-template package.

Tokens
7.6K
Snippets
23
Records
27
Agent score
63%

What's inside Notion Workers

  1. How Notion Workers work: Tools, Syncs, and Webhooks

    main

    Notion Workers are small Node/TypeScript programs hosted by Notion. They provide three distinct capability types:

    • Tools: Callable functions designed for Notion custom agents (e.g., giving an agent the ability to search or perform actions).
    • Syncs: Processes that pull data from external sources into Notion databases.
    • Webhooks: HTTP endpoints that allow external services (like GitHub or Stripe) to push events into your worker.
    NOTE

    Notion Workers is currently in beta. APIs, CLI commands, templates, and hosting behavior may evolve.

  2. Understand Sync Modes: Replace vs Incremental

    main

    When configuring a sync, you can choose between two modes:

    1. Replace (mode: "replace"): Each sync cycle returns the full dataset. After the final call where hasMore: false is returned, any records in the Notion database not present in the latest sync results are automatically deleted. Best for smaller datasets (<1k records).

    2. Incremental (mode: "incremental"): Each cycle returns only what has changed since the last run. Records not mentioned in the changes array are left untouched in Notion. To delete records, you must explicitly return a change with type: "delete". Best for larger datasets or APIs that support change tracking.

    Pagination: Syncs run as a chain of execute calls. If you return hasMore: true and a nextState value, the runtime will call execute again with that state. The cycle continues until hasMore: false is returned.

  3. Create a Sync to pull external data into Notion

    main

    A sync pulls data from an external source into a Notion database. You can use the /sync slash command in supported coding agents (like Claude Code) to scaffold this interactively, or define it manually using worker.database() and worker.sync().

    Manual implementation steps:

    1. Declare a database using worker.database() with a primaryKeyProperty.
    2. Define the database schema using Schema helpers.
    3. Create a pacer (optional) using worker.pacer() to manage rate limits.
    4. Define the sync using worker.sync(), providing the database and an execute function that returns an object containing changes and hasMore status.

    Deploy with ntn workers deploy. Syncs run automatically on a schedule (default: every 30 minutes).

    import { Worker } from "@notionhq/workers";
    import * as Builder from "@notionhq/workers/builder";
    import * as Schema from "@notionhq/workers/schema";
    
    const worker = new Worker();
    export default worker;
    
    const issues = worker.database("issues", {
    	type: "managed",
    	initialTitle: "Issues",
    	primaryKeyProperty: "Issue ID",
    	schema: {
    		properties: {
    			Title: Schema.title(),
    			"Issue ID": Schema.richText(),
    		},
    	},
    });
    
    const issueTracker = worker.pacer("issueTracker", { allowedRequests: 10, intervalMs: 1000 });
    
    worker.sync("issuesSync", {
    	database: issues,
    	execute: async () => {
    		await issueTracker.wait();
    		const items = await fetchIssues(); // your data source
    		return {
    			changes: items.map((issue) => ({
    				type: "upsert" as const,
    				key: issue.id,
    				properties: {
    					Title: Builder.title(issue.title),
    					"Issue ID": Builder.richText(issue.id),
    				},
    			})),
    			hasMore: false,
    		},
    	},
    });
  4. Handle incoming webhooks

    main

    Webhooks allow external services to push events to your worker via an HTTP endpoint. Use worker.webhook(name, options) to register a handler.

    Webhook Execution Model:

    • Requests are acknowledged with 202 Accepted immediately.
    • The execute function runs asynchronously.
    • If the handler throws a non-verification error, Notion retries up to 3 times.
    • WebhookVerificationError is never retried.
    • After 5 consecutive WebhookVerificationError failures, Notion blocks the webhook. You must redeploy the worker to reset the counter.

    Commands:

    • List webhook URLs: ntn workers webhooks list

    Security Warning: Webhook URLs act as shared secrets. Anyone with the URL can send events. Always implement signature verification using event.rawBody and event.headers.

    import { Worker } from "@notionhq/workers";
    
    const worker = new Worker();
    export default worker;
    
    worker.webhook("onExternalEvent", {
    	title: "External Event Handler",
    	description: "Processes incoming webhook requests",
    	execute: async (events) => {
    		for (const event of events) {
    			console.log("Delivery:", event.deliveryId);
    			console.log("Method:", event.method);
    			console.log("Body:", event.body);
    		}
    	},
    });
  5. Expose a Webhook endpoint

    main

    Webhooks allow external services to push events to your worker. Use worker.webhook() to define an endpoint. After deploying with ntn workers deploy, use ntn workers webhooks list to retrieve the URL to provide to your external service.

    The execute function receives an array of events. Each event contains deliveryId, method, and body.

    import { Worker } from "@notionhq/workers";
    
    const worker = new Worker();
    export default worker;
    
    worker.webhook("onExternalEvent", {
    	title: "External Event Handler",
    	description: "Processes incoming webhook requests",
    	execute: async (events) => {
    		for (const event of events) {
    			console.log("Delivery:", event.deliveryId);
    			console.log("Method:", event.method);
    			console.log("Body:", event.body);
    		}
    	},
    });
  6. Manage environment secrets and variables

    main

    Use the ntn workers env command to manage secrets like API keys.

    • Set a secret: ntn workers env set KEY=value
    • Local development: Pull remote secrets to a local .env file using ntn workers env pull.
    • Accessing in code: Use process.env.KEY to access these values in your worker code.
    # Set a secret
    ntn workers env set API_KEY=your-secret
    
    # Pull secrets for local development
    ntn workers env pull
  7. Set up local development for Notion Workers

    main

    To develop locally, use the following npm scripts to ensure type safety and build your project. Store secrets in a .env file for local testing by running ntn workers env pull.

    npm run check # type-check
    npm run build # emit dist/
    
    # Pull secrets for local development
    ntn workers env pull
  8. Implement OAuth for external services

    main

    For services requiring user authorization (e.g., GitHub, Google), use worker.oauth.

    1. Define the OAuth configuration in your worker using worker.oauth.
    2. Configure the provider: After deployment, use the CLI to find the redirect URL and start the flow.
    3. Use the token: Access the token via githubAuth.accessToken() within your tools or syncs.

    Note: A Notion-managed OAuth shorthand (e.g., { provider: "google" }) is currently in alpha and requires a feature flag. Use the manual approach for production.

    // 1. Define OAuth
    const githubAuth = worker.oauth("githubAuth", {
    	name: "github-oauth",
    	authorizationEndpoint: "https://github.com/login/oauth/authorize",
    	tokenEndpoint: "https://github.com/login/oauth/access_token",
    	scope: "repo user",
    	clientId: process.env.GITHUB_CLIENT_ID ?? "",
    	clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "",
    });
    
    // 2. Use the token in a tool
    worker.tool("getGitHubRepos", {
    	title: "Get GitHub Repos",
    	description: "Fetch user's GitHub repositories",
    	schema: j.object({}),
    	execute: async () => {
    		const token = await githubAuth.accessToken();
    		const response = await fetch("https://api.github.com/user/repos", {
    			headers: { Authorization: `Bearer ${token}` },
    		});
    		return response.json();
    	},
    });
    # 3. CLI commands to manage flow
    ntn workers oauth show-redirect-url
    ntn workers oauth start githubAuth
  9. Sync external data into Notion databases

    main

    Workers can automatically maintain Notion databases by syncing data from external sources. Use worker.sync(key, options) to define a sync process.

    Key Configuration:

    • primaryKeyProperty: The property in the Notion database used as the unique identifier.
    • schema: Defines the database structure using Schema helpers (e.g., Schema.title(), Schema.richText()).
    • execute: An async function that returns an object containing changes (an array of upsert, create, or delete operations) and a hasMore boolean.

    Important Commands:

    • Monitor sync status: ntn workers sync status
    • Reset sync state (to start from scratch): ntn workers sync state reset <key>

    Note: Deploying does not reset sync state; it resumes from the last cursor position.

    import { Worker } from "@notionhq/workers";
    import * as Builder from "@notionhq/workers/builder";
    import * as Schema from "@notionhq/workers/schema";
    
    const worker = new Worker();
    export default worker;
    
    worker.sync("issuesSync", {
    	primaryKeyProperty: "Issue ID",
    	schema: {
    		defaultName: "Issues",
    		properties: {
    			Title: Schema.title(),
    			"Issue ID": Schema.richText(),
    		},
    	},
    	execute: async () => {
    		const issues = await fetchIssues(); // your data source
    		return {
    			changes: issues.map((issue) => ({
    				type: "upsert" as const,
    				key: issue.id,
    				properties: {
    					Title: Builder.title(issue.title),
    					"Issue ID": Builder.richText(issue.id),
    				},
    			})),
    			hasMore: false,
    		},
    	},
    });
  10. Install the ntn CLI and scaffold a new worker

    main

    To get started with Notion Workers, install the ntn CLI and use it to scaffold a new project directory. This creates the base structure for your Node/TypeScript worker.

    curl -fsSL https://ntn.dev | bash
    ntn workers new my-worker
    cd my-worker
  11. Manage secrets and environment variables

    main

    Use the CLI to set environment variables and secrets for your workers. For local development, you can pull these secrets into a .env file.

    # Store API keys and secrets
    ntn workers env set API_KEY=your-secret
    
    # Pull secrets for local development into .env
    ntn workers env pull
  12. Verify webhook requests using signatures

    main

    To secure your webhooks, verify the request signature using event.rawBody and event.headers. If verification fails, throw a WebhookVerificationError.

    Important: After 5 consecutive WebhookVerificationError failures, Notion blocks the webhook. You must redeploy the worker to reset this counter.

    Webhook requests are acknowledged with 202 Accepted and run asynchronously. If your handler throws any error other than WebhookVerificationError, Notion will retry the run up to 3 times. WebhookVerificationError is never retried.

    import * as crypto from "node:crypto";
    import { WebhookVerificationError, Worker } from "@notionhq/workers";
    
    const worker = new Worker();
    export default worker;
    
    function verifyGitHubSignature(
    	rawBody: string,
    	headers: Record<string, string>,
    ): void {
    	const secret = process.env.GITHUB_WEBHOOK_SECRET;
    	if (!secret) {
    		throw new WebhookVerificationError("GITHUB_WEBHOOK_SECRET not configured");
    	}
    
    	const signature = headers["x-hub-signature-256"];
    	if (!signature?.startsWith("sha256=")) {
    		throw new WebhookVerificationError("Invalid GitHub signature");
    	}
    
    	const expected = `sha256=${crypto
    		.createHmac("sha256", secret)
    		.update(rawBody)
    		.digest("hex")}`;
    
    	if (
    		signature.length !== expected.length ||
    		!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    	) {
    		throw new WebhookVerificationError("Invalid GitHub signature");
    	}
    }
    
    worker.webhook("onGithubPush", {
    	title: "GitHub Push Webhook",
    	description: "Handles push events from GitHub repositories",
    	execute: async (events) => {
    		for (const event of events) {
    			verifyGitHubSignature(event.rawBody, event.headers);
    			console.log("Verified GitHub event:", event.body);
    		}
    	},
    });