Guru to Slack Canvas Migration: API Limits & Trust State
Guru to Slack Canvas migration guide covering trust state loss, API extraction, HTML-to-Markdown conversion, image re-hosting, and structural mapping.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Guru to Slack Canvas Migration: API Limits & Trust State
Migrating from Guru to Slack Canvas is not a format conversion — it is a trust-model downgrade. Every Guru Card carries a verifier, a verification interval, a last-verified date, and a visible trust indicator that tells readers whether information has been reviewed recently. Slack Canvas has no verification workflow, no card expiry, and no staleness signal of any kind. If your team's primary reason for using Guru is its verification lifecycle, Canvas is the wrong destination — and this guide will help you understand exactly why, what workarounds exist, and how to run the migration if you decide to proceed anyway.
There is no native migration path between these platforms. You must extract Guru's HTML payload, convert it to a restricted Markdown subset, re-host all assets on Slack's infrastructure, and pipe results into Slack's API while navigating strict structural limits — one channel canvas per channel, 1 MiB per document_content object, headings capped at h3, 300 cells per table.
This guide covers what trust state is and why Canvas cannot hold it, the structural mapping from Guru's hierarchy to Slack's channel model, extraction constraints of the Guru API, the content-conversion pipeline from HTML to Canvas Markdown, image re-hosting with concrete performance estimates, permission mapping, and the honest trade-offs of abandoning Guru's trust engine.
What Is Trust State and Why Can't Canvas Hold It?
Trust state is the combination of verification status, assigned verifier, verification interval, last-verified date, and trust indicator that Guru attaches to every Card. At the API level, a Guru card object includes:
verificationState—TRUSTED,NEEDS_VERIFICATION, orSTALEverificationInterval— frequency in days; passingnulldefaults to 30 daysverifier— individual or group responsible for re-verificationlastVerifiedandlastVerifiedBy— timestamp and user identity
Guru's verification workflow notifies the assigned verifier via Slack, email, or the web app when a Card is due for review. Verification requires one click if the content is still accurate, or an update-and-re-verify cycle if it is not. (help.getguru.com)
Slack Canvas has none of this. There is no metadata layer on a canvas, no scheduled review, no owner-as-verifier concept, no trust badge, and no API field that could store any of it. A canvas is a Markdown document with an author, comments, permissions, and a last-edited timestamp. That is the entire data model.
What Replacing Verification Actually Costs
If you migrate to Canvas and still need content freshness signals, three workarounds exist — none as effective as what you are leaving:
- Review convention written into each canvas. Add a "Last reviewed" line and owner name at the top of every canvas. This requires human discipline and provides no automated alerting. It is easily ignored and invisible to Slack search automation.
- Recurring Slack reminders or Workflow Builder triggers. Set a scheduled reminder in each channel that pings a designated reviewer on a cadence. This approximates Guru's interval-based notifications but has no connection to the canvas itself — there is no "verified" state to flip, no audit record that review happened.
- Accept that content goes unverified. For reference material that changes rarely (office addresses, brand guidelines), this may be acceptable. For fast-moving content (pricing, competitive intel, compliance policies), it is a risk your team needs to consciously accept and document.
Teams whose primary Guru value is verification — where trust scores, verifier assignments, and scheduled review cycles drive day-to-day operations — should seriously question whether Canvas is the right destination. Platforms like Notion or Discourse expose metadata fields that can approximate verification. Canvas does not.
A practical pattern that works for teams who proceed: make the first lines of every migrated canvas a review contract. If those lines are blank six months later, you have your answer about whether the content is still governed.
**Former verifier:** Revenue Ops
**Former verification interval:** 90 days
**Last verified in Guru:** 2026-08-14
**Former verification state:** TRUSTED
**Tags:** pricing, renewal
**Migration note:** This canvas is not automatically re-verified in Slack.That header block is not a substitute for native trust state. It is honest labeling. It also preserves the verificationState value as a human-readable record, which matters if you ever need to audit which content arrived verified versus stale.
Should You Migrate Guru into Slack Canvas?
The answer depends on why your team uses Guru.
- Canvas works well for lightweight runbooks, project briefs, meeting notes, and channel landing pages — content that lives close to conversations and does not require formal governance.
- Canvas is a poor fit for regulated knowledge, support macros, policy docs, and anything where people act differently based on whether the content is verified.
- Consider a hybrid when Guru remains the verified source of truth and Canvas becomes the access layer — linking out to Guru cards rather than duplicating content.
If your Guru instance devolved into a static wiki where verification was routinely ignored and your goal is simply to put documentation closer to where your team chats, Canvas is effective. If the green Guru checkmark changes behavior in your org — support uses it before replying, sales uses it before sending collateral, ops uses it before executing a process — moving into Canvas is a governance downgrade unless you replace that behavior with explicit process.
How Does Guru's Hierarchy Map to Slack's Channel-and-Canvas Model?
Guru organizes content using a Collection → Folder → Card structure. Guru previously used Board Groups, Boards, and Sections, but has since migrated all teams to a foldered structure — those legacy elements are now folders. Folders nest up to three levels deep within Collections. Legacy terminology still appears in collection exports and Guru Query Language, where field names like boards and boardCount still operate on folders.
Slack Canvas is a document surface inside Slack, not a knowledge base. Content lives in two forms:
- Channel canvas: a single canvas pinned to a channel, created via
conversations.canvases.create. One channel, one channel canvas — that is a hard constraint. A second call toconversations.canvases.createfor the same channel returnschannel_canvas_already_exists. (api.slack.com) - Standalone canvas: created via
canvases.create, optionally shared to a channel via thechannel_idparameter. (api.slack.com)
Since April 2025, Slack has been converting channel and DM canvases toward canvases in tabs, and conversations can hold up to 15 tabs for canvases, lists, workflows, messages, and files.
Why You Cannot Map Each Folder to a Channel Canvas
The naive mapping — one Guru folder per Slack channel, with its cards combined into the channel canvas — breaks immediately. A channel holds exactly one channel canvas. If a folder contains 15 cards, you cannot create 15 channel canvases.
| Guru structure | Slack target | Trade-off |
|---|---|---|
| Collection | Slack channel | One channel canvas as index; cards become standalone canvases |
| Top-level folder | Slack channel | Works if folder count is manageable; cards become standalone canvases |
| Card | Standalone canvas | Preserves 1:1 mapping; loses positional hierarchy |
| Card | Section within one large canvas | Keeps content together but unwieldy for large folders; risks 1 MiB limit |
The cleanest pattern for most teams: one Slack channel per Collection, with the channel canvas serving as an index page linking to standalone canvases (one per card). Folders become heading sections within the index canvas. For deeply nested structures, consider sub-channels in a channel group.
API constraint: conversations.canvases.create creates one channel canvas. A second call fails with channel_canvas_already_exists. For idempotent migrations, check channel.properties.canvas from conversations.info before creating anything. Do not loop conversations.canvases.create over boards and hope the second call succeeds — it will not.
How shareStatus Maps to Canvas Permissions
Guru's shareStatus field controls card visibility and has the following values:
TEAM— visible to all members of the Guru teamCOLLECTION— visible only to members of the parent CollectionPRIVATE— visible only to the card owner (draft state)
Slack Canvas access is controlled per canvas via canvases.access.set, which accepts a list of principals (users, user groups, or channels) and an access_level of read or write. (api.slack.com)
The mapping is approximate:
Guru shareStatus |
Canvas equivalent | Notes |
|---|---|---|
TEAM |
No access restriction, canvas shared to team channel | Default for open org canvases |
COLLECTION |
canvases.access.set with Collection member user group |
Requires a Slack user group mirroring Collection membership |
PRIVATE |
Canvas created but not shared to any channel | Visible only to the creating user until access is granted |
If your org uses Guru Collection-level access control as a security boundary — not just an organizational tool — you must pre-provision corresponding Slack user groups and call canvases.access.set for each canvas. Skipping this step after migrating COLLECTION-scoped cards means previously restricted content becomes visible to your entire workspace. Audit shareStatus before migration begins, not after.
Custom fields and tags — which have no field model in Slack Canvas — must be flattened into prose or index rows at the top of standalone canvases.
How Do You Extract Cards from the Guru API?
Endpoints and Pagination
The primary extraction endpoint is GET /api/v1/search/query. (developer.getguru.com) It returns card objects including title, HTML content, verification metadata, tags, and folder membership. Guru also provides GET /api/v1/cards/ for direct card listing and GET /api/v1/search/cardmgr for Card Manager-style queries.
Key constraints:
- Page size:
maxResultscaps at 50 per request. Responses with additional results include aLinkheader (rel="next-page") with a cursor token for the next page. When a response has noLinkheader, you have retrieved all results. - Rate limits: Guru does not publicly document specific numeric rate limits for its REST API. In practice, sustained bursts trigger HTTP 429 responses. Build in exponential backoff and honor any
Retry-Afterheader your tenant emits. Self-throttle, parallelize conservatively, and respond to what the API tells you — do not hardcode a ceiling. - Content format: card content is returned as HTML in the
contentfield. You cannot request Markdown directly from the API. - Authentication: HTTP Basic Auth using a User token (read/write across the account) or a Collection token (read-only, scoped to one collection). A User token is required for full card access across collections.
Guru also offers collection exports that produce a ZIP containing cards, folders, resources, and collection.yaml. The ZIP structure is:
collection-export.zip
├── collection.yaml # Collection metadata: id, name, color, collectionType
├── cards/
│ └── {cardId}.html # Card HTML content; filename is the card's GUID
├── folders/ # Folder metadata as JSON files
│ └── {folderId}.json
└── resources/ # Embedded image files referenced in card HTML
└── {fileId}.{ext}
collection.yaml contains collection-level metadata (id, name, slug, color, collectionType) but does not include per-card verification metadata — verificationState, verificationInterval, lastVerified, and lastVerifiedBy are only available through the REST API. If verification history matters, you must extract it via API regardless of whether you also use the ZIP export.
Collection exports do not include archived cards. Handle archived content extraction explicitly through the API if it matters for your migration.
import requests
import time
import re
import base64
def encode_credentials(email, api_token):
credentials = f"{email}:{api_token}"
return base64.b64encode(credentials.encode()).decode()
def get_guru_cards(api_token, user_email):
headers = {
"Accept": "application/json",
"Authorization": f"Basic {encode_credentials(user_email, api_token)}"
}
cards = []
url = "https://api.getguru.com/api/v1/search/query?maxResults=50"
while url:
response = requests.get(url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
time.sleep(retry_after)
continue
data = response.json()
cards.extend(data)
# Follow cursor-based pagination via Link header
link_header = response.headers.get("Link", "")
next_match = re.search(r'<([^>]+)>;\s*rel="next-page"', link_header)
url = next_match.group(1) if next_match else None
return cardsWhat a Card Object Contains
A card response includes the fields you need for migration and the verification fields you cannot port:
{
"preferredPhrase": "What is our refund policy?",
"content": "<p>Our refund policy allows...</p>",
"verificationState": "TRUSTED",
"verificationInterval": 90,
"lastVerified": "2026-06-15T14:30:00.000+0000",
"lastVerifiedBy": { "email": "jane@company.com" },
"verifier": { "email": "jane@company.com" },
"tags": [{ "value": "billing" }],
"shareStatus": "TEAM",
"collection": { "name": "Support" }
}You can sort by verificationState, verificationInterval, lastVerified, and other fields using the sortField parameter. This is useful for pre-migration auditing — run a pass to identify stale cards before migrating content nobody has looked at in a year.
Guru Query Language for Pre-Migration Auditing
Guru Query Language supports filtering by verification state, interval, folder membership (through the legacy boards field), date, tags, and creator. Use it to inventory risk before migration:
verificationState != trusted
verInterval = 30
boards CONTAINS ("folder-id")
creatorId = "user@company.com"Card Versions and Verification History Are Read-Only
Guru exposes a Historical Card Versions endpoint (/cards/{cardId}/versions), but this data is read-only and cannot be written to Canvas. Canvas has no version history API, no audit log for content changes, and no mechanism to record prior review dates. If your compliance posture requires an audit trail of who verified what and when, export verification history to a spreadsheet or database before migration — Slack Canvas will not hold it.
How Do You Convert Guru HTML to Canvas Markdown?
Slack Canvas accepts a document_content object with type: "markdown". The Markdown content is limited to 1 MiB (1,048,576 characters) per document_content object. (api.slack.com) Most Guru cards are well under this limit, but if you merge multiple cards into a single canvas, monitor the total.
Supported Formatting
Canvas Markdown supports:
- Headings: h1, h2, h3 only — h4, h5, h6 are silently dropped by the parser
- Text: bold, italic, strikethrough, inline code
- Lists: bulleted, ordered, checklists
- Blocks: code blocks, blockquotes, callouts, dividers
- Tables: pipe-delimited Markdown tables with a 300-cell limit per table
- Links: inline links and channel/user mentions
- Embeds: file unfurls, canvas unfurls, message unfurls
- Not supported: Block Kit JSON, iframes, custom HTML elements, inline CSS, h4–h6
Guru cards commonly use h4–h6 headings, nested tables, iframes (for embedded videos or forms), and <span> tags with inline CSS for color-coded content. None of these survive the conversion without explicit handling.
Conversion Library Comparison
A generic HTML-to-Markdown library produces output that Slack's Canvas parser will reject or misrender. The three most common libraries and their failure modes against Canvas:
| Library | Behavior on h4–h6 | Behavior on nested lists | Behavior on unsupported HTML | Canvas compatibility |
|---|---|---|---|---|
html2text (Python) |
Outputs #### which Canvas drops silently |
Correct indent but fails in blockquote context | Strips tags, leaves text content | Needs post-processing pass for heading levels |
markdownify (Python) |
Outputs #### which Canvas drops silently |
Handles most nesting correctly | Strips tags cleanly | Needs heading downgrade pass; better default choice |
pandoc (CLI) |
Configurable via Lua filter | Best nested list handling | Most complete stripping | Best output quality; requires Lua filter for heading cap |
Recommendation: use markdownify for Python-based pipelines with an explicit post-processing pass to downgrade h4–h6 to h3 and validate table cell counts. Use pandoc with a Lua filter if your card corpus has complex nested structures. Do not use html2text directly — it produces ####-prefixed headings that Canvas silently ignores, creating invisible content loss.
Known Canvas parser rejection cases (errors you will encounter in bulk migration):
invalid_markdown— returned when the Markdown contains structures the parser cannot handle, such as a list directly inside a blockquotedocument_content_too_large— returned whendocument_contentexceeds 1 MiBinvalid_table— returned when a table exceeds 300 cells (10 columns × 31 rows = 310 cells triggers this)channel_canvas_already_exists— returned byconversations.canvases.createwhen the channel already has a canvas
Log these errors per card during migration and handle them as a separate remediation pass rather than stopping the migration.
Conversion Pipeline
from bs4 import BeautifulSoup
import markdownify
def guru_html_to_canvas_markdown(html: str) -> tuple[str, list[str]]:
soup = BeautifulSoup(html, 'html.parser')
# Downgrade h4-h6 to h3 (Canvas silently drops unsupported heading levels)
for tag in soup.find_all(['h4', 'h5', 'h6']):
tag.name = 'h3'
# Extract Guru-hosted images for separate processing
images = []
for img in soup.find_all('img'):
src = img.get('src', '')
if 'content.api.getguru.com' in src:
images.append(src)
img.decompose() # Remove placeholder; re-insert Slack URL after upload
# Strip iframes and inline-styled spans
for tag in soup.find_all(['iframe']):
tag.decompose()
for tag in soup.find_all(style=True):
del tag['style']
# Convert to Markdown
markdown_text = markdownify.markdownify(str(soup), heading_style="ATX")
# Validate table cell counts
markdown_text = split_oversized_tables(markdown_text, max_cells=300)
return markdown_text, images
def split_oversized_tables(markdown: str, max_cells: int) -> str:
"""Split any Markdown table exceeding max_cells into multiple tables."""
# Implementation: parse pipe-delimited rows, count cols × rows,
# split at row boundary before limit, prepend header to continuation table
# ... implementation omitted for brevity ...
return markdownWhat a Valid Canvas API Request Looks Like
This is the complete structure of a canvases.create request body with document_content:
{
"title": "Refund Policy",
"document_content": {
"type": "markdown",
"markdown": "## Overview\n\nOur refund policy allows customers to request a refund within 30 days of purchase.\n\n**Former verifier:** Revenue Ops\n**Former verification interval:** 90 days\n**Last verified in Guru:** 2026-06-15\n\n---\n\n## Eligibility\n\n- Purchase within the last 30 days\n- Original payment method still active\n"
}
}The channel_id parameter is optional for standalone canvases on paid plans and required on the free plan. To create a channel canvas instead of a standalone canvas, use conversations.canvases.create with channel_id in the request body and document_content as shown above.
How Do You Migrate Images and Attachments?
Images in Guru cards are hosted at URLs like https://content.api.getguru.com/files/view/{fileId}. These URLs require Guru authentication to access — once your Guru subscription lapses or the token expires, the images are inaccessible. You must re-host images before writing the canvas that references them. (developer.getguru.com)
The migration sequence for each card's images:
- Parse the card HTML for
<img>tags with Guru CDNsrcURLs - Download each image via authenticated GET to
https://content.api.getguru.com/files/view/{fileId} - Upload to Slack using
files.getUploadURLExternalplusfiles.completeUploadExternal(Slack's legacyfiles.uploadendpoint is being retired; Slack has not published a fixed sunset date but has marked it deprecated since 2024) (api.slack.com) - Get the Slack file permalink from the upload response
- Replace the Guru CDN URL in your Canvas Markdown with the Slack file reference
- Write the canvas only after all image uploads for that card are complete
Do not write the canvas before uploading its images. A canvas referencing a Guru CDN URL will display the image initially (while the URL is still live), but will break silently once Guru access is revoked. Upload to Slack first, then write the canvas with Slack file references.
Image Migration Performance Estimates
Image re-hosting is the slowest part of the migration. To set concrete expectations:
- Each image requires two API calls: one GET (Guru download) + two calls (Slack upload URL + complete)
- Slack's
files.getUploadURLExternalandfiles.completeUploadExternalare Tier 4 rate-limited - A realistic single-threaded round-trip per image (download + upload + API overhead) averages 2–4 seconds under normal network conditions
Example: 500 cards with an average of 4 images each = 2,000 images. At 3 seconds per image single-threaded: ~100 minutes. With 8 concurrent async workers respecting rate limits: ~15 minutes. With 16 concurrent workers: ~8 minutes, approaching Tier 4 throttle risk.
Budget conservatively. Use async workers (Python asyncio + aiohttp, or Node.js with Promise.all pools), cap concurrency at 8–12 workers, and implement exponential backoff on 429 responses. Store the Guru-URL-to-Slack-permalink mapping in a local SQLite database so partial runs can resume without re-uploading already-migrated assets.
What Happens to Custom Fields, Tags, and History?
Tags and Custom Fields
Guru tags are categorized metadata applied to cards for search and filtering. Guru custom fields (available on enterprise plans) add structured key-value pairs to cards.
Slack Canvas has no field model — no custom properties, no tag taxonomy, no structured metadata on a canvas. Your options:
- Embed as prose: add a "Tags" or "Metadata" section at the top of each canvas with original tag values as text. Searchable within Slack's canvas search, but not filterable or queryable.
- Index table: create a master "Card Index" canvas with a Markdown table mapping canvas titles to their original tags, custom field values, collection, and verification status.
- External index: store the mapping in a spreadsheet or database. Useful for audit but disconnected from the canvases.
None of these recreate Guru's tag-based filtering or the ability to query cards by custom field values. If your workflows depend on tag-driven automation — for example, Guru webhooks that fire when a card with a specific tag is updated — those workflows break entirely and have no Canvas equivalent.
Version History
Guru exposes historical card versions through /cards/{cardId}/versions. Slack canvases have their own revision history, but that history starts once the canvas exists in Slack and covers document restoration, not Guru-style verification ownership or expiry. Export version and verification history to an external store before migration if your compliance posture requires it.
Slack Canvas API Constraints for the Write Side
Once content is extracted, converted, and re-hosted, writing to Canvas has its own limits:
| Method | Rate limit tier | Practical ceiling | Notes |
|---|---|---|---|
conversations.canvases.create |
Tier 2 | ~20 req/min burst | One per channel; second call returns channel_canvas_already_exists |
canvases.create |
Tier 2 | ~20 req/min burst | Standalone canvases; channel_id optional on paid plans |
canvases.edit |
Tier 3 | ~50 req/min burst | One operation per API call |
canvases.access.set |
Tier 2 | ~20 req/min burst | Grant read/write access per canvas |
files.getUploadURLExternal |
Tier 4 | ~100 req/min burst | Required for image re-hosting |
files.completeUploadExternal |
Tier 4 | ~100 req/min burst | Completes each upload |
Rate limit tiers follow Slack's published tier definitions (api.slack.com/docs/rate-limits); exact burst ceilings vary by workspace plan and API method. Always handle 429 responses with backoff rather than relying on hardcoded ceilings.
Other hard constraints:
- Content size: 1 MiB per
document_contentobject - Table limit: 300 cells per table
- Canvas sharing: each canvas can be shared in up to 1,000 channels
- Tabs: up to 15 tabs per conversation (canvases, lists, workflows, messages, files)
- Free plan: limited to one canvas tab per channel; standalone canvases require a
channel_id
At Tier 2 rate limits, writing 500 standalone canvases takes roughly 25 minutes for creation calls alone — not counting image uploads (Tier 4) or access-setting calls (Tier 2). Plan your migration window accordingly.
Step-by-Step Guru to Slack Canvas Migration Process
Step 1: Audit and Decide What Moves
Export your Guru card inventory via CSV from Card Manager. The CSV includes title, card ID, folders, tags, date created, created by, last modified, last verified, verifier, trust state, verification interval, and collection name. Use this to:
- Identify stale or unverified cards (
verificationState != TRUSTED) that should be archived rather than migrated - Map collections to Slack channels
- Flag cards with
shareStatus = COLLECTIONthat require access-controlled canvases - Flag high-image-count cards that will extend your migration timeline
- Record verification metadata (
verificationState,lastVerified,verifier) to your external audit log before any data is lost
Step 2: Provision Slack Channels and User Groups
Map each Guru Collection to a Slack channel. For large collections with many top-level folders, consider one channel per folder instead. Create channels before running the migration script — you need channel IDs.
For cards with shareStatus = COLLECTION, create corresponding Slack user groups mirroring Collection membership. You will need these group IDs for canvases.access.set calls.
Step 3: Extract Cards via the Guru API
Paginate through GET /api/v1/search/query with maxResults=50. Follow Link headers until exhausted. For each card, store:
- Card ID, title, HTML content,
shareStatus - Verification metadata (
verificationState,verificationInterval,lastVerified,lastVerifiedBy,verifier) — write to your audit log - Tags and custom fields
- Folder path (for mapping to channels)
- Image URLs parsed from the content HTML
Step 4: Download and Re-Host Images
For every Guru CDN image URL found in card content, download via authenticated GET and upload to Slack using files.getUploadURLExternal + files.completeUploadExternal. Store the mapping of old Guru URLs to new Slack file permalinks in a local SQLite database. Use 8–12 async workers. This is the migration bottleneck — see the performance estimates above.
Step 5: Convert HTML to Canvas Markdown
Run each card's HTML through your conversion pipeline using markdownify (or pandoc with a Lua filter for complex content). Replace Guru image URLs with Slack file references from the SQLite mapping. Downgrade h4–h6 headings to h3. Strip iframes and inline styles. Validate output against: under 1 MiB total, tables under 300 cells. Log any cards requiring manual remediation rather than halting the pipeline.
Step 6: Write Canvases to Slack
For each card, call canvases.create with the title and converted Markdown. Tab each standalone canvas to its target channel via channel_id as appropriate. For the channel-level index, call conversations.canvases.create once per channel — check channel.properties.canvas from conversations.info first to avoid channel_canvas_already_exists errors — with a Markdown document linking to all standalone canvases for that collection.
Step 7: Set Access and Validate
Call canvases.access.set for any canvas migrated from a COLLECTION-scoped Guru card, granting access to the appropriate Slack user group. For TEAM-scoped cards, leave canvases accessible to the channel's membership.
Spot-check a sample of migrated canvases against original Guru cards for: content fidelity, image rendering, link integrity, heading level accuracy, and table completeness. Verify that review convention headers are in place. Confirm that access-controlled canvases are not visible to users outside their intended group before cutover.
What Cannot Be Migrated
| Guru feature | Canvas equivalent | Status |
|---|---|---|
Verification state (verificationState) |
None | Lost — no field, no workflow |
Verification interval (verificationInterval) |
None | Lost |
Verifier assignment (verifier) |
None | Lost |
| Trust indicator (UI badge) | None | Lost |
| Scheduled verification notifications | None | Lost |
| Card version history | None | Lost — Canvas has no version write API |
| Tags | Prose header or index table | Degraded — searchable, not filterable |
| Custom fields (enterprise) | Prose header or index table | Degraded |
| Folder hierarchy (3 levels) | Channels + index canvas | Approximated |
Collection-level permissions (shareStatus) |
canvases.access.set + user groups |
Approximated — requires pre-provisioned groups |
| Favorites / view counts | None | Lost |
| Tag-driven webhook automation | None | Lost — no Canvas equivalent |
The Real Decision
The technical migration is manageable. The trust-model downgrade is the real decision.
If your team uses Guru primarily as a searchable wiki and does not rely on verification workflows, Canvas can work as a lightweight reference layer inside Slack — a landing canvas per channel, standalone canvases per card, and a written review convention gets you most of the way there.
If your team has invested in Guru's verification lifecycle — if trust scores drive accountability, if card expiry prevents stale answers from reaching customers — Canvas is a downgrade that no amount of Slack reminders can fully replace. In that case, consider migrating to a platform with structured metadata: Notion, Confluence, or Discourse.
For teams that do move forward, the hard parts are image re-hosting (slow, three API calls per image, parallelizable to ~8–15 minutes for typical corpora), content conversion (HTML to a restricted Markdown subset with library-specific failure modes), permission mapping (shareStatus to canvases.access.set), and the organizational question of how to map a hierarchical knowledge base onto a flat channel structure. The Guru API gives you everything you need to extract. The Canvas API accepts Markdown and creates documents. The gap is entirely in what gets lost between the two.
For related migration patterns, see our guides on Quip to Slack Canvases and migrating images and attachments without broken links.
Frequently Asked Questions
- Can Slack Canvas replace Guru's verification workflow?
- No. Slack Canvas has no verification status, no assigned verifier, no review interval, and no trust indicator. You can approximate review cadence with Slack reminders or Workflow Builder triggers, but there is no built-in mechanism to mark a canvas as verified or flag it as stale.
- How many channel canvases can a Slack channel have?
- Exactly one. Calling conversations.canvases.create a second time on the same channel returns a channel_canvas_already_exists error. Additional content must go into standalone canvases created via canvases.create and shared or tabbed to the channel.
- What Guru card data is lost when migrating to Slack Canvas?
- Verification state, verifier assignments, verification interval, trust indicators, card version history, favorites, and view counts are lost entirely. Tags and custom fields can only be preserved as plain text in the canvas body or an index table — not as structured, filterable metadata.
- Does the Guru API return card content as HTML or Markdown?
- HTML. The card content field contains rendered HTML. You must convert this to the Canvas-supported Markdown subset, which limits headings to h1–h3, caps tables at 300 cells, and does not support iframes, Block Kit, or inline CSS.
- How do you handle Guru images when migrating to Slack Canvas?
- Download each image from the Guru CDN (content.api.getguru.com) using an authenticated GET request, upload it to Slack via files.getUploadURLExternal and files.completeUploadExternal, then reference the Slack-hosted file in the canvas Markdown. Do not write canvases with Guru CDN URLs — they break when Guru access is revoked.