Launched:self-serve migrations intoSuperhuman Docs (Coda)
Try it now
01Agent-first
Runs where you already work
Plug it into Claude, ChatGPT or Cursor. Describe the move in plain English; the agent runs it.
02Engineer-led
Our production engine, unlocked
The pipeline our engineers use on managed enterprise migrations — the same code, now something you can drive yourself.
03Pricing
Try 10 pages free, then $1 a page
Credit-based, pay-as-you-go. No scoping call, no quote — sample it on your own docs before you spend anything.
04Sources
NotionSlabConfluenceSoonGoogle DocsSoon
Skip to content

Notion to Slack Canvas Migration: Database Limits & Block Mapping

Notion databases can't fit in Slack Canvas's 300-cell table. A technical guide to block mapping, API rate limits, image re-upload, and migration architecture.

Roopendra Talekar Roopendra Talekar · · 18 min read
Notion to Slack Canvas Migration: Database Limits & Block Mapping
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Notion to Slack Canvas Migration: Database Limits & Block Mapping

As we covered in our Slack Canvas vs Notion architecture breakdown, Slack Canvas is a flat markdown document with no schema, no formulas, and a 300-cell table cap. Notion is a block-based workspace with typed relational databases, rollups, synced blocks, and deeply nested page trees. If your Notion workspace is built around databases, most of that data cannot land in a canvas. You need a second destination — a spreadsheet, Airtable, Slack Lists, or a purpose-built database tool — and the canvas becomes a narrative layer that links out to wherever the structured data actually lives.

There is no native Notion-to-Canvas migration path. Slack publishes an official Quip-to-Canvas document conversion guide, but its Notion-related public docs cover Workflow Builder connectors and notifications — not bulk document import. Notion-to-Canvas is a custom migration problem, not a product feature.

This guide covers the database question first (because it decides the entire project shape), then walks through block-type mapping, extraction mechanics on the Notion API side, write constraints on the Slack Canvas side, and the end-to-end pipeline architecture.

Why Notion databases cannot land in a Slack Canvas

A Notion database is a typed collection of pages with a property schema: text, number, select, multi-select, date, person, relation, rollup, formula, files, URL, email, phone, checkbox, and created/last-edited timestamps. Every row is itself a page that can contain arbitrary nested block content.

A Slack Canvas table is a static markdown grid capped at 300 total cells — any combination of rows and columns that multiplies to 300 or fewer. There are no formulas, no cell-level formatting beyond basic markdown, no column types, no filtering, no sorting, and no relations.

Notion database feature Slack Canvas table equivalent Result
Properties (typed columns) Plain-text columns Type information lost
Relations (cross-database links) None Cannot be represented
Rollups None Cannot be represented
Formulas None Cannot be represented
Views (filtered/sorted) None Single static table only
50-column × 200-row database 10,000 cells needed Exceeds 300-cell cap
Row page content No nested content in cells Content lost or flattened

A Notion database with 10 columns and 30 rows already uses all 300 cells. A 15-column database can hold exactly 20 rows. A standard project tracker with 15 columns and 25 rows needs 375 cells — the Canvas API will reject or truncate it. Most real-world Notion databases — sprint boards, CRM tables, project trackers — blow past 300 cells trivially.

Warning

The 300-cell cap is the hard boundary. If your database has more than 300 cells (rows × columns), you cannot represent it as a canvas table at all. Even if it fits, you lose every relation, rollup, formula, and view. Evaluate every Notion database independently before committing to this migration.

What to do with databases that don't fit

The honest answer: move them somewhere else and link from the canvas. Options include:

  • Google Sheets or Excel Online — export the Notion database as CSV, import into a sheet, link the sheet URL from the canvas.
  • Airtable — use the Notion API to read properties and the Airtable API to write records. Relations can be partially preserved.
  • Slack Lists — Slack's Lists feature supports typed columns including text, number, select, date, user, channel, checkbox, attachment, and link. Lists are available on paid plans and are a better fit than canvas tables for simple operational tables. The Lists API uses POST /api/lists.create with a schema definition; records are written via lists.items.create. Row limits are not publicly documented but Lists are not designed for relation-heavy or formula-heavy schemas.
  • A standalone database tool — if the database actually behaves like a CRM, backlog, asset register, or request queue, it belongs in a system built for that job.

The canvas that replaces the original Notion page becomes a narrative document with links to those external destinations. This is a different project shape than a one-to-one migration, and it needs to be scoped that way.

Do not engineer around the 300-cell limit by splitting large databases into multiple Canvas tables. The result is unreadable and impossible to maintain.

Tip

Pre-migration audit. Before writing a single line of code, use the Notion API to count all database objects versus standard page objects in your workspace. If databases make up more than 20% of your total objects, a direct Slack Canvas migration requires a secondary destination for that data.

Integration permissions: the migration staller nobody documents

Before writing any extraction code, configure your Notion integration with the correct capability scopes. Migrations stall at this step constantly.

Your integration needs at minimum:

  • Read content — required to retrieve page blocks and database rows
  • Read user information without email addresses — required to resolve person property values and @mention annotations

These are set in the integration settings at notion.so/my-integrations. Capabilities alone are not sufficient. The integration must also be explicitly shared with every page and database you want to extract, either by adding it to each page individually or by configuring it as a workspace-level integration on Business or Enterprise plans. If the integration is not shared with a page, the API returns a 404 — not a 403 — which makes permission gaps easy to misdiagnose as deleted pages.

During Phase 1 (audit), verify access by calling GET /v1/pages/{page_id} for a sample of pages across different sections of the workspace. Any 404 response is a permission miss, not a missing page.

Workspace enumeration: search vs. database query

Phase 1 requires enumerating all pages and databases in the workspace. The standard method is POST /v1/search, which returns all pages and databases the integration has access to, paginated via start_cursor and page_size (maximum 100 per response). Results are sorted by last-edited time descending by default.

Key limitations of the search endpoint:

  • It returns only objects explicitly shared with the integration. Unshared pages are invisible, not errored.
  • filter.value can be "page" or "database" — run two separate calls to get clean counts of each type.
  • Archived pages (archived: true) and trashed pages (in_trash: true) are included in results by default. Filter these out during audit. Migrating archived or trashed content is a common mistake that inflates scope estimates and produces orphaned canvases.
import requests
 
def enumerate_workspace(token: str, object_type: str) -> list[dict]:
    """Return all non-archived, non-trashed objects of a given type."""
    headers = {
        "Authorization": f"Bearer {token}",
        "Notion-Version": "2022-06-28",
    }
    results = []
    payload = {
        "filter": {"value": object_type, "property": "object"},
        "page_size": 100,
    }
    while True:
        resp = requests.post(
            "https://api.notion.com/v1/search",
            headers=headers,
            json=payload,
        )
        resp.raise_for_status()
        data = resp.json()
        for obj in data["results"]:
            if not obj.get("archived") and not obj.get("in_trash"):
                results.append(obj)
        if not data.get("has_more"):
            break
        payload["start_cursor"] = data["next_cursor"]
    return results

Run this once for "page" and once for "database" to get your full workspace inventory before any extraction begins.

Which Notion block types have no Canvas equivalent?

Slack Canvas supports a specific subset of markdown: headings h1–h3, bold, italic, strikethrough, bulleted and ordered lists, checklists, code blocks and code spans, quotes, callouts, dividers, column layout, links, markdown tables, mentions, emojis, and several unfurl types. That covers runbooks, project briefs, onboarding pages, and meeting notes well.

Several Notion block types have no canvas representation at all:

Notion block type Canvas support Migration strategy
Toggle list ❌ None Flatten to heading + indented content
Synced block ❌ None Resolve to source content, write once
Child database (inline) ❌ None Export separately, link from canvas
Equation (KaTeX) ❌ None Render as image or write raw LaTeX in code span
Embed (iframe) ❌ None Convert to link; unfurl if Slack supports the domain
Video block ❌ None Upload to Slack via files_upload_v2, link from canvas
Audio block ❌ None Upload to Slack, link from canvas
PDF block ❌ None Upload to Slack, link from canvas
Bookmark ⚠️ Partial Write as a plain link; Slack may unfurl it
Table of contents ❌ None Omit (canvas has no anchor links)
Breadcrumb ❌ None Omit
Link to page ⚠️ Partial Replace with link to the new canvas
Column list (layout) ✅ Supported Canvas supports column layout via markdown

How to handle toggles

Notion toggle blocks are collapsible containers. Canvas has no collapsible element. Two options:

  1. Flatten: Write the toggle's heading as a bold line or an h3, then write its children as indented content below it. The reader loses the ability to collapse.
  2. Omit children: Write only the toggle heading with a note that expanded content was removed. Useful when toggles held supplementary detail.

Neither is great. If your workspace relies heavily on toggles for information density, canvas is a poor target.

How to handle synced blocks

Notion's API represents synced blocks with a synced_block type: the original block carries its children directly, while copies reference the original via synced_from.block_id. Canvas has no sync concept. The migration script must resolve every synced block reference back to the original's content and write it inline. If the same synced block appears on 50 Notion pages, it becomes 50 isolated, disconnected blocks of text in Slack. Future edits in one place will not propagate.

An alternative approach: replace secondary copies with a link back to one authoritative canvas, preserving the "single source of truth" intent if not the automatic sync behavior.

How to handle equations

Equation blocks in Notion contain a KaTeX-compatible expression string. Canvas cannot render math. Your options:

  • Write the raw expression in a code span: `e=mc^2`. Readable for simple expressions, unreadable for complex ones.
  • Pre-render each equation to a PNG or SVG, upload to Slack, and embed the image. Correct but labor-intensive for workspaces with many equations.

How to handle embeds

Notion embeds can point to websites or uploaded files, and Notion's own docs note that embed behavior in the app and the API is not identical. Canvas cannot render live iframes. Extract the source URL from the Notion embed object and write it as a standard hyperlink. Slack may auto-unfurl supported domains (YouTube, Figma, Google Docs), which partially preserves the experience — but arbitrary embedded app behavior is lost.

Block-by-block mapping reference

This table maps every common Notion block type to its Slack Canvas markdown output:

Notion block type Canvas markdown syntax Notes
paragraph Plain text Direct
heading_1 # Heading ✅
heading_2 ## Heading ✅
heading_3 ### Heading ✅
bulleted_list_item - item Nesting supported
numbered_list_item 1. item Nesting supported
to_do - [ ] task / - [x] task Maps to checklist
code ```language ... ``` Language tag preserved
quote > text ✅
callout > emoji text Canvas has callout support; map icon to emoji
divider --- ✅
image Upload → file unfurl link Must re-upload; see file URL section
table Markdown pipe table Only if ≤ 300 cells
toggle Flatten to heading + content Collapse behavior lost
synced_block Resolve to inline content Sync behavior lost
equation Code span or image Math rendering lost
embed Plain URL or Slack unfurl Embed behavior lost
child_database Link to external destination Data cannot live in canvas
bookmark [title](url) Slack may auto-unfurl
link_to_page Link to new canvas URL Requires ID mapping
Warning

Rich text concatenation. Notion API rich text elements cap at 2,000 characters per object. A long paragraph gets split into multiple rich text objects within the same block. Your migration script must concatenate the plain_text values of the rich_text array before emitting markdown for Slack — otherwise you will introduce arbitrary line breaks mid-sentence or silently truncate content.

Extraction mechanics: reading from the Notion API

How does the blocks endpoint work for nested pages?

The Notion API serves page content through GET /v1/blocks/{block_id}/children, which returns one level of children per call, paginated at a maximum of 100 blocks per response. The endpoint is not recursive — your client must recurse. Fetch the top-level children, check each block's has_children flag, and make a separate request for every block that has nested content.

General formula for estimating API call count:

total_calls = Σ ceil(children_count / 100) for every block where has_children = true

For a page with 50 top-level blocks where 10 have children averaging 30 nested items each:

  • 1 call for top-level children (50 blocks, fits in one page)
  • 10 calls for the 10 parent blocks' children
  • Total: 11 calls for that page

For a moderately nested page — three levels deep with 50 blocks per level — the cost compounds: 1 call (level 1) + 50 calls (level 2) + 2,500 calls (level 3) = 2,551 API calls to read one page's full content tree. At 3 requests/second (180 req/min), that is 2,551 ÷ 3 ≈ 850 seconds, or ~14 minutes per page.

Column layouts amplify this further: fetch the column_list, then the child column blocks, then each column's contents. A database inside one of those columns adds another branch.

Pagination handling for block children

The 100-block-per-response limit requires a cursor loop. Missing this produces silently truncated pages — the API returns no error, just has_more: true with a next_cursor value.

import requests
import time
 
def get_all_block_children(block_id: str, token: str) -> list[dict]:
    """Fetch all children of a block, handling pagination and rate limits."""
    headers = {
        "Authorization": f"Bearer {token}",
        "Notion-Version": "2022-06-28",
    }
    children = []
    params = {"page_size": 100}
 
    while True:
        resp = requests.get(
            f"https://api.notion.com/v1/blocks/{block_id}/children",
            headers=headers,
            params=params,
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 1))
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        data = resp.json()
        children.extend(data["results"])
        if not data.get("has_more"):
            break
        params["start_cursor"] = data["next_cursor"]
 
    return children
 
 
def get_block_tree(block_id: str, token: str) -> dict:
    """Recursively fetch a block and all its descendants."""
    children = get_all_block_children(block_id, token)
    tree = []
    for block in children:
        node = {"block": block, "children": []}
        if block.get("has_children"):
            node["children"] = get_block_tree(block["id"], token)
        tree.append(node)
    return tree

Notion API rate limits

The Notion API rate limits are tiered by workspace plan. Workspaces on Business and Enterprise plans get an average of 600 requests per minute per connection. All other plans get 180 requests per minute per connection (roughly 3 requests per second). Short bursts above these averages are tolerated, but sustained throughput triggers 429 responses with a Retry-After header. (developers.notion.com)

At 180 req/min on a Free or Plus workspace, a deeply nested page requiring 2,551 API calls takes 2,551 ÷ 3 ≈ 850 seconds (~14 minutes) to fully extract. A workspace with 500 pages of moderate depth can take 8–12 hours via the API.

Design your extraction around a rate-limited queue. Workers pull page IDs from the queue, traverse block trees using depth-first search, and respect Retry-After headers on 429 responses with exponential backoff and jitter. Do not run highly concurrent extractions against a single integration token — you will trigger sustained rate limits and your extraction will stall.

Info

Plan for extraction time. A workspace with 500 pages of moderate depth can take 8–12 hours to fully extract via the API. On Business or Enterprise plans, parallelize across multiple integration tokens. On Free or Plus plans, 180 req/min is a firm ceiling per integration.

Notion file URLs expire after one hour

This is the single most common cause of broken images in Notion migrations. Every image, file attachment, video, and PDF hosted on Notion's S3 backend comes back with a signed URL that expires in 60 minutes. The expiry_time field is included on the file object in the API response. (developers.notion.com)

If your migration pipeline extracts the entire workspace over several hours and then attempts to write canvases using the original Notion URLs, every single image will be a broken link.

The safe order is: fetch the block, download the binary immediately, upload to Slack, rewrite the URL, then write the canvas.

import requests
from slack_sdk import WebClient
 
def download_notion_image(image_url: str) -> bytes:
    """Download before the 1-hour expiry."""
    resp = requests.get(image_url, timeout=30)
    resp.raise_for_status()
    return resp.content
 
def upload_to_slack(client: WebClient, image_bytes: bytes, filename: str, channel_id: str) -> str:
    """Upload via files_upload_v2 and return the permalink."""
    result = client.files_upload_v2(
        file=image_bytes,
        filename=filename,
        channel=channel_id,
        title=filename,
    )
    return result["file"]["permalink"]

If you write the canvas first and plan to "fix images later," the original URLs will have expired before you get to them. Download first, write canvas second.

Writing to Slack Canvas: API constraints

How do you create a canvas via the Slack API?

When importing data into Slack Canvas, Slack's Canvas API accepts markdown through a document_content object with "type": "markdown". The canvases.create method is rate-limited at Tier 2 (20+ requests per minute). The canvases.edit method runs at Tier 3, which is more permissive for update passes.

The markdown content is limited to 1 MiB (1,048,576 characters) per document_content object—one of several Canvas API limits that can break bulk migrations. When editing a canvas via canvases.edit, this limit applies to each individual change in the changes array. For most Notion pages, 1 MiB is generous — the binding constraint is the Tier 2 rate limit. At 20 canvases per minute, migrating 1,000 pages takes at least 50 minutes of pure write time, before accounting for extraction and image uploads.

Very large Notion pages may need to be split into multiple canvases or written in sections using canvases.edit. The changes array accepts objects with an operation field ("insert_at_start", "insert_at_end", "replace", "delete") and a document_content block containing the markdown for that section.

from slack_sdk import WebClient
 
client = WebClient(token="xoxb-your-bot-token")
 
# Create the canvas with initial content
markdown_body = """# Project Overview
 
This document was migrated from Notion.
 
## Status
:large_green_circle: On Track
 
## Resources
- [Sprint Database (Google Sheets)](https://docs.google.com/spreadsheets/d/...)
- [Design Files (Figma)](https://figma.com/...)
 
| Owner | Task | Due |
|---|---|---|
| @alice | Write spec | Oct 10 |
| @bob | Review | Oct 12 |
"""
 
result = client.canvases_create(
    title="Project Overview",
    document_content={"type": "markdown", "markdown": markdown_body},
)
canvas_id = result["canvas_id"]
 
# Update a section using canvases.edit (e.g., to resolve cross-canvas links)
client.canvases_edit(
    canvas_id=canvas_id,
    changes=[
        {
            "operation": "insert_at_end",
            "document_content": {
                "type": "markdown",
                "markdown": "\n## Related Canvases\n- [Team Handbook](https://slack.com/canvas/...)\n",
            },
        }
    ],
)
Info

Canvas model change. Slack started converting channel and DM canvases to canvases in tabs on April 9, 2025. Design your migration for standalone canvases and tabbed canvases rather than older channel-canvas assumptions. (slack.com)

End-to-end migration pipeline

The migration has four phases, and the ordering is non-negotiable.

Phase 1: Audit and classify every Notion page

Use POST /v1/search with filter.value = "page" and filter.value = "database" (separate calls) to enumerate the full workspace. Filter out objects where archived: true or in_trash: true — these are common scope-inflation sources. For each surviving object, determine:

  • Is it a database? Count cells (rows × columns). If over 300, route to an external destination.
  • Does it contain child databases? Those databases also need external routing.
  • Does it use toggles, equations, synced blocks, or embeds heavily? Flag for manual review.
  • How deeply nested is it? Use the formula Σ ceil(children_count / 100) for all blocks with has_children = true to estimate API call count and extraction time.
  • Does the integration have access? Spot-check with GET /v1/pages/{page_id}. A 404 response means a permission gap, not a deleted page.

Phase 2: Extract and download

  1. Use GET /v1/blocks/{block_id}/children recursively to build the full block tree for each page. Loop on has_more and next_cursor — never assume a single response contains all children.
  2. For every file-type block (image, video, file, PDF), immediately download the binary. Notion file URLs include an expiry_time field — after that timestamp, you must re-fetch the block from the API to get a new URL.
  3. Store downloaded files locally or in cloud storage with a mapping of notion_block_id → local_file_path.
  4. Rate-limit requests to stay under your plan's threshold (180 req/min for Free/Plus, 600 req/min for Business/Enterprise). Use the Retry-After header on 429 responses with exponential backoff and jitter.

Phase 3: Transform blocks to Canvas markdown

For each page, walk the block tree and emit markdown:

  • Concatenate rich_text arrays, applying bold (**), italic (*), strikethrough (~~), code (`), and link ([text](url)) annotations.
  • Flatten toggles. Resolve synced block references by fetching the original block's children via its block_id. Convert equations to code spans or pre-rendered images.
  • For inline databases that fit in 300 cells, emit a markdown pipe table. For those that don't, emit a link to the external destination.
  • Replace Notion link_to_page references with canvas URLs from your ID map (populated during Phase 4, first pass).

Phase 4: Write canvases and upload files

  1. Upload all images and files to Slack using files_upload_v2. Record the Slack permalink for each.
  2. Splice Slack file URLs into the markdown where Notion image blocks existed.
  3. First pass: Call canvases.create for each page with placeholder content. Record the canvas_id against the original Notion page ID. This builds the complete ID map before any cross-canvas links need to be resolved.
  4. Second pass: Call canvases.edit for each canvas with the final markdown, now that all link_to_page references can be replaced with real canvas URLs.
Tip

Build the ID map before resolving links. Creating all canvases in a first pass (even with placeholder content) gives you every canvas_id before you need to resolve cross-document links. This is the only reliable way to handle link_to_page blocks without circular dependency problems.

What the migration timeline actually looks like

For a 200-page Notion workspace with moderate nesting and ~50 images:

Phase Time estimate Bottleneck
Audit & classification 2–4 hours Manual review of database pages + permission checks
API extraction 4–8 hours Notion rate limit (plan-dependent)
Image download & re-upload 1–3 hours File sizes; Slack upload rate
Markdown transformation Minutes (scripted) Script complexity
Canvas creation (first pass) ~10 minutes 20 canvases/min Tier 2 limit
Canvas update (second pass) ~10 minutes Tier 3 edits
Cross-link resolution Included in second pass ID map lookup
QA & manual fixes 4–8 hours Toggle/equation/embed review
Total ~2–3 days Extraction + QA dominate

Scaling to 1,000 pages roughly 4×'s the extraction and QA time. Deeply nested workspaces — pages with 5+ levels of child blocks — push extraction time significantly higher because each additional nesting level multiplies the API call count per the formula above.

When Notion to Slack Canvas is the wrong migration

Be honest about the fit. Canvas is a good destination for:

  • Narrative documentation — meeting notes, onboarding guides, runbooks, READMEs.
  • Lightweight reference pages — team directories, channel guidelines, link collections.
  • Pages with minimal structured data — fewer than 300 cells of tabular content, no relations or formulas.

Canvas is a poor destination for:

  • Database-heavy workspaces — if your Notion setup is five interconnected databases with rollups and formulas, canvas cannot hold the structure. You are not migrating; you are rebuilding in a different paradigm.
  • Equation-heavy technical docs — every math block becomes a workaround.
  • Toggle-driven knowledge bases — the information-density advantage of toggles disappears entirely.
  • Workspaces over 1,000 pages — the combination of Notion's extraction rate limit (180 req/min on Free/Plus), Slack's 20 canvases/minute Tier 2 write rate, and image re-upload overhead pushes the migration into multi-day territory with significant manual QA burden.

If your Notion workspace is primarily databases, consider migrating to a platform that supports structured data natively, or evaluate tools that can hold both narrative and structured content in the same object model.

Tip

Don't migrate if your real need is just visibility. Slack Workflow Builder includes a Notion connector, and Notion's Slack integration supports notifications and database updates from Slack. If the goal is to bring Notion activity into Slack conversations, integrations may solve it with far less risk than a full content migration.

The pragmatic path

Notion to Slack Canvas is viable for narrative content with light tabular data. It is not viable for database-centric workspaces.

The decision tree is straightforward:

  1. Enumerate the workspace using POST /v1/search filtered to non-archived, non-trashed objects.
  2. Classify every database by cell count (rows × columns). Anything over 300 cells goes to an external destination.
  3. Estimate extraction time using the API call formula: Σ ceil(children_count / 100) for all blocks with has_children = true. If it exceeds your tolerance at 180 req/min, either upgrade the Notion plan or budget the calendar time.
  4. Verify integration permissions before extraction begins. A 404 on GET /v1/pages/{page_id} is a permission gap — resolve it before building the extraction queue.
  5. Stage assets immediately during extraction. Do not let Notion file URLs age past their expiry_time.
  6. Two-pass canvas writes — create all canvases first to build the ID map, then update with final markdown to resolve cross-canvas links.

The technical execution is deterministic once scoped. The hard part is the data-model translation — deciding what goes where when the target system is fundamentally simpler than the source.

Frequently Asked Questions

Can you migrate a Notion database to a Slack Canvas table?
Only if the database has 300 or fewer total cells (rows × columns). Canvas tables have no formulas, no relations, no rollups, and no column types. Databases exceeding 300 cells need a second destination like Google Sheets, Airtable, or Slack Lists, linked from the canvas.
What Notion block types are not supported in Slack Canvas?
Toggle lists, synced blocks, equation blocks (KaTeX), embeds (iframes), child databases, video blocks, audio blocks, PDF blocks, table of contents, and breadcrumbs have no native Canvas equivalent. Toggles must be flattened, equations converted to code spans or images, and embeds reduced to links.
Do Notion image URLs expire during migration?
Yes. Notion-hosted file URLs are signed and expire after 1 hour. Your migration script must download every image immediately after fetching its block, then re-upload it to Slack before writing the canvas that references it.
How long does a Notion to Slack Canvas migration take?
For a 200-page workspace with moderate nesting, expect 2–3 days total. Notion's API rate limit makes extraction the bottleneck. Slack's Canvas API is Tier 2 (20+ requests/minute), so writing canvases is faster than reading Notion pages.
What is the Notion API rate limit for extraction?
Business and Enterprise workspaces get an average of 600 requests per minute per connection. All other plans get 180 requests per minute (roughly 3 requests per second). Sustained throughput above these averages triggers 429 responses.

More from our Blog