---
title: "Slack Canvas to Coda Migration: API Limits & Data Mapping"
slug: slack-canvas-to-coda-migration-api-limits-data-mapping
date: 2026-10-01
author: Abdul Wahab
categories: [Slack Canvas, Coda, Migration Guide]
excerpt: "Slack Canvas tables cap at 300 cells with no formulas. Migrating to Coda requires manual schema setup in the UI, then API-driven data population and validation."
tldr: "The Coda API can't create table schemas, so every Slack Canvas to Coda migration is a two-phase project: build tables manually in the UI, then script the data load."
canonical: https://clonepartner.com/blog/slack-canvas-to-coda-migration-api-limits-data-mapping
---

# Slack Canvas to Coda Migration: API Limits & Data Mapping


# Slack Canvas to Coda Migration: API Limits & Data Mapping

**Slack Canvas tables cap at 300 cells** — any combination of rows × columns — with no formulas, no typed columns, and no cell-level formatting. That hard ceiling is the single most common reason teams move their structured data from Canvas into Coda, where tables support thousands of rows, typed columns, cross-table lookups, and a full formula language.

The catch: **the Coda API (v1, verified May 2025) cannot create or modify table schemas.** No table creation, no column creation, no column-type changes. Only row data is writable via API. Every destination table must be built by hand in the Coda UI before any script can populate it. That makes this a two-phase project: design first, automate second.

This guide covers the full path from inventory to validation, including the API constraints on both sides, required OAuth scopes, failure modes, and the canvas permission variations that shape the migration plan.

## Why the 300-Cell Limit Forces the Move

**A Slack Canvas table allows a maximum of 300 cells per table**, counted as rows × columns. A 10×30 grid, a 15×20 grid, or a 5×60 grid all hit the same wall. Canvas tables also lack formulas entirely — no `=SUM()`, no conditionals, no lookups. Cell content supports basic inline formatting (bold, links, mentions, checkboxes) but no background colors, no number formatting, no conditional formatting, and no typed columns. ([docs.slack.dev](https://docs.slack.dev/surfaces/canvases/))

For a team tracking 50 items across 8 fields, that is 400 cells — already over the limit. The moment your tracking table outgrows the cap, Canvas stops being useful.

Coda tables don't share these constraints. A single Coda table handles thousands of rows with typed columns (text, number, date, person, select, lookup), formulas that reference other tables, conditional formatting, and filtered views. The [architectural gap between the two platforms](https://clonepartner.com/blog/blog/slack-canvas-vs-coda-architecture-limits-and-migration) is not subtle.

## Required OAuth Scopes and Token Types

Before writing a line of code, confirm you have the correct permissions on both sides. Missing scopes cause silent failures or 403 errors that are easy to misattribute.

**Slack side — required OAuth scopes:**

| Scope | Purpose |
|-------|---------|
| `files:read` | List canvases via `files.list` |
| `canvases:read` | Read canvas content via `canvases.getContent` |
| `channels:history` | Read channel messages for comment extraction |
| `channels:read` | Resolve channel IDs to names |
| `users:read` | Resolve Slack user IDs to display names for mention normalization |

Your Slack app must be installed to the workspace as an internal custom app (not submitted to the Marketplace) to retain Tier 3 rate limits on `files.list` and `conversations.history`. Marketplace app rate limits are significantly more restrictive for the comment extraction step.

**Coda side — token type:**

Coda uses API tokens scoped to a single user account, generated at `coda.io/account`. The token owner must have **Doc Maker** role (or higher) in the target workspace to create pages and write rows. Doc Maker is Coda's term for a workspace member with full editing rights; Editors can modify existing content but cannot create new docs. If the token owner is a viewer or editor without Doc Maker, page creation returns a 403. Confirm the token owner's role before queuing any batch operations.

## Why This Is a Two-Phase Project

**The Coda API does not support creating or modifying table or column schemas.** There is no `POST /tables` endpoint. There is no `POST /columns` endpoint. You can read table metadata, list columns, and read column types — but every write operation is limited to row data. The `upsertRows` endpoint inserts or updates rows in an existing table, but the table and its columns must already exist. ([coda.io](https://coda.io/developers/apis/v1))

This means:

- **Phase 1 (manual):** Build the destination Coda doc structure in the UI. Create every table, define every column name and type, set up select lists, relations, and formulas.
- **Phase 2 (scripted):** Use the Coda API to populate those tables with row data extracted from Slack. Push page content as Markdown or HTML.

There is no shortcut around Phase 1. Even if you script the extraction perfectly, the data has nowhere to land without a human setting up the schema first. Plan your column mapping before writing a line of code.

> [!WARNING]
> **Formulas do not migrate.** Slack Canvas has zero formula support — no computed cells, no expressions, no references between tables. Coda's formula language (`thisRow`, lookups, filters, date math, conditionals) must be authored entirely fresh. Plan dedicated time for formula authoring after your data lands, especially for cross-table lookups or rollup columns that did not exist in Canvas.

## Canvas Permission Types and How They Affect Extraction

Not all canvases behave the same. Before running extraction scripts, classify which canvas types exist in your workspace — they have different access patterns and failure modes.

| Canvas type | Access behavior | Migration note |
|-------------|-----------------|----------------|
| Channel-attached canvas | Readable if bot is in channel and has `canvases:read` | Most common; `channel_id` in API response |
| Standalone (personal) canvas | Created by a user, not attached to a channel | Appears in `files.list` but may not be shared with your bot |
| DM-attached canvas | Attached to a direct message thread | Requires `im:history` scope; often inaccessible to bots |
| Cross-workspace shared canvas | Shared with external workspaces via Slack Connect | Access restricted to the originating workspace token |
| Private channel canvas | Attached to a private channel | Bot must be explicitly invited to the private channel |

Private and DM canvases are the most common silent failure point. The `files.list` API returns metadata for canvases accessible to the requesting user, but `canvases.getContent` may return a 403 for canvases where your bot was not explicitly granted access. Build a pre-flight check that attempts `canvases.getContent` on each discovered canvas and logs 403s separately rather than treating all inventory items as extractable.

## `canvases.getContent` — Availability and Rate Tier

**As of May 2025, `canvases.getContent` is generally available** for workspaces on paid Slack plans (Pro, Business+, Enterprise Grid). It is not available on free Slack plans, where canvas functionality itself is restricted. ([docs.slack.dev](https://docs.slack.dev/reference/methods/canvases.getContent/))

`canvases.getContent` is a **Tier 2** method — 20+ requests per minute for internal custom apps. This is slower than `files.list` (Tier 3, 50+ per minute), so content extraction is the rate-limiting step in your pipeline, not the initial inventory. For a workspace with 200 canvases, sequential extraction at Tier 2 takes a minimum of 10 minutes without any backoff — budget more.

There is no documented size limit on what `canvases.getContent` returns per canvas, but extremely large canvases (many embedded images or long tables) return proportionally large markdown payloads. Parse and validate payload size before transformation to avoid downstream truncation errors.

## How to Inventory Slack Canvases

**There is no `canvases.list` method in the Slack API.** Slack treats canvases as a file type, so you discover them using `files.list` filtered to type `canvas`: ([docs.slack.dev](https://docs.slack.dev/surfaces/canvases/))

```bash
curl -H "Authorization: Bearer $SLACK_TOKEN" \
  "https://slack.com/api/files.list?types=canvas&count=100&page=1"
```

This returns canvas files as part of the standard file listing, paginated. Each canvas object includes its `id`, `title`, `created` and `updated` timestamps, the `user` who created it, and the channels it is shared in. `files.list` is a **Tier 3** method — 50+ requests per minute per workspace per app. ([api.slack.com](https://api.slack.com/methods/files.list))

Paginate until Slack stops returning results, and keep the inventory idempotent so reruns only pick up new or changed canvases. Honor `429` responses and the `Retry-After` header.

Capture at least:

- Canvas ID and title
- Canvas type (channel-attached, standalone, DM-attached, private)
- Owner/creator and timestamps
- Channel context, if the canvas is channel-linked
- Embedded file or image references
- Planned Coda destination page and table mapping
- Pre-flight access check result (200 or 403 from `canvases.getContent`)

> [!NOTE]
> Slack canvases carry **no folder structure**. A canvas is either standalone or attached to a channel. There is no hierarchy, no nested folders, no parent-child relationship. The Coda page tree and document organization is entirely your design decision.

## Designing the Coda Document Tree

Since Slack canvases are flat—a structural gap you also face when [migrating Canvas to Notion](https://clonepartner.com/blog/blog/slack-canvas-to-notion-migration-api-limits-data-mapping) or [Confluence](https://clonepartner.com/blog/blog/slack-canvas-to-confluence-migration-apis-limits-mapping)—you need to decide how to organize them in Coda. Common approaches:

- **One Coda doc per channel**, grouping canvases by the channel they were attached to. Preserves team context.
- **One Coda doc per workspace** with a page per canvas. Works for small teams with fewer than 100 canvases.
- **One Coda doc per functional area** (engineering, marketing, ops), if canvases span channels but cluster by function.

Channel-attached canvases include a `channel_id` in the Slack API response, giving you a natural grouping key. Standalone canvases have no channel association — classify those manually or by naming convention.

## Building Table Schemas in the Coda UI (Phase 1)

For every canvas table you plan to migrate, create the corresponding Coda table in the UI:

1. Open your destination Coda doc.
2. Add a new table with column names matching your canvas data.
3. Set column types explicitly — text, number, date, select, checkbox, etc. Canvas cells are untyped, so you are making typing decisions now.
4. If you need formulas, computed columns, or lookups, add them here — they cannot come from the source.

Don't clone the Slack table 1:1. A 10-column canvas table often becomes one base table for primary records, a People or Teams lookup table if owners repeat, select columns for status, date columns for deadlines, and formula columns for rollups or derived fields.

After setup, call `GET /docs/{docId}/tables/{tableId}/columns` to retrieve the column IDs you need for row upserts in Phase 2.

> [!WARNING]
> **Column IDs must match exactly.** The Coda row upsert API expects values keyed to column IDs, not column names. Sending a value for a nonexistent column ID returns a 400 error. Map columns carefully before running any batch. Column IDs look like `c-abc123` and are stable — they do not change if you rename the column.

| Slack Canvas field | Coda target | Migration note |
|---|---|---|
| Task / Name | Text or display column | Use as the stable row key if unique |
| Owner mention | Text or relation | Resolve Slack user IDs to names before import; make relational only if a People table exists |
| Status | Select list | Normalize values before import; mismatched option names create new select options silently |
| Date text | Date column | Parse to ISO 8601 before loading; Coda rejects freeform date strings |
| Notes | Text or page content | Keep long narrative out of row cells; use sibling page instead |

## How Does Page Content Transfer?

**Extract Slack canvas content as markdown, normalize it, then write it to Coda as Markdown or HTML.** Slack's canvas API is markdown-first and supports a documented subset of elements: headings `h1`–`h3`, lists, checklists, callouts, tables, mentions, and unfurls. Coda's page API accepts both formats. ([docs.slack.dev](https://docs.slack.dev/surfaces/canvases/))

```json
{
  "name": "Sprint Retro Notes",
  "parentPageId": "canvas-Parent123",
  "pageContent": {
    "type": "canvas",
    "canvasContent": {
      "format": "markdown",
      "content": "## Summary\n\nKey takeaways from the retro..."
    }
  }
}
```

You can also append content to existing pages using `contentUpdate` with `insertionMode: "append"`.

### Converting Slack's canvas markdown subset

Slack canvas markdown supports **headings h1 through h3 only** — no h4, h5, or h6. If your Coda documentation standards require deeper nesting, adjust heading structures during transformation.

Coda pages accept standard Markdown, so h1–h3 content transfers directly. Handle these conversions explicitly:

- **Slack @mentions** use Slack-specific user and channel syntax (`<@U012AB3CD>`, `<#C012AB3CD>`) — convert to plain display text, links back to Slack profiles, or Coda @mentions if users match. Use the `users:read` scope to resolve user IDs to names before transformation.
- **Canvas unfurls** (embedded Slack messages, files, and channel links) have no Coda equivalent — flatten them into plain links with explicit labels.
- **Checklists** transfer as standard Markdown checkboxes (`- [ ]` / `- [x]`), which Coda supports natively.

Slack canvas markdown is a Slack-specific dialect, so a cleanup pass is part of the migration, not an edge case.

### Alternative migration paths: Packs and automation tools

Before scripting the entire migration, consider whether existing tooling fits your scale:

- **Coda Packs:** Coda's native integration layer. There is no official Slack Canvas Pack as of May 2025. Community Packs exist for general Slack data but do not handle canvas content extraction.
- **Zapier / Make:** Support Slack triggers and Coda row actions but cannot call `canvases.getContent` directly or handle batch page creation. Viable only for simple, ongoing sync of new content — not for bulk historical migration.
- **Direct scripting (this guide):** The only approach that handles the full canvas content, table data, images, and comments in a single controlled pass. Recommended for migrations involving more than ~20 canvases or any structured table data.

The automation tools are not dismissed arbitrarily — they simply lack the API surface to address the core extraction problem. Use them for post-migration sync, not for the migration itself.

## Populating Tables via the Coda API (Phase 2)

Use the **upsert rows** endpoint to push data into your pre-built tables:

```bash
POST https://coda.io/apis/v1/docs/{docId}/tables/{tableId}/rows
```

The request body contains an array of row objects, each mapping column IDs to values. Include a `keyColumns` array to update existing rows on re-runs instead of creating duplicates — this makes the migration idempotent.

```python
import requests

url = "https://coda.io/apis/v1/docs/{doc_id}/tables/{table_id}/rows"
headers = {
    "Authorization": "Bearer YOUR_CODA_TOKEN",
    "Content-Type": "application/json"
}

payload = {
    "rows": [
        {
            "cells": [
                {"column": "c-123456", "value": "Row 1 Data"},
                {"column": "c-789012", "value": "Active"}
            ]
        }
        # Batch multiple rows per request
    ],
    "keyColumns": ["c-123456"]
}

response = requests.post(url, headers=headers, json=payload)
```

Coda row inserts and updates return **asynchronous `202` responses** with a `requestId` in the response body. Poll `GET /docs/{docId}/tables/{tableId}/rows` or use the mutation status endpoint to confirm completion before validating row counts. Do not assume synchronous success.

Watch payload size: Coda enforces a **2 MB request limit** and an **85 KB limit per row**. If a canvas table has one giant notes column, keep structured fields in the table and move long narrative to a sibling page.

Given that a Slack Canvas table is hard-capped at 300 cells, even the largest single canvas table fits comfortably in one upsert call. The rate limits matter more when you are migrating dozens of tables across hundreds of canvases. Coda row writes only work on **base tables**, not views — import into base tables and let views sit on top later.

### Coda API rate limits (v1, verified May 2025)

Rate limits apply per-token across all endpoints and all docs:

| Operation | Limit |
|-----------|-------|
| Reading data | 100 requests per 6 seconds |
| Writing data (POST/PUT/PATCH) | 10 requests per 6 seconds |
| Writing doc content (pages) | 5 requests per 10 seconds |
| Listing docs | 4 requests per 6 seconds |

The **write limit of 10 requests per 6 seconds** is the practical bottleneck for row data. If you batch 100 rows per upsert call, you can push roughly 1,000 rows per minute under ideal conditions. The **page write limit of 5 requests per 10 seconds** governs canvas-to-page content migration and is the tighter constraint for content-heavy workspaces.

Implement exponential backoff on HTTP 429 responses — the `Retry-After` header specifies the wait duration in seconds. Do not use fixed-interval polling; the backoff must be adaptive or you will re-hit the limit immediately after the wait.

### Common API errors and what they mean

| HTTP status | Trigger | Resolution |
|-------------|---------|------------|
| 400 | Invalid column ID in `cells` payload | Verify column IDs against `GET /columns`; column IDs are case-sensitive |
| 400 | Malformed date string in a Date column | Reformat to ISO 8601 (`YYYY-MM-DD`) before upsert |
| 403 | Token owner lacks Doc Maker role | Upgrade token owner's workspace role |
| 403 | Canvas is private or DM-attached (Slack) | Bot needs explicit channel invitation or `im:history` scope |
| 409 | Row key conflict on upsert | Confirm `keyColumns` values are unique in source data |
| 429 | Rate limit exceeded | Honor `Retry-After` header; implement exponential backoff |

**Silent failure modes to audit explicitly:**

- Select column values that don't match existing options are created as new options without error — validate select values against the defined option list before import, or you will end up with duplicate near-identical options ("Active", "active", "ACTIVE") that require manual cleanup.
- Image URLs from Slack that require authentication render as broken images in Coda without raising an error. Validate all image references post-migration by loading pages in a browser.
- Long text values silently truncate at the column's character limit (varies by column type). If notes fields exceed limits, Coda accepts the row but stores truncated content.

## What Happens to Comments, Images, and Mentions?

### Comments

**Canvas comments in Slack are channel messages, not document annotations.** When someone comments on a canvas, that comment appears as a threaded message in the channel where the canvas was shared. There is no positional anchor — the comment is not tied to a specific paragraph, cell, or section. ([slack.com](https://slack.com/help/articles/203950418-Use-a-canvas-in-Slack))

This means comments cannot be migrated as inline annotations in Coda. Extract comment threads using `conversations.history` and `conversations.replies` on the relevant channel, then archive them as a clearly labeled section at the bottom of the Coda page (use a toggle section titled "Archived Slack Comments") or as a separate audit table with canvas ID, author, timestamp, and text.

> [!WARNING]
> **Slack rate limit for non-Marketplace apps (May 2025):** `conversations.history` and `conversations.replies` are limited to **1 request per minute** returning a **maximum of 15 messages** for non-Marketplace apps. Internal custom-built apps retain Tier 3 limits (50+ per minute). Ensure your migration app qualifies as internal, or budget significantly more time for comment extraction.

### Images

**Images in Slack canvases are hosted on Slack's infrastructure.** The URLs require an authenticated Bearer token and expire or become inaccessible outside of Slack. Pasting Slack-private URLs directly into Coda produces broken images with no error — the page loads silently with missing visuals. ([docs.slack.dev](https://docs.slack.dev/reference/objects/file-object/))

You must re-host every image:

1. Download each image from Slack using the file URL and your bot token (requires `files:read` scope).
2. Upload to a publicly accessible host (S3, GCS, Cloudinary, or another CDN).
3. Replace the Slack image URL in your markdown content with the new hosted URL.

Coda's Markdown renderer displays images referenced by URL. For table cells, use an Image URL column type. Once Coda ingests the page, it downloads the image from your hosted URL and renders it natively. You can safely clean up staging storage after the migration is verified.

For asset-heavy workspaces, our guide on [migrating images, attachments, and embeds](https://clonepartner.com/blog/blog/how-to-migrate-images-attachments-embeds-without-broken-links) covers the full pattern.

### Mentions

Slack canvas markdown uses Slack-specific user and channel mention syntax (`<@U012AB3CD>`, `<#C012AB3CD|channel-name>`). Resolve user IDs to display names using `users.info` before transformation. Decide up front whether mentions become plain names, links back to Slack, or relations to a People table in Coda — and apply that decision consistently across all canvases. Inconsistent mention handling produces a mix of resolved names and raw Slack IDs in your Coda content that is expensive to fix after the fact.

## What a Realistic Timeline Looks Like

For a workspace with 200 canvases, roughly 30 containing tables:

| Phase | Work | Time Estimate |
|-------|------|---------------|
| Inventory | Script `files.list` extraction, classify canvases by type and channel, run pre-flight access checks | ~2 hours |
| Schema design | Review tables, decide column types, build Coda doc in UI, define select option lists | 1–2 days |
| Scripted migration | Extract from Slack, transform markdown, resolve mentions, upsert rows, push pages | ~1 day |
| Image re-hosting | Download from Slack, upload to CDN, update references, validate rendering | 2–4 hours |
| Formula authoring | Build computed columns, lookups, automations in Coda | 1–3 days |
| Validation | Spot-check row counts, select option integrity, image rendering, truncated fields | ~half day |

**Total: 4–8 days** depending on table complexity, formula requirements, and the proportion of private or DM-attached canvases requiring manual access resolution. The manual schema design in Phase 1 and formula authoring in the final phase are what prevent this from being an overnight script.

## When to Do This In-House vs. Call for Help

This decision reduces to two variables: canvas count and data model complexity.

**In-house is appropriate when:**
- Fewer than 50 canvases, mostly narrative content
- One or two small tables with simple column types (text, date, select)
- No DM-attached or private canvases requiring manual access grants
- Limited image content

**Bring in outside help when:**
- More than 100 canvases, especially with mixed types (channel, private, DM)
- Repeated table patterns that require consistent schema normalization across many sources
- Heavy image content requiring CDN setup and reference replacement at scale
- Tight cutover window with zero tolerance for partial migration

The hard part at scale is not the API call volume — it is the mapping and validation work. Normalizing select option values across 30 tables, resolving 500 Slack user mentions, and confirming no rows were silently truncated requires systematic validation tooling that takes longer to build than most teams expect.

The short version: Slack Canvas is a strong collaboration surface, but it is not a database. (This fundamental difference is also why [migrating from Coda back to Slack Canvas](https://clonepartner.com/blog/blog/coda-to-slack-canvas-migration-api-limits-triage-what-breaks) is notoriously difficult.) When teams outgrow the 300-cell table limit, the move to Coda is less about exporting documents and more about re-establishing structure. Treat the job as two paired tracks — **Slack extraction** and **Coda rebuild** — and you will make better decisions on schemas, comments, images, and formulas from day one.

> Need help migrating from Slack Canvas to Coda? Our engineers handle the two-phase migration end to end — schema setup, data extraction, image re-hosting, and validation included.
>
> [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 the Coda API create tables or columns?

No. The Coda API only supports reading table and column metadata and writing row data. There is no endpoint to create tables, add columns, or change column types. All schema changes must be done manually in the Coda UI.

### How do you list all Slack canvases via API?

Use the files.list method with the types parameter set to canvas: GET https://slack.com/api/files.list?types=canvas. There is no dedicated canvases.list endpoint. files.list is a Tier 3 method (50+ requests per minute).

### What is the Slack Canvas table cell limit?

Slack Canvas tables have a maximum of 300 cells per table, counted as rows multiplied by columns. There are no formulas, no cell-level formatting like background colors, and no typed columns.

### Can Slack Canvas comments be migrated as inline annotations in Coda?

No. Canvas comments are channel messages in Slack, not positional annotations tied to specific content. You can extract them via conversations.history and conversations.replies, but they can only be archived as a separate section or table in Coda.

### How long does a Slack Canvas to Coda migration take?

For a workspace with about 200 canvases, expect 3–7 days total. The biggest time sink is manually building table schemas in the Coda UI, plus formula authoring after data lands.
