SuperAPI AI Directory

notion-report

Office & productivity

View repository

Updated 10-15 19:58

About this skill

/notion-report — write results back to Notion

/notion-plan the reverse of: that one isfrom Notion reads requirements in, this one isto Notion writes results out.

Two write paths

Path When to use Cost Prerequisite
API(preferred) Has Notion integration token Doesn't open a browser, doesn't inject snapshots, barely consumes context Requires creating your own internal integration,and that page must be added to Connections
browser(fallback) No token, or no permission to create an integration Requires opening a browser, slower and consumes some context Reuses /notion-plan created by ~/.playwright-cli/notion-profile login session

Automatic path selection:NOTION_TOKEN if it has a value, use API; otherwise use browser. --via api|browser can force it.

Company-type workspaces often require administrator approval to create an integration, so the browser path is not a second-class option, but the only feasible path for many people. Both paths share Steps 1–3 completely (define audience, compose draft, dry-run), only the write action in Step 4 differs.

/notion-plan The reason it absolutely must open a browser is that it needs toread Notion's client-side rendered UI (see global rule webfetch-blocklist.md). The reason this skill's API path can avoid opening a browser is thatwriting does not require seeing the UI— the conclusion "Notion requires a browser" only holds for reading.

Setup

Using the API: requires one-time integration setup (optional)

The Notion API requires an internal integration token,which must be created by the user themselves, Claude cannot do it for them.

Permission threshold (confirmed against official docs on 2026-08-27): Only a workspace owner can create an internal integration. Notion officially states: "Only Workspace owners will be able to access the Connections tab and create integrations." Regular members cannot even get into the Connections tab.

Adding a page to Connections requires Full accesson that page — Can edit is not enough. Official explanation: when adding a connection, the page selector "only displays pages or databases to which a user has full access".

On Enterprise plans, an owner can additionally restrict which connections members can install, or set it to "Connection owners only" so members cannot attach pages themselves at all.

Conclusion: most people don't have this permission, so the browser path is the normal path. When unsure whether you are a workspace owner, don't waste time trying — just use the browser.

  1. Go to https://www.notion.so/my-integrations → New integration → select workspace → create
  2. Copy the Internal Integration Secret (starts withntn_ )
  3. Save and lock permissions:
mkdir -p ~/.config/notion && chmod 700 ~/.config/notion
# 貼上 token 後 Ctrl-D(不要用 echo,避免進入 shell history)
cat > ~/.config/notion/token && chmod 600 ~/.config/notion/token
  1. Share the target page with the integration(the step most often missed): In the top right of the Notion page, ··· → Connections → add that integration. If you skip this step, the API will return object_not_found, which looks like a wrong URL but is actually permissions not being enabled.

Load (before every run):

export NOTION_TOKEN="$(tr -d '[:space:]' < ~/.config/notion/token)"

Red line: never output the token value. Do not echo it, do not write it into reports, do not put it in command arguments (arguments go into shell history and the process list). Always use environment variables.

Go via browser: reuse the existing login, usually no configuration needed

/notion-plan has already created a persistent profile ~/.playwright-cli/notion-profile. As long as that session is still valid you can write directly, no token needed, no admin approval needed.

When the session expires (Step 0 will detect it), log in again once:

playwright-cli -s=notion close
playwright-cli -s=notion open "https://www.notion.com/login" \
  --profile ~/.playwright-cli/notion-profile --headed
# 使用者手動完成登入後,profile 會保存,之後不必再登

Step 0: Pre-checks and path selection

API=~/.claude/skills/notion-report/scripts/notion_api.py
BROWSER=~/.claude/skills/notion-report/scripts/notion_browser.py
export NOTION_TOKEN="$(tr -d '[:space:]' < ~/.config/notion/token 2>/dev/null)"

NOTION_TOKEN non-empty → use API; empty → use browser. If the user specifies with --via , that takes precedence.

API path pre-checks

python3 "$API" probe "<notion_url>"

Expected output: page title,archived: False, existing block count.

Symptom Meaning Handling
NOTION_TOKEN 未設定 No integration created Switch to the browser path, do not get stuck
unauthorized Token invalid/revoked Ask the user to regenerate it, or switch to browser
object_not_found Page not shared with the integration(it is not a wrong URL) Ask the user to do step 4 of the setup, or switch to browser
archived: True Page is in the trash Stop and confirm with the user whether the wrong page was found

Browser path pre-checks

playwright-cli -s=notion close        # 清掉可能殘留的 session
playwright-cli -s=notion open "<notion_url>" --profile ~/.playwright-cli/notion-profile

# 等 Notion 的 CSR 內容真的畫出來,不要用固定 sleep
playwright-cli -s=notion eval '() => new Promise((res, rej) => { const t = Date.now(); const c = () => { if (document.querySelector(".notion-page-content") || document.querySelector("[data-content-editable-root=\"true\"]")) return res("loaded"); if (Date.now() - t > 20000) return rej("timeout"); setTimeout(c, 500); }; c(); })'

playwright-cli -s=notion eval "$(python3 "$BROWSER" probe-js)"

probe-js returns JSON; read each field:

Field Expected If it does not match
ok true See the fields below
loginRedirect false true = session expired; log in again per "Go via browser" above
editables > 0 0 = No edit permission for that page, stop and inform the user
blocks Reasonable number 0 is most likely not fully loaded; rerun the waiting step
title / tailText Matches the expected page Mismatch = wrong page opened, stop

editables: 0 must be treated as a hard stop condition—that means the account only has view permission, forcing a write will not succeed, but may leave half-finished operations on the page.

Step 1: Determine the recipients

**This step cannot be skipped.** The same work written for different people should retain completely different content.

If the user has specified with --to , use it directly.If not specified, always ask with AskUserQuestion , do not guess on your own— guessing the recipient from the conversation context is easy to get wrong (for the same fix, you may think it is for engineering records, but it is actually to give the PM a progress update), and things written into a shared page are hard to retract. Ask with the question below, do not rewrite the options yourself each time:

AskUserQuestion({ questions: [{
  question: "這份要寫給誰看?決定內容深度與用詞。",
  header: "收件對象",
  multiSelect: true,
  options: [
    { label: "PM / 專案經理",
      description: "結論、對時程的影響、風險、需要他裁決的事項、待辦歸屬。不寫技術根因與指令。" },
    { label: "設計師",
      description: "視覺/互動有什麼變化、before-after、哪些點需要設計決策、走查連結。不寫後端架構與監控數字。" },
    { label: "營運 / 客服",
      description: "使用者可感知的變化、對外怎麼說、要不要公告、客訴應對。不寫內部代號、PR 編號、技術術語。" },
    { label: "工程團隊",
      description: "根因、變更內容、驗證證據、回滾方式、後續技術債。細節照留。" },
  ],
}] })

multiSelect: true is intentional—one ticket often needs to be reported to both the PM and design at the same time. When multiple recipients are selected, each recipient forms its own independent section(### 給 PM、### 給設計), and each applies the rules for its own row;do not merge them into one shared piece of text, that would make everyone read halfway through content irrelevant to them, and the discipline of "deliberately not writing" would fail along with it.

Likewise,--mode defaults to appendwhen unspecified, no need to ask; only switch to comment。

--to Audience What they want Deliberately not writing
pm Project manager / PM Conclusion, impact on schedule, risks,items needing their decision, to-do ownership Technical root cause, commands, stack details
design Designer What changed visually/interactively, before-after,which points need design decisions, review link Backend architecture, monitoring numbers
ops Operations / Customer service User-perceivable changes,how to phrase it externally, whether to make an announcement, customer complaint response script, scope and timing of impact Internal codenames, PR numbers, technical jargon
eng Engineering team Root cause, change content, verification method and evidence,rollback method, follow-up technical debt — (keep the details)

Writing rules (shared across all audiences):

  • Traditional Chinese (Taiwan usage); keep technical terms in their original English, don't force a translation
  • Conclusion first: state the result in the first paragraph, don't bury the conclusion behind the narrative
  • For pm / ops / design: when technical details are too complex, give a plain-language analogy first, then expand
  • External documents must not use labels like "plain-language version" or "human version"
  • Items requiring action from the other party should be their own paragraph, specifyingwho, and what to do, don't mix them into the narrative

Step 2: Drafting

Write it to a Markdown scratch file (don't hand-code Notion block JSON directly, it's error-prone):

mkdir -p .verification/$(date +%F)/notion-report
DRAFT=.verification/$(date +%F)/notion-report/draft.md

Supported Markdown subset:##/### headings,- items,1. numbering,> callout、 ``` code block、--- divider, inline **粗體**/`code`/[文字](url)。

Tables are not supported. Notion's table block structure is complex and easy to break; to present comparison data, use bullet lists instead (- 指標:before → after)。

Suggested skeleton (adjust depth by audience):

Single audience:

## <YYYY-MM-DD> <一句話標題>(給 <對象>)

**結論**:<一句話講完結果>

### 影響
- <對他而言重要的變化>

### 需要你確認 / 待辦
- <對象>:<具體動作>

### 證據
- [<連結名>](<url>)

---
> 由 Claude Code 於 <時間> 寫入 · 來源:<PR / report 路徑>

Multiple audiences (multi-select in Step 1):Share one conclusion, then separate sections for each, don't mix content for different audiences in the same paragraph.

## <YYYY-MM-DD> <一句話標題>

**結論**:<一句話講完結果,這句所有人都該看>

### 給 PM
- <時程影響 / 待裁決事項>

### 給設計
- <視覺互動變化 / 待設計決策點>

---
> 由 Claude Code 於 <時間> 寫入 · 來源:<PR / report 路徑>

Self-check before writing(every item must pass):

  • no secrets at all (API key / token / password / JWT / internal IP)
  • no other people's personal data or email addresses
  • For ops version: no internal codenames, PR numbers, service names
  • all numbers have a source, no values guessed from memory
  • links actually open

Step 3: dry-run review (mandatory gate)

Notion pages are usually shared by the team; writes are immediately visible to colleagues and hard to retract, so regardless of whether the user authorized it, always preview before submitting.

cat "$DRAFT"                         # 給使用者看實際文字(兩條路徑都要)
python3 "$API" render "$DRAFT"       # 僅 API 路徑:印出 block JSON 與數量,不送出

Show the draft to the user for review, and explicitly state "this will append to the end of <頁面標題> "—— Use the page title obtained in Step 0 probe / probe-js , do not use the URL alias. Proceed to Step 4 only after the user says it's OK. When carrying --dry-run , stop here and do not perform the write.

Step 4: Write

A. API path

mode=append(default)——append a block at the end of the page, leaving existing content completely untouched:

python3 "$API" append "<notion_url>" "$DRAFT"

mode=comment——post as a page comment, least invasive but only supports plain text:

python3 "$API" comment "<notion_url>" "$DRAFT"

comment mode sends Markdown markup as-is (Notion comments do not parse Markdown), so for the comment draft, write it as a plain-text paragraph containing no ##/- .

B. browser path

Reuse the session already opened in Step 0 (do not reopen; reopening returns to the top of the page):

playwright-cli -s=notion eval "$(python3 "$BROWSER" insert-js "$DRAFT")"

Interpreting the returned JSON:

  • FAIL: ... → handle according to the message; it is usually because the page hasn't finished loading or there is no edit permission
  • dispatched: true → the event was dispatched.This does not mean the write succeeded, nor does it mean it failed.
  • blocksBefore → note this number down; Step 5 will use it to compare against the block count after writing
  • preventDefaultCalled → A pure factual record, not a success signal; do not use it to make a judgment

Test record (first end-to-end execution on 2026-08-27):preventDefaultCalled: false but the content actuallywas written completely successfully(block count 8 → 50, all type conversions correct). Notion's paste handler does not go through preventDefault, so the return value of dispatchEvent has no discriminating power for this editor.The only authoritative success determination is the read-back verification in Step 5.

If it had been treated as a failure signal and retried at the time, two copies would have been written——this is exactly why red line #7 "on write failure, do not retry by default; verify the actual state first" exists.

Why use synthetic paste instead of typing character by character: Notion's paste handler parses the text/plain Markdown directly into the corresponding blocks (it recognizes headings, lists, and code blocks). Typing character by character, on the other hand, would rely on live Markdown shortcuts, and when encountering list line breaks or leaving a code block, extra Escape/Backspace would need to be sent, making the state machine prone to going off track. paste is one-shot and atomic, and our self-built DataTransfer, does not touch the system clipboard and does not need clipboard permission。

browser pathdoes not support mode=comment(Notion's comment interface requires multi-step interaction, and it is not worth maintaining a fragile UI flow for it). When the user specifies comment and only browser is available, explain the limitation and ask whether they want to switch to append.

After writing, give Notion time to dispatch:

playwright-cli -s=notion eval '() => new Promise(r => setTimeout(() => r("waited"), 2500))'

Step 5: Read-back verification (cannot be omitted)

After writing, you cannot claim completion based only on the API returning 200 or the paste event being accepted——follow the existing discipline:write-then-verify。

API path:

python3 "$API" readback "<notion_url>" 15

browser path (pick a sufficiently unique string from the draft as the needle, for example the heading line):

playwright-cli -s=notion eval "$(python3 "$BROWSER" verify-js "<草稿標題那一行>")"

This step is the only success determination for the browser path; none of the return values from Step 4 count. All three must hit to pass:

  1. found: true
  2. occurrences: 1——greater than 1 means it was dispatched twice (commonly seen after a misjudged failure and retry); inform the user and ask them to manually delete the duplicate copy; do not operate the page yourself to delete it
  3. block count compared with Step 4's blocksBefore has clearly increased

Want to double-check that the formatting was parsed correctly (rather than the whole thing turning into a blob of plain text); count the block type distribution:

playwright-cli -s=notion eval '() => { const c = document.querySelector(".notion-page-content") || document.querySelector("[data-content-editable-root=\"true\"]"); const counts = {}; [...c.querySelectorAll("[data-block-id]")].forEach(b => { const cl = [...b.classList].find(x => x.startsWith("notion-") && x.endsWith("-block")) || "other"; counts[cl] = (counts[cl] || 0) + 1; }); return JSON.stringify(counts); }'

Seeing notion-header-block / notion-bulleted_list-block / notion-divider-block and other types means Markdown was parsed; if it's almost all notion-text-block, that means it was pasted in as plain text.

Confirm that the last few blocks are exactly the content just written, in the correct order, with no duplicate submissions. Save the readback output into .verification/<date>/notion-report/readback.txt as evidence.

When reporting back to the user, include: the page title, the number of blocks written, and the page URL.

Failure handling

Situation Handling
append fails midway (when sending in batches) Do not rerun the whole thing, first readback to see how much has been written, and only fill in the rest
Wrong content written The API cannot restore the whole thing; tell the user they need to manually delete that block in Notion, and include the title of what to delete
The page is very long so readback cannot see it Normal,readback only prints the last n; use probe to compare changes in the total block count
validation_error mentions rich_text length A single paragraph exceeds 2000 characters, and the script will automatically split it; if it still fails, that means a single block's content is too long, so split it into multiple paragraphs
(browser) Step 4 looks like it failed First do Step 5 to confirm the actual state before saying anything—it has been tested in practice that "the signal says failure, but the write actually succeeded." Blind retries are the most likely cause of duplicate writes on this path
(browser)editables: 0 The account only has view permission for that page. Stop, do not try to work around it
(browser)loginRedirect: true The profile session has expired; log in again once according to the "go through browser" section
(browser) The content went in but the formatting is off Notion's paste parsing differs from Markdown.Do not manually fix it on the page, revise the draft and ask the user to delete the old one and rewrite it once
(browser) The browser was closed midway Step 5 verify confirms how much was written;occurrences >1 means duplicates, ask the user to delete manually

Red lines

  1. Never output token values, always use NOTION_TOKEN environment variables.
  2. Never write secrets or personal datato Notion—that is a shared team space and will be indexed.
  3. Do not delete or overwrite existing content. This skill only does append and comment; to modify existing blocks, handle it manually, and do not use the API or browser to modify what others wrote on their behalf.
  4. Writes to shared pages are treated as external actions, and must always be reviewed via Step 3 before sending.
  5. If the ownership of the target page is unclear, ask the user first; do not decide on your own based on the URL whether it can be written to.
  6. The browser path only does the few evals listed in Step 0/4/5; do not improvise operations on the page. That is the user's own login session, and every action performed on it remains in the edit history under their name.
  7. By default, do not retry when a write fails. First verify the actual state—duplicate writes are harder to clean up than no writes.