---
title: "Notion to Slack Canvas Migration: Database Limits & Block Mapping"
slug: notion-to-slack-canvas-migration-database-limits-block-mapping
date: 2026-09-29
author: Roopendra Talekar
categories: [Notion, Slack Canvas, Migration Guide]
excerpt: "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."
tldr: Most Notion databases exceed Canvas's 300-cell table cap and need a second destination. Narrative pages migrate well; database-heavy workspaces do not.
canonical: https://clonepartner.com/blog/notion-to-slack-canvas-migration-database-limits-block-mapping
---

# Notion to Slack Canvas Migration: Database Limits & Block Mapping


# Notion to Slack Canvas Migration: Database Limits & Block Mapping

As we covered in our [Slack Canvas vs Notion architecture breakdown](https://clonepartner.com/blog/blog/slack-canvas-vs-notion-architecture-limits-and-migration), 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](https://www.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.

```python
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.

```python
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](https://developers.notion.com/reference/request-limits))

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.

> [!NOTE]
> **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](https://developers.notion.com/reference/file-object))

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.

```python
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](https://clonepartner.com/blog/blog/how-to-import-data-into-slack-canvas-api-limits-guide), 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](https://clonepartner.com/blog/blog/slack-canvas-api-limits-that-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.

```python
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",
            },
        }
    ],
)
```

> [!NOTE]
> **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](https://slack.com/help/articles/21290478840979-Feature-change-notice--Channel-canvases))

## 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.

> We handle Notion migrations — including database routing, image re-upload pipelines, and block mapping — so your team doesn't have to build throwaway scripts. Book a 30-minute scoping call.
>
> [Talk to us](https://clonepartner.com/talk-to-us?duration=30&utm_source=blog&utm_medium=button&utm_campaign=demo_bookings&utm_content=cta_click&utm_term=demo_button_click)

## 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.
