> ## Documentation Index
> Fetch the complete documentation index at: https://developers.notion.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom blocks quickstart

> Scaffold a custom block, preview it locally, and deploy it into a Notion page.

export const developerWorkersUrl = "https://www.notion.so/developers/workers";

<Warning>
  Custom blocks are in private alpha:

  * Do not discuss publicly or share with anyone outside your workspace.
  * Access must be enabled per workspace. Installing the SDK does not grant access.
  * These APIs may experience breaking changes without notice.
</Warning>

This guide takes you from nothing to a custom block rendering in a Notion page, then adds Notion data to it.

## Prerequisites

* Node.js 22 or later.
* The `ntn` CLI. See [installation](/cli/get-started/installation).
* A workspace enabled for the custom blocks alpha.

<Steps>
  <Step title="Update the ntn CLI">
    ```zsh theme={null}
    ntn update
    ```

    Older `ntn` versions lack support for custom blocks. Run `ntn --version` to confirm version `0.21.2` or later.
  </Step>

  <Step title="Log in">
    ```zsh theme={null}
    ntn login
    ```

    This connects `ntn` to a workspace and stores credentials in your OS keychain. Run `ntn whoami` to confirm.
  </Step>

  <Step title="Scaffold a new Worker">
    ```zsh theme={null}
    ntn workers new my-example-worker --template custom
    cd my-example-worker
    npm install
    ```

    The `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`:

    | Template | What it renders |
    | - | - |
    | `custom` | Minimal, with no data sources |
    | `habit-tracker` | A month-at-a-glance habit tracker with streaks |
    | `org-chart` | A tree org chart from a People data source |
    | `whiteboard` | A collaborative whiteboard canvas with sticky notes and arrows |
  </Step>

  <Step title="Understand the two parts">
    Custom block code has two parts, each with its own SDK. At deploy time, `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:

    ```typescript src/index.ts theme={null}
    import { Worker } from "@notionhq/workers"

    const worker = new Worker()
    export default worker

    // The block key. Any stable name you choose. The template uses "custom".
    worker.customBlock("custom", {
    	// Where to find the block's frontend source code. This can be any arbitrary
    	// path within the Worker's directory.
    	path: "./blocks/custom",

    	// How to build the source code.
    	command: "npx vite build",

    	// Where to output the built files.
    	output: "dist",

    	// Data source schemas expected by the block's source code. These can be
    	// bound to arbitrary Notion data sources.
    	dataSources: {},
    })
    ```

    `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](/custom-blocks/guides/lifecycle), and `NotionTokenScope`, which makes Notion's design tokens available so the block matches the surrounding page:

    ```tsx blocks/custom/src/index.tsx theme={null}
    import "@notionhq/custom-blocks/nds.css"
    import {
    	NotionCustomBlock,
    	NotionTokenScope,
    } from "@notionhq/custom-blocks/react"
    import ReactDOM from "react-dom/client"

    function App() {
    	return <h1>Hello world</h1>
    }

    const root = document.getElementById("root")
    if (!root) throw new Error("Missing #root element")

    ReactDOM.createRoot(root).render(
    	<NotionCustomBlock>
    		<NotionTokenScope>
    			<App />
    		</NotionTokenScope>
    	</NotionCustomBlock>,
    )
    ```

    The built-in templates use React. `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](/custom-blocks/sdk/initialization) and [appearance and sizing](/custom-blocks/sdk/appearance).
  </Step>

  <Step title="Preview locally (optional)">
    Before you deploy, run the block against a local mock of the Notion host:

    ```zsh theme={null}
    ntn workers customblocks dev
    ```

    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](/custom-blocks/guides/preview) for usage instructions, including how to test against sample data.
  </Step>

  <Step title="Deploy the Worker">
    A custom block ships with its Worker, so there is no separate custom block deploy step:

    ```zsh theme={null}
    ntn workers deploy
    ```

    This builds the block's source using the declared `command`, uploads the contents of `output`, and publishes it with the Worker.
  </Step>

  <Step title="Share the Worker (optional)">
    Share with workspace members from the <a href={developerWorkersUrl}>Developer portal</a>. 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](/workers/guides/sharing-workers) for more.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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](/custom-blocks/guides/data-sources) explains the full model.

In the Worker code, declare data sources on the capability:

```typescript src/index.ts theme={null}
import { Worker } from "@notionhq/workers"

const worker = new Worker()
export default worker

worker.customBlock("custom", {
	path: "./blocks/custom",
	command: "npx vite build",
	output: "dist",
	dataSources: {
		issues: {
			name: "Issues",
			description: "The team's issues",
			properties: {
				title: { name: "Title", type: "title" },
				status: { name: "Status", type: "status" },
			},
		},
	},
})
```

Replace the earlier `App` component with this component. Keep the imports, root creation, and provider wrappers from that example.

```tsx theme={null}
import { useDataSource } from "@notionhq/custom-blocks/react"

function App() {
	const { items, isLoading, hasMore, error } = useDataSource("issues", {
		limit: 100,
		filter: { key: "title", title: { is_not_empty: true } },
		sorts: [{ key: "title", direction: "ascending" }],
	})

	if (error) return <p role="alert">Could not load issues: {error.message}</p>
	if (isLoading) return <p>Loading issues…</p>
	if (items.length === 0) return <p>No matching issues.</p>

	return (
		<div>
			<ul>
				{items.map((item) => {
					const title = item.propertiesByKey.title
					return <li key={item.id}>{typeof title === "string" ? title : "Untitled"}</li>
				})}
			</ul>
			{hasMore && <p>More issues match this query. These results are incomplete.</p>}
		</div>
	)
}
```

The keys you declare in the Worker are the keys the frontend queries by, so keep them in sync. Deploy again and Notion will ask you to bind `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 existing `package.json` in the Worker's root directory. Run this command from that directory:

```zsh theme={null}
npm install @notionhq/custom-blocks
```

Then register the block in `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](/custom-blocks/workers/capability) for every option.

## Next steps

<CardGroup cols={2}>
  <Card title="Read Notion data" icon="database" href="/custom-blocks/sdk/data-sources">
    Query rows and update them from the block.
  </Card>

  <Card title="Write to Notion" icon="pen" href="/custom-blocks/sdk/pages">
    Create, update, and archive pages from the block.
  </Card>

  <Card title="Handle errors" icon="triangle-exclamation" href="/custom-blocks/sdk/errors">
    Understand the error contract and what is retryable.
  </Card>

  <Card title="Worker capability" icon="gear" href="/custom-blocks/workers/capability">
    Every option on `worker.customBlock()`.
  </Card>
</CardGroup>
