Skip to main content
RapidDev - Software Development Agency
API AutomationsSlackAPI Key

How to Automate Slack Project Updates using the API

Automate Slack project updates by receiving external webhooks (GitHub, Jira, custom), formatting data as Block Kit messages, and posting via chat.postMessage to project channels. Use thread_ts to organize follow-ups. chat.scheduleMessage for timed updates, and chat.update to edit sent messages. Rate limit: 1 message per second per channel. Block Kit payload limit: 50 blocks, approximately 40,000 characters — msg_blocks_too_long fires near 13,000 characters in practice.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesSlackMay 2026RapidDev Engineering Team
TL;DR

Automate Slack project updates by receiving external webhooks (GitHub, Jira, custom), formatting data as Block Kit messages, and posting via chat.postMessage to project channels. Use thread_ts to organize follow-ups. chat.scheduleMessage for timed updates, and chat.update to edit sent messages. Rate limit: 1 message per second per channel. Block Kit payload limit: 50 blocks, approximately 40,000 characters — msg_blocks_too_long fires near 13,000 characters in practice.

API Quick Reference

Auth

Bot Token (xoxb-)

Rate limit

1 message/second/channel

Format

JSON

SDK

Available

Understanding the Slack API

Slack project update automation follows a standard pattern: an external system fires an event (GitHub push, Jira status change, CI/CD deploy), your server receives it, formats it as a Block Kit message, and posts it to the relevant Slack channel. The Web API's chat.postMessage method handles the posting, with optional thread_ts to organize updates under a parent message.

Block Kit is Slack's message layout framework — up to 50 blocks per message, 3,000 characters per section text block, and a total payload of approximately 40,000 characters. In practice, the msg_blocks_too_long error can fire near 13,000 characters due to a known framework issue — keep your messages concise. Use chat.update to modify a sent message (e.g., changing a 'build in progress' message to 'build complete') and chat.scheduleMessage to pre-schedule status updates.

For real-time project updates, the flow is: webhook triggers your endpoint → you format and post → team members see the update without leaving Slack. Official docs: https://docs.slack.dev/messaging/sending-messages

Base URLhttps://slack.com/api

Setting Up Slack API Authentication

Bot tokens (xoxb-) are workspace-scoped and long-lived. For project updates, the bot needs chat:write to post messages, and optionally chat:update if you'll edit messages. Invite the bot to each project channel. Token rotation is opt-in — without it, tokens don't expire.

  1. 1Go to https://api.slack.com/apps and click Create New App > From scratch.
  2. 2Click OAuth & Permissions. Add Bot Token Scopes: chat:write, chat:write.public (optional, allows posting to public channels without joining).
  3. 3Click Install to Workspace and authorize.
  4. 4Copy the Bot User OAuth Token (xoxb-).
  5. 5Invite the bot to your project channels: /invite @YourBotName.
  6. 6Optionally set up Event Subscriptions (incoming webhooks from GitHub/Jira) or a Route Handler endpoint to receive external webhooks.
  7. 7Store SLACK_BOT_TOKEN in environment variables.
  8. 8Store channel IDs as environment variables (not names) — channel IDs never change, names can.
auth.py
1import os
2import requests
3
4BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
5HEADERS = {"Authorization": f"Bearer {BOT_TOKEN}"}
6
7# Verify token
8resp = requests.post("https://slack.com/api/auth.test", headers=HEADERS)
9data = resp.json()
10if data.get("ok"):
11 print(f"Authenticated as bot in: {data['team']}")

Security notes

  • Store SLACK_BOT_TOKEN in environment variables — never hardcode.
  • Validate incoming webhook signatures from GitHub, Jira, and other sources before processing.
  • Request only chat:write — avoid admin scopes for a project update bot.
  • Use channel IDs (C...), not channel names (#...) — names can change silently.
  • Store webhook secrets from GitHub/Jira in environment variables and verify HMAC signatures on every incoming request.

Key endpoints

POST/chat.postMessage

Sends a message to a channel. Supports Block Kit, threading via thread_ts, and optional reply_broadcast to also show thread replies in the main channel. Returns ts for future updates.

ParameterTypeRequiredDescription
channelstringrequiredChannel ID
textstringoptionalFallback plain text — always include even with blocks
blocksarrayoptionalBlock Kit payload — max 50 blocks, ~40,000 chars total
thread_tsstringoptionalTimestamp of parent message — posts as thread reply
reply_broadcastbooleanoptionalSet to true to also show thread reply in main channel

Request

json
1{"channel":"C01234567","text":"Build succeeded: v2.1.4","blocks":[{"type":"section","text":{"type":"mrkdwn","text":"*Build succeeded* :white_check_mark: `main` branch\n>Version: v2.1.4\n>Triggered by: @john"}},{"type":"context","elements":[{"type":"mrkdwn","text":"Duration: 3m 42s | <https://ci.example.com/build/123|View build>"}]}]}

Response

json
1{"ok":true,"channel":"C01234567","ts":"1746612000.000100","message":{"type":"message","text":"Build succeeded: v2.1.4"}}
POST/chat.update

Updates an existing message. Requires the channel ID and ts from the original chat.postMessage response. Use to change a status message (e.g., 'deploying...' to 'deployed').

ParameterTypeRequiredDescription
channelstringrequiredChannel ID of the original message
tsstringrequiredTimestamp of the message to update — from chat.postMessage response
blocksarrayoptionalNew Block Kit blocks to replace the original

Request

json
1{"channel":"C01234567","ts":"1746612000.000100","text":"Deploy complete","blocks":[{"type":"section","text":{"type":"mrkdwn","text":"*Deploy complete* :white_check_mark: v2.1.4 to production"}}]}

Response

json
1{"ok":true,"channel":"C01234567","ts":"1746612000.000100","text":"Deploy complete"}
POST/chat.scheduleMessage

Schedules a message to be posted at a future Unix timestamp. Cannot be edited after creation — must delete (chat.deleteScheduledMessage) and recreate to change. Use for scheduled daily standup reminders or end-of-sprint summaries.

ParameterTypeRequiredDescription
channelstringrequiredChannel ID
post_atnumberrequiredUnix timestamp (future) when the message should be sent

Request

json
1{"channel":"C01234567","text":"Sprint 12 ends today — retrospective at 4pm!","post_at":1746957600}

Response

json
1{"ok":true,"scheduled_message_id":"Q01234567890","post_at":1746957600,"channel":"C01234567"}

Step-by-step automation

1

Receive and Validate an Incoming Webhook

Why: External tools (GitHub, Jira, CI/CD) send webhooks to trigger project updates — validating their signatures prevents spoofed notifications.

Set up an HTTPS endpoint on your server to receive webhooks. For GitHub, verify the X-Hub-Signature-256 header using your webhook secret. For Jira, verify using the shared secret. For generic integrations, use a shared secret in the URL or header. After validation, parse the JSON payload and extract the fields you'll include in the Slack message.

request.sh
1# Test your webhook endpoint with a GitHub-style payload
2curl -X POST \
3 -H "Content-Type: application/json" \
4 -H "X-GitHub-Event: push" \
5 -H "X-Hub-Signature-256: sha256=$(echo -n '{"ref":"refs/heads/main"}' | openssl dgst -sha256 -hmac 'your_secret' | cut -d' ' -f2)" \
6 -d '{"ref":"refs/heads/main","head_commit":{"message":"Fix: login bug","author":{"name":"John"}},"repository":{"name":"myapp"}}' \
7 "https://yourapp.com/webhook/github"

Pro tip: Always use raw (un-parsed) request body bytes for HMAC verification — parsing JSON first (which re-serializes the payload) changes whitespace and breaks signature validation.

Expected result: The webhook endpoint validates incoming requests, extracts event data, and dispatches to the Slack notification function. Invalid signatures return 403.

2

Format the Update as a Block Kit Message

Why: Block Kit messages are significantly more readable and actionable than plain text — team members can see the key info at a glance without opening an external tool.

Build a Block Kit array from your webhook data. Use section blocks for main content, a divider, and context blocks for metadata. Keep each section's text under 3,000 characters. Include an action link to the relevant external resource (GitHub commit, Jira ticket, build log). Always include a text fallback for push notifications.

request.sh
1curl -X POST \
2 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "channel": "C01234567",
6 "text": "Push to main: Fix login bug",
7 "blocks": [
8 {"type":"section","text":{"type":"mrkdwn","text":"*Push to `main`* :arrow_up_small:\n>Fix login bug\n>by @john — <https://github.com/org/repo/commit/abc123|View commit>"}},
9 {"type":"context","elements":[{"type":"mrkdwn","text":"myapp | May 7, 2026 10:15 AM"}]}
10 ]
11 }' \
12 "https://slack.com/api/chat.postMessage"

Pro tip: Truncate commit messages to 100 characters — long commit messages can push your block payload toward the size limit and make the notification hard to read in Slack's sidebar.

Expected result: A Block Kit array ready to pass to chat.postMessage. The rendered message shows key data (branch, commit, author, link) in a clean two-line format.

3

Post the Update and Save the Message Timestamp

Why: The ts (timestamp) from chat.postMessage enables threading follow-up updates and editing the original message — save it whenever you want future updates to be organized.

POST to chat.postMessage and save the ts and channel from the response. For multi-step projects (build started → tests running → deploy complete), post the first message and update it with chat.update as statuses change. For update sequences you want to keep organized, post follow-ups as thread replies using thread_ts.

request.sh
1# Post and capture the ts
2curl -s -X POST \
3 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
4 -H "Content-Type: application/json" \
5 -d '{"channel":"C01234567","text":"Build started","blocks":[{"type":"section","text":{"type":"mrkdwn","text":"*Build started* :hourglass_flowing_sand: v2.1.4"}}]}' \
6 "https://slack.com/api/chat.postMessage" \
7 | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('ts',''))"

Pro tip: Store (channel, ts) in a database keyed by your internal job/build ID. This lets you look up the Slack message to update when the job completes — even if the complete event arrives minutes later via a separate webhook.

Expected result: The message is posted and the ts is captured. The message can later be updated with chat.update or threaded with thread_ts.

4

Schedule Future Status Updates with chat.scheduleMessage

Why: For predictable milestones (end-of-sprint, weekly standup reminders), scheduling messages ensures they arrive at the right time without keeping a process running.

Call chat.scheduleMessage with a channel, text/blocks, and post_at (Unix timestamp). The message is queued and delivered by Slack at the scheduled time — your server doesn't need to be running at delivery time. Important: scheduled messages cannot be edited after creation. To change a scheduled message, call chat.deleteScheduledMessage and then re-create it.

request.sh
1# Schedule a message for tomorrow 9am UTC
2POST_AT=$(date -d 'tomorrow 09:00:00' +%s)
3curl -X POST \
4 -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
5 -H "Content-Type: application/json" \
6 -d "{\"channel\":\"C01234567\",\"text\":\"Sprint 12 ends today — retro at 4pm!\",\"post_at\":$POST_AT}" \
7 "https://slack.com/api/chat.scheduleMessage"

Pro tip: Store scheduled_message_id in a database with your sprint/project records. If the sprint end date changes, delete the old scheduled message and create a new one with the updated time.

Expected result: A scheduled_message_id is returned. The message will be delivered at the specified time. The ID can be used to delete the scheduled message if plans change.

Complete working code

A complete GitHub webhook handler that receives push and deploy events, formats Block Kit messages, posts to the project Slack channel with live status updates (posting initial message then updating it when the build completes), and includes rate-limit-aware retry logic.

automate_slack_project_updates.py
1import os, time, hmac, hashlib, logging, requests
2from flask import Flask, request, jsonify
3
4logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
5
6BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
7GITHUB_SECRET = os.environ["GITHUB_WEBHOOK_SECRET"]
8PROJECT_CHANNEL = os.environ["SLACK_PROJECT_CHANNEL_ID"]
9H = {"Authorization": f"Bearer {BOT_TOKEN}"}
10
11# Store active build messages: {build_id: {channel, ts}}
12build_messages = {}
13
14flask_app = Flask(__name__)
15
16def post_slack(channel, text, blocks=None, thread_ts=None):
17 for _ in range(5):
18 payload = {"channel": channel, "text": text}
19 if blocks: payload["blocks"] = blocks
20 if thread_ts: payload["thread_ts"] = thread_ts
21 resp = requests.post("https://slack.com/api/chat.postMessage", headers=H, json=payload)
22 if resp.status_code == 429:
23 time.sleep(int(resp.headers.get("Retry-After", 2)) + 0.5)
24 continue
25 data = resp.json()
26 if not data.get("ok"): raise ValueError(data.get("error"))
27 return data
28 raise RuntimeError("Max retries")
29
30def update_slack(channel, ts, text, blocks=None):
31 payload = {"channel": channel, "ts": ts, "text": text}
32 if blocks: payload["blocks"] = blocks
33 resp = requests.post("https://slack.com/api/chat.update", headers=H, json=payload)
34 return resp.json()
35
36@flask_app.route("/webhook/github", methods=["POST"])
37def github_webhook():
38 sig = request.headers.get("X-Hub-Signature-256", "")
39 expected = "sha256=" + hmac.new(GITHUB_SECRET.encode(),
40 request.data, hashlib.sha256).hexdigest()
41 if not hmac.compare_digest(expected, sig):
42 return jsonify({"error": "bad signature"}), 403
43
44 event = request.headers.get("X-GitHub-Event", "")
45 data = request.json
46
47 if event == "push" and data.get("ref", "").startswith("refs/heads/main"):
48 branch = data["ref"].replace("refs/heads/", "")
49 commit = data["head_commit"]
50 blocks = [{
51 "type": "section",
52 "text": {"type": "mrkdwn", "text": (
53 f"*Push to `{branch}`* :arrow_up_small:\n"
54 f">*{commit['message'][:80]}*\n"
55 f">by {commit['author']['name']} | "
56 f"<{commit['url']}|View commit>"
57 )}
58 }]
59 result = post_slack(PROJECT_CHANNEL, f"Push to {branch}: {commit['message'][:50]}",
60 blocks=blocks)
61 logging.info(f"Posted push notification: ts={result['ts']}")
62
63 return jsonify({"ok": True})
64
65if __name__ == "__main__":
66 flask_app.run(port=3000)
67

Error handling

429HTTP 429, body: {"ok":false,"error":"ratelimited"}
Cause

More than 1 message per second was sent to the same channel (typical during high-traffic deploy events) or the workspace-level method rate limit was hit.

Fix

Read the Retry-After header and sleep for that duration. Add 1.1-second delays between sequential project update messages to the same channel.

Retry strategy

Sleep for Retry-After + 0.5 seconds, then retry once.

200 (ok: false){"ok":false,"error":"msg_blocks_too_long"}
Cause

The Block Kit payload exceeds the practical size limit (~13,000 characters) even with fewer than 50 blocks. This is a known Bolt framework issue (#2509).

Fix

Truncate long text fields (commit messages, PR descriptions, error logs) to under 200 characters each. If you need full details, link to the external source rather than embedding the text.

Retry strategy

Do not retry — reduce payload size.

200 (ok: false){"ok":false,"error":"message_not_found"}
Cause

chat.update was called with a ts that doesn't exist in the specified channel — the message may have been deleted or the ts is from a different channel.

Fix

Verify you're using the ts and channel from the original chat.postMessage response. A message's ts is unique per channel — the same ts in a different channel returns not_found.

Retry strategy

Do not retry — post a new message if the original is missing.

200 (ok: false){"ok":false,"error":"cant_update_message"}
Cause

Only the bot that originally posted the message can update it. If a different app or user posted the message, chat.update will fail.

Fix

Ensure your update call uses the same bot token that posted the original message. Store message ownership in your database.

Retry strategy

Do not retry — post a follow-up thread reply instead.

Rate Limits for Slack API

ScopeLimitWindow
chat.postMessage (per channel)1 messageper second
chat.updateTier 3 (~50+/min)per workspace per minute
chat.scheduleMessageTier 3 (~50+/min)per workspace per minute
retry-handler.ts
1import time
2
3def post_with_retry(url, headers, payload, max_retries=5):
4 import requests
5 for i in range(max_retries):
6 resp = requests.post(url, headers=headers, json=payload)
7 if resp.status_code == 429:
8 wait = int(resp.headers.get('Retry-After', 2)) + 0.5
9 time.sleep(wait)
10 continue
11 data = resp.json()
12 if not data.get('ok'):
13 if data.get('error') == 'ratelimited':
14 time.sleep(2)
15 continue
16 raise ValueError(data.get('error'))
17 return data
18 raise RuntimeError('Max retries exceeded')
  • Post one message per channel per second maximum — queue rapid-fire events and process them sequentially.
  • Use chat.update to edit an existing message instead of posting new messages for status transitions — reduces channel noise and respects rate limits.
  • For multi-step build pipelines, post one message per build and update it with the latest status rather than posting a new message for each step.
  • Store message (channel, ts) in a database keyed by your build/job ID so you can update it when the job completes via a separate webhook.
  • Use thread_ts to post detailed logs as thread replies — keeps the main channel clean while preserving drill-down details.

Security checklist

  • Store SLACK_BOT_TOKEN, GITHUB_WEBHOOK_SECRET, and other secrets in environment variables.
  • Always verify incoming webhook signatures (HMAC-SHA256 for GitHub, shared secrets for others) before processing events.
  • Use express.raw() (Node.js) or request.data (Flask) to get the raw body for signature verification — parsing JSON first breaks HMAC.
  • Limit chat.postMessage calls to authenticated and validated webhook payloads only.
  • Sanitize external data before including in Slack messages — prevent injection of @channel or @here mentions.
  • Store only essential data from webhook payloads — do not log full commit diffs or PR descriptions to your application logs.

Automation use cases

Live Build Status Board

intermediate

Post a 'build started' message and update it in place as the pipeline progresses through build, test, and deploy stages.

Jira Status Changes

intermediate

Receive Jira webhooks when ticket status changes (In Progress, In Review, Done) and post formatted updates to the relevant project channel.

PR Review Reminders

intermediate

Schedule reminder messages for open pull requests that haven't been reviewed after 24 hours, mentioning the assigned reviewers.

Sprint Summary

advanced

At sprint end, aggregate all Jira tickets completed that sprint and post a summary with stories completed, velocity, and blockers.

No-code alternatives

Don't want to write code? These platforms can automate the same workflows visually.

Zapier

Free tier available

Zapier has native GitHub and Jira integrations that can post formatted messages to Slack channels when events trigger, without writing webhook handlers.

Pros
  • + Native GitHub + Jira triggers
  • + No webhook handler code needed
  • + Easy setup
Cons
  • - Limited Block Kit customization
  • - No message update (chat.update) support
  • - Expensive at high event volume

Make (formerly Integromat)

Free tier available (1,000 operations/month)

Make's visual builder supports complex webhook flows with conditional logic, data transformation, and Slack posting with Block Kit control.

Pros
  • + More affordable than Zapier
  • + Full Block Kit JSON control
  • + Conditional routing
Cons
  • - Complex scenario setup for multi-step status updates
  • - No native chat.update support
  • - Execution limits

n8n

Free self-hosted; Cloud from €20/month

n8n's Slack node, GitHub node, and code node combination provides full project update automation including message updates and complex formatting.

Pros
  • + Self-hostable
  • + Built-in GitHub and Slack nodes
  • + Code node for complex logic
Cons
  • - Requires self-hosting for production
  • - More initial setup
  • - chat.update requires HTTP node

Best practices

  • Save the (channel, ts) from every chat.postMessage response in your database keyed by job/build/ticket ID — enables status updates and threading.
  • Use chat.update for in-place status changes rather than new messages — a single live-updating message is cleaner than a stream of status messages.
  • Keep Block Kit text fields under 200 characters for webhook data — truncate and link to source for full details.
  • Always include a plain text fallback alongside blocks — used in push notifications and accessibility.
  • For high-frequency events (e.g., every commit), consider batching updates every 30 seconds rather than posting for each event.
  • Validate and sanitize all external data before including in Block Kit text — long commit messages, unusual characters, and mrkdwn-like text can break formatting.
  • Schedule batch reports (daily, weekly) during off-peak hours — reduces noise and respects rate limits.

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm building a Slack project update bot that receives GitHub webhooks and posts formatted notifications. Help me: 1) verify the X-Hub-Signature-256 header in Python/Flask using HMAC-SHA256, 2) build a Block Kit message for push events with branch, commit message (truncated to 100 chars), author, and link, 3) post via chat.postMessage and save the ts and channel, 4) when a build completion webhook arrives later, use chat.update with the saved ts to update the message in place, and 5) handle 429 rate limits with Retry-After. Use Python with Flask and requests library.

Lovable / V0 Prompt

Build a Slack project update dashboard. Features: webhook URL management (GitHub, Jira, custom) with test-send buttons; Block Kit message template editor for each webhook source with live preview; a message log showing all sent project updates with channel, timestamp, and content preview; edit-in-place UI that calls chat.update on existing messages; a scheduled messages manager showing pending chat.scheduleMessage entries with delete/reschedule options; and analytics showing update frequency by project and day.

Frequently asked questions

Is the Slack API free for project update bots?

Yes. The Slack Web API (chat.postMessage, chat.update, chat.scheduleMessage) is free with no per-message pricing. Rate limits apply regardless of plan.

Why do I get msg_blocks_too_long even with fewer than 50 blocks?

There is a known issue (Bolt #2509) where msg_blocks_too_long fires near 13,000 characters total payload size, even with a small block count. The documented limit is 50 blocks / ~40,000 characters, but in practice the error appears earlier. Truncate long text fields to under 200 characters each and link to external sources for full details.

Can I edit a scheduled message after creating it?

No. chat.scheduleMessage cannot be edited after creation. To change it, call chat.deleteScheduledMessage with the scheduled_message_id, then create a new scheduled message with the updated content or time.

How do I update a message that was posted by my bot?

Call chat.update with the channel ID and ts from the original chat.postMessage response. Only the bot that posted the original message can update it — the bot token must match. Store (channel, ts) in your database when posting to enable future updates.

What happens if my webhook endpoint is down when GitHub fires an event?

GitHub retries webhook deliveries on failure with exponential backoff — three times over 18 hours. Slack's Events API (for Slack-originated events) retries with exponential backoff for up to 24 hours. Implement idempotency in your webhook handler: track event IDs (GitHub delivery IDs) and skip re-processing if already handled.

Can I post to multiple channels from one webhook event?

Yes. In your webhook handler, call chat.postMessage once for each target channel. Add a 1.1-second delay between calls if sending to the same channel repeatedly, but different channels can receive simultaneous posts.

Can RapidDev help build a custom Slack CI/CD integration?

Yes. RapidDev has built 600+ apps including Slack bots with GitHub, Jira, and Linear integrations for live build status updates, PR review workflows, and sprint reporting. Visit rapidevelopers.com for a free consultation.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.