Slack Plan and Task Card Blocks: Every Status, Update and Limit Tested
We posted a plan block with tasks in all four statuses and standalone task cards from a bot, updated them with chat.update, and probed the limits. pending works only inside a plan, and titles have no length limit we could find.
On this page
The task_card block shows one step an app or agent is working on, with a status icon, optional details and output, and source links. The plan block groups up to 50 such tasks under one title. Slack added both in February 2026 and changed how they look on 20 August 2026. On 3 October 2026 we posted them from a test bot in our own workspace, in Slack web, and tried every status, an update, and bad values. A standalone task_card rejects pending, while a plan accepts it. The general-purpose card and carousel blocks are tested in Slack carousel and card blocks.
The blocks we posted
import json
from lab import raw # our helper: POSTs JSON to https://slack.com/api/<method> with the bot token
DM = "D0C6CRJSZK8"
def rt(t): return {"type": "rich_text", "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": t}]}]}
def task(i, status, title=None, **kw):
d = {"task_id": f"t{i}", "title": title or f"Task {i}: {status}", "status": status}
d.update(kw); return d
tasks = [task(1, "pending", "Read the 4.53 changelog"),
task(2, "in_progress", "Group changes by area", details=rt("Sorting 38 entries")),
task(3, "complete", "Find breaking changes", output=rt("2 breaking changes found"),
sources=[{"type": "url", "text": "docs.slack.dev", "url": "https://docs.slack.dev/changelog"}]),
task(4, "error", "Fetch the GitHub release", output=rt("HTTP 404 from the release URL"))]
r = raw("chat.postMessage", {"channel": DM, "text": "617 plan test",
"blocks": [{"type": "plan", "title": "Release summary plan", "tasks": tasks}]})
cards = [{"type": "task_card", **task(i + 10, s)} for i, s in enumerate(["pending", "in_progress", "complete", "error"])]
r2 = raw("chat.postMessage", {"channel": DM, "text": "617 cards test", "blocks": cards})
cards3 = [c for c in cards if c["status"] != "pending"]
r3 = raw("chat.postMessage", {"channel": DM, "text": "617 cards test 3", "blocks": cards3})
| Message | Response |
|---|---|
plan with tasks in pending, in_progress, complete, error | {"ok": true, "ts": "1790989727.294849"} |
Four task_card blocks including pending | invalid_blocks, [ERROR] must be a valid enum value [json-pointer:/blocks/0/status] |
Three task_card blocks without pending | {"ok": true, "ts": "1790989728.050639"} |
The task card reference lists only in_progress, complete and error. The plan reference's own example uses pending, and Slack accepted it there.
How each status looks in Slack
Slack showed the plan collapsed to one line, "Release summary plan", with a spinner because a task was still running. Clicking the title opened it:
What each status got:
| Status | Inside a plan | As a standalone task_card |
|---|---|---|
pending | Grey dot | Rejected (invalid_blocks) |
in_progress | Spinner | Spinner |
complete | Grey dot, the same as pending | Check mark |
error | Red warning triangle | Red warning triangle |
Inside a plan, pending and complete look the same. The plan's header icon is what tells the reader the plan is done. details showed under the title, output under that, and sources as links. Tasks with details or output got an expand arrow.
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Updating the plan with chat.update
We sent the same plan again with every task set to complete and a new output on the failed one:
for t in tasks:
t["status"] = "complete"
tasks[3]["output"] = rt("Found it on the second try")
raw("chat.update", {"channel": DM, "ts": plan_ts, "text": "617 plan test",
"blocks": [{"type": "plan", "title": "Release summary plan", "tasks": tasks}]})
The call returned {"ok": true}. The plan collapsed again, and its header spinner became a check mark. Opened, it read:
So to show progress, send the full plan each time. There is no call that changes one task; chat.update replaces the whole block. chat.startStream also accepts task_update chunks, which we did not test; our chat.startStream test covers the text side.
Limits and errors
Each row is one chat.postMessage with a bot token:
| What we sent | Response |
|---|---|
plan task with status: "done" | invalid_blocks, [ERROR] must be a valid enum value [json-pointer:/blocks/0/tasks/0/status] |
Two tasks with the same task_id | invalid_blocks, [ERROR] task_ids must be unique [json-pointer:/blocks/0] |
| plan with 0 tasks | ok |
| plan with 50 tasks | ok |
| plan with 51 tasks | invalid_blocks, [ERROR] Cannot have more than 50 tasks [json-pointer:/blocks/0] |
| Two plan blocks in one message | invalid_blocks, [ERROR] Cannot have more than 1 plan block |
task_card without title | invalid_blocks, [ERROR] missing required field: title [json-pointer:/blocks/0] |
details as a plain string | invalid_blocks, [ERROR] must provide an object [json-pointer:/blocks/0/details] |
A source with no url | invalid_blocks, [ERROR] missing required field: url [json-pointer:/blocks/0/sources/0] |
| task_card title of 3,001, 12,000, 50,000 and 200,000 characters | ok every time |
| plan title of 3,001 characters | ok |
We found no title limit. Slack accepted 200,000 characters and showed the long titles in full, wrapped across the screen, so cap titles in your own code. details and output must be a single rich_text object, the same structure as the rich_text block.
We did not check the mobile app. When a block fails, our invalid_blocks guide explains how to read the json-pointer.
Stop Jiggling Your Mouse.
Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.
Related Articles
How to See All Your DMs in Slack, Including the Hidden Ones
The DMs tab lists your direct messages with people. App DMs, Slackbot and DMs with deleted accounts sit elsewhere. We compared the tab with the full list the Slack API returned.
Slack Images Not Showing? 5 Causes We Reproduced
We uploaded images to a Slack test channel and then switched each image setting off, ran /collapse, and posted from a bot that was not in the channel. Five causes, each reproduced.
How to Message Yourself on Slack (and What It Notifies)
Your own name under Direct messages opens a private DM with yourself. We sent it a note, a file, a scheduled message and a reminder, and checked which ones raised a badge.