@pan-enso/nctl
v0.4.2
Published
Headless Notion CLI for Markdown-driven page and database operations.
Downloads
1,051
Readme
nctl
A headless Notion CLI from Japan
nctl is a CLI for writing to Notion from CI/CD pipelines, cron jobs, and shell scripts — without OAuth, without a browser, without interactive login.
It uses only integration tokens, accepts Markdown files as input, and is safe to run headlessly at any time.
$ nctl page upsert --md report.md
✓ updated abc123 sha256:e3b0c44...Table of Contents
Install
# npm
npm install -g @pan-enso/nctl
# bun
bun add -g @pan-enso/nctlRequires Bun at runtime.
Authentication
nctl resolves your Notion integration token in this order:
| Method | Example |
|--------|---------|
| Environment variable (default) | NOTION_API_KEY=ntn_xxx nctl ... |
| Google Cloud Secret Manager | nctl --token-from gcloud:NOTION_TOKEN ... |
| Any shell command | nctl --token-from "command:vault read secret/notion" ... |
[!TIP] For CI/cron environments, the
gcloud:method keeps tokens out of environment files entirely.
[!WARNING]
command:executes an arbitrary shell command on the host. Use it only with trusted commands and avoid embedding secrets in shell history or process lists.
Security note: The command: token source passes the provided string directly to bash -lc, enabling arbitrary shell execution. Only use trusted commands here.
# Simplest: env var
NOTION_API_KEY=ntn_xxx nctl db query DB_ID
# Google Cloud Secret Manager
nctl --token-from gcloud:NOTION_TOKEN page upsert --md report.md
# Custom command (any secret manager)
nctl --token-from "command:gcloud secrets versions access latest --secret=NOTION_TOKEN" \
page upsert --md report.mdCommands
nctl page upsert
Create or update a Notion page from a Markdown file. Uses content hashing — if nothing changed, the page is not touched (no-op).
nctl page upsert --md report.md
nctl page upsert --md report.md --dry-run # preview without writingThe Markdown file must have a YAML frontmatter block:
---
parent_page_id: PAGE_ID # for pages under another page
# or:
database_id: DB_ID # for database entries
title: Weekly Report
properties:
Status: Done
Tags:
- engineering
- weekly
---
# Heading
Body content goes here.Output:
✓ created abc123 sha256:e3b0c44...
✓ updated abc123 sha256:e3b0c44...
- noop abc123 sha256:e3b0c44... ← content unchanged, skippednctl db query
Query a Notion database and output results in various formats.
# Human-readable table
nctl db query DB_ID --format table
# JSON Lines (for piping)
nctl db query DB_ID --format jsonl --limit 10
# Page IDs only
nctl db query DB_ID --format ids
# With filter (Notion filter JSON)
nctl db query DB_ID \
--filter '{"property":"Status","status":{"equals":"Done"}}' \
--format table| Flag | Description |
|------|-------------|
| --format | table | jsonl | ids | json |
| --filter | Notion filter object (JSON string) |
| --limit | Max results (default: all) |
nctl block marker-replace
Replace a named section inside an existing Notion page — without touching anything outside the markers. Safe to run repeatedly (idempotent).
Step 1. Add markers to your Notion page (as code blocks or paragraph blocks containing):
<!-- nctl:marker:morning-patrol -->
... this section will be replaced ...
<!-- /nctl:marker:morning-patrol -->Step 2. Run:
nctl block marker-replace PAGE_ID --marker morning-patrol --md update.md
nctl block marker-replace PAGE_ID --marker morning-patrol --md update.md --dry-runnctl api
Low-level escape hatch for direct Notion REST API calls.
# GET
nctl api get /users/me
# POST with body
nctl api post /search --body '{"query":"Roadmap"}'
# PATCH
nctl api patch /blocks/BLOCK_ID \
--body '{"paragraph":{"rich_text":[{"text":{"content":"Updated"}}]}}'Real-world Examples
GitHub Actions: publish a daily report to Notion
name: Daily Report
on:
schedule:
- cron: "0 9 * * *"
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: npm install -g @pan-enso/nctl
- run: nctl page upsert --md reports/daily.md
env:
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}cron + Google Cloud Secret Manager
# /etc/cron.d/notion-sync
0 6 * * * user nctl --token-from gcloud:NOTION_TOKEN page upsert --md /opt/reports/morning.mdScripting with db query
# Get all Done items and process them
nctl db query "$DB_ID" --format jsonl --filter '{"property":"Status","status":{"equals":"Done"}}' \
| jq -r '.id' \
| while read id; do
echo "Processing $id..."
doneMarkdown Format
nctl parses standard Markdown with YAML frontmatter.
Supported frontmatter fields:
| Field | Type | Description |
|-------|------|-------------|
| title | string | Page title (required) |
| parent_page_id | string | Parent page UUID |
| database_id | string | Target database UUID |
| properties | object | Database properties (key: value) |
Supported block types:
| Markdown | Notion Block |
|----------|-------------|
| # H1 / ## H2 / ### H3 | Heading 1/2/3 |
| Paragraph text | Paragraph |
| > quote | Quote |
| - item | Bulleted list item |
| 1. item | Ordered list item (not supported) |
| ```lang ``` | Code block |
| --- | Divider |
Exit Codes
| Code | Name | Meaning |
|------|------|---------|
| 0 | success | OK |
| 1 | api-error | Notion API returned an error |
| 2 | auth-error | Token missing or invalid |
| 3 | validation-error | Bad input (missing frontmatter, invalid args) |
| 4 | not-found | Page or block not found |
Contributing
Issues and PRs are welcome at github.com/pan-enso/nctl.
Please open an issue before submitting large changes.
Author
Built by @AgentGymLeader · pan-enso
Inspired by @sakasegawa/ncli.
License
MIT
