---
title: "Google Docs to Slack Canvas Migration: The Technical Guide"
slug: google-docs-to-slack-canvas-migration-the-technical-guide
date: 2026-09-28
author: Rishabh Makhar
categories: [Google Docs, Slack Canvas, Migration Guide]
excerpt: Google Docs is a paginated document; Slack Canvas is a continuous markdown surface. Here's exactly what survives the migration and what doesn't.
tldr: "There is no native Google Docs to Slack Canvas import. Headings flatten from six to three, tables cap at 300 cells, images must be re-hosted, and several elements are permanent losses."
canonical: https://clonepartner.com/blog/google-docs-to-slack-canvas-migration-the-technical-guide
---

# Google Docs to Slack Canvas Migration: The Technical Guide


# Google Docs to Slack Canvas Migration: The Technical Guide

Migrating a Google Doc into a Slack Canvas means converting a paginated, page-layout document into a continuous markdown surface. There is no native import path — no button, no connector, no one-click migration tool. You need to extract content from the Google Docs API or the Google Drive export endpoint, transform it into [markdown that Canvas accepts](https://clonepartner.com/blog/blog/slack-canvas-markdown-what-renders-what-fails-what-drops), and push it through Slack's `canvases.create` API.

The fundamental problem is not the mechanics of extraction and loading. It is that a Google Doc carries layout, annotation, and structural elements that Canvas cannot represent — and several of those losses are permanent.

This guide covers the data model mismatch, extraction options, heading mapping, size arithmetic, table and image constraints, the comment and suggestion problem, the OAuth scopes required on both sides, the API error responses you will encounter, and the rate limits you will hit during bulk migration. If you need a deeper look at getting content out of Google Docs first, see [How to Export Data from Google Docs: API Limits, Methods & Portability](https://clonepartner.com/blog/blog/how-to-export-data-from-google-docs-api-limits-methods-portability).

> **API constraints in this guide reflect the state of the Google Docs API, Google Drive API, and Slack Canvas API as of Q2 2025.** Both platforms update their limits without versioned documentation — verify current values before building a production pipeline.

---

## Required OAuth Scopes

Before any API calls, you need the right scopes on both sides. Missing scopes produce 403 errors that look identical to permission errors, which wastes debugging time.

**Google (OAuth 2.0):**

| Scope | Purpose |
|---|---|
| `https://www.googleapis.com/auth/documents.readonly` | Read document body, paragraphs, tables, inline images |
| `https://www.googleapis.com/auth/drive.readonly` | Export via Drive (HTML/DOCX), fetch comments and replies |

Use `documents.readonly` alone if you are only walking the Docs API body tree. You need `drive.readonly` for the export endpoint and for all comment operations.

**Slack (Bot Token Scopes):**

| Scope | Purpose |
|---|---|
| `canvases:write` | Create and edit canvases |
| `files:write` | Upload images via `files.getUploadURLExternal` |
| `files:read` | Read back uploaded file URLs for canvas embedding |

If you are creating canvases in a channel context, you also need `channels:read` or `groups:read` depending on channel type. (See our [import guide](https://clonepartner.com/blog/blog/how-to-import-data-into-slack-canvas-api-limits-guide) for the differences between channel and standalone canvases.)

---

## The Model Mismatch: Paginated Document vs. Continuous Markdown

**A Google Doc is a page-layout engine designed for printing.** It stores content as a JSON Abstract Syntax Tree (AST) composed of `StructuralElement` objects — paragraphs, tables, section breaks, and tables of contents. The top-level elements include the Body, DocumentStyle, and List. The Docs API also exposes separate maps for `headers`, `footers`, `footnotes`, and `positionedObjects`, each a distinct structural segment. Everything is organized around physical pages with margins, headers, footers, and exact positioning.

**A Slack Canvas is a continuous markdown surface.** When creating or editing a canvas with the API, you supply a `document_content` object with two properties: `type` and `markdown`. The only supported type is `markdown`. There are no pages, no print layout, no margin model, and no annotation system.

Because of this mismatch, a migration is not a copy — it is a lossy transformation. You are stripping away the physical constraints of a printed page to fit content into an infinite vertical scroll.

---

## Migration Pipeline Decision Tree

Use this flowchart before writing any transformation code. It determines which path to take for each document and each element type.

```
Does the document have active suggestions?
├── YES → Accept or reject all suggestions before extraction. Archive suggestion history separately.
└── NO → Proceed.

Does the document have comments?
├── YES → Fetch via Drive API (separate quota). Decide: inline callouts, endnotes, or separate archive.
└── NO → Proceed.

Does the document have images?
├── YES → Plan image re-hosting pipeline BEFORE transformation. Download contentUri immediately.
└── NO → Proceed.

Does any table exceed 300 cells (rows × columns)?
├── YES → Plan split or CSV conversion strategy per table.
└── NO → Proceed.

Does the document use Heading 4–6?
├── YES → Choose: flatten to H3, split to child canvases, or demote to bold paragraphs.
└── NO → Proceed.

Estimate post-transformation markdown size. Does it exceed 1 MiB?
├── YES → Identify heading-level split points. Plan multi-canvas output with linking.
└── NO → Proceed.

Choose extraction method:
├── Prototype or simple content → Drive API HTML export (single call, 10 MB cap)
└── Production or complex content → Docs API body tree (full control, higher engineering cost)
```

Run this analysis on a representative sample of your document library before committing to a pipeline design. One table over 300 cells or one document over 1 MiB will fail at API call time, not at transformation time.

---

## Which Google Docs Elements Have No Canvas Equivalent?

Several Google Docs features have no native Canvas target. These represent **permanent fidelity losses** — not things you can work around with clever markdown, but structural gaps in the destination format. Establish your downgrade policy for each before writing any code.

| Google Docs Element | Why It Is Lost |
|---|---|
| **Headers and footers** | Canvas has no page concept. The Docs API exposes these as separate `Header` and `Footer` objects keyed by section. No equivalent surface exists. Drop them, or move recurring metadata to the top of the canvas. |
| **Footnotes** | The Docs API provides `Footnote` objects with full rich-text content. Canvas markdown has no footnote syntax. Convert to inline parenthetical notes or an endnotes section. Either way, the semantic link is gone. |
| **Section breaks and page breaks** | Google Docs uses `SectionBreak` elements to control page layout, column counts, and per-section headers/footers. Canvas has a horizontal rule (`---`), but it carries none of the layout semantics. |
| **Page margins and page size** | `DocumentStyle` includes margins, page size, and orientation. Canvas has no layout properties. Landscape pages and custom margin widths are discarded silently. |
| **Positioned images** | A `PositionedObject` is an object tethered to a paragraph and positioned relative to the beginning of that paragraph. Canvas can only place images inline in the markdown flow. Wrap, offset, and overlap behavior are permanently lost. |
| **Suggested changes** | Google Docs tracks pending edits as suggestion markers with `suggestedInsertionIds`, `suggestedDeletionIds`, and `suggestedTextStyleChanges`. Canvas has no track-changes or suggestion system. Accept or reject all suggestions before export, or archive the history separately. |
| **Anchored comments** | Comments live in the Drive API and are anchored to text ranges. The Canvas API has no endpoint for programmatically attaching comments to specific text. See the [comments section](#comments-replies-and-the-two-api-problem) below for workarounds. |
| **Deeply nested lists** | Canvas markdown supports a maximum of two levels of list nesting. Google Docs lists can nest arbitrarily deep. Content beyond two levels of indentation is either flattened or dropped depending on your renderer. |

> [!WARNING]
> **Suggestions are a one-way loss.** You can use the Docs API to view suggested changes inline using `suggestionsViewMode=SUGGESTIONS_INLINE`, and you can read, create, reply to, update, or delete comment and suggestion threads via the Drive API. But Canvas has zero concept of tracked changes. If you need to preserve suggestion history, archive it separately before retiring the source document.

---

## Extracting Content: HTML Export vs. Document Body Tree

You have two primary extraction paths. The right choice depends on whether you are prototyping or building a production pipeline.

### Option 1: Drive API export

```bash
GET https://www.googleapis.com/drive/v3/files/{fileId}/export?mimeType=text/html
```

This exports the full document as rendered HTML. You can also request `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (DOCX) or `text/plain`.

**Pros:** Single API call. Includes all visible content. You can use standard HTML-to-Markdown libraries (like Turndown) to generate your Canvas payload.

**Cons:** Google's HTML export relies heavily on inline CSS (`<span style="font-weight:700">`) rather than semantic tags (`<strong>`), making parsing brittle. Images may be base64-encoded inline, inflating size. The `files.export` endpoint caps the exported payload at **10 MB** — images and complex formatting inflate this fast. The export also strips comment anchors, document metadata, and specific user attributions.

Google's Markdown export (`text/x-markdown`) is worth knowing about, but it is not a direct load into Canvas. Slack extends markdown for mentions, unfurls, and file references, and exported Markdown cannot encode page geometry or review state.

### Option 2: Walk the Docs API body tree

```bash
GET https://docs.googleapis.com/v1/documents/{documentId}
```

This returns the full structured JSON representation: every paragraph, text run, table, list, and inline image as a typed object with start/end character indexes and style information.

```json
{
  "body": {
    "content": [
      {
        "paragraph": {
          "elements": [
            {
              "textRun": {
                "content": "This is bold text\n",
                "textStyle": {
                  "bold": true
                }
              }
            }
          ],
          "paragraphStyle": {
            "namedStyleType": "HEADING_1"
          }
        }
      }
    ]
  }
}
```

**Pros:** Full control over element-by-element transformation. Clean access to heading levels, table structure, link targets, footnote references, and positioned object IDs. You can make deterministic split decisions instead of reverse-engineering rendered HTML. List state tracking, heading hierarchy decisions, and oversized table detection are all straightforward against the typed AST.

**Cons:** You must write a recursive parser to walk the `content` array, evaluating every `StructuralElement`, `ParagraphElement`, and `TextRun`. This requires significantly more engineering effort.

If the source documents use tabs, request `includeTabsContent=true`. If you need to process suggestions inline, set `suggestionsViewMode=SUGGESTIONS_INLINE`.

> [!TIP]
> **For production migrations, the body tree approach produces better results** because you can make heading-level decisions, handle table cells individually, skip unsupported elements cleanly, and track list state across interruptions. The HTML export is faster to prototype but harder to control at scale.

---

## How Do Google Docs Headings Map to Canvas Markdown?

Google Docs named styles support **Heading 1 through Heading 6**. Slack Canvas supports headings H1 through H3 only — that is a hard ceiling enforced at render time. Any document using Heading 4, 5, or 6 loses its full hierarchy when migrated.

A workable default mapping:

```text
HEADING_1 → h1  (# in markdown)
HEADING_2 → h2  (## in markdown)
HEADING_3 → h3  (### in markdown)
HEADING_4 → h3  (only if the section is short; otherwise split to a new canvas)
HEADING_5 → **bold paragraph**  (loses outline navigation)
HEADING_6 → **bold paragraph**  (loses outline navigation)
```

Your three real options:

- **Flatten deep headings.** Map H4–H6 to `### h3`. This works for shallow documents but makes a deeply nested outline unreadable. The common mistake is flattening every H4–H6 node into H3 — that preserves syntax but destroys hierarchy.
- **Split across canvases.** Treat each major section (H1 or H2) as a separate canvas and link them from a parent canvas. This preserves more structural depth but fragments the document. If a Google Doc relies heavily on H4–H6, it is usually a monolithic wiki page that benefits from being split anyway.
- **Demote to bold text.** Render H4–H6 as `**bold text**` on its own line. Visually passable, but semantically they become paragraphs — you lose outline navigation entirely.

There is no correct universal answer. The right choice depends on how the original outline was structured and whether readers need to scan the hierarchy.

> [!WARNING]
> **List continuation failures:** Google Docs tracks lists via a `listId` reference, meaning a numbered list can be interrupted by a paragraph and resume numbering later. Markdown does not support this natively. If you do not track `listId` state in your parser, interrupted numbered lists will reset to `1.` in Slack Canvas. Additionally, Canvas markdown supports a maximum of two levels of list nesting — any deeper nesting from Google Docs must be flattened or restructured before the API call.

---

## Will a Large Google Doc Fit Inside a Slack Canvas?

Google Docs documents can hold up to **1,024,000 characters**, regardless of page count or font size. The Canvas `document_content` markdown field is limited to **1,048,576 characters (1 MiB)** per canvas object.

The Canvas ceiling (1,048,576) is slightly higher than the Google Docs ceiling (1,024,000), leaving roughly **24,576 characters** of apparent headroom. Do not rely on that margin.

Markdown transformation inflates character counts. Every pipe character in table syntax, every image reference URL, every link (`[text](url)`), every heading prefix (`#`, `##`, `###`), and every horizontal rule adds characters that did not exist in the source. A 900,000-character Google Doc with 50 tables and 200 links can produce a markdown blob that exceeds 1 MiB once you add pipe delimiters, URL strings, and Slack file permalinks.

**Always calculate output size after transformation, not before.** Run a byte-length check on your final markdown string before calling `canvases.create`. If it exceeds 1,048,576 bytes, split the markdown at a logical heading break and generate two linked canvases.

**`canvases.edit` accepts only one operation per API call** (as of Q2 2025). The supported operation types are:

| Operation | Effect |
|---|---|
| `insert_at_end` | Appends markdown content to the end of the canvas |
| `replace_all` | Replaces the entire canvas content with new markdown |

Because batch operations are not available, multi-part canvas construction requires sequential API calls with state tracking between each call. Design your write pipeline accordingly — a canvas assembled from 10 sequential `insert_at_end` calls requires 10 round trips, each subject to the Tier 2 rate limit.

---

## Tables: The 300-Cell Canvas Ceiling

Tables are the most frequent failure point in document migrations. Google Docs allows massive, multi-page tables with merged cells and no documented per-table cell limit. Canvas imposes a hard architectural limit of **300 cells per table** — any combination of rows and columns that totals 300 or fewer (a 10×30 table hits the limit exactly; a 15×21 table at 315 cells fails).

**What the API returns when this limit is exceeded:** `canvases.create` returns a `400 Bad Request` with an error body similar to:

```json
{
  "ok": false,
  "error": "invalid_arguments",
  "response_metadata": {
    "messages": ["table exceeds maximum cell count"]
  }
}
```

This is a hard rejection — the entire canvas creation call fails. No partial content is written. You must fix the table before retrying.

Markdown tables also do not support merged cells (rowspans or colspans). If your Docs parser encounters a `tableCell` with a `rowSpan` greater than 1, you must duplicate the cell content across the affected rows or the table columns will misalign.

**Handling oversized tables:**

1. **Detect cell count during AST parsing.** Calculate `rows × columns` for every table before transformation. Flag any table over 300 cells before the transformation step — not at API call time.
2. **Split vertically.** Break a long table into multiple markdown tables by row ranges, repeating the header row each time.
3. **Split horizontally.** Break a wide table into multiple tables by column groups.
4. **Convert to CSV or Sheets.** If the table is strictly tabular data (no rich text or images in cells) and exceeds roughly 1,000 cells, convert it to a CSV file, upload it to Slack via `files.getUploadURLExternal` + `files.completeUploadExternal`, and embed the file link in the canvas.

Audit your documents for oversized tables before building the transform pipeline. One table over the limit causes the entire canvas creation call to return `400` with no partial write.

---

## Images: Re-host or Lose Them

Canvas markdown cannot contain binary image data. Images must be referenced by URL — either a publicly accessible URL or a Slack-hosted file URL.

When you fetch a Google Doc via the Docs API, each `InlineObjectElement` includes a `contentUri` inside `imageProperties`:

```json
"imageProperties": {
  "contentUri": "https://lh3.googleusercontent.com/..."
}
```

> [!CAUTION]
> **Short-lived URIs.** The `contentUri` provided by the Google Docs API is a short-lived, authenticated URL tied to the requesting session. You cannot drop it into your Canvas markdown and expect it to work — by the time a user views the Canvas, the image link will return a 403 or 404. Download images immediately during parsing before moving to the next element.

To get images into a canvas permanently:

1. **Download the image** from Google's `contentUri` immediately during parsing, before it expires.
2. **Upload to Slack** using `files.getUploadURLExternal` (Tier 3: ~50 requests/minute) + `files.completeUploadExternal` (Tier 2: ~20 requests/minute). The image upload endpoints have their own rate limits separate from `canvases.create` — in image-heavy migrations, file upload throughput is typically the actual bottleneck.
3. **Reference the Slack-hosted URL** in the canvas markdown using `! [alt](url)` syntax.

Alternatively, upload images to a public storage service (S3, GCS, Cloudflare R2) and reference those URLs. If the public URL goes down later, the image in the Canvas breaks silently with no notification to canvas readers or owners.

**What the API returns for malformed image markdown:** Canvas does not reject markdown containing broken image URLs at write time — the create call succeeds, but the image renders as a broken link indicator in the Canvas UI. There is no API-level validation of image URL reachability during `canvases.create`.

For a detailed breakdown of image migration patterns, see [How to Migrate Images, Attachments & Embeds Without Broken Links](https://clonepartner.com/blog/blog/how-to-migrate-images-attachments-embeds-without-broken-links).

---

## Comments, Replies, and the Two-API Problem

Google Docs comments and replies do not live in the Docs API. They live under the **Drive API**:

```bash
GET https://www.googleapis.com/drive/v3/files/{fileId}/comments
GET https://www.googleapis.com/drive/v3/files/{fileId}/comments/{commentId}/replies
```

Each comment is anchored to a text range within the document using a `quotedFileContent` field (the text the comment was placed on) and an `anchor` property. Comments also carry `resolved` status, `author`, `createdTime`, and the full reply thread.

**Slack Canvas has no comment anchoring API.** You cannot attach a comment to a specific range of text in a canvas programmatically. This is a permanent fidelity loss.

**What the Drive API returns for a comment:**

```json
{
  "id": "COMMENT_ID",
  "content": "This section needs a legal review.",
  "author": { "displayName": "Jane Smith" },
  "createdTime": "2024-11-15T09:23:00.000Z",
  "resolved": false,
  "quotedFileContent": {
    "value": "All users agree to the terms."
  },
  "replies": [
    {
      "content": "Flagged for legal team.",
      "author": { "displayName": "John Doe" },
      "createdTime": "2024-11-15T10:05:00.000Z"
    }
  ]
}
```

If your documents have active comment threads, pick a strategy:

- **Archive comments separately** — export them as structured JSON or CSV linked from the canvas. Preserves full fidelity, but requires a separate artifact.
- **Inline as callout blocks** — append comment text at the relevant position in the canvas using a blockquote or bold prefix (`> **[Jane Smith, 2024-11-15]:** This section needs a legal review.`). You lose threading and resolution status, but the content survives.
- **Append as endnotes** — extract comments via the Drive API, match the `quotedFileContent` to your markdown, and list them at the bottom with numbered references.
- **Accept the loss** — if comments are stale review artifacts with no operational relevance, let them go.

The safest pattern for documents with legally or operationally important review trails: **separate document content from review metadata.** Migrate accepted body text into Canvas. Put unresolved comments, replies, and suggestion summaries into an appendix canvas or a linked artifact. Do not retire the source Google Doc just because the prose moved.

This two-API dependency also means your migration script needs two independent rate-limit budgets: the Docs API quota for content and the Drive API quota for comments.

---

## Rate Limits on Both Sides

### Google APIs

Read requests are limited to **3,000 per minute per project** and **300 per minute per user per project**. Write requests are limited to **600 per minute per project** and **60 per minute per user per project**.

The per-user read limit of **300 requests/minute** is the constraint you will hit during bulk extraction. Each `documents.get` call counts as one read. If you are also calling the Drive API for comments, those count against Drive's separate quota (which has the same structure but is tracked independently).

If you are migrating thousands of documents and your script makes a `documents.get` call, a Drive comments call, and multiple image download calls per document, you will exhaust the 300 reads/minute quota rapidly. Google responds with `429 Too Many Requests` with a `Retry-After` header indicating the backoff period.

**To survive at scale:**

- **Exponential backoff.** When you receive a `429`, pause for 1 second, then 2, then 4, doubling up to a maximum of 64 seconds before failing the job.
- **Service account pooling.** For enterprise migrations, distribute read requests across multiple GCP service accounts using domain-wide delegation to impersonate different users. Each service account has its own 300 reads/minute per-user quota.
- **Concurrency control.** Do not use unbounded `Promise.all()` in Node.js. Use a concurrency limiter (like `p-limit`) capped at 5 concurrent in-flight requests as a starting baseline. Tune based on observed `429` frequency.

### Slack Canvas API

`canvases.create` is a **Tier 2** method: guaranteed at least **20 requests per minute**, with occasional burst allowance above that threshold. Do not design your pipeline around burst capacity — it is not guaranteed and the burst window is not documented by Slack. At exactly 20 canvases per minute, a library of 500 Google Docs requires at minimum **25 minutes** of pure create calls, assuming zero errors and zero retries.

`files.getUploadURLExternal` is Tier 3 (~50 requests/minute). `files.completeUploadExternal` is Tier 2 (~20 requests/minute). In image-heavy migrations, `files.completeUploadExternal` typically becomes the throughput ceiling before `canvases.create` does.

**What Slack returns when you exceed rate limits:**

```json
HTTP 429 Too Many Requests
{
  "ok": false,
  "error": "ratelimited"
}
```

The response includes a `Retry-After` header (in seconds). Honor it exactly — retrying before the window expires restarts the backoff period.

**What Slack returns for malformed markdown at canvas creation:**

```json
HTTP 400 Bad Request
{
  "ok": false,
  "error": "invalid_arguments"
}
```

Canvas does not return a line number or element-level pointer to the invalid markdown. If a canvas creation fails with `invalid_arguments`, binary-search your markdown payload — split it in half, attempt each half, and recurse until you isolate the problematic element.

For QA after loading, use `canvases.getContent` to read back the finished canvas as markdown, and `canvases.sections.lookup` to verify heading structure programmatically.

> [!NOTE]
> **Plan for 20 canvases per minute as your throughput ceiling for `canvases.create`, and 20 image completions per minute for `files.completeUploadExternal`.** Both are Tier 2. For image-heavy document libraries, calculate total image count across all documents and divide by 20 to estimate minimum image upload time independently of canvas creation time.

---

## When This Migration Makes Sense (and When It Does Not)

Migrating Google Docs to Slack Canvas makes sense when the goal is to surface reference content where teams already work — inside Slack channels. As detailed in our [architecture and limits comparison](https://clonepartner.com/blog/blog/slack-canvas-vs-google-docs-architecture-limits-and-migration), it does not make sense as a wholesale document management migration. Canvas is not a document management system; it is a lightweight collaborative surface with a 1 MiB content ceiling, no version history comparable to Google Docs, and no comment anchoring.

**Good candidates:** team wikis, runbooks, onboarding docs, meeting notes, process documentation — anything where the content matters more than the formatting and where active collaboration in Slack is the primary use pattern.

**Bad candidates:** legal documents with footnotes, deeply structured technical specifications with six heading levels, documents where comment history is legally or operationally required, any document with tables that exceed the 300-cell ceiling in ways that cannot be split, and any document whose structure depends on H4–H6 hierarchy for navigation.

For bad candidates, keep the document in Google Docs and use Canvas as the entry point — a summary, status panel, or index that links back to the source. The real value is access inside Slack, not perfect format conversion.

---

If you are evaluating other destinations for your Google Docs content, see [Google Docs Alternatives (2026): Features, Migration & TCO](https://clonepartner.com/blog/blog/google-docs-alternatives-2026-features-migration-tco). If you are planning a Quip move rather than a Docs move, see [Quip to Slack Canvases Migration: The Official Salesforce Path](https://clonepartner.com/blog/blog/quip-to-slack-canvases-migration-the-official-salesforce-path). For Atlassian environments, see our [Confluence to Slack Canvas migration guide](https://clonepartner.com/blog/blog/confluence-to-slack-canvas-migration-the-technical-guide).

---

> Migrating a library of Google Docs into Slack Canvas involves API orchestration, format translation, and deciding what to do with the content that will not survive the trip. ClonePartner has built migrations across 1,500+ projects — if you would rather not build and debug the pipeline yourself, we will handle it.
>
> [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 I migrate Google Docs to Slack Canvas automatically?

There is no native import path. You need to extract content via the Google Docs API or Drive export endpoint, transform it to markdown, and push it through Slack's canvases.create API. The entire pipeline must be custom-built or handled by a migration service.

### What do you lose migrating Google Docs to Slack Canvas?

Headers, footers, footnotes, section breaks, page margins, positioned images, suggested changes, and anchored comments are all permanently lost. Headings 4–6 must be flattened since Canvas only supports h1–h3. Tables over 300 cells must be split.

### What is the Slack Canvas character limit?

Slack Canvas document_content is limited to 1 MiB (1,048,576 characters) per object. Google Docs allows up to 1,024,000 characters. The ceilings are close, so long documents need post-transformation size checking since markdown syntax inflates character counts.

### How many cells can a Slack Canvas table have?

Canvas tables are capped at 300 cells per table, in any combination of rows and columns. Google Docs has no equivalent limit, so large tables must be split into multiple canvas tables during migration.

### Why do my migrated images show as broken links?

Google Docs API returns short-lived, authenticated contentUri links for images. You cannot embed these directly in Canvas markdown — they will expire. Download the images, upload them to Slack via files.getUploadURLExternal, and reference the new Slack-hosted permalink.
