Prerequisites
- Node.js 22 or later.
- The
ntnCLI. See installation. - A workspace enabled for the custom blocks alpha.
1
Update the ntn CLI
ntn versions lack support for custom blocks. Run ntn --version to confirm version 0.21.2 or later.2
Log in
ntn to a workspace and stores credentials in your OS keychain. Run ntn whoami to confirm.3
Scaffold a new Worker
custom template is the smallest Worker template containing a custom block and its required dependencies, and it works without any data source setup.Other starting points are available with --template:4
Understand the two parts
Custom block code has two parts, each with its own SDK. At deploy time, The built-in templates use React.
ntn reads the Worker declaration to build and publish the block. The frontend runs in the browser whenever someone views the Notion page.src/index.ts is the Worker, written with @notionhq/workers. It declares the block, which includes information about building its frontend code and which data sources it expects:src/index.ts
blocks/custom/src/index.tsx is the block’s frontend code, written with @notionhq/custom-blocks. Wrap UI code in NotionCustomBlock, which performs the handshake with the Notion host, and NotionTokenScope, which makes Notion’s design tokens available so the block matches the surrounding page:blocks/custom/src/index.tsx
NotionCustomBlock automatically resizes React blocks to fit their content, but any frontend works. For a framework-neutral frontend, initCustomBlock() runs the same handshake and resolves with the block’s context. See initialization and appearance and sizing.5
Preview locally (optional)
Before you deploy, run the block against a local mock of the Notion host:The dev shell runs on
localhost. For blocks that declare data sources, connect sample sources and map their properties before the block renders.See previewing before deploy for usage instructions, including how to test against sample data.6
Deploy the Worker
A custom block ships with its Worker, so there is no separate custom block deploy step:This builds the block’s source using the declared
command, uploads the contents of output, and publishes it with the Worker.7
Share the Worker (optional)
Share with workspace members from the Developer portal. With at least Can connect access, members can find the block’s registered slash command in the
/ menu and insert it on any page they can edit. See sharing workers for more.8
Insert a custom block into a page
On desktop or web, type
/custom on a page you can edit and choose your block.If the block declares data sources, Notion asks you to bind each one to a real data source before it renders. The custom template declares none, so it renders immediately.Add Notion data
A block never names a data source. It declares the shape of the data it needs, and whoever inserts the block binds that declaration to something real — which is what keeps a block reusable across workspaces. Binding a block to Notion data explains the full model. In the Worker code, declare data sources on the capability:src/index.ts
App component with this component. Keep the imports, root creation, and provider wrappers from that example.
issues the next time the block is configured — until it is bound, the block will not render, so declare only what the block genuinely needs.
Add a block to an existing Worker
You do not need a fresh project. A custom block is a Worker capability, so it can live in a Worker you already have alongside its syncs, tools, and webhooks. Add the custom blocks SDK to the existingpackage.json in the Worker’s root directory. Run this command from that directory:
src/index.ts with worker.customBlock, pointing path at that directory. If your assets are already built and there is no build step to run, declare a static source instead. See the Worker capability reference for every option.
Next steps
Read Notion data
Query rows and update them from the block.
Write to Notion
Create, update, and archive pages from the block.
Handle errors
Understand the error contract and what is retryable.
Worker capability
Every option on
worker.customBlock().