npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@notionhq/apps

v0.0.59

Published

An SDK for building workflow apps for Notion

Readme

Apps SDK

CI Publish

Imports

The package root exports only resource and capability creation helpers:

import {
	access,
	page,
	database,
	teamspace,
	customAgent,
	sync,
	workflow,
	customBlock,
} from "@notionhq/apps";

Import the helpers your app uses:

import { page, workflow } from "@notionhq/apps";
import { notion } from "@notionhq/apps/notion-as-code";

const home = page({
	resourceId: "home",
	properties: { title: notion.text("App home") },
});

export default workflow({
	name: "Follow up",
	description: "Runs when a page is created",
	triggers: ({ events }) => [events.notionPageCreated()],
	handler: async () => {},
});

Namespace imports (import * as Notion from "@notionhq/apps") are also supported for autocomplete and API exploration.

sync accepts a Notion-as-Code data-source handle. customBlock creates a custom-block capability; it is not the browser runtime object. The capability factories use these short names on both the root and their subpaths. Their behavior is unchanged.

The old capability factory names are no longer exported. Update existing imports and calls:

| Removed name | Use instead | Subpath | | ---------------------- | ------------- | ----------------------------- | | createWorkflow | workflow | @notionhq/apps/workflow | | createDataSourceSync | sync | @notionhq/apps/sync | | createCustomBlock | customBlock | @notionhq/apps/custom-block |

Everything else keeps its subpath import, including builders, value helpers, types, connections, triggers, pacers, and the custom-block browser runtime:

import { Builder } from "@notionhq/apps/builder";
import { notion } from "@notionhq/apps/notion-as-code";
import type { DataSourceSyncConfiguration } from "@notionhq/apps/sync";

Existing subpaths remain available, with the capability factory renames above. The root uses explicit exports and does not re-export utility types or the custom-block browser runtime. Browser code should keep using @notionhq/apps/custom-blocks and @notionhq/apps/react; the root includes Node-only workflow code. Styles stay in @notionhq/apps/nds.css.

Agent skills

The package includes skills for workflows, connections, database syncs, custom blocks, and Notion as Code. Read the matching skill before creating, modifying, or troubleshooting an App capability. Shipping the skills with the SDK keeps their instructions up to date with the installed API.

To agree on an App's architecture before building it, use the sketch skill. It writes the App as a small JSON file and renders it as a flow diagram you can review and edit in the browser.

Development

  1. Install mise
  2. Run mise install
  3. Run mise run setup
  4. Run mise run check && mise run test

The SDK requires Node 26 or newer. It supports workflows and database syncs.

import { events } from "@notionhq/apps/events";
import { workflow } from "@notionhq/apps";
import { connections } from "@notionhq/apps/workflow";

Add a connection requirement to a workflow when it needs Calendar:

export default workflow({
	name: "Schedule follow-up",
	description: "Schedules a follow-up after a page is created",
	triggers: [events.notionPageCreated()],
	connections: {
		calendar: connections.calendar({
			targets: {
				followUps: {
					description: "Create follow-up events",
					permissions: "read_write",
					multiple: false,
				},
			},
		}),
	},
	handler: async () => {},
});

The property name is the connection key. Access its typed client with context.connections.calendar; use separate keys for separate connections to the same provider, except Calendar. Declare one Calendar connection and reusable targets; each target chooses its required permissions and whether multiple calendars are allowed. For a multiple-calendar write target, call listCalendars({ calendars: target }) and pass one whole returned calendar object to each write (for example, calendar: selectedCalendar). The same object works for reads with calendars: selectedCalendar.

Every Calendar read requires calendars: pass a named target or a returned calendar object. Omitting it fails instead of reading every calendar in the connection.

Workflow reliability

A workflow's retry policy is the default for every step. A step's own retry replaces it; the two never stack. A run is only retried for failures outside a step, such as an error thrown directly in the handler. When a step fails after using its retries, the run fails without being retried.

import { FatalError, RetryableError } from "@notionhq/apps/workflow";

export default workflow({
	name: "Charge customer",
	description: "Charges a customer when an invoice is created",
	triggers: [events.notionPageCreated()],
	deadline: { afterMs: 7 * 24 * 60 * 60 * 1000 },
	retry: { maxAttempts: 3, retryOn: PaymentGatewayError },
	handler: async (_event, context) => {
		await context.step(
			"Charge customer",
			{ timeoutMs: 30_000, retry: { maxAttempts: 2 } },
			chargeCustomer,
		);
	},
});

maxAttempts includes the initial attempt. Throw RetryableError to explicitly permit another attempt, optionally with retryAfterMs. Throw FatalError to skip remaining retries. Set retryOn to an error class, a predicate, or an array of them to retry only matching errors; RetryableError is always retried and FatalError never is.

| Setting | Default | Limit | | ------------------------- | ------------------------------- | ---------- | | retry.maxAttempts | 3 | 1 to 3 | | retry.initialDelayMs | 1 second | 7 days | | retry.backoffMultiplier | 2 | at least 1 | | retry.maxDelayMs | 5 minutes | 7 days | | retry.retryOn | every error except FatalError | — | | deadline.afterMs | none | 7 days | | step timeoutMs | none | — |

A workflow invocation runs for at most 5 minutes, and step retries wait inside it. A step's longest possible retry delays plus timeoutMs for every attempt must fit in 5 minutes, and so must the workflow retry policy because steps use it by default. Policies that don't fit are rejected. Time already spent in the invocation counts too: if the next retry would run past 5 minutes from the handler's start, the step stops retrying and the run is retried instead. Use context.wait.until() for longer waits.

When a step exceeds timeoutMs, the SDK aborts the attempt's signal (context.step(name, options, ({ signal }) => ...)) and retries. The same signal also aborts when the run is cancelled. It can't stop code that ignores the signal, so pass signal to fetch and other cancellable calls to keep a timed-out attempt from repeating side effects alongside the retry.

Step retry delays use jittered exponential backoff. A delay from RetryableError (retryAfterMs) or RateLimitError (retryAfter, in seconds) replaces the computed backoff.

Manual workflows

Use events.manual({ inputSchema }) to declare user-supplied inputs. The SDK infers event.input from a j.object(...) schema builder and validates it with Ajv. Schemas support primitive values, nested objects, and arrays. Every key is required; use .nullable() to allow null. Objects reject unknown keys automatically, so you do not need to specify additionalProperties.

See the manual workflow example for the supported schema subset and a complete typed workflow.

Workflow outputs

Use outputSchema to declare the object a workflow returns. It takes the same j.object(...) schema builder as manual inputs. The handler's return type is inferred from the schema, and the SDK validates the returned value before the run succeeds. The output must be at most 256 KB as JSON. A workflow without outputSchema returns nothing.

See the workflow output example.

Invoking workflows

A workflow can start another workflow in the same app with context.workflows.start(workflow, input). Pass the imported workflow; it must have a manual trigger, and its input is typed from that trigger's inputSchema. It resolves with { runId } once the run is created and does not wait for it to finish.

Starting is durable: retries and replays reuse the run already started. Call it directly from the handler, where it is its own step, or inside context.step(), where it is part of that step. Pass { key } to start the same workflow more than once.

See the workflow invocation example.

Notion API access

Use context.notion for Notion API calls. Deployed apps receive Notion API credentials automatically, so do not configure or push NOTION_API_TOKEN. Local execution needs the token in .env before making a Notion API request.

For a resource declared with Notion as Code, add a named runtime binding with access: { handbook: access.view(handbook) }. Cloud deployment resolves the declaration's NaC resourceId to a live ID and exposes it to the handler as context.access.handbook.id. Deployment also reconciles the declared access through the existing Notion-module permissions. Those permissions, not the bindings, authorize context.notion calls.

Resources granted to the workflow in Notion's UI do not need an access entry, a NaC declaration, or a NaC resourceId. Pass the live record ID directly to the appropriate context.notion call, for example context.notion.pages.retrieve({ page_id: "page-id" }); the call remains subject to the existing workflow permissions. UI grants do not create context.access aliases. Pages, databases, data sources, and custom agents are supported by named bindings. See the workflow skill for access levels and a complete example.

Data-source syncs

Declare a database with Notion as Code, then pass one of its data-source handles to sync:

import { database, sync } from "@notionhq/apps";
import { Builder } from "@notionhq/apps/builder";

const issues = database("issues-db", {
	dataSourceResourceId: "issues-source",
	name: "GitHub Issues",
	schema: {
		Name: { resourceId: "name", type: "title" },
		"GitHub ID": { resourceId: "github-id", type: "text" },
	},
});

export default sync({
	dataSource: issues.dataSource,
	primaryKey: "GitHub ID",
	handler: async () => ({
		changes: [
			{
				type: "upsert",
				key: "123",
				properties: { Name: Builder.title("Fix the bug") },
			},
		],
	}),
});

Put each default-exported sync directly in src/syncs/. A primary key must be a title or text property. Each upsert must include at least one known non-primary-key schema property, while any other non-primary-key properties may be omitted; unknown property keys are rejected by TypeScript. Do not include the primary-key property in an upsert: the SDK fills it from the change's key.

Notion-as-Code databases

Use schema and an explicit dataSourceResourceId to declare a database with one data source:

import { notion } from "@notionhq/apps/notion-as-code";

const singleSourceIssues = notion.database("branch-check-issues", {
	dataSourceResourceId: "branch-check-issues-source",
	name: "Issues",
	schema: {
		Name: { resourceId: "issue-title", type: "title" },
		ID: { resourceId: "issue-id", type: "text" },
	},
});

Pass singleSourceIssues.dataSource as the dataSource argument to sync from @notionhq/apps, or use singleSourceIssues.dataSource.addPage(...) to declare a page in the source. Both database forms expose addView.

For multiple sources, use datasources instead of the top-level schema:

const issues = notion.database("issues-db", {
	datasources: {
		Issues: {
			resourceId: "issues-source",
			schema: {
				Name: { resourceId: "issue-title", type: "title" },
				"GitHub ID": { resourceId: "github-id", type: "text" },
			},
		},
	},
});

issues.datasources.Issues.schema.Name;

schema and datasources are mutually exclusive. A database declaration must include at least one data source or at least one view. A linked-only database may omit datasources, or use datasources: {}, only when views is nonempty; {}, { datasources: {} }, and { datasources: {}, views: [] } are invalid. Page and teamspace child helpers accept the same forms through page.addDatabase("database-id", args) and teamspace.addDatabase("database-id", args).

Each datasources key is the source's name, and each schema key is the property's name used for typed access, sync keys, and serialized properties. Nested data-source configs and property configs do not accept name. The top-level database name remains supported; in the single-source shorthand it supplies both the database and data-source display names. In a multi-source declaration, it names the database while each data-source name comes from its key.

All database, source, and property IDs remain explicit and must be globally unique. No IDs are generated from keys or names. The build emits the existing dataSources and properties arrays with exact IDs and names from those keys; authoring schema, datasources, and database-level dataSourceResourceId fields do not leak into the serialized database intent. The provisioning JSON envelope and server API are unchanged. See the build guide for complete single-, multi-source, and linked-only examples.

Declare database views with views or addView, or use the top-level view function from @notionhq/apps (notion.view is also available). The standalone function accepts ViewArgs: a ViewSchema plus the owning databaseResourceId, and returns a handle with resourceId. Both types are exported from @notionhq/apps/notion-as-code, with table, board, calendar, list, gallery, feed, and timeline variants. View types include sorting, nested filters, grouping, property visibility, and layout-specific options.

import { view } from "@notionhq/apps";

view({
	databaseResourceId: issues.resourceId,
	resourceId: "issues-table",
	type: "table",
	dataSourceResourceId: issues.datasources.Issues.resourceId,
	properties: [{ property: "issue-title", visible: true }],
	sorts: [{ propertyId: "issue-title", direction: "ascending" }],
});

Each view requires a dataSourceResourceId. Property references use property resource IDs, not display names. Calendar views require calendarBy; timeline views require timelineBy.

Custom blocks

Declare browser blocks in src/customBlocks/<key>.ts:

import { customBlock } from "@notionhq/apps";

export default customBlock({
	path: "./blocks/hello",
	slashCommand: "hello",
	dataSources: {},
});

path is relative to the project root. Keep frontend project code out of src/ to avoid hitting upload caps. notion-apps build-blocks and notion-apps upload-blocks are used for local builds.

See the build and deployment guide for provisioning artifacts, cloud builds with server coordination, and local builds with CLI coordination.

Local / Boxy end to end testing

To build and deploy the app template against a running local Notion server:

# Installed ntn and the notion-cookbook template from main
scripts/smoketest.sh

# CLI binary and template from sibling checkouts
CLI=local TEMPLATE=local scripts/smoketest.sh

Publishing New Versions

Publishing is initiated only from the GitHub Actions UI:

  1. Open the Publish workflow.
  2. Select Run workflow on the main branch.

The workflow always increments the patch version. It builds, checks, and tests the SDK before committing the new version, creating a v* GitHub release, and publishing @notionhq/apps to npm.

Configured workflow events

Use events from @notionhq/apps (or @notionhq/apps/events) inside the workflow's triggers array. The old triggers helper and /triggers export have been removed; replace helper imports and calls, but keep the triggers configuration key. Connection-scoped callbacks now use triggers: ({ events }) => [...].

import { database, events, workflow } from "@notionhq/apps";

const tasks = database("tasks-db", {
	dataSourceResourceId: "tasks-source",
	name: "Tasks",
	schema: {
		Name: { resourceId: "task-name", type: "title" },
		Status: { resourceId: "task-status", type: "select", options: [] },
	},
});

export default workflow({
	name: "Task maintenance",
	description: "Processes tasks on a schedule and when they change",
	triggers: [
		events.scheduled({
			frequency: "week",
			interval: 1,
			weekdays: { MO: true, WE: true, FR: true },
			start: "2026-09-23T09:00:00",
			timeZone: "America/Los_Angeles",
		}),
		events.notionPageCreated({ dataSource: tasks.dataSource }),
		events.notionPageUpdated({
			dataSource: tasks.dataSource,
			properties: [tasks.dataSource.properties.Status],
		}),
	],
	handler: async (event, context) => {
		await context.step("Log event", () => console.log(event.type));
	},
});

Property handles autocomplete from the declared schema; use handles, not property-name strings. To watch page content without properties, pass includePageContent: true. Page events watch rows in the selected data source, not arbitrary individual pages. Scheduled events accept hourly through yearly recurrence settings.

Deploy configured events with ntn apps deploy after the backend support is deployed. Deployment updates the workflow draft's configured trigger set while preserving triggers added in Notion. Open the workflow in Notion and save it to activate the schedule or page watch; redeployments also require a save to apply changes. Watched databases receive read access; add access declarations when the workflow needs more access. Zero-argument helpers such as events.scheduled() leave configuration to Notion instead.