---
title: "Slack Canvas to SharePoint Migration: The Complete Technical Guide"
slug: slack-canvas-to-sharepoint-migration-the-complete-technical-guide
date: 2026-10-02
author: Rishabh Makhar
categories: [Slack Canvas, SharePoint, Migration Guide]
excerpt: "How to migrate Slack Canvas to SharePoint: site page vs. document library decisions, metadata mapping, permissions, and API extraction mechanics."
tldr: "Slack Canvas to SharePoint is a reconstruction project: canvases carry no metadata, permissions are flat, and comments lose positional anchoring — plan for the gap, not just the content."
canonical: https://clonepartner.com/blog/slack-canvas-to-sharepoint-migration-the-complete-technical-guide
---

# Slack Canvas to SharePoint Migration: The Complete Technical Guide


# Slack Canvas to SharePoint Migration: The Complete Technical Guide

**There is no native migration path from Slack Canvas to SharePoint.** No import wizard, no connector, no "Move to SharePoint" button. Microsoft's SPMT supports SharePoint Server and file shares; Migration Manager supports Google, Dropbox, Box, and Egnyte; ShareGate lists file shares, Google Drive, and Box. Slack Canvas appears in none of those source lists. ([learn.microsoft.com](https://learn.microsoft.com/en-us/sharepointmigration/introducing-the-sharepoint-migration-tool))

Slack canvases are lightweight collaborative documents — markdown bodies with section-level comments, flat permissions, and zero structured metadata. SharePoint expects content types, site columns, managed metadata term sets, and `.aspx` pages rendered with `CanvasContent1` JSON or files organized in document libraries with rich column schemas. The [architectural gap between these two models](https://clonepartner.com/blog/blog/slack-canvas-vs-sharepoint-architecture-limits-and-migration) means every Slack Canvas migration is a reconstruction project, not a lift-and-shift.

This guide covers the decision that shapes the entire project — site page vs. document library file — then walks through the metadata gap, permissions mapping, extraction mechanics from the Slack API, upload constraints through Microsoft Graph, and the parts of a canvas that cannot be migrated at all.

> [!NOTE]
> **Last verified:** API behavior in this guide was checked against the Slack Web API and Microsoft Graph API v1.0 as of late 2025. Canvas API methods, Graph upload limits, and SharePoint page creation endpoints change between versions — confirm against current references before building a pipeline.

## Site Page or Document Library File — How to Decide

The first architectural decision is whether each canvas lands as a **modern SharePoint site page** (an `.aspx` page with web parts) or as a **file in a document library** (typically `.docx` or `.md`). Most real migrations end up with a mix. The right question is not what Slack Canvas is closest to, but what the content should become in SharePoint.

**Choose a site page when:**

- The canvas is a living reference document — onboarding guide, team wiki, process doc — that people browse in the browser
- You need the content indexed in SharePoint search as page content, not just as a file attachment
- You want to use SharePoint's page layout features: sections, columns, web parts for embedded lists or media

**Choose a document library file when:**

- The canvas is archival — meeting notes, project post-mortems, decision logs that rarely change
- You need versioning, check-in/check-out, or compliance labels on the document
- You want to attach rich column metadata (content types, managed metadata) without building a separate page for each item
- You have thousands of canvases and cannot justify per-page reconstruction

### The cost of each path

| Factor | Site Page (`.aspx`) | Document Library File (`.docx`/`.md`) |
|---|---|---|
| **Fidelity** | Highest — headings, tables, checklists render natively | Good — Word handles headings and tables; raw `.md` files render poorly in browser |
| **Metadata** | Page properties only; limited custom columns on the Site Pages library | Full content type and site column support on the library |
| **Automation** | Graph Pages API creates pages, but web part construction is manual | Graph driveItem upload is straightforward |
| **Scale** | Each page requires layout + web part JSON; slow to script at volume | Bulk upload is fast |
| **Searchability** | Full-text indexed as page content | Indexed as file content; metadata columns are filterable |

Converting a canvas to a site page requires parsing the Slack markdown and generating `CanvasContent1` JSON, mapping text to `TextWebParts` and images to `ImageWebParts`. Every section, column layout, and web part has to be constructed in code — there is no source web-part model in Slack to map directly. Because SharePoint page updates require the full `canvasLayout` rather than partial layout edits, page rendering logic deserves its own test cycle separate from the file upload code path. ([learn.microsoft.com](https://learn.microsoft.com/en-us/graph/api/sitepage-create?view=graph-rest-1.0))

A minimal `CanvasContent1` JSON structure for a single full-width text section looks like this:

```json
{
  "canvasLayout": {
    "horizontalSections": [
      {
        "layout": "oneColumn",
        "id": "1",
        "emphasis": "none",
        "columns": [
          {
            "id": "1",
            "width": 12,
            "webparts": [
              {
                "id": "a1de57b4-bf7a-4e06-b4b1-f38e00dc2a4b",
                "type": "rte",
                "data": {
                  "innerHtml": "<h2>Section Title</h2><p>Canvas paragraph text here.</p>"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}
```

Key field semantics: `layout` values are `oneColumn`, `twoColumns`, `threeColumns`, or `oneThirdLeftColumn`/`oneThirdRightColumn`. The `width` field on a column is a number from 1–12 following a 12-column grid — full width is 12, a two-column layout uses 6+6 or 8+4. The `type` field on a webpart distinguishes `rte` (rich text), `image`, `spacer`, and embedded app parts. Every webpart requires a unique GUID for `id` — generate these programmatically; reused GUIDs cause page render failures. The `innerHtml` inside `rte` web parts accepts standard HTML (headings, paragraphs, lists, tables) but not arbitrary JavaScript or custom elements.

For a checklist block from Slack, the migration choices are: (a) render as a static HTML unordered list with checkbox characters inside the `innerHtml`, (b) create a Microsoft To Do task list separately and embed the To Do web part by reference, or (c) create a Planner plan and embed the Planner web part. Options (b) and (c) require separate API calls to create the task data before the page can reference it. For archival canvases, static HTML is almost always sufficient.

Converting to a document library file is computationally cheaper. Slack returns a canvas as HTML or markdown via the `canvases.getContent` endpoint, and Pandoc handles markdown-to-`.docx` conversion for most elements — but with known gaps covered in the Pandoc fidelity section below. ([docs.slack.dev](https://docs.slack.dev/reference/methods/canvases.getContent/))

Most teams push archival canvases into a document library as `.docx` files and reconstruct the 20–50 "living" canvases as site pages by hand or with semi-automated scripts.

## The Metadata Problem: Canvases Have No Fields

**A Slack Canvas carries no structured metadata.** No tags, no categories, no custom fields, no content type. It has a title, a creator, a created timestamp, a last-modified timestamp, and that is it.

SharePoint is a metadata-driven platform. **Content types** are reusable collections of metadata and behavior for a category of content — a "Project Brief" content type might include columns for Department, Project Code, Status, and Document Owner. **Site columns** are reusable field definitions shared across lists and libraries. **Managed metadata** columns link to hierarchical term sets in the Term Store, where each term has a stable GUID. These structures define how content is organized, filtered, and governed.

This mismatch—which is equally challenging when [migrating from SharePoint back to Slack Canvas](https://clonepartner.com/blog/blog/sharepoint-to-slack-canvas-migration-metadata-permissions-guide)—means that every column value in your target SharePoint library must be **derived**, **defaulted**, or **entered by hand**. If you skip that design and start uploading first, required columns leave files checked in without publishing, invisible to end-users until an administrator fills in the missing required fields.

### Strategies for populating metadata

| Strategy | Works when | Limitation |
|---|---|---|
| **Derive from channel** | Canvases are organized by Slack channel and channel names map to departments or projects | Assumes clean channel naming; many canvases live in DMs or are standalone |
| **Derive from canvas title** | Titles follow a convention (e.g., "Q3 Sprint Retro — Platform Team") | Fragile; requires regex or NLP parsing |
| **Default all values** | You assign every imported canvas the same content type and default column values | Fast but defeats the purpose of metadata |
| **Manual entry** | A human reviews each canvas and fills in metadata post-migration | Accurate but does not scale past ~100 canvases |
| **Hybrid** | Derive what you can, default the rest, queue exceptions for human review | Most realistic for migrations above 200 canvases |

> [!WARNING]
> **Term Store GUIDs are created fresh on import.** If you create new term sets during migration, every term gets a new GUID in the target tenant. Downstream systems that reference terms by GUID — Power Automate flows, custom SPFx web parts, search-driven pages — will break unless you update those references. Plan term creation as a separate, pre-migration step and maintain a crosswalk from source labels to target term IDs.

If your SharePoint environment uses a strict taxonomy, your migration script must query the Microsoft Graph API to resolve term names to their specific GUIDs before executing the upload payload. Treat the SharePoint term GUIDs as new destination-side identifiers.

## How Do You Map Slack Canvas Permissions to SharePoint?

Slack Canvas permissions are flat: a canvas has an **owner**, and other users or channels receive **read** or **write** access via the `canvases.access.set` method. Only users can be owners — channels can receive read or write but not owner. Channel-linked canvases inherit the channel's membership as their default audience. Standalone canvases shared via link may have workspace-wide visibility depending on sharing settings.

SharePoint permissions are hierarchical: site-level, library-level, and item-level, governed by SharePoint groups, Azure AD (Entra ID) groups, and individual grants.

### The identity mapping problem

**Identity mapping** is the programmatic process of matching a Slack user ID to a Microsoft Entra ID User Principal Name (UPN) or Object ID. Before you can set any SharePoint permission, you need this mapping built — and it is rarely one-to-one:

- **Email mismatch:** Slack accounts may use personal emails, aliases, or vanity domains; Microsoft tenant accounts use the organization's UPN
- **Guest accounts:** Slack guests and multi-channel guests may not have a corresponding Azure AD identity at all
- **Service accounts and bots:** Slack bot users and integration accounts have no Microsoft equivalent
- **Departed users:** Slack accounts for former employees may still own canvases, but the Azure AD account is disabled or deleted

Build the identity map early. Export Slack users via `users.list`, export Azure AD users via Graph's `/users` endpoint, and match on primary email. Every unmatched Slack user is a permission you either drop, reassign to a fallback account, or flag for manual resolution.

**Enterprise Grid note:** On Enterprise Grid workspaces, `users.list` requires organization-level token scopes and returns users across all workspaces in the org. Permissions and canvas ownership can span org boundaries in ways that standard workspace exports do not capture. Use the Discovery API or Audit Logs API for complete permission data in Enterprise Grid environments — the standard `files.list` and `canvases.access.set` endpoints do not expose org-level sharing state.

### Mapping access levels to SharePoint

| Slack Access | SharePoint Equivalent | Notes |
|---|---|---|
| **owner** | Full Control on the library or site | SharePoint has no single "owner" on a document — ownership is approximated with Full Control |
| **write** | Contribute or Edit permission level | Contribute allows add/edit/delete on items |
| **read** | Read permission level | Straightforward |
| **channel membership (implicit)** | SharePoint group or Azure AD security group membership | Requires mapping the Slack channel's member list to a group |

Slack also supports a **comment-only** access level in its UI — users who can view and comment but not edit. SharePoint does not have a clean equivalent for migrated page or file content. A modern SharePoint page supports page-level comments, but that is not the same as Slack's section-anchored comment model. You need a policy decision for this edge case. ([slack.com](https://slack.com/help/articles/15678967614611-Manage-access-permissions-for-canvases-and-lists))

### Channel-level vs. item-level permissions

In Slack, a canvas shared in a channel inherits that channel's membership. In SharePoint, you have two approaches:

1. **Site-level inheritance:** Map the Slack channel to a Microsoft 365 Group (Team). Migrate the canvases to that Group's underlying SharePoint site. The canvases inherit the site's permissions. This is the recommended, scalable approach.
2. **Item-level permissions:** Break inheritance on the specific file and explicitly grant access to the individual users who were in the Slack channel.

> [!CAUTION]
> **Item-level permissions are expensive in SharePoint.** Microsoft recommends staying under 50,000 unique permission scopes per library. If every canvas gets its own permission set, you will hit governance and performance degradation fast. Group canvases by access profile into separate libraries or folders and apply permissions at the library or folder level.

## How to Extract Canvas Content from Slack

### Inventory: there is no canvases.list

Slack does not expose a dedicated `canvases.list` endpoint. Whether you are moving to SharePoint or [migrating Slack Canvas to Confluence](https://clonepartner.com/blog/blog/slack-canvas-to-confluence-migration-apis-limits-mapping), the only programmatic inventory path is `files.list` filtered to `types=canvas`. This returns canvas metadata — ID, title, creator, timestamps — paginated in the standard Slack way. ([api.slack.com](https://api.slack.com/methods/files.list))

```bash
curl -s "https://slack.com/api/files.list?types=canvas&count=100" \
  -H "Authorization: Bearer $SLACK_TOKEN" \
  -H "Content-Type: application/json"
```

The `files.list` method is **Tier 3** rate-limited. Slack documents Tier 3 as "50+ per minute" — the floor is 50 requests per minute, but actual limits are workspace-dependent and not published as a fixed ceiling. For large workspaces with thousands of canvases, implement exponential backoff on HTTP 429 responses and respect the `Retry-After` header value.

**Enterprise Grid:** On Enterprise Grid, canvas inventory requires Discovery API access (`discovery:read` scope), which is granted at the organization level, not the workspace level. Standard `files.list` with a workspace token will not enumerate canvases owned by users in other workspaces within the same org.

### Reading canvas content

The `canvases.getContent` method returns the full body of a canvas as either markdown or HTML. The default format is markdown, matching the format accepted by `canvases.create` and `canvases.edit`. The method requires the `canvases:read` scope. It is also Tier 3 rate-limited — plan the same 50+ rpm constraint and 429 backoff logic as `files.list`. ([docs.slack.dev](https://docs.slack.dev/reference/methods/canvases.getContent/))

```bash
curl -s -X POST "https://slack.com/api/canvases.getContent" \
  -H "Authorization: Bearer $SLACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"canvas_id": "F1234ABCD", "content_type": "markdown"}'
```

The response contains the entire canvas in a single `content` string with no pagination. Slack caps each `document_content` object at **1 MiB**.

**Handling 429 responses from canvases.getContent:**

```python
import time, requests

def get_canvas_content(canvas_id, token, max_retries=5):
    url = "https://slack.com/api/canvases.getContent"
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    payload = {"canvas_id": canvas_id, "content_type": "markdown"}

    for attempt in range(max_retries):
        resp = requests.post(url, json=payload, headers=headers)
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        data = resp.json()
        if not data.get("ok"):
            raise ValueError(f"Slack API error: {data.get('error')}")
        return data["content"]
    raise RuntimeError(f"Max retries exceeded for canvas {canvas_id}")
```

### What the markdown actually contains

Canvas markdown is a **supported subset**, not full CommonMark or GFM. The elements available are:

- **Headings: h1, h2, and h3 only** — no h4 through h6. Any nested logical structure relying on deeper heading levels will flatten during extraction.
- Bold, italic, strikethrough
- Bulleted and ordered lists
- Checklists (task lists with checkboxes)
- Code blocks, block quotes, and dividers
- Links and @mentions
- **Tables: capped at 300 cells**, with no formula support, no merged cells — plain-text grids only. Exceeding this cell count truncates the data.

Images and file attachments embedded in a canvas are referenced by URL, not inlined as base64. Download those assets separately using the `url_private_download` field from `files.info`.

### Pandoc conversion fidelity

Pandoc (`pandoc input.md -o output.docx`) handles most canvas markdown cleanly, but with documented gaps:

| Canvas Element | Pandoc Conversion to `.docx` | Notes |
|---|---|---|
| Headings (h1–h3) | ✅ Clean | Maps to Word Heading 1–3 styles |
| Bold, italic, strikethrough | ✅ Clean | Standard character formatting |
| Bulleted and ordered lists | ✅ Clean | Nested lists preserved |
| Checklists (task lists) | ⚠️ Degraded | Pandoc renders `- [ ]` as a plain hyphen + brackets in some `.docx` backends; test your Pandoc version. Use `--reference-doc` with a custom Word template to improve output |
| Code blocks | ✅ Clean | Renders as `Code Text` paragraph style |
| Block quotes | ✅ Clean | Renders as block quote paragraph style |
| Tables | ⚠️ Conditional | Tables with merged cells or complex alignment may lose formatting; Slack tables have no merges, so standard Slack tables convert cleanly. Verify row/column counts post-conversion |
| Links | ✅ Clean | Hyperlinks preserved |
| @mentions | ❌ Lost | Pandoc passes through Slack-format mentions (`<@U12345>`) as literal text; pre-process to resolve to names before conversion |
| Images (by URL) | ❌ Not inlined | Pandoc does not download remote images; pre-download and reference local paths in the markdown before conversion |
| Dividers (`---`) | ✅ Clean | Renders as horizontal rule |

To produce the cleanest `.docx` output, pre-process the markdown to: (1) replace `<@UXXXXXXX>` mentions with resolved display names, (2) download images to a local directory and rewrite image URLs to local paths, (3) strip any Slack-specific syntax not in the subset above. Then run Pandoc with `--from=markdown+task_lists --to=docx`.

### Canvas comments do not migrate cleanly

Canvas comments are anchored to sections in the UI — you click the six-dot icon on a block and add a comment, and the resulting thread appears visually attached to that section. But in the API, these comments are stored as **channel messages** (thread replies on a file conversation), not as inline annotations on the markdown body.

There is no stable inline anchor ID in the markdown content that maps to a specific comment thread. You can export the comments, and you can export the canvas body, but you cannot programmatically reconstruct which comment was attached to which paragraph. That positional anchoring is visual-only in Slack's UI and does not survive extraction. ([slack.com](https://slack.com/help/articles/15708101445011-How-data-management-features-apply-to-canvases-and-lists))

On the SharePoint side, modern site pages support page-level comments but not inline section-level comments. Document library files support no inline commenting natively. Either way, the comment-to-section relationship is lost.

During migration, comments must be handled as one of:

- A static text log appended to the bottom of the SharePoint page or document
- Separate JSON or HTML evidence files stored alongside the migrated canvas
- Messages in a dedicated Microsoft Teams channel

### Version history

`canvases.getContent` returns the current state only. Canvas version history is not exposed via the standard API for bulk programmatic extraction. If version history is a compliance requirement, it must be sourced from Slack's full workspace export (available to workspace admins) or the Discovery API on Enterprise Grid, which includes prior document states in the export package.

## Uploading to SharePoint Through Microsoft Graph

### File uploads: the 250 MB boundary

Microsoft Graph's simple upload endpoint (`PUT /drive/items/{id}/content`) supports files up to **250 MB** in a single request. For anything larger, use a **resumable upload session** via `createUploadSession`, which supports files up to 250 GB. Chunk size must be a **multiple of 320 KiB** (327,680 bytes) — misaligned chunk sizes cause commit errors at the final merge step.

For canvas-derived `.docx` or `.md` files, the 250 MB single-request limit is almost never the bottleneck. Canvases with heavily embedded video assets or large images could exceed it, but that is uncommon.

```bash
# Simple upload for files under 250 MB
PUT https://graph.microsoft.com/v1.0/sites/{site-id}/drives/{drive-id}/root:/{filename}:/content
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
Authorization: Bearer {token}

{binary file content}
```

**Common failure modes on upload:**
- `HTTP 401 Unauthorized`: The token lacks `Files.ReadWrite.All` or `Sites.ReadWrite.All` scope, or the token has expired mid-batch.
- `HTTP 403 Forbidden`: The target library requires a content type the uploaded item does not satisfy, or the uploading account lacks Contribute permission on the library.
- `HTTP 409 Conflict`: A file with the same name already exists and the request did not specify a conflict behavior. Add `@microsoft.graph.conflictBehavior=rename` or `replace` to the upload URL query string.
- `HTTP 423 Locked`: The file is checked out by another user. Use `_api/web/lists/.../items/.../uncheckout` via SharePoint REST before retrying.

### Preserving created and modified timestamps

When you upload a file through Graph, the service stamps `createdDateTime` and `lastModifiedDateTime` with the current time. To preserve the original Slack timestamps, write the `fileSystemInfo` facet on the `driveItem`:

```json
{
  "fileSystemInfo": {
    "createdDateTime": "2024-03-15T10:30:00Z",
    "lastModifiedDateTime": "2025-06-20T14:22:00Z"
  }
}
```

The `fileSystemInfo` properties are read/write — you can set them on upload or update them afterward via `PATCH`. The values on the `driveItem` resource itself (`createdDateTime`, `lastModifiedDateTime`) are service-controlled and read-only. The `fileSystemInfo` values are what surface in document library views when configured. ([learn.microsoft.com](https://learn.microsoft.com/en-us/graph/api/resources/baseitem?view=graph-rest-1.0))

### Author and modifier identity: a harder problem

The `createdBy` and `lastModifiedBy` fields on a `driveItem` are **read-only** in the Graph API. There is no direct Graph endpoint to set the original author or last modifier of an uploaded file. The file will show the service principal or the user account that performed the upload as the creator.

The workaround uses the **SharePoint REST API** — specifically `ValidateUpdateListItem` — to write the `Author` and `Editor` fields on the underlying list item:

```json
POST https://{tenant}.sharepoint.com/sites/{site}/_api/web/Lists/GetbyTitle('Documents')/items({id})/ValidateUpdateListItem()

{
  "formValues": [
    {
      "FieldName": "Author",
      "FieldValue": "[{'Key':'i:0#.f|membership|user@tenant.onmicrosoft.com'}]"
    },
    {
      "FieldName": "Editor",
      "FieldValue": "[{'Key':'i:0#.f|membership|user@tenant.onmicrosoft.com'}]"
    },
    {
      "FieldName": "Created",
      "FieldValue": "2024-03-15T10:30:00Z"
    },
    {
      "FieldName": "Modified",
      "FieldValue": "2025-06-20T14:22:00Z"
    }
  ]
}
```

This requires the uploading account to have site collection administrator rights or Full Control on the list. If the uploading account has only Contribute, the `Author` and `Editor` fields will silently ignore the override — no error is returned, but the values do not change. Confirm the permission level before running Author/Editor updates in bulk.

The target user identity must already exist in the tenant — another reason the identity map must be built before migration begins.

**Common failure modes for ValidateUpdateListItem:**
- `HTTP 403`: Insufficient permissions on the list. Requires site collection admin or Full Control, not just Contribute.
- `"fieldName":"Author","hasException":true` in the response body (HTTP 200 with partial failure): The identity key format is wrong. The `i:0#.f|membership|` prefix is required for standard member accounts. Guest accounts use `i:0#.f|membership|` only if they are B2B guests with a UPN in the tenant. External users without a tenant identity cannot be set as Author.
- Silent no-op on `Created`/`Modified`: Some SharePoint configurations protect system timestamp fields. Verify by reading the item back immediately after the PATCH.

If you skip this step, every migrated canvas appears as if it was created by the service principal on the day of the cutover, destroying your audit trail and breaking search relevance.

### PnP PowerShell alternative path

For SharePoint practitioners who work primarily in PowerShell, PnP PowerShell covers most of the upload and metadata-setting workflow without requiring raw REST calls:

```powershell
# Connect to the target site
Connect-PnPOnline -Url "https://tenant.sharepoint.com/sites/target" -Interactive

# Upload file
Add-PnPFile -Path ".\canvas-export.docx" -Folder "Shared Documents/MigratedCanvases"

# Set metadata including Author and timestamps
Set-PnPListItem -List "Documents" -Identity 42 -Values @{
    "Author"   = "user@tenant.onmicrosoft.com"
    "Editor"   = "user@tenant.onmicrosoft.com"
    "Created"  = "2024-03-15T10:30:00Z"
    "Modified" = "2025-06-20T14:22:00Z"
    "ContentType" = "Project Brief"
}
```

PnP PowerShell's `Set-PnPListItem` calls `ValidateUpdateListItem` internally. The same permission requirements apply — site collection admin or Full Control. PnP also provides `Set-PnPPage` for site page creation, which wraps the Graph Pages API and simplifies some of the `CanvasContent1` construction for common layouts.

### Creating site pages via Graph

The SharePoint Pages API in Graph v1.0 supports creating modern site pages programmatically. You define the page layout, title area, and canvas layout with horizontal sections and web parts. The body content goes into `TextWebPart` objects within the canvas layout.

Specific mapping examples from Slack markdown to SharePoint page structure:

- A heading + paragraph block becomes a `TextWebPart` with the corresponding HTML inside a `horizontalSection` with `layout: oneColumn` and column `width: 12`
- A checklist becomes a static HTML unordered list inside a `TextWebPart` (archival use), or a Planner web part (interactive use, requires a separate Planner plan created via the Planner API first)
- A canvas table becomes an HTML `<table>` inside a `TextWebPart`, or an embedded list web part pointing at a separately created SharePoint list
- An image becomes an `ImageWebPart` with the re-hosted image URL (Slack image URLs expire; images must be downloaded and re-uploaded to SharePoint or another durable host before the page is created)

None of these mappings are automatic. Every web part requires a unique GUID for its `id` field — reused or empty GUIDs cause page render failures that surface as blank sections with no error message in the UI.

## What Gets Lost: An Honest Inventory

Be direct with stakeholders about what this migration cannot preserve:

| Canvas Feature | Migrates? | Notes |
|---|---|---|
| **Body text** (headings, paragraphs, lists) | ✅ Yes | Markdown converts cleanly to HTML or DOCX |
| **Tables** (up to 300 cells, no formulas) | ✅ Yes | Render as HTML tables or Word tables |
| **Checklists** | ⚠️ Partial | Become static checkboxes in DOCX; no interactivity without Planner/To Do integration on site pages |
| **Embedded images** | ✅ Yes | Must be downloaded separately and re-uploaded; URLs change |
| **Code blocks** | ✅ Yes | Render as preformatted text; no syntax highlighting in SharePoint text web parts |
| **@mentions** | ❌ No | Slack user references have no SharePoint equivalent; become plain text after pre-processing |
| **Section-level comments** | ❌ No | Anchoring is lost; comments export as a flat list but cannot be re-attached to specific sections |
| **Comment threads** | ⚠️ Partial | Thread text can be archived; positional context is lost |
| **Version history** | ❌ No | Not exposed via standard API; requires full workspace export or Discovery API |
| **Canvas-specific permissions** | ⚠️ Manual | Must be re-created in SharePoint using identity-mapped grants |
| **Real-time collaboration state** | ❌ No | Runtime feature, not data |
| **Embedded Slack workflows** | ❌ No | Workflow Builder automations do not migrate; rebuild in Power Automate |
| **Third-party embeds** (e.g., Jira tickets) | ❌ No | Authentication contexts are disconnected; degrade to static hyperlinks |

The body text and tables make it across. The collaboration context — who commented where, who was editing, what automations ran — does not.

## Step-by-Step Migration Workflow

### 1. Build the identity map

Export Slack users via `users.list`. Export Azure AD users via Microsoft Graph `/users`. Match on primary email. Flag unmatched accounts for manual resolution or fallback assignment. On Enterprise Grid, use organization-level token scopes and supplement with Discovery API data.

### 2. Inventory all canvases

Call `files.list?types=canvas` with pagination to get every canvas ID, title, creator, and timestamp. Record which channel each canvas is associated with from the `channels` array in the file metadata. Classify each canvas as a site page target or a document library file target.

### 3. Extract canvas content and comments

For each canvas, call `canvases.getContent` with Tier 3 rate-limit backoff. Use `files.info` to get full metadata including `url_private_download` for embedded images. Export comments from file-conversation data separately. For Enterprise Grid permission data, use the Discovery API or Audit Logs API rather than relying on `canvases.access.set` response data alone, as org-level sharing state is not visible from workspace-scoped tokens.

### 4. Prepare the SharePoint target

Create the destination site, libraries, content types, site columns, and term sets **before** importing any content. Populate the Term Store with your taxonomy and record the GUID crosswalk. Assign permissions at the library or site level to avoid per-item permission sprawl. Set content type defaults on target libraries so uploaded files inherit correct metadata without requiring per-item updates.

### 5. Convert and upload

- **For document library files:** Pre-process markdown (resolve @mentions, download and relink images). Convert to `.docx` using Pandoc with `--from=markdown+task_lists --to=docx`. Upload via Graph with `fileSystemInfo` timestamps. Set Author/Editor via SharePoint REST `ValidateUpdateListItem` (requires site collection admin). Apply content type and column values. Verify Author/Editor update was not silently dropped.
- **For site pages:** Build `CanvasContent1` JSON from the markdown. Assign unique GUIDs to every web part. Create the page via Graph Pages API. Publish. Verify each section renders — blank sections indicate a web part GUID collision or malformed `innerHtml`.

### 6. Validate

Spot-check body content, table rendering, image links, timestamps, author attribution, and permissions. Run automated comparison scripts against source markdown to flag mismatches. Verify that managed metadata columns resolve correctly and that search indexing picks up the new content within the expected crawl window (typically 15 minutes to 4 hours for new SharePoint content).

## When Not to Do This Migration

Not every Slack Canvas migration is worth the reconstruction cost:

- **If you have fewer than 50 canvases**, manual copy-paste into SharePoint pages is likely faster and cheaper than building a migration pipeline with rate-limit handling, Pandoc pre-processing, and `ValidateUpdateListItem` calls.
- **If the canvases are ephemeral** — sprint notes, daily standups, meeting agendas never referenced after the meeting — archive to PDF and drop into a document library as read-only records rather than reconstructing them as live pages.
- **If you are migrating the entire Slack workspace** to Microsoft Teams, canvas content may be better handled as part of a broader Teams/SharePoint migration where channel-linked canvases become tabs in the associated Teams channel, preserving the channel-to-site relationship.

## Summary: What This Migration Actually Requires

Slack Canvas to SharePoint is not a migration you automate once and forget. The structural gap between a flat markdown document with no metadata and a metadata-rich SharePoint environment means every project involves real decisions about information architecture, taxonomy design, and permission governance.

The extraction side — `files.list`, `canvases.getContent`, image download — is well-documented and API-accessible with standard rate-limit handling. The reconstruction side is where the effort lives: building valid `CanvasContent1` JSON with correctly assigned web part GUIDs, resolving identity maps before setting permissions, populating managed metadata with pre-created term GUIDs, and using `ValidateUpdateListItem` with site collection admin rights to preserve author attribution.

The body text and tables migrate. The positional comments, version history, @mention semantics, embedded workflows, and third-party integrations do not. Set those expectations before the project starts, not after the first validation pass.

## Frequently asked questions

### Is there a native way to migrate Slack Canvas to SharePoint?

No. Microsoft SPMT, Migration Manager, and ShareGate do not list Slack Canvas as a supported source. You must extract canvas content via the Slack API (files.list filtered to type canvas and canvases.getContent) and load it into SharePoint via Microsoft Graph or SharePoint REST.

### Should a Slack Canvas become a SharePoint page or a document library file?

It depends on the use case. Living reference docs (wikis, onboarding guides) work best as modern site pages. Archival content (meeting notes, decision logs) is easier to bulk-import as .docx files in a document library. Most migrations use a mix of both.

### How do you preserve Slack Canvas timestamps and author data in SharePoint?

Use the fileSystemInfo facet on the driveItem when uploading via Microsoft Graph to set createdDateTime and lastModifiedDateTime. For the Author and Editor fields, which are read-only in Graph, use the SharePoint REST API's ValidateUpdateListItem method to write them on the underlying list item.

### Do Slack Canvas comments migrate to SharePoint?

The text of comments can be exported, but the section-level anchoring is lost. Slack stores canvas comments as channel thread messages with no inline anchor IDs in the markdown, so you cannot programmatically reconstruct which comment was attached to which paragraph.

### What Slack Canvas content is lost during migration to SharePoint?

Version history, @mentions (become plain text), section-level comment anchoring, real-time collaboration state, embedded Slack workflows, third-party embeds like Jira tickets (degrade to static links), and canvas-specific permissions (must be manually reconstructed).
