skills/slack-channel-monitor/SKILL.md
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.
npx skillsauth add openhands/skills slack-channel-monitorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
On every subsequent run:
Local mode only. This automation targets the local OpenHands setup (
dev:automationstack). A cloud/webhook-based variant is out of scope here.
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.
| Secret name | Default | Purpose |
|---|---|---|
| OPENHANDS_URL | http://localhost:8000 | Base URL used to build conversation links posted in Slack |
Follow these steps in order.
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"]
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.
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.
Determine the Automation backend URL and auth from the <RUNTIME_SERVICES>
block in your system context:
url_from_agent as OPENHANDS_HOSTX-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEYIf no Automation backend is listed in <RUNTIME_SERVICES>, stop and tell
the user to start the full automation stack.
tar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .
# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent
OPENHANDS_HOST="<automation-url-from-runtime-services>"
TARBALL_PATH=$(curl -s -X POST \
"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor" \
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
-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.
curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
-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.
Tell the user:
✅ Slack Channel Monitor is running!
- Automation ID:
{id}- Channels:
{channel list}- Trigger phrase:
{phrase}- Polling every minute via cron
* * * * *- State file:
~/.openhands/workspaces/automation-state/slack_poller_{id}.jsonSend 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.
Each cron run executes main.py, which runs 10 polling iterations (every
5 seconds) within the 55-second timeout window. Each iteration:
references/state-schema.md).SLACK_USER_TOKEN then SLACK_BOT_TOKEN.search:read + > 1 channel → single search.messages call
(searches for the trigger phrase across all channels).conversations.history call per channel.processed_ts (dedup across the overlap window).ts in bot_message_ts.time.time() - last_activity > 15 s:
idle, finished, error, or stuck → fetch the agent's
final response via /api/conversations/{id}/agent_final_response and post
it to the Slack thread using Slack's markdown_text field so Markdown
formatting renders correctly. Mark the record watching for five minutes
so triggered follow-up replies can continue the same conversation.last_poll to now - 10 s (overlap window prevents boundary
races). If a conversation creation failed, pins last_poll further back to
retry on the next iteration.processed_ts) and continues to the next iteration.Debug output is written to both stdout and a persistent log at:
{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log
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.scripts/main.py - The complete automation script. Customise the three
constants at the top (TRIGGER_PHRASE, CHANNEL_IDS, DEFAULT_OPENHANDS_URL)
before packaging.| 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 | 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 |
tools
Iterate on a GitHub pull request — drive it through CI, code review, and QA until it is merge-ready. Poll verification layers with `gh` CLI, diagnose and fix CI failures, address review feedback, retry flaky checks, push fixes, and repeat. The agent is the orchestration loop.
development
Guides technical explanations toward flowing, direct, conversational prose. This skill should be used for engineering chat, design discussion, architecture analysis, code-review explanations, and technical recommendations that should be concise without becoming fragmented or vague.
tools
This skill should be used when the user asks to "set up a Jira automation to create pull requests", "poll Jira for create-pr issues", "automatically create GitHub PRs from Jira tickets", "deploy a Jira issue-to-PR automation", "create a Jira to GitHub PR workflow", or mentions automating GitHub PR creation from a Jira label. Deploys a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: "create-pr") and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repository is read from the body of the Jira ticket - no repo parameter is required at deploy time.
tools
This skill should be used when the user asks to "create an automation", "schedule a task", "set up a cron job", "webhook integration", "event-triggered automation", or mentions automations, scheduled tasks, cron scheduling, or webhook events in OpenHands Cloud.