---
title: Notion board (work a Notion database as a board)
---

# Notion (in development)

## What it is

> **Status:** experimental / in-development. Hidden by default. Turn it on in
> **Settings → Lab → "Notion board"** (or set `AMC_SHOW_NOTION_BOARD=1` in dev).

### What it is

A first-party Omniscio integration that brings a Notion database into the app as a
kanban board, so you can work pages without opening the Notion website. It
surfaces as a **"Notion" row in the projects sidebar** that opens a full-pane
board view.

It is gated exactly like the Linear board / Jira board / Pull Requests tab —
through the unreleased-feature registry (`notion-board`), **not** a plain feature
flag — so it stays invisible until a developer flips its status to `shipped`.

It is a near 1:1 port of the **Linear integration**, adapted from Linear's
GraphQL endpoint to Notion's REST API (`https://api.notion.com/v1`).

## Where to find it

Once revealed, a **Notion** row appears in the projects sidebar and opens the board as a full pane. The reveal switch is **Settings → Lab → Notion board**.

## How it behaves

### Board model mapping

- A Notion **database** is the **board** (picked from a dropdown — populated by
  `POST /search` — which, on Notion's `2025-09-03` API, returns the data sources
  the integration can see, mapped back to their databases).
- On Notion's newer API (`2025-09-03`) a database can hold **multiple data
  sources** (e.g. a CRM with Contacts + Companies + Deals). When it does, a second
  dropdown appears so you pick which source to render as the board; a database with
  a single source shows no extra picker and behaves exactly as before.
- The database's first **status** property (or, failing that, its first
  **select** property) is the **grouping property**; its **options** are the
  **columns**.
- Each **row (page)** in the database is a **card**.
- **Move a card** = `PATCH /pages/{id}` setting the grouping property to the
  target column's option name.
- **Create a card** = `POST /pages` into the database (title + optional option).
- **Edit** = `PATCH /pages/{id}` (title and/or option).
- **Page detail** = `GET /pages/{id}` (properties) + `GET /blocks/{id}/children`
  (page body, rendered as **structured blocks** — paragraphs, headings, lists,
  to-dos, toggles, quotes, callouts, code, dividers, tables, columns, bookmarks,
  equations, and images — with bold/italic/link/mention formatting). Nested
  content (toggles, columns, tables, sub-lists) loads on demand as you expand or
  scroll. Any block type Omniscio doesn't render yet shows a small "open in Notion"
  placeholder rather than disappearing.

- **Colors** — Notion's named color palette (~20 colors: 9 text + 9 background +
  default) is mapped to Tailwind classes in `notion-colors.ts`. Rich-text
  annotations carry per-span text/background colors; blocks (paragraph, heading,
  quote, callout) carry a block-level color. Select/multi-select option pills
  render in their Notion-assigned color. All colors have `dark:` variants. Colors
  are content/data-viz (raw Tailwind), not status/severity (`status-*` tokens) —
  baselined in `no-raw-status-color-baseline.json`.

Status group names map to three UI buckets: **To-do → todo**, **In progress →
in-progress**, **Complete → done** (a plain `select` property has no group, so
its options are **unknown**, like the Linear "Other" fallback).

### How to use it

1. **Enable it:** Settings → Lab → toggle **Notion board** on. A "Notion" row
   appears in the sidebar.
2. **Connect (in the panel):** open the **Notion** sidebar row and click **Set up
   Notion** (or the header **gear**, which carries a status dot). In the drawer,
   paste an **internal integration token** — create one at **notion.so → Settings →
   Connections → Develop or manage integrations → New integration**, then **share
   the pages/databases** with that integration (Notion only exposes what's shared).
   The token starts `ntn_…` or `secret_…`.
3. Click **Test connection** (`GET /users/me`). A green dot + "Connected as …"
   confirms it works.
4. Open the **Notion** sidebar row. A **Databases / Pages** toggle (top-left)
   switches between your database boards and your standalone pages/notes:
   - **Databases** — pick a **database** from the dropdown; a database is your
     "board". If it has more than one **data source**, a second dropdown appears
     to pick which one to show. (Databases with a status/select column render as a
     kanban board — that's why a task database looks like a to-do list.)
   - **Pages** — a browse of your standalone pages/notes (pages that aren't rows
     in a database). Filter/search them, then click one to open it **full-page**
     (like Notion) with the full editor; a **← All pages** button takes you back
     to the list. **+ New note** creates one and drops you straight into typing.
     (Notes are created inside an existing page, since Notion's API can't create a
     page at the workspace root.) Only pages **shared with your integration**
     appear here.
   - See your **columns** (the chosen status/select property's options) with
     page **cards** (title, current option, multi-select label chips).
   - **Board / Table / Gallery / List** views: the board groups the loaded page
     into columns; table, gallery, and list all show the same server-queried rows
     (a table, a card grid, or a compact list). Filters + sort carry across all of
     them. (Calendar and timeline views are planned.)
   - **Filter + sort**: build property-aware filters (e.g. Status is Doing,
     Priority is High, Due on-or-after a date) combined with Match all / Match any,
     and sort by any property — all computed by Notion (correct across every page,
     not just the first).
   - **Search** (the header search icon): search across every page and database
     shared with your integration — filter by Pages/Databases, and click a result
     to open a database as a board or a page in the drawer. (Only content shared
     with the integration is searchable; an empty search browses everything shared.)
   - Click a card to open the **page drawer**: properties + the page body
     rendered as structured blocks, with inline move + rename.
   - **Edit properties inline**: title, text, number, select, status,
     multi-select, date, checkbox, URL, email, and phone fields are all editable
     right in the drawer (changes save to Notion immediately). People, relations,
     formulas, rollups, files, and timestamps show read-only.
   - **Edit the page body** (simple pages): if the page is plain text (paragraphs,
     headings, lists, quotes, code — no tables/columns/toggles/images/mentions),
     an **Edit** button appears; edit inline and **Save** writes back to Notion.
     Richer pages stay read-only with a note to open them in Notion. Reordering
     existing blocks isn't supported yet (Notion has no move API) — text edits,
     new blocks, and deletions all save.
   - **Comments**: the drawer shows each page's comment threads; reply to a thread
     or add a new comment (Ctrl+Enter to send). Resolving comments isn't supported
     yet (Notion's API doesn't expose it).
   - **Duplicate / Add subpage / Delete**: from a page's drawer, duplicate it
     (copies its properties + supported body; a note appears if nested or
     unsupported content couldn't be copied), add a subpage, or delete it (a
     reversible Notion archive, behind a confirm). Moving a page to a different
     database, and page templates, are planned.
   - Drag a card between columns to move it (sets the page's grouping option).
   - **+ New page** creates a row in the database.
   - **Auto-refresh**: while the board is open and the window is focused, Omniscio
     quietly keeps the open page and view in sync with Notion (pausing when the
     window is hidden). Toggle it at Settings → Lab → "Auto-refresh Notion".

### Security & privacy

- The token is **encrypted at rest** (`ENCRYPTED_APP_SETTINGS_KEYS`, `enc:`
  prefix) and **stripped from every IPC/CLI read** (`CLI_SETTINGS_SENSITIVE_KEYS`)
  — it never crosses IPC back to the renderer.
- Every request is asserted through a fixed-host SSRF guard
  (`assertSafeNotionOrigin`): **https only**, host must equal `api.notion.com`.
  No Notion fetch bypasses the single `notionRequest` chokepoint.
- **No external images by default** — people properties render as names; avatars /
  page cover images are deliberately not loaded (CSP + privacy), mirroring the
  Linear integration's no-avatar rule. Image blocks in a page body are opt-in:
  **Settings → Lab → "Show images in Notion pages"** (off by default; while off an
  image shows a placeholder with a link). Callout file/upload icons are never
  loaded — only emoji icons.
- Errors are **humanized at the client source** (`humanizeNotionError`) — raw
  HTTP status / Notion error blobs go to the file log, never to the user.

### Inbox source ("notion-inbox")

When **Settings → Features → "Show recently-edited Notion pages in the Inbox"**
is on (and the board feature is on and a token is saved), a background poller
lists the integration's pages newest-edited-first (`POST /search` sorted by
`last_edited_time`) and wholesale-replaces a `notion_inbox_snapshots` cache. The
unified Inbox derives one self-clearing amber row per recently-edited page. An
edit advances the page's timestamp and re-fires the inbox push. Read-only; it
never spawns a session. Env kill switch: `AMC_DISABLE_NOTION_INBOX_POLL=1`.

Clicking an inbox row's **Go to Notion** opens the board in Omniscio (not the browser).
Surfacing more inbox triggers (comments that mention you, pages assigned to you,
watched-database schema changes) is planned but limited by what Notion's API can
query cheaply.

## For agents

### CLI routes

All routes live on the CLI control server (`127.0.0.1:19519`, bearer auth). Every
route is gated: feature-off → 404, no credentials → 401. Mutations add a shared
rate-limit cap (429). Errors from Notion are humanized (502, never a raw HTTP
blob).

| Method | Path | Body | Notes |
|--------|------|------|-------|
| `GET` | `/notion/search?query=&type=&cursor=` | — | Search workspace (pages and/or databases) |
| `GET` | `/notion/databases` | — | List all databases shared with the integration |
| `POST` | `/notion/databases/:id/query` | `{ filter?, sorts?, page_size?, start_cursor? }` | Query a database via data-source resolution |
| `GET` | `/notion/pages/:id` | — | Get page detail (properties + metadata) |
| `POST` | `/notion/pages` | `{ database_id, properties, children? }` | Create a page in a database (mutation, rate-limited) |
| `PATCH` | `/notion/pages/:id` | `{ properties }` | Update page properties (mutation, rate-limited) |
| `POST` | `/notion/comments` | `{ page_id?, discussion_id?, text }` | Add a comment (mutation, rate-limited) |
| `POST` | `/notion/move-to-column` | `{ pageId, databaseId, dataSourceId?, optionName }` | Move a card between columns (mutation, rate-limited) |

All POST/PATCH bodies are Zod-validated; invalid payloads return 400 with a
flattened issue list. The `databases/:id/query` route resolves the database ID to
a data-source ID automatically (Notion's `2025-09-03` data-source model).

### Workflow engine

### Action nodes

Three action nodes auto-register via `import.meta.glob('./nodes/*-node.ts')`:

| Node type | What it does | Config fields |
|-----------|-------------|---------------|
| `action.notion_update_page` | Update properties of a Notion page | `credentialId`, `pageId`, `properties` (JSON object) |
| `action.notion_search` | Search for pages or databases | `credentialId`, `query`, `type?` (page/database) |
| `action.notion_comment` | Add a comment to a page | `credentialId`, `pageId`, `text` |

Each resolves credentials via `resolveWorkflowCredential(credentialId,
'notion-token', workflowRunId)` → calls the corresponding port method →
delegates to the Notion client function. The existing `action.notion_create_page`
(from the automation engine) remains separate.

### Trigger node

`trigger.notion_page` — starts a workflow when a page in a watched database is
created, updated, or has a specific property changed.

| Config field | Values |
|-------------|--------|
| `databaseId` | The Notion database ID to watch |
| `event` | `created` · `updated` · `property_changed` |
| `watchProperty` | (only for `property_changed`) the property name to monitor |

### Trigger watcher

`notion-page-trigger.ts` — a 2-minute polling watcher (same pattern as
`jira-issue-trigger.ts`).

- Polls each active, approved workflow that has a `trigger.notion_page` node.
- Queries the database via data-source resolution, diffs page timestamps against
  an in-memory per-workflow snapshot.
- **First tick baselines only** (no fires) — prevents a storm of events on
  startup.
- Delta detection: `created` = new page ID within a 5-minute lookback;
  `updated` = advancing `last_edited_time`; `property_changed` = watched
  property value differs from snapshot.
- Duplicate fires absorbed by `startWorkflowRun`'s `clientRequestId` dedup.
- Snapshot capped at 1000 entries per workflow (oldest pruned).
- Kill switch: `AMC_DISABLE_NOTION_WORKFLOW_TRIGGER=1`.
- Registered in the service registry as `notion-page-trigger` (pausable,
  recurring).

## Related

- [mission-control.md](mission-control.md) — Omniscio's own built-in project management, for boards you want here rather than in Notion.
- [monday-cloud.md](monday-cloud.md) — the other first-party board integration, pointed at a hosted service.

