Launched:self-serve migrations intoSuperhuman Docs (Coda)
Try it now
01Agent-first
Runs where you already work
Plug it into Claude, ChatGPT or Cursor. Describe the move in plain English; the agent runs it.
02Engineer-led
Our production engine, unlocked
The pipeline our engineers use on managed enterprise migrations — the same code, now something you can drive yourself.
03Pricing
Try 10 pages free, then $1 a page
Credit-based, pay-as-you-go. No scoping call, no quote — sample it on your own docs before you spend anything.
04Sources
NotionSlabConfluenceSoonGoogle DocsSoon
Skip to content

Glean to Slack Canvas Migration: Why the Export Doesn't Exist

Glean is a search index, not a content store. There's no export endpoint. Your real migration starts at Confluence, Google Drive, or SharePoint — not Glean.

Abdul Aleem Abdul Aleem · · 18 min read
Glean to Slack Canvas Migration: Why the Export Doesn't Exist
TALK TO AN ENGINEER

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

Glean to Slack Canvas Migration: Why the Export Doesn't Exist

You cannot migrate "your Glean content" into Slack Canvas because Glean does not store your content. Glean is an enterprise search and AI indexing layer, not a content management system. The documents you find through Glean search — Confluence pages, Google Drive files, SharePoint sites — live in those source platforms. Glean indexes them; it does not host them. There is no bulk export endpoint, no tenant-wide data download, and no official migration tooling that extracts full document bodies from a Glean instance.

When teams say "Glean to Slack Canvas migration," they are actually describing two separate projects: migrating upstream source content from the systems Glean was crawling, and moving the smaller layer of Glean-native curation — Collections, Answers, Shortcuts, Announcements, and Pins — into Slack canvases.

This guide covers what Glean-native objects exist, how to extract them via API (including pagination and error handling), how to map them into Slack canvases, and what API constraints shape the implementation. For the heavier upstream source migration, we point you to the right starting points.

For overall migration planning, see our knowledge base migration checklist.

Why Glean Has No Export Path

Glean is built around custom connectors that ingest and index data from external sources. The Bulk Indexing endpoint (/api/index/v1/bulkindexdocuments) is used to index all documents of a custom datasource — and bulk indexing fully replaces the entire list of documents stored in Glean. The Indexing API is strictly one-directional: you push documents into Glean. There is no corresponding "pull" or "export" endpoint that retrieves indexed document content back out.

Glean provides three REST API families:

API Family Direction Purpose
Indexing API Push (inbound) Index documents, users, groups, permissions into Glean
Client API Read (query) Search, chat, retrieve metadata, manage native objects
Platform API Read (query) Agents, skills, search, chat

None of these families include a "get all indexed document bodies" or "export tenant" endpoint. The Client API's search endpoint returns results — titles, snippets, URLs, metadata — not the full body text that was originally indexed. For Slack specifically, Glean never runs scheduled crawls for Slack messages; message bodies are retrieved live from Slack at query time via the RTS API and processed in memory only, not written into an index in RTS-only mode.

Warning

There is no "Export from Glean" button. If someone on your team says "we need to migrate out of Glean," the first question is: where does the content actually live? The answer is almost always Confluence, Google Drive, SharePoint, Notion, or another source platform. That platform is your migration source — not Glean.

What Is Glean-Native Content?

Glean-native content is data created directly inside Glean that has no upstream source. It exists only within your Glean tenant. If you decommission Glean without extracting it, it is gone.

The Glean Client API exposes CRUD operations for five native object types:

  • Collections — curated lists of documents organized around a topic (e.g., "Q3 Onboarding Materials"). The Collections API supports creating, reading, updating, and deleting collections and individual collection items, including listing all existing collections via /rest/api/v1/listcollections.
  • Answers — authoritative question-and-answer pairs for common queries. The Answers API enables you to create, manage, and retrieve user-generated answers to frequently asked questions.
  • Shortcuts (Go Links) — alias-to-URL mappings like go/benefits or go/roadmap. The Shortcuts API enables you to create and manage search shortcuts that provide quick access to frequently used queries and resources.
  • Announcements — time-bound company communications targeted by department and location. The Announcements API allows administrators to create, update, and manage organizational announcements visible to users based on department and location targeting.
  • Pins — search results pinned to the top for specific queries. The Pins API allows you to create, manage, and organize pinned search results that appear prominently for specific queries.

These are the only objects that genuinely "live in Glean." Everything else — the thousands of documents surfaced in search results — is a reference to content stored elsewhere.

Glean Object Key Fields Notes
Collection Name, description, list of document references (URLs or Glean doc IDs) Items are references, not full document bodies
Answer Question text, answer body, source links Self-contained Q&A pairs
Shortcut Alias (e.g., go/hr), destination URL Simple key-value redirects
Announcement Title, body, start/end dates, department/location targeting No list endpoint; track IDs manually
Pin Pinned document reference, associated search queries Pins reference documents by ID, not content

Scoping a typical tenant: In practice, the Glean-native layer is small relative to the indexed corpus. A mid-size deployment might have 200–800 Collections, 500–2,000 Answers, and 300–1,000 Shortcuts — extractable in a few hours of API work. The upstream content those Glean objects reference (Confluence pages, Drive docs, SharePoint sites) typically numbers in the tens of thousands and is the months-long workstream.

How to Extract Glean-Native Objects via API

Each native object type has a list endpoint in the Client API — with two important exceptions. You need a Glean-issued Client API token with the appropriate scopes.

Available scopes include ANNOUNCEMENTS for company announcements, PINS for pinned content, and a Content Management scope combination of COLLECTIONS, PINS, and SHORTCUTS. Some of these scopes require admin-level tokens. A standard user token will successfully authenticate but silently return only the objects owned by that user — not the org-wide set. If you are building a tenant-wide extraction, obtain an admin-scoped token before writing the script. Attempting the extraction with a non-admin token and assuming completeness is a common failure mode.

Pagination and error handling

All list endpoints on Glean paginate on large tenants using a cursor pattern. Extraction code that omits cursor handling will silently truncate results — returning the first page only with no error. Every extraction loop must handle pagination explicitly, and every API call must handle transient errors with backoff. Glean does not publish its Client API rate limits publicly, so treat any 429 response as a signal to back off with exponential delay.

import os
import time
from glean.api_client import Glean
 
client = Glean(
    api_token=os.getenv("GLEAN_API_TOKEN"),
    server_url=os.getenv("GLEAN_SERVER_URL"),
)
 
def extract_all_pages(list_fn, **kwargs):
    """
    Generic paginator for Glean list endpoints.
    Passes cursor from each response into the next call until exhausted.
    Retries on transient errors with exponential backoff.
    """
    results = []
    cursor = None
    max_retries = 5
 
    while True:
        params = {**kwargs}
        if cursor:
            params["cursor"] = cursor
 
        for attempt in range(max_retries):
            try:
                response = list_fn(**params)
                break
            except Exception as e:
                if attempt == max_retries - 1:
                    raise
                wait = 2 ** attempt
                print(f"Retrying after error: {e}. Waiting {wait}s.")
                time.sleep(wait)
 
        results.extend(response.items or [])
 
        cursor = getattr(response, "cursor", None)
        if not cursor:
            break
 
    return results
 
# Extract all Collections (org-wide, requires COLLECTIONS scope)
collections = extract_all_pages(client.client.collections.list)
 
# Extract Answers (user-scoped unless admin token; see note below)
answers = extract_all_pages(client.client.answers.list)
 
# Extract Shortcuts (user-scoped unless admin token; see note below)
shortcuts = extract_all_pages(client.client.shortcuts.list)
 
# Extract all Pins (org-wide, requires PINS scope)
pins = extract_all_pages(client.client.pins.list)

Two caveats that will shape your extraction plan:

List endpoints are not equally broad. listcollections and listpins return org-wide results. listanswers returns answers created by the authenticated user, and listshortcuts returns shortcuts owned or editable by the authenticated user. Org-wide extraction of Answers and Shortcuts requires owner-by-owner collection, delegated auth, or coordination with Glean's admin team — not one clean tenant-wide pull.

Info

Announcements have no list endpoint. A conformance audit against Glean's OpenAPI spec (version 0.9.0) confirmed that Glean documents only create, update, and delete operations for announcements — there is no list or read-all endpoint. This is not an oversight in the SDK; it reflects the API design.

Workarounds for Announcement ID recovery:

  1. Network traffic inspection: Open the Glean admin console, navigate to the Announcements section, and use browser DevTools (Network tab) to capture the underlying API requests. The response payloads will contain announcement IDs.
  2. Proactive ID logging: Going forward, have admins log announcement IDs at creation time. The create response always returns the ID even though listing is unsupported.
  3. Glean support escalation: For tenants with large announcement histories, open a support ticket requesting an admin data export. Glean's support team has internal tooling that does not surface in the public API.

If your team created announcements ad hoc without logging IDs and is unwilling to escalate to support, expect manual recovery from the admin console. This is the one Glean-native object type where the API genuinely fails you.

Mapping Glean Objects to Slack Canvases

Slack offers two canvas types, and picking the right one for each Glean object avoids rework later.

Channel canvases are created with conversations.canvases.create, tied to one channel, and inherit access from that channel's membership. There is exactly one channel canvas per channel. A second call to create a channel canvas returns channel_canvas_already_exists.

Standalone canvases are created with canvases.create, owned by the acting app or user, and shared separately via canvases.access.set. A standalone canvas is a free-floating document visible to nobody until you grant access explicitly.

Use channel canvases when the audience should exactly follow channel membership. Use standalone canvases when one artifact must be shared across several channels or individual users.

Glean Object Best Slack Canvas Target Rationale
Collection Standalone canvas (index page) A Collection is a curated list of links — an index canvas mirrors this exactly
Answers Channel canvas or standalone canvas Team FAQs belong in a channel canvas; company-wide FAQs work as a standalone shared broadly
Shortcuts Standalone canvas (link directory) A single canvas listing all go-links is more useful than one per link
Announcements Channel canvas Post as the channel canvas in #announcements or a team-specific channel
Pins No direct equivalent Pins are a search-layer concept; Slack has no "pin to search" feature

Collections → Index canvases

A Glean Collection — a title, a description, and a list of categorized links — maps nearly 1:1 to a Slack canvas used as an index page.

import time
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
 
slack = WebClient(token=os.getenv("SLACK_BOT_TOKEN"))
 
def create_canvas_with_retry(slack_client, title, markdown, max_retries=5):
    """Create a standalone canvas with exponential backoff on rate limiting."""
    for attempt in range(max_retries):
        try:
            resp = slack_client.canvases_create(
                title=title,
                document_content={"type": "markdown", "markdown": markdown},
            )
            return resp["canvas_id"]
        except SlackApiError as e:
            if e.response.status_code == 429:
                retry_after = int(e.response.headers.get("Retry-After", 2 ** attempt))
                print(f"Rate limited. Waiting {retry_after}s.")
                time.sleep(retry_after)
            else:
                raise
    raise RuntimeError(f"Failed to create canvas '{title}' after {max_retries} attempts.")
 
canvas_ids = {}
for collection in collections:
    items_md = "\n".join(
        f"- [{item.title}]({item.url})" +
        (f" — {item.description}" if item.description else "")
        for item in collection.items
    )
    markdown = f"# {collection.name}\n\n{collection.description or ''}\n\n{items_md}"
 
    # Check size before submitting — 1 MiB limit (1,048,576 characters)
    if len(markdown.encode("utf-8")) > 1_048_576:
        print(f"WARNING: Collection '{collection.name}' exceeds 1 MiB. Splitting required.")
        # Split logic: chunk items into groups of N and create multiple canvases
        # (implementation below in the size-limit section)
        continue
 
    canvas_id = create_canvas_with_retry(slack, collection.name, markdown)
    canvas_ids[collection.name] = canvas_id
    time.sleep(0.5)  # Stay under Tier 2 limit (20+/min)

Markdown content is limited to 1 MiB (1,048,576 characters) per document_content object. Collections with hundreds of items can approach this ceiling. When a collection exceeds 1 MiB, split items into chunks and create a parent index canvas that links to part canvases:

def split_collection_to_canvases(slack_client, collection, chunk_size=100):
    """Split a large collection into multiple canvases with a parent index."""
    items = list(collection.items)
    chunks = [items[i:i+chunk_size] for i in range(0, len(items), chunk_size)]
    part_canvas_ids = []
 
    for idx, chunk in enumerate(chunks, 1):
        chunk_md = f"# {collection.name} (Part {idx} of {len(chunks)})\n\n"
        chunk_md += "\n".join(f"- [{item.title}]({item.url})" for item in chunk)
        canvas_id = create_canvas_with_retry(slack_client, f"{collection.name} — Part {idx}", chunk_md)
        part_canvas_ids.append((idx, canvas_id))
        time.sleep(0.5)
 
    # Create parent index canvas linking to parts
    index_md = f"# {collection.name}\n\n{collection.description or ''}\n\n"
    index_md += "\n".join(f"- [Part {idx}](https://slack.com/canvas/{cid})" for idx, cid in part_canvas_ids)
    parent_id = create_canvas_with_retry(slack_client, collection.name, index_md)
    return parent_id, part_canvas_ids

Answers → FAQ canvases

Group related Answers into a single FAQ canvas instead of creating one canvas per answer. A channel canvas in a team's Slack channel is a natural home:

faq_md = "# Frequently Asked Questions\n\n"
for answer in answers:
    faq_md += f"## {answer.question}\n{answer.body}\n\n"
 
try:
    slack.conversations_canvases_create(
        channel_id="C0123TEAM",
        document_content={"type": "markdown", "markdown": faq_md},
    )
except SlackApiError as e:
    if e.response["error"] == "channel_canvas_already_exists":
        # Canvas exists — append to it instead
        existing = slack.conversations_info(channel="C0123TEAM")
        canvas_id = existing["channel"]["properties"]["canvas"]["file_id"]
        slack.canvases_edit(
            canvas_id=canvas_id,
            changes=[{"operation": "insert_at_end",
                       "document_content": {"type": "markdown", "markdown": faq_md}}],
        )

Consolidate all shortcuts into a single reference canvas. With two columns, the 300-cell-per-table limit caps you at 150 shortcuts per table. Split into multiple tables within the same canvas if needed:

def build_shortcut_canvas(shortcuts, chunk_size=150):
    """Build a go-links directory canvas, splitting into multiple tables if needed."""
    chunks = [shortcuts[i:i+chunk_size] for i in range(0, len(shortcuts), chunk_size)]
    canvas_md = "# Go Links Directory\n\n"
 
    for idx, chunk in enumerate(chunks, 1):
        if len(chunks) > 1:
            canvas_md += f"## Shortcuts {(idx-1)*chunk_size + 1}–{min(idx*chunk_size, len(shortcuts))}\n\n"
        canvas_md += "| Shortcut | Destination |\n|---|---|\n"
        for shortcut in chunk:
            canvas_md += f"| `{shortcut.alias}` | [{shortcut.url}]({shortcut.url}) |\n"
        canvas_md += "\n"
 
    return canvas_md
 
links_md = build_shortcut_canvas(shortcuts)
create_canvas_with_retry(slack, "Go Links Directory", links_md)

Slack Canvas does not natively act as a browser-level redirect engine. If your team relies on go-links for quick navigation (typing go/benefits in a browser), the canvas directory preserves reference but not the redirect behavior. To restore redirect functionality, build a custom Slackbot that listens for /go <alias> slash commands and responds with the destination URL. The canvas directory becomes the human-readable source of truth; the Slackbot handles the lookup.

Slack Canvas API Constraints That Shape the Migration

The canvases.access.set method sets the access level to a canvas for specified entities, but it carries several constraints that will break your migration if you ignore them.

Rate limits

  • canvases.create and conversations.canvases.create: Tier 2 — 20+ per minute. At this rate, creating 200 canvases takes approximately 10 minutes of API time, not counting setup logic. Budget accordingly.
  • canvases.edit and canvases.access.set: Tier 3 — 50+ per minute.

Slack publishes these as "20+" and "50+", meaning the actual burst capacity may be higher, but the guaranteed sustained rate is the published floor. Plan for the floor when estimating total migration time.

Access rules

  • You cannot pass channel_ids and user_ids in the same canvases.access.set call. Pick one per request.
  • The owner access level only works with user_ids; passing channel_ids with owner returns invalid_arguments.
  • Access levels can only be set for regular channels. Channel IDs for DMs or MPDMs are not accepted — use user_ids for DM sharing.
  • If you pass user_ids, you must have sent the user the canvas directly first. Even if the user is a member of a channel where the canvas has been shared, calling the method with user_ids will fail unless the canvas was sent to them individually.
  • Each canvas can be shared in up to 1,000 channels. This limit applies to both manual and automated sharing.

Content limits

  • 1 MiB (1,048,576 bytes) per document_content object.
  • 300 cells per markdown table (150 rows at two columns).
  • canvases.edit supports only one change operation per API call, so bulk updates require sequencing individual calls — not batching changes within a single request.

Batch size for access calls

The Slack docs do not publish an explicit maximum for the channel_ids or user_ids array in canvases.access.set. In practice, batch at ~20 IDs per request to reduce timeout risk and simplify retry logic. For a canvas that needs sharing to 200 channels, this means 10 sequential calls.

def share_canvas_to_channels(slack_client, canvas_id, channel_ids, access="read", batch=20):
    """Share a standalone canvas to channels in batches with retry on rate limiting."""
    for i in range(0, len(channel_ids), batch):
        batch_ids = channel_ids[i:i + batch]
        for attempt in range(5):
            try:
                slack_client.canvases_access_set(
                    canvas_id=canvas_id,
                    access_level=access,
                    channel_ids=batch_ids,
                )
                break
            except SlackApiError as e:
                if e.response.status_code == 429:
                    retry_after = int(e.response.headers.get("Retry-After", 2 ** attempt))
                    print(f"Rate limited on access.set. Waiting {retry_after}s.")
                    time.sleep(retry_after)
                else:
                    raise
        time.sleep(1.5)  # Stay under Tier 3 sustained rate
Warning

Always implement retry logic with Retry-After header respect when hitting canvases.access.set. If you loop through batches too quickly, Slack's rate limiter will throttle your application, leaving canvases with incomplete permissions. A canvas that appears created but has no sharing applied is invisible to its intended audience. Silent permission failures are the most common migration defect in Canvas API work.

Why Glean Permissions Don't Transfer to Slack

Glean permissions are inherited from source connectors, not independently stored. Connectors let Glean index content, mirror permissions from the source, and keep that data current in your isolated tenant. When Glean indexes a Confluence page, it mirrors Confluence's permission model — who can view that page in Confluence can view it in Glean search results. This is evaluated at query time.

Slack canvas permissions are completely separate. Channel canvases inherit channel membership — there are no separate access controls beyond channel membership for channel canvases. Standalone canvases require explicit sharing via canvases.access.set.

There is no Glean-side permission export that tells you "these 50 users had access to Collection X." Glean's permissions live in the source systems. You would need to reconstruct audience lists from Confluence spaces, Google Drive folders, or SharePoint sites — not from Glean. Plan to rebuild permissions from scratch based on your Slack channel structure (a common challenge when shifting audiences into Slack Canvas).

Danger

A private page that was safely restricted in Glean can become overexposed the moment someone places it into a broadly shared Slack canvas. Recompute access from the source-of-truth audience, not from the assumption that "Glean already handled permissions." This is especially high-risk for HR, finance, and legal content that may have appeared in Collections alongside less sensitive material.

The practical approach: identify the target audience for each Glean Collection, find the Slack channel whose membership most closely matches that audience, and map the Collection to that channel canvas. This lets Slack's existing channel membership govern visibility. For Announcements targeted by department or location, you will need a fresh audience map into Slack channels or standalone shares — these departmental targeting scopes do not have a direct Slack equivalent and require manual audience reconstruction.

Migrating the Upstream Content Glean Was Crawling

This is the real migration. The documents your team searches for in Glean — typically 95%+ of what users interact with daily — live in their original platforms. If you want them in Slack canvases, you extract from each source directly. Glean is the search layer sitting on top; the content was never Glean's to give.

Source Platform Export Method Key Conversion Challenges
Confluence REST API (/wiki/rest/api/content?expand=body.storage) with pagination via start + limit XHTML storage format requires macro stripping and element-by-element conversion; tables, code blocks, and info macros each need separate handling
Google Drive Drive API (list files) + Docs API (export as text/markdown or text/html) Large docs exceed the 1 MiB canvas limit; Drive export produces raw HTML that requires post-processing
SharePoint Microsoft Graph API (/sites/{id}/pages) Modern pages use JSON-structured content; classic ASPX pages require separate handling; both need permission reconstruction
Notion Notion API (GET /blocks/{id}/children, recursive) Nested block tree requires depth-first traversal; callouts, toggles, and databases have no direct Canvas equivalents

Confluence XHTML → Markdown conversion is the most common problem and deserves specifics. Confluence stores page content in a proprietary XHTML format called "storage format." A page exported via the REST API with expand=body.storage returns XML like:

<ac:structured-macro ac:name="info">
  <ac:rich-text-body>
    <p>This is an info panel.</p>
  </ac:rich-text-body>
</ac:structured-macro>

Slack Canvas has no callout equivalent in its markdown renderer. Conversion options: strip the macro wrapper and keep the inner text, or convert to a blockquote. Libraries like confluence2md handle common macros but require custom rules for org-specific macro plugins. Plan for manual review of any page using more than three distinct macro types.

The 1 MiB canvas size limit means a single long Confluence page — a detailed runbook or specification document — may need to be split across multiple canvases. At approximately 800 words per canvas-safe chunk (accounting for markdown overhead), a 15,000-word specification becomes roughly 19 canvases. Decide upfront whether you want fidelity (split the document) or summary (write a new canvas that links back to the source).

Each of these source migrations is its own project with its own API limits, format conversions, and permission models. There is no shortcut through Glean — the search index does not retain the full document bodies needed for reconstruction.

Slack Canvas works best as a hub surface for curated indexes, onboarding pages, team runbooks, project pages, and living FAQs — not as a blind drop-in replacement for a large document corpus. If the upstream source system is being retired alongside this migration, plan that as a separate workstream with its own timeline.

For teams coming from Quip, see our Quip to Slack Canvases guide for Salesforce's official transition path. For Confluence format conversion challenges, see our Confluence to Google Workspace guide.

Should Glean Keep Indexing After the Migration?

Keep Glean connected to Slack if Slack remains a major work surface, but verify that canvas content actually becomes searchable in your tenant before assuming it will. Glean's Slack RTS connector requests the canvases:read and canvases:write scopes, which confirms Glean is canvas-aware at the Slack integration level. The Slack RTS connector integrates Glean with Slack workspaces and Slack Enterprise Grid organizations via Slack's real-time search APIs.

Glean's public Slack documentation emphasizes message search and metadata crawling. The connector scopes suggest canvas support exists, but there is no public Glean document that explicitly confirms Slack canvases are indexed and searchable as first-class content objects. Treat canvas re-indexing as a tenant-level validation task before declaring the migration complete.

Canvas indexing validation checklist:

  1. Create one channel canvas and one standalone canvas with content containing unique, unlikely-to-appear-elsewhere phrases (e.g., "migration-validation-test-canvas-2024").
  2. Wait for Glean's indexing cycle (typically 24–48 hours for Slack content; confirm with your Glean admin).
  3. Search for the unique phrases in Glean. A successful result should return the canvas title, a content snippet, and a link to the Slack canvas URL.
  4. Test with a non-admin user who is a channel member to confirm member-level discoverability.
  5. Test with a user who is not a channel member to confirm access restrictions are respected.
  6. Test with a standalone canvas shared to a specific channel — confirm only channel members can find it via search.

If validation passes, the new canvases become nodes in your Glean knowledge graph alongside content from other connected platforms. If validation fails or returns no results, open a Glean support ticket referencing the canvases:read scope and confirm whether canvas indexing is enabled for your tenant configuration.

Tip

Don't decommission Glean just because you moved native objects to Slack. Glean's value is cross-platform search. If your team still uses Confluence, Google Drive, Jira, or other tools, Glean remains the connective tissue. Collections, Answers, and Shortcuts were a convenience feature, not the core product. Removing Glean severs search across all connected platforms, not just the five native object types covered in this guide.

A Realistic Migration Sequence

Break the work into two parallel tracks:

  1. Classify the scope. Split upstream source documents from Glean-native curation. Mixing these scopes is how teams waste weeks searching for an export button that does not exist. A mid-size tenant typically has 200–800 Collections and 500–2,000 Answers (Glean-native, extractable in hours) alongside tens of thousands of upstream documents (the months-long workstream).

  2. Obtain the right token. Confirm your Glean Client API token has admin scope before starting extraction. A non-admin token will authenticate successfully and return results — just not org-wide results. Verifying token scope upfront prevents discovering the problem mid-extraction.

  3. Extract Glean-native objects with pagination. Use the Client API with cursor-based pagination to pull Collections, Pins, Answers, and Shortcuts. Manually catalog Announcements — via network traffic inspection of the admin console, an ID log from admins, or a Glean support request — since there is no list endpoint. Account for user-scoped endpoints on Answers and Shortcuts requiring delegated auth or admin help for org-wide coverage.

  4. Map objects to Slack channels. Decide which team channels get FAQ canvases, which get collection index canvases, and where the go-link directory lives. This is also where you flatten Glean's federated permissions into Slack's channel-membership model. Recompute audiences from source systems — not from Glean.

  5. Create canvases via API with retry logic. Use canvases.create for standalone canvases and conversations.canvases.create for channel canvases. Format Glean content as markdown. Check byte length before submission (1 MiB limit). Budget approximately 10 minutes per 200 canvases at Tier 2 rate limits. Implement Retry-After header handling in all create loops.

  6. Set access in batches. Share standalone canvases to relevant channels using canvases.access.set, batching at ~20 IDs per call. Respect the channel_ids/user_ids exclusivity constraint. Log every successful access grant — incomplete permissions are the most common migration defect and are invisible without explicit verification.

  7. Migrate upstream content separately. For the actual documents Glean was indexing, go to each source platform and migrate from there. This is the months-long project, not the Glean extraction. Confluence XHTML conversion and Notion block traversal are the highest-complexity workstreams.

  8. Validate Glean re-indexing. If Glean stays in the stack, run the six-step canvas indexing validation before closing the migration. Confirm discoverability and access restriction for both canvas types from multiple user perspectives.

What This Migration Actually Is

Most of the work in a "Glean to Slack Canvas migration" has nothing to do with Glean. The native objects — Collections, Answers, Shortcuts, Announcements, Pins — are a one-afternoon extraction job for a typical tenant, assuming correct token scope and cursor-based pagination. The real project is migrating from Confluence, Google Drive, SharePoint, Notion, or whatever platforms Glean was crawling.

Glean is the search index, not the document store. Treating it as the migration source is like trying to export your bookmarks from Google and expecting the full text of every bookmarked page. The bookmarks are easy. The pages live somewhere else.

When the pair is handled honestly — Glean-native curation as a small, scriptable extraction, upstream content as the real multi-system migration — the project stops being mysterious and becomes a straightforward source migration plus Slack automation job.

Frequently Asked Questions

Can you export data from Glean?
No. Glean's Indexing API is inbound-only, and the Client and Platform APIs are query-based. There is no bulk export, tenant export, or full document retrieval endpoint. You can extract Glean-native objects like Collections, Answers, Shortcuts, and Pins via the Client API, but indexed documents must be exported from their original source platforms.
What content is actually stored in Glean?
Glean stores search index data, metadata, and references to documents from connected sources. The only content created and stored natively in Glean includes Collections (curated document lists), Answers (FAQ pairs), Shortcuts (go-links), Announcements, and Pins. All other content lives in upstream platforms like Confluence, Google Drive, or SharePoint.
What are the Slack Canvas API rate limits for migration?
canvases.create and conversations.canvases.create are Tier 2 (20+ per minute). canvases.edit and canvases.access.set are Tier 3 (50+ per minute). Canvas markdown content is limited to 1 MiB per document_content object, and tables are capped at 300 cells.
Do Glean permissions transfer to Slack Canvas?
No. Glean mirrors source-system permissions at query time, while Slack canvas access comes from channel membership or explicit channel/user sharing via canvases.access.set. You must rebuild permissions from scratch based on your Slack channel structure.
Does Glean index Slack canvases after migration?
Glean's Slack RTS connector requests canvases:read and canvases:write scopes, which confirms it is canvas-aware. However, Glean's public docs emphasize message search, not canvases as a documented indexed content type. Verify behavior in your own tenant before assuming canvases are searchable in Glean.

More from our Blog