Skip to content

Notion import runbook

ops/planner/import_notion.py is a one-off, owner-run script (planner slice 2) that copies the old Notion task list into Jarvis's tasks once. Every page whose status is still open becomes one task (source: notion_import, source_ref: the Notion page id, state: open); a date becomes a soft due date, a project is matched by name against the existing projects. Pages already imported are skipped, so running it twice creates nothing new.

It is a single stdlib file (like seed_projects.py), so it runs on the box with no repo checkout. It only reads Notion; it never changes it.

Prerequisites

  • The planner schema (modules/planner/schema/pb_migrations/4_tasks.js) is deployed.
  • Projects are seeded (ops/planner/seed_projects.py) or created in the app, so Notion project names have something to match.

1. Create the Notion integration

  1. Open https://www.notion.so/profile/integrations, New integration, type Internal, workspace: the one holding the old list.
  2. Capabilities: Read content only (no update, no insert, no user information).
  3. Copy the Internal Integration Secret. It is the NOTION_TOKEN; it never goes into the repo, a chat or an issue.

2. Share the database with it

Open the database as a full page, ⋯ › Connections › Connect to, pick the integration. If the project property is a relation, share the related database too. Otherwise those tasks import with no project and the dry run counts them under unmapped_projects as (relation not shared).

The database id is the 32 hex characters at the end of its URL, before ?v=. --database accepts the id or the whole URL.

3. Copy the script and store the token

ssh mailserver 'mkdir -p /root/notion-import'
scp ops/planner/import_notion.py mailserver:/root/notion-import/

# on the box, in an interactive shell (the token never reaches shell history)
ssh -t mailserver
cd /root/notion-import
( umask 077; read -rs -p 'Notion token: ' t; printf 'NOTION_TOKEN=%s\n' "$t" > notion.env ); echo

4. Inspect the database

python3 import_notion.py --database <DB_ID> --notion-env notion.env --inspect

Prints each property's name and type, and the option count and labels of select/status properties (pick --open-values from them). It does not touch PocketBase. Pick:

Flag Property type Default
--title-prop title the database's title property
--status-prop status, select or checkbox (checked = done) none: every page is open
--open-values the status labels that mean open Not started,In progress
--project-prop select, multi_select (first value) or relation (related page title) none
--due-prop date none
--notes-prop rich_text none

Status and project names match case- and whitespace-insensitively.

5. Dry run

python3 import_notion.py --database <DB_ID> --notion-env notion.env --env-file /opt/jarvis/.env \
  --status-prop Status --project-prop Project --due-prop Due --notes-prop Notes

No writes. Prints counts only, never a page title:

{"pages": 120, "open": 41, "planned": 41, "imported": 0, "skipped_existing": 0,
 "unmapped_projects": 3, "per_project": {"Site refonte": 12, "(none)": 29}, "failed": 0}

unmapped_projects counts Notion project names with no Jarvis project; those tasks import with no project ((none)). Add --show-unmapped to list the names, then create the projects you want in the app and dry-run again.

6. Apply

python3 import_notion.py <same flags as the dry run> --apply

The undo file (notion-import-undo-<timestamp>.jsonl, or --undo-file PATH) is created before the first task, never overwritten, and gets one created id per line. Its path is printed on stderr. Copy it off the box:

scp mailserver:/root/notion-import/notion-import-undo-*.jsonl .

A non-zero failed exits 1; re-run the same command, the imported pages are skipped.

7. Undo

python3 import_notion.py --undo notion-import-undo-<timestamp>.jsonl --env-file /opt/jarvis/.env

tasks rows cannot be deleted by the app user, so undo signs in as the PocketBase superuser (SUPERUSER_EMAIL / SUPERUSER_PASSWORD). It deletes only the listed ids that are still Notion imports and prints {"deleted": n, "skipped": n, "failed": n}. Ids already gone are skipped, so running undo twice is harmless. A line whose id is not a PocketBase record id (15 lowercase letters or digits) counts as failed and is never sent. Tasks you already finished in the app are deleted too. Notion is untouched, so a later --apply imports again.

8. Clean up

shred -u /root/notion-import/notion.env

Then delete the integration in Notion (Integrations › ⋯ › Delete), which revokes the token. Keep the undo file until you are happy with the list.