From openclaudia-openclaudia-skills
Sends messages and rich Block Kit content to Slack channels via webhooks or Bot API. Useful for formatted announcements, marketing reports, metrics, and updates.
npx claudepluginhub joshuarweaver/cascade-communication --plugin openclaudia-openclaudia-skillsThis skill is limited to using the following tools:
Send messages and rich content to Slack channels using Incoming Webhooks or the Slack Web API.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Send messages and rich content to Slack channels using Incoming Webhooks or the Slack Web API. Build formatted announcements, marketing reports, metrics dashboards, and community updates with Block Kit.
Requires either SLACK_WEBHOOK_URL or SLACK_BOT_TOKEN set in .env, .env.local, or
~/.claude/.env.global.
source ~/.claude/.env.global 2>/dev/null
source .env 2>/dev/null
source .env.local 2>/dev/null
if [ -n "$SLACK_WEBHOOK_URL" ]; then
echo "SLACK_WEBHOOK_URL is set. Webhook mode available."
elif [ -n "$SLACK_BOT_TOKEN" ]; then
echo "SLACK_BOT_TOKEN is set. Web API mode available."
else
echo "Neither SLACK_WEBHOOK_URL nor SLACK_BOT_TOKEN is set."
echo "See the Setup Guide below to configure Slack credentials."
fi
If neither variable is set, instruct the user to follow the Setup Guide section below.
Incoming Webhooks are the fastest way to post messages. They require no OAuth scopes and are scoped to a single channel.
https://hooks.slack.com/services/...).echo 'SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../xxxx' >> .env
Limitations: One webhook per channel. Cannot read messages, list channels, or reply to
threads programmatically (you must know the thread_ts from a prior API response).
Bot tokens give access to the full Slack Web API: post to any channel the bot is in, reply to threads, list channels, upload files, and more.
chat:write - Post messageschat:write.public - Post to channels without joiningchannels:read - List public channelsfiles:write - Upload files (optional, for images/reports)reactions:write - Add emoji reactions (optional)xoxb-).echo 'SLACK_BOT_TOKEN=xoxb-your-token-here' >> .env
/invite @YourBotName in each channel.Optional: Set a default channel for convenience:
echo 'SLACK_DEFAULT_CHANNEL=#marketing' >> .env
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"text": "Hello from the marketing bot!"
}'
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"text": "New blog post published!",
"username": "Marketing Bot",
"icon_emoji": ":mega:"
}'
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "New Product Launch"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Product X* is now live! Check out the announcement."
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Read the full announcement on our blog."
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Read More"
},
"url": "https://example.com/blog/launch"
}
}
]
}'
The Web API provides full control over message delivery, threading, channel management, and more.
All requests go to https://slack.com/api/ with the header Authorization: Bearer {SLACK_BOT_TOKEN}.
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"channel": "#marketing",
"text": "Weekly metrics report is ready!",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Weekly Marketing Metrics"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Here are the numbers for this week."
}
}
]
}'
The response includes a ts (timestamp) field which identifies the message. Save this value
for threading replies:
# Post and capture the message timestamp for threading
RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"channel": "#marketing",
"text": "Thread parent message"
}')
MESSAGE_TS=$(echo "$RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ts',''))")
echo "Message timestamp: $MESSAGE_TS"
Use the thread_ts parameter to reply inside an existing thread:
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"channel\": \"#marketing\",
\"thread_ts\": \"${MESSAGE_TS}\",
\"text\": \"This is a threaded reply with additional details.\"
}"
To also broadcast the reply to the channel (so it appears in the main conversation as well),
add "reply_broadcast": true.
curl -s -X POST "https://slack.com/api/chat.update" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"channel\": \"#marketing\",
\"ts\": \"${MESSAGE_TS}\",
\"text\": \"Updated message content.\",
\"blocks\": []
}"
curl -s -X POST "https://slack.com/api/chat.delete" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"channel\": \"#marketing\",
\"ts\": \"${MESSAGE_TS}\"
}"
Useful for discovering which channel to post to:
curl -s "https://slack.com/api/conversations.list?types=public_channel&limit=100" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
for ch in data.get('channels', []):
members = ch.get('num_members', 0)
print(f\"#{ch['name']} | Members: {members} | ID: {ch['id']}\")
"
curl -s -X POST "https://slack.com/api/files.uploadV2" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-F "file=@report.pdf" \
-F "filename=weekly-report.pdf" \
-F "channel_id=C0123456789" \
-F "initial_comment=Here is this week's marketing report."
curl -s -X POST "https://slack.com/api/reactions.add" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"channel\": \"C0123456789\",
\"timestamp\": \"${MESSAGE_TS}\",
\"name\": \"white_check_mark\"
}"
Block Kit is Slack's UI framework for building rich, interactive messages. Messages are composed of an array of blocks, each with a specific type and structure.
| Block Type | Purpose | Supports mrkdwn |
|---|---|---|
header | Large bold title text | No (plain_text only) |
section | Primary content block with text and optional accessory | Yes |
divider | Horizontal line separator | N/A |
image | Full-width image with alt text | N/A |
context | Small, muted text and images (for metadata, timestamps) | Yes |
actions | Row of interactive elements (buttons, selects, date pickers) | N/A |
rich_text | Advanced formatted text (lists, quotes, code blocks) | N/A |
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Weekly Marketing Report",
"emoji": true
}
}
Plain text section:
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Traffic is up 23%* this week compared to last week.\nOrganic search drove most of the growth."
}
}
Section with fields (two-column layout):
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Visitors*\n12,450"
},
{
"type": "mrkdwn",
"text": "*Signups*\n342"
},
{
"type": "mrkdwn",
"text": "*MRR*\n$28,500"
},
{
"type": "mrkdwn",
"text": "*Churn*\n1.2%"
}
]
}
Section with a button accessory:
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "New blog post: *How We Grew 10x in 6 Months*"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Read Post"
},
"url": "https://example.com/blog/growth-story",
"action_id": "read_blog_post"
}
}
Section with an image accessory:
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*New Feature: Dark Mode*\nOur most requested feature is finally here."
},
"accessory": {
"type": "image",
"image_url": "https://example.com/images/dark-mode-preview.png",
"alt_text": "Dark mode preview"
}
}
{
"type": "divider"
}
{
"type": "image",
"image_url": "https://example.com/images/chart.png",
"alt_text": "Weekly traffic chart",
"title": {
"type": "plain_text",
"text": "Traffic Overview"
}
}
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Posted by *Marketing Team* | Feb 10, 2026"
},
{
"type": "image",
"image_url": "https://example.com/logo-small.png",
"alt_text": "Company logo"
}
]
}
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Approve"
},
"style": "primary",
"action_id": "approve_action",
"value": "approved"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Reject"
},
"style": "danger",
"action_id": "reject_action",
"value": "rejected"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Details"
},
"url": "https://example.com/details",
"action_id": "view_details"
}
]
}
Button styles: "primary" (green), "danger" (red), or omit for default (gray).
Note on interactivity: Buttons with action_id (no url) require a Request URL configured
in your Slack App settings under Interactivity & Shortcuts to receive the button click
payload. Buttons with a url field open the link directly and do not require a backend.
| Limit | Value |
|---|---|
| Blocks per message | 50 |
| Characters per text block | 3,000 |
| Characters per header | 150 |
| Fields per section | 10 |
| Elements per actions block | 25 |
| Elements per context block | 10 |
Use the visual builder to design and preview messages before coding them: https://app.slack.com/block-kit-builder
Slack uses its own markdown variant called mrkdwn. It differs from standard Markdown in several ways.
| Formatting | Syntax | Example |
|---|---|---|
| Bold | *text* | bold text |
| Italic | _text_ | italic text |
| Strikethrough | ~text~ | |
| Code (inline) | `text` | inline code |
| Code block | ```text``` | Multi-line code block |
| Blockquote | >text | Quoted text |
| Link | <https://url|display text> | Clickable link |
| User mention | <@U0123456> | @username |
| Channel mention | <#C0123456> | #channel |
| Emoji | :emoji_name: | :rocket: |
| Bulleted list | Start line with - or * | Bullet point |
| Numbered list | Start line with 1. | Numbered item |
| Line break | \n in JSON string | New line |
Important differences from standard Markdown:
*bold*, not double **bold**._italic_, not single asterisks.<url|text> format with a pipe, not [text](url).header block type instead.image block or accessory instead.curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":rocket: New Feature: [Feature Name]",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "We just shipped *[Feature Name]* — here is what it does and why it matters.\n\n:point_right: *What it does:* [One-sentence description]\n:point_right: *Why it matters:* [Key benefit for users]\n:point_right: *How to try it:* [Quick instructions or link]"
}
},
{
"type": "image",
"image_url": "https://example.com/feature-screenshot.png",
"alt_text": "Feature screenshot"
},
{
"type": "divider"
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": ":newspaper: Read Announcement",
"emoji": true
},
"url": "https://example.com/blog/feature-launch",
"action_id": "read_announcement"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": ":play_or_pause_button: Watch Demo",
"emoji": true
},
"url": "https://example.com/demo",
"action_id": "watch_demo"
}
]
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Posted by *Product Team* | :speech_balloon: Reply in thread with questions"
}
]
}
]
}'
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":bar_chart: Weekly Marketing Metrics — Feb 3-9, 2026",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Here is this week'\''s performance snapshot."
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":globe_with_meridians: *Website Traffic*"
},
"fields": [
{
"type": "mrkdwn",
"text": "*Sessions*\n45,230 (:arrow_up: 12%)"
},
{
"type": "mrkdwn",
"text": "*Unique Visitors*\n31,870 (:arrow_up: 8%)"
},
{
"type": "mrkdwn",
"text": "*Bounce Rate*\n42.3% (:arrow_down: 2.1%)"
},
{
"type": "mrkdwn",
"text": "*Avg. Session Duration*\n3m 42s (:arrow_up: 15s)"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":money_with_wings: *Conversion Metrics*"
},
"fields": [
{
"type": "mrkdwn",
"text": "*Signups*\n487 (:arrow_up: 18%)"
},
{
"type": "mrkdwn",
"text": "*Trial-to-Paid*\n12.4% (:arrow_up: 1.2%)"
},
{
"type": "mrkdwn",
"text": "*MRR*\n$52,300 (:arrow_up: $3,200)"
},
{
"type": "mrkdwn",
"text": "*Churn*\n1.8% (:arrow_down: 0.3%)"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":email: *Email Performance*"
},
"fields": [
{
"type": "mrkdwn",
"text": "*Emails Sent*\n12,400"
},
{
"type": "mrkdwn",
"text": "*Open Rate*\n34.2%"
},
{
"type": "mrkdwn",
"text": "*Click Rate*\n4.8%"
},
{
"type": "mrkdwn",
"text": "*Unsubscribes*\n23"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*:bulb: Key Takeaways*\n- Organic traffic up 15% after publishing 3 new blog posts\n- Email welcome sequence A/B test: Variant B outperformed by 22%\n- Trial signup spike on Thursday correlated with Product Hunt feature"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Full Dashboard",
"emoji": true
},
"url": "https://analytics.example.com/dashboard",
"action_id": "view_dashboard"
}
]
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Auto-generated by Marketing Bot | Data from Google Analytics + Stripe"
}
]
}
]
}'
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":pencil: New Blog Post Published",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*<https://example.com/blog/post-slug|Blog Post Title Here>*\n\nA brief summary of what the post covers — keep it to 2-3 sentences that capture the key value and make people want to click through."
},
"accessory": {
"type": "image",
"image_url": "https://example.com/blog/post-og-image.png",
"alt_text": "Blog post cover image"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": ":bust_in_silhouette: Author: *Jane Smith* | :clock1: 6 min read | :label: SEO, Growth"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":mega: *Help us amplify!* Share this post on your socials. Here are ready-to-use snippets:\n\n*Twitter/X:* _Just published: [title]. [Key insight from the post]. Link in reply._\n\n*LinkedIn:* _We just published a deep dive on [topic]. Here is the #1 takeaway: [insight]._"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Read the Post",
"emoji": true
},
"style": "primary",
"url": "https://example.com/blog/post-slug",
"action_id": "read_post"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Share on Twitter",
"emoji": true
},
"url": "https://twitter.com/intent/tweet?text=Check%20out%20this%20post&url=https://example.com/blog/post-slug",
"action_id": "share_twitter"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Share on LinkedIn",
"emoji": true
},
"url": "https://www.linkedin.com/sharing/share-offsite/?url=https://example.com/blog/post-slug",
"action_id": "share_linkedin"
}
]
}
]
}'
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":clipboard: Marketing Team Update — Monday, Feb 10",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*:white_check_mark: Completed Last Week*\n- Launched email welcome sequence v2\n- Published 3 blog posts (SEO, product, case study)\n- Set up Google Ads remarketing campaign\n- Shipped landing page A/B test (Variant B live)"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*:construction: In Progress*\n- Content calendar for March (70% done)\n- Competitor analysis report (due Wednesday)\n- Social media campaign for Product Hunt launch"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*:dart: This Week'\''s Priorities*\n1. Finalize Product Hunt launch assets\n2. Send weekly newsletter (Thursday 9am)\n3. Review and approve Q1 ad spend budget\n4. Onboard new content writer"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*:warning: Blockers*\n- Waiting on design team for Product Hunt gallery images\n- Need legal review on new case study before publishing"
}
},
{
"type": "divider"
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": ":speech_balloon: Reply in thread with your own updates or questions"
}
]
}
]
}'
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":rotating_light: Marketing Alert",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Issue:* [Brief description of the problem]\n*Impact:* [Who/what is affected]\n*Status:* :red_circle: Active\n*Owner:* <@U0123456>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Details:*\n[Longer explanation. What happened, when it started, what we know so far.]"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Next Steps:*\n1. [Action item 1]\n2. [Action item 2]\n3. [Action item 3]"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Status Page"
},
"url": "https://status.example.com",
"action_id": "status_page"
}
]
}
]
}'
When the user asks to post an announcement, product update, or news to Slack:
ts for threading.When the user asks to send a metrics report or dashboard to Slack:
fields for the two-column metric layout.When a new blog post needs to be distributed to the team:
For managing Slack community channels (public communities, customer channels):
TITLE="Product X v2.0 Released"
DESCRIPTION="Version 2.0 includes dark mode, API improvements, and 3x faster performance."
LINK="https://example.com/changelog/v2"
IMAGE_URL="https://example.com/images/v2-banner.png"
CHANNEL="#announcements"
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json
payload = {
'channel': '${CHANNEL}',
'text': '${TITLE}',
'blocks': [
{
'type': 'header',
'text': {'type': 'plain_text', 'text': '${TITLE}', 'emoji': True}
},
{
'type': 'section',
'text': {'type': 'mrkdwn', 'text': '${DESCRIPTION}'}
},
{
'type': 'image',
'image_url': '${IMAGE_URL}',
'alt_text': '${TITLE}'
},
{
'type': 'actions',
'elements': [{
'type': 'button',
'text': {'type': 'plain_text', 'text': 'Learn More'},
'url': '${LINK}',
'style': 'primary',
'action_id': 'learn_more'
}]
}
]
}
print(json.dumps(payload))
")"
For complex messages, write the payload to a file first:
# Write the payload
cat > /tmp/slack-message.json << 'PAYLOAD'
{
"channel": "#marketing",
"text": "Fallback text for notifications",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Message Title"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Message body with *bold* and _italic_ formatting."
}
}
]
}
PAYLOAD
# Send it
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/slack-message.json
Post a message at a specific future time using chat.scheduleMessage:
# Schedule a message for a specific Unix timestamp
# Use: date -d "2026-02-12 09:00:00" +%s (Linux) or date -j -f "%Y-%m-%d %H:%M:%S" "2026-02-12 09:00:00" +%s (macOS)
SEND_AT=$(date -j -f "%Y-%m-%d %H:%M:%S" "2026-02-12 09:00:00" +%s 2>/dev/null || date -d "2026-02-12 09:00:00" +%s)
curl -s -X POST "https://slack.com/api/chat.scheduleMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"channel\": \"#marketing\",
\"post_at\": ${SEND_AT},
\"text\": \"Good morning team! Here is today's marketing agenda.\",
\"blocks\": []
}"
List scheduled messages:
curl -s "https://slack.com/api/chat.scheduledMessages.list" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" | \
python3 -c "
import json, sys, datetime
data = json.load(sys.stdin)
for msg in data.get('scheduled_messages', []):
ts = datetime.datetime.fromtimestamp(msg['post_at']).strftime('%Y-%m-%d %H:%M')
print(f\"ID: {msg['id']} | Channel: {msg['channel_id']} | Scheduled: {ts}\")
"
Delete a scheduled message:
curl -s -X POST "https://slack.com/api/chat.deleteScheduledMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"channel": "C0123456789",
"scheduled_message_id": "Q0123456789"
}'
Post the same message to multiple channels:
CHANNELS=("#marketing" "#general" "#product")
MESSAGE='{"text":"Big announcement coming tomorrow!","blocks":[{"type":"section","text":{"type":"mrkdwn","text":":mega: *Big announcement coming tomorrow!* Stay tuned."}}]}'
for CHANNEL in "${CHANNELS[@]}"; do
echo "Posting to ${CHANNEL}..."
echo "$MESSAGE" | python3 -c "
import json, sys
msg = json.load(sys.stdin)
msg['channel'] = '${CHANNEL}'
print(json.dumps(msg))
" | curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d @- | python3 -c "
import json, sys
r = json.load(sys.stdin)
if r.get('ok'):
print(f' Sent. ts={r[\"ts\"]}')
else:
print(f' Error: {r.get(\"error\", \"unknown\")}')
"
done
| Error | Cause | Fix |
|---|---|---|
invalid_auth | Bad or expired token | Regenerate the bot token in Slack App settings |
channel_not_found | Bot not in channel or wrong channel name | Invite bot with /invite @BotName or use channel ID |
not_in_channel | Bot needs to join the channel first | Invite the bot or use chat:write.public scope |
too_many_attachments | Over 50 blocks | Split the message into multiple posts or thread replies |
msg_too_long | Text exceeds 40,000 characters | Shorten the message or split into parts |
rate_limited | Too many requests | Wait the number of seconds in the Retry-After header |
missing_scope | Token lacks required permission | Add the scope in OAuth & Permissions and reinstall the app |
RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"channel":"#marketing","text":"Test message"}')
python3 -c "
import json, sys
r = json.loads('${RESPONSE}'.replace(\"'\", \"\"))
if r.get('ok'):
print(f'Message sent successfully. ts={r[\"ts\"]} channel={r[\"channel\"]}')
else:
print(f'Error: {r.get(\"error\", \"unknown\")}')
if r.get('response_metadata', {}).get('messages'):
for m in r['response_metadata']['messages']:
print(f' Detail: {m}')
" 2>/dev/null || echo "$RESPONSE"
A more robust approach using a temp file:
RESPONSE_FILE=$(mktemp)
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"channel":"#marketing","text":"Test message"}' \
-o "$RESPONSE_FILE"
python3 -c "
import json
with open('${RESPONSE_FILE}') as f:
r = json.load(f)
if r.get('ok'):
print(f'Sent. ts={r[\"ts\"]}')
else:
print(f'Error: {r.get(\"error\")}')
"
rm -f "$RESPONSE_FILE"
text field alongside blocks — it serves as the fallback for
notifications, accessibility readers, and clients that do not support Block Kit.chat.postMessage (Web API) over webhooks. It returns a
message ts you can use for threading, updating, and deleting.:emoji: codes in plain_text fields with "emoji": true to render emoji in headers
and button labels.& becomes &, < becomes <, > becomes >.fields for the two-column layout rather than trying to
format tables in mrkdwn (Slack does not support tables).