Slack Channel Monitor
This skill should be used when the user asks to "monitor a Slack channel", "watch Slack for messa…
by openhands · MIT
The skill
This skill should be used when the user asks to "monitor a Slack channel", "watch Slack for messages", "create a Slack bot that responds to mentions", "set up an OpenHands Slack integration", "trigger OpenHands from Slack", "respond to @openhands in Slack", or "poll Slack channels for a trigger phrase". Guides the user through creating a cron automation that watches up to 10 Slack channels and starts an OpenHands conversation whenever a configurable trigger phrase is detected.
Slack Channel Monitor
Create a cron automation that polls up to 10 Slack channels every minute.
Windows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in references/windows.md.
When a message containing the trigger phrase (default: @openhands) is
detected it:
- Adds a 👀 reaction to the triggering message.
- Opens an OpenHands conversation with the message and recent channel context.
- Posts a reply in the Slack thread with a link to the conversation.
On every subsequent run:
- New Slack thread replies are forwarded only when they contain the trigger phrase, so unrelated conversation in the thread is ignored.
- When the conversation finishes (or errors), the agent's final response is posted back to the Slack thread.
- Completed conversations stay in a short follow-up watch window, allowing triggered Slack replies to continue the same OpenHands conversation.
Runs on a local Agent Canvas (
dev:automationstack) and on OpenHands Cloud and Enterprise. On Cloud and Enterprise each conversation is started through the OpenHands API in a sandbox of its own, with the user's own settings, and the automation keeps its state in the automation service's KV store.
Prerequisites
Required secrets
Verify that at least one of the following secrets is set in OpenHands Settings → Secrets before proceeding:
| Secret name | Token type | Minimum scopes |
|---|---|---|
SLACK_BOT_TOKEN |
Bot (xoxb-…) |
channels:history, channels:read, reactions:write, chat:write |
SLACK_USER_TOKEN |
User (xoxp-…) |
Same as bot, plus search:read for multi-channel efficiency |
Check with:
# For bot token:
curl -s https://slack.com/api/auth.test -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))"
# For user token:
curl -s https://slack.com/api/auth.test -H "Authorization: Bearer $SLACK_USER_TOKEN" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))"
If neither token is present, inform the user and stop - the automation cannot function without Slack credentials.
Optional secret
| Secret name | Default | Purpose |
|---|---|---|
OPENHANDS_URL |
http://localhost:8000 |
Base URL used to build conversation links posted in Slack (local Agent Canvas only; on OpenHands Cloud and Enterprise the links open the deployment's own Canvas) |
Setup Workflow
Follow these steps in order.
Step 1 - Collect channels
Ask the user: "Which Slack channels should be monitored? You can provide
channel names (e.g. #general) or IDs (e.g. C0123456789)."
If the user provides channel names, resolve them to IDs:
SLACK_TOKEN="${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}"
curl -s "https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true" \
-H "Authorization: Bearer $SLACK_TOKEN" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
print('ERROR:', data.get('error'))
exit(1)
names = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])
for ch in data.get('channels', []):
if ch['name'] in names:
print(f\"{ch['name']} → {ch['id']}\")
"
Replace CHANNEL_NAMES_HERE with the comma-separated names the user provided.
If conversations.list returns missing_scope or not_authed:
Inform the user: "The token doesn't have permission to list channels. Please
provide the channel IDs directly (right-click a channel in Slack → Copy link -
the last path segment starting with C is the ID)."
If the bot token lacks channels:read for private channels, the user can
either invite the bot first (/invite @botname) or switch to a user token.
Collect up to 10 channel IDs. Record them as a Python list literal, e.g.:
["C0123456789", "C9876543210"]
Step 2 - Collect trigger phrase
Ask the user: "What trigger phrase should OpenHands respond to?
(Press Enter to use the default: @openhands)"
Accepted values: any non-empty string unlikely to appear accidentally, e.g.
@openhands, jazz hands, take-me-to-funky-town.
Step 3 - Generate the automation script
Read scripts/main.py from this skill's directory and copy it verbatim.
Apply exactly three constant substitutions near the top of the file:
Do not reimplement, simplify, or hand-write a replacement script. The template already contains the correct secret-loading, state-path, conversation-creation, and context-forwarding logic. Only the three configuration constants below should change unless syntax validation fails.
| Placeholder | Replace with |
|---|---|
TRIGGER_PHRASE = "@openhands" |
TRIGGER_PHRASE = "{user_phrase}" |
CHANNEL_IDS: list[str] = [] |
CHANNEL_IDS: list[str] = {channel_id_list} |
DEFAULT_OPENHANDS_URL = "http://localhost:8000" |
DEFAULT_OPENHANDS_URL = "{url}" (keep default if user has no preference) |
Write the customised script to a temporary directory:
mkdir -p /tmp/slack-monitor-build
# copy scripts/main.py to /tmp/slack-monitor-build/main.py
# then replace only the three constants above
Validate syntax before packaging:
python3 -m py_compile /tmp/slack-monitor-build/main.py && echo "Syntax OK"
Then run a quick integrity check to confirm the template structure is still present and only the configuration block was customised:
grep -n 'TRIGGER_PHRASE = "' /tmp/slack-monitor-build/main.py
grep -n 'CHANNEL_IDS: list\[str\] =' /tmp/slack-monitor-build/main.py
grep -n 'DEFAULT_OPENHANDS_URL = "' /tmp/slack-monitor-build/main.py
grep -n 'def get_secret' /tmp/slack-monitor-build/main.py
grep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py
grep -n 'def create_conversation' /tmp/slack-monitor-build/main.py
If any of those checks fail, stop and re-copy the template instead of trying to repair a hand-written variant.
Step 4 - Package and upload
Set OPENHANDS_HOST and AUTH_HEADER for the commands below:
- OpenHands Cloud or Enterprise - a
<HOST>value is in your system prompt: use it asOPENHANDS_HOST, withAUTH_HEADER="Authorization: Bearer $OPENHANDS_API_KEY" - Local Agent Canvas - an Automation backend is listed in the
<RUNTIME_SERVICES>block of your system context: use itsurl_from_agentasOPENHANDS_HOST, withAUTH_HEADER="X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY"
If neither is available, stop and tell the user to start the full automation stack.
On OpenHands Cloud or Enterprise each run starts in a fresh sandbox, so the
automation keeps its state in the KV store: check that kvStore is listed under
features in GET ${OPENHANDS_HOST}/api/automation/v1/capabilities before
deploying.
tar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .
TARBALL_PATH=$(curl -s -X POST \
"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/gzip" \
--data-binary @/tmp/slack-monitor.tar.gz \
| python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
echo "Uploaded: $TARBALL_PATH"
If the upload fails with a size error, the tarball must be under 1 MB.
main.py is under 15 KB so this should never trigger.
Step 5 - Create the automation
curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Slack Channel Monitor\",
\"trigger\": {\"type\": \"cron\", \"schedule\": \"* * * * *\"},
\"tarball_path\": \"$TARBALL_PATH\",
\"entrypoint\": \"python3 main.py\",
\"timeout\": 55
}" | python3 -m json.tool
A 55-second timeout keeps runs well within the 60-second cron window.
Record the returned id - share it with the user as confirmation.
Step 6 - Confirm
Tell the user:
✅ Slack Channel Monitor is running!
- Automation ID:
{id}- Channels:
{channel list}- Trigger phrase:
{phrase}- Polling every minute via cron
* * * * *- State: the automation service's KV store (a local Agent Canvas without one uses
~/.openhands/workspaces/automation-state/slack_poller_{id}.json)Send a message containing
{phrase}in any monitored channel to test it. The bot will react with 👀 and reply with a link to the new conversation.
Runtime Behaviour (per poll)
Each cron run executes main.py, which runs 10 polling iterations (every
5 seconds) within the 55-second timeout window. Each iteration:
- Loads state from the KV store, or from the JSON file on a local Agent
Canvas without one (see
references/state-schema.md). - Resolves the Slack token - checks
SLACK_USER_TOKENthenSLACK_BOT_TOKEN. - Fetches new messages:
- User token +
search:read+ > 1 channel → singlesearch.messagescall (searches for the trigger phrase across all channels). - Otherwise → one
conversations.historycall per channel.
- User token +
- Fetches due thread replies - polls at most one tracked thread per iteration using per-thread exponential backoff to stay within Slack rate limits.
- Processes messages in chronological order:
- Skips messages already in
processed_ts(dedup across the overlap window). - Skips bot messages and any
tsinbot_message_ts. - Reply in a tracked thread whose text contains the trigger phrase → forwards a follow-up request to the existing conversation and resets the follow-up watch window. Replies without the trigger phrase are marked processed and ignored.
- Contains trigger phrase outside a tracked conversation → 👀 reaction, create
a new conversation, post link.
- Records
defaultand the concrete model from the server settings already used to start the conversation, for the response footer. - On OpenHands Cloud and Enterprise the conversation is started through the OpenHands API in a sandbox of its own, with the user's own settings; the link opens it in the deployment's Canvas, and the footer names the model the conversation reports once it is up.
- Thread replies: agent receives full thread history for context.
- Root messages: agent receives the trigger text only.
- Records
- Skips messages already in
- Checks conversation statuses - for each active conversation where
time.time() - last_activity > 15 s:- If status is
idle,finished,error, orstuck→ fetch the agent's final response via/api/conversations/{id}/agent_final_response, append the LLM profile and model, and post it to the Slack thread using Slack'smarkdown_textfield so Markdown formatting renders correctly. Mark the recordwatchingfor five minutes so triggered follow-up replies can continue the same conversation.
- If status is
- Advances
last_polltonow - 10 s(overlap window prevents boundary races). If a conversation creation failed, pinslast_pollfurther back to retry on the next iteration. - Saves state (including
processed_ts) and continues to the next iteration. - After all iterations, fires the completion callback.
Debug output is written to both stdout and a persistent log at:
{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log
Additional Resources
Reference Files
references/slack-api.md- Slack token types, required scopes, API endpoint reference, rate limits, and common error codes.references/state-schema.md- State JSON schema, field definitions, example file, and conversation lifecycle diagram.
Script Template
scripts/main.py- The complete automation script. Customise the three constants at the top (TRIGGER_PHRASE,CHANNEL_IDS,DEFAULT_OPENHANDS_URL) before packaging.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with auth.test; /invite @botname |
not_in_channel error in run logs |
Bot token used but bot not a member | Invite bot or switch to user token |
missing_scope error |
Token lacks required scopes | Re-install Slack app with correct scopes (see references/slack-api.md) |
| No messages detected | last_poll timestamp is in the future |
Delete the state file to reset; it will be recreated on next run |
| Conversation link 404 | OPENHANDS_URL points to wrong host (local Agent Canvas) |
Set the OPENHANDS_URL secret to the correct base URL |
| Summary never posted | Conversation stuck in running state |
Check conversation in the OpenHands UI; the agent may need intervention |
| Duplicate conversations created | processed_ts state missing or corrupted |
Delete the state file to reset; dedup will rebuild on next run |
| Trigger message processed on each cron run | State file deleted between runs | Ensure automation-state/ directory is persistent across runs |
| Debug info needed | Need detailed per-message trace | Check {WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log |
Use this skill
Connect your agent to the Skill Store MCP server and ask for it by ref: openhands/slack-channel-monitor-21817078411.
Details
- Publisher
- openhands
- Category
- AI & Agents › Tool & MCP integration
- License
- MIT
- Updated
- 2026-10-19
References
More from openhands
- Build Setup Set up build environment and test fixtures for COBOL-to-Java migrations.
- Cobol Modernization End-to-end COBOL to Java migration workflow.
- Mainframe Planning Create a transformation guide for replacing mainframe-specific COBOL constructs with standard COB…
- Mainframe Removal Apply mainframe dependency transformations to COBOL code using a pre-generated transformation guide.