Move a Notion workspace into AI Matrx
You need exactly two connectors in this AI, both added by clicking Connect and signing in:
AI Matrx (tools how_to, tables, pages, agents) and Notion (Notion's own connector).
No keys, files, scripts or commands — ever. Never ask the person for any.
First call: how_to with {"topic": "notion"}. It is the live copy of this recipe. If it and
this file ever disagree, follow how_to.
0. Before you start
- Call
tables {"action": "list_tables"}. If the AI Matrx tool is missing, tell the person in one
sentence: "Please connect AI Matrx in this app's connector settings, then tell me." Same for Notion
(use Notion's search tool to check). Then wait.
- Never ask which organization: AI Matrx already knows where this connection files new work
(
create_table answers its organization_id). Only if an answer says it needs one, ask once,
offering the organizations it listed, and pass organization_id on every call after.
list_tables and pages list show everything the person can see, across all their
organizations — many tables may share a name like "Clients". So after you create something,
always refer to it by the id in your ledger, never by its name.
- A call that fails with a connection error, "connection was closed", or HTTP 401/502/503 is
safe to send again unchanged — every call is idempotent. Retry up to 3 times.
- The person is not technical. Plain words. Ask exactly ONE question — the plan — then work
without asking. One short progress line after each database or top-level page.
1. Inventory (the ledger)
With Notion's search and fetch tools, list everything and keep a ledger as you go:
- every database (data source): its name, every property with its type, its row count, its
views (board/calendar/gallery/timeline + group/date field), its templates;
- every page that is not a database row: title, parent page, icon, cover;
- every database row's page: whether it has body content;
- comments (on pages and rows).
Starting again in a new conversation? Rebuild the ledger without listing anything: send each
create_table again exactly as before — it answers created: false with the same table id and
its organization_id. Then pages {"action": "list", "key_prefix": "notion:", "in_organization": "<that organization_id>", "limit": 200} gives external_key → space_id for
THIS organization (the same Notion workspace imported elsewhere has the same keys), and tables
{"action": "list_rows", "table": "<id>", "limit": 200} gives each row's "Notion ID" → row id.
(To look for a table: tables {"action": "list_tables", "search": "<words in its name>", "organization": "<organization_id>"}.)
As you create things, add the AI Matrx ids to the ledger: Notion database id → table id,
Notion page id → space_id, Notion row id → row id. Every key you send is "notion:<id>" for
pages/comments and the raw Notion page id in the "Notion ID" column for rows.
2. Show the plan, ask once
One line per database and one for pages, e.g.:
Posts (54) → table: Status (board), Platform (several choices), Publish date (calendar),
links to Campaign and Client, each post's page attached. Pages: 31 pages and sub-pages, 5 comments.
Ask: "Shall I move it all over?" Then do every phase below without asking again.
3. Phases — in this order
3a. Databases → tables. For each database, tables:
{"action": "create_table", "name": "<database name>", "columns": [
{"name": "<title property>", "type": "text"},
{"name": "Notion ID", "type": "text", "unique": true},
{"name": "Status", "type": "status", "choices": ["Idea", "Drafting", "Published"]},
{"name": "Platform", "type": "choices", "choices": ["Instagram", "TikTok"]},
{"name": "Publish date", "type": "date"}, {"name": "Publish date end", "type": "date"},
{"name": "Budget", "type": "currency"},
{"name": "Assets", "type": "file", "many": true}]}
Property map: title → text (first column) · rich_text → text / long_text · number → number,
currency or percent by its format · select → choice · multi_select → choices · status → status
· date → date (a range adds "<name> end", also date) · checkbox · url · email · phone_number →
phone · files → file ("many": true) · people → text (names) · created/edited time → date ·
unique_id → text. Always give choices for choice/choices/status. (A currency column reads back
as number with unit: "USD" — that is it.) Sent again, create_table answers created: false
with the same id; its differs list just names columns you added later — not an error.
Then, once EVERY table exists, add the computed and linking columns, one per call, tables
{"action": "add_column", "table": "<table id>", "columns": [ … ]} (sent again it answers
unchanged: true). Always use the other table's id in to:
- relation →
{"name": "Client", "type": "relation", "to": "<other table name or id>", "many": true}
(a two-way Notion relation appears on both databases — make each side once, as Notion shows it);
- rollup (count/sum/avg/min/max) →
{"name": "Total billed", "type": "rollup", "via": "Invoices", "of": "Amount", "agg": "sum"};
a "count of <relation>" rollup → "agg": "count" with of = the linked table's first (title)
column; a rollup that filters (e.g. only open ones) → count without the filter and say so;
- lookup →
{"type": "lookup", "via": "<relation column>", "of": "<far column>"};
- formula that is plain arithmetic →
{"type": "formula", "formula": "{Price} * {Quantity}"};
any other formula or rollup → a column of its result type holding its current values (say so in
the final report).
3b. Rows. tables {"action": "upsert_rows", "table": "<id>", "key_column": "Notion ID", "add_choices": true, "rows": [{"Notion ID": "<page id>", "Name": "…", "Status": "Idea", "Platform": ["Instagram"], "Publish date": "2026-10-07", "Publish date end": "2026-10-14"}]} —
at most 100 rows per call. Dates YYYY-MM-DD. Keep each answer's row id in the ledger.
3c. Links and files (second pass, same call, only the changed columns):
- relation cell →
{"match": "Notion ID", "keys": ["<linked page id>", …]};
- file cell →
[{"url": "<Notion file url>", "name": "brief.pdf"}] — send a Notion file url right
after reading it (they expire within the hour); AI Matrx keeps its own copy. On a rerun, skip
file cells that already hold files (read the row first) — re-sending re-saves the same file.
The answer's notices list anything left out (a link to a page you did not move) — keep them for
the report.
3d. Row pages. For each row whose Notion page has body content, pages:
{"action": "upsert", "key": "notion:<row page id>", "title": "<row title>", "row_id": "<AI Matrx row id>", "snapshot": {"v": 1, "blocks": [ … ]}}.
3e. Pages and sub-pages, parents first. pages:
{"action": "upsert", "key": "notion:<page id>", "title": "…", "parent_key": "notion:<parent page id>", "icon": {"icon": "BookOpen"}, "cover": {"url": "<cover url>"}, "snapshot": {"v": 1, "blocks": [ … ]}}
- top-level pages: leave out
parent_key;
- a sub-page is TWO things:
parent_key on the child AND a page block (props.spaceId = the
child's space_id) in the parent's body where Notion showed it. First run: send the parent
without its page blocks, make the children, then send the parent once more with them. If the
ledger already has the children's space_ids (a rerun), send the parent ONCE with its full body
— that answer is content_changed: false when nothing changed;
- a database that sits inside a page → a
database block (props.source {"kind": "table", "tableId": "<table id>"}, props.inline true for inline, false for full page);
- icons: the closest Lucide icon name in PascalCase for a Notion emoji — 📘📖 BookOpen · ✅ BadgeCheck
· ☑️📋 ListChecks · 💡 Lightbulb · ℹ️ Info · ⚠️ TriangleAlert · 📌 Pin · 📅 Calendar · 🏠 House ·
👥 Users · 💰 Wallet · 🎯 Target · 🚀 Rocket · 📝 NotebookPen · 📁 Folder · ⭐ Star · 🔗 Link;
anything else → the closest common Lucide name, or leave it out. An uploaded icon →
{"url": "<its url>"}; covers → {"url": "<url>"}; a gradient or none → leave it out.
- the answer:
space_id, created, content_changed, version (a new page starts at 2), and
placement — placed (put under its parent), row_body (attached to the row), unchanged.
3f. Comments. On a page: pages {"action": "comment", "space_id": "…", "key": "notion:<comment id>", "block_id": "<the block it was on, if any>", "body": "**Maya Okonkwo** · 2026-10-03\n\nThe comment text"}. On a database row: tables {"action": "add_comments", "table": "<id>", "key_column": "Notion ID", "comments": [{"key": "<row page id>", "body": "…", "author": "Maya Okonkwo", "at": "2026-10-03"}]}.
add_comments answers counts (added, unchanged), not ids.
3g. Views and templates. For each board/calendar/gallery/timeline (a plain table view with
no filter or sort needs nothing — every table has one): tables
{"action": "create_view", "table": "<id>", "name": "By status", "layout": "board", "group_by": "Status"}
(calendar: date_column, end_date_column; gallery: cover_column; timeline: date_column,
end_date_column, group_by). For each database template: tables
{"action": "create_row_template", "table": "<id>", "name": "New Instagram post", "values": {"Status": "Idea", "Platform": ["Instagram"]}}.
4. Page blocks (the snapshot)
{"v": 1, "blocks": [block, …]}. A block: {"id": "b1", "type": "…", "text": [spans], "props": {…}, "children": [blocks]}. id is unique within the page and stable — number them in
order (b1, b2, … ; children b5a, b5b …) so a rerun produces the same ids.
A span: {"text": "…"} plus any of bold, italic, underline, strike, code (true),
link (URL), color / background (default gray brown orange yellow green blue purple pink red).
| Notion | type | props / children |
|---|
| Paragraph | text | children = indented blocks |
| Heading 1/2/3 (toggle heading) | heading | level 1-3; toggleable: true + children = body |
| Bulleted / numbered list | bulleted / numbered | children = nested items |
| To-do | todo | checked true/false |
| Toggle | toggle | text = summary, children = body |
| Quote | quote | |
| Callout | callout | icon (Lucide, PascalCase), block background a color (Notion's yellow_bg → yellow); children = body |
| Divider | divider | no text |
| Code | code | text = the code, language |
| Equation | equation | expression (KaTeX) |
| Child page | page | spaceId |
| Link to page | linkToPage | spaceId |
| Simple table | table | headerRow, headerColumn (always send both), rows [{"cells": [[spans], …]}] |
| Table of contents | tableOfContents | |
| Columns | columnList | children = column blocks with width fractions summing to 1 |
| Image / video / audio / file / PDF | image video audio file pdf | url (AI Matrx keeps the file), caption spans |
| Web bookmark / embed | bookmark / embed | url |
| Inline or full-page database | database | source, inline |
| Anything else (synced block, button, …) | text | text says what it was; unsupported: {"from": "notion", "kind": "<Notion type>", "source": "<its text or url>"} |
Never invent a type. If pages answers refused naming a block, fix that block as the message says
and send the page again.
5. Check and report
- Each table:
tables {"action": "aggregate", "table": "<id>", "measure": "count"} against the
Notion row count. Spot-check links, roll-ups and files with tables {"action": "get_row", "table": "<id>", "row_id": "<id>"} or {"action": "list_rows", "table": "<id>"}.
- Pages:
pages {"action": "list", "key_prefix": "notion:", "in_organization": "<organization_id>", "limit": 200} (follow next_offset): every space_id in your ledger must be listed.
- Name the organization in your final message ("filed under <organization name>"); the
list_tables answer with organization set names it.
- Tell the person a short list: one line per database ("✓ Posts — 54 of 54"), one for pages
("✓ 31 pages, 5 comments"), then "Could not carry over:" with each item and why in plain words
(unsupported blocks, template page text, files whose links had expired, people kept as names,
complex formulas kept as values). Then where to look: their pages at
https://www.aimatrx.com/spaces and their tables at https://www.aimatrx.com/data.
Running again, stopping, fixing
- Everything is matched by Notion id: running any phase again changes only what changed — rows
report "same", unchanged pages make no new version, comments with the same key land once.
- Stopped partway (big workspace, lost connection)? Run the same phases again from the top.
- A page went wrong?
pages {"action": "archive", "key": "notion:<page id>"} (its sub-pages go
with it; "restore" brings it back), then upsert it again. Nothing is ever deleted.
- Work one top-level page or database at a time in a big workspace.
When it's done, offer the next step: turning their work into AI Matrx agents
(how_to topic "agents").