Canvas
The canvases.create / canvases.edit write path, channel canvases, the access.set read flow, and hard limits.
A canvas is a markdown document surface built into Slack — "simple but powerful documents" that live either attached to a channel or on their own (surfaces/canvases). This page covers the bot-facing write path: creating one, keeping it in sync, granting read-only access, and where its hard limits are. For how Canvas ranks against the other four data surfaces for a read-only dashboard, see Choosing a Dashboard Surface.
Creating a canvas
canvases.create takes an optional title, an optional document_content (markdown), and an optional channel_id (reference/channel_id is what makes it a channel canvas: the canvas "may be automatically added to a channel tab" in that channel's header (surfaces/canvases), rather than existing as a standalone document the bot has to share out by link. Both canvases.create and canvases.edit require the canvases:write scope (reference/
{
"channel_id": "C0123456789",
"title": "Sync Status",
"document_content": {
"type": "markdown",
"markdown": "# Sync Status\n\nLast run: ..."
}
}Editing a canvas — one operation per call
canvases.edit's own docs state it plainly: "Only one operation per API call is currently supported" (reference/changes array, and that array can carry several change objects — but each one is still a single atomic operation:
insert_after/insert_before— add content relative to an existing sectioninsert_at_start/insert_at_end— add content at either end of the documentreplace— overwrite a sectiondelete— remove a sectionrename— change the canvas title
Because there's no cheap "patch this one cell" operation, the refresh pattern that stays sane for a bot-owned canvas regenerated from source data is a whole-document replace: build the full markdown string on every tick and send it as one replace operation, rather than trying to diff the previous document and send incremental changes.
{
"canvas_id": "F0123456789",
"changes": [
{
"operation": "replace",
"section_id": "<root-section-id>",
"document_content": {
"type": "markdown",
"markdown": "# Sync Status\n\nLast run: ..."
}
}
]
}Granting read-only access
canvases.access.set takes a canvas_id, an access_level of read, write, or owner, and either channel_ids or user_ids — not both (reference/user_ids form specifically, "you must have sent the user the canvas directly first," or the access.set call fails. Share the canvas before you set access on it, not after.
Read access is a real ceiling, not just a UI hint. Someone holding view-only access to a canvas "cannot make changes or comments" (Manage access permissions for canvases and lists) — canvases don't have the carve-out Lists has, where a viewer can still comment on an item.
Persistence as a channel tab
A canvas created (or later attached) with a channel_id sits in the channel header as its own tab, alongside Messages — durable UI real estate a pinned message doesn't get. It doesn't scroll out of view as the channel gets used, and there's no equivalent of a channel member accidentally unpinning it.
Limits
| Limit | Value | Source |
|---|---|---|
Operations per canvases.edit call | 1 | canvases.edit |
document_content markdown size | 1 MiB / 1,048,576 characters — applies per change when a changes array has more than one entry | canvases.edit, surfaces/canvases |
| Table cells per table | 300 — any mix of rows x columns | surfaces/canvases |
canvases.edit rate limit | Tier 3 (50+ requests/min) | canvases.edit |
| Required scope | canvases:write — create, edit, and access.set all need it | scopes/ |
What a canvas can't do
No board layout, no columns, no per-cell writes. If the content is really a status board rather than a document, see Choosing a Dashboard Surface for the other four options.