slack-channel-monitor ยท diff
git:20261016.5aeba9b to git:20261027.f79208b
43 added, 289 removed. Audit A to A.
---
name: slack-channel-monitor
description: >
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.
triggers:
- /slack-monitor:poll
---
# 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:
-
- 1. Adds a ๐ reaction to the triggering message.
- 2. Opens an OpenHands conversation with the message and recent channel context.
- 3. 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:automation` stack) 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:
- ```bash
- # 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:
-
- ```bash
- 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.:
- ```python
- ["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:
- ```bash
- 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:
- ```bash
- 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:
- ```bash
- 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 as `OPENHANDS_HOST`, with `AUTH_HEADER="Authorization: Bearer $OPENHANDS_API_KEY"`
- - **Local Agent Canvas** - an **Automation backend** is listed in the
- `<RUNTIME_SERVICES>` block of your system context: use its `url_from_agent` as
- `OPENHANDS_HOST`, with `AUTH_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.
+ This automation now runs from the executable automation plugin at `plugins/slack-channel-monitor`. Do not copy or package code from `skills/slack-channel-monitor`; the skill is only a setup guide for creating the plugin-spec automation.
- ```bash
- tar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .
+ ## Create with the plugin-spec endpoint
- 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'])")
+ Use `POST /api/automation/v1/plugin-spec` with this plugin source:
- echo "Uploaded: $TARBALL_PATH"
+ ```json
+ {
+ "source": "github:OpenHands/extensions",
+ "repo_path": "plugins/slack-channel-monitor",
+ "ref": "main"
+ }
```
- 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.
+ `ref` is `main` while this plugin layout is unreleased. After an `openhands-extensions` release includes this plugin, prefer the matching release tag, for example `v<extensions-version>`. If setup code can inspect the installed package version, parameterize this value as `v{version('openhands-extensions')}`; otherwise default to `main`.
- ### Step 5 - Create the automation
+ For local Agent Canvas development from this checkout, use a local source instead:
- ```bash
- 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
+ ```json
+ {
+ "source": "/absolute/path/to/OpenHands/extensions",
+ "repo_path": "plugins/slack-channel-monitor"
+ }
```
- 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:
-
- 1. **Loads state** from the KV store, or from the JSON file on a local Agent
- Canvas without one (see `references/state-schema.md`).
- 2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.
- 3. **Fetches new messages:**
- - User token + `search:read` + > 1 channel โ single `search.messages` call
- (searches for the trigger phrase across all channels).
- - Otherwise โ one `conversations.history` call per channel.
- 4. **Fetches due thread replies** - polls at most one tracked thread per
- iteration using per-thread exponential backoff to stay within Slack rate
- limits.
- 5. **Processes messages** in chronological order:
- - Skips messages already in `processed_ts` (dedup across the overlap window).
- - Skips bot messages and any `ts` in `bot_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 `default` and 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.
- 6. **Checks conversation statuses** - for each active conversation where
- `time.time() - last_activity > 15 s`:
- - If status is `idle`, `finished`, `error`, or `stuck` โ 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'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.
- 7. **Advances `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.
- 8. **Saves state** (including `processed_ts`) and continues to the next iteration.
- 9. After all iterations, fires the completion callback.
+ ## Request template
- Debug output is written to both stdout and a persistent log at:
- ```
- {WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log
+ ```json
+ {
+ "name": "Create a cron automation that polls up to 10 Slack channels every minute and starts an OpenHands conversation when a configurable trigger phrase is detected",
+ "plugin": {
+ "source": "github:OpenHands/extensions",
+ "repo_path": "plugins/slack-channel-monitor",
+ "ref": "main"
+ },
+ "config": {
+ "channel_ids": [],
+ "trigger_phrase": "@openhands",
+ "openhands_url": "http://localhost:8000"
+ },
+ "trigger": {
+ "type": "cron",
+ "schedule": "* * * * *",
+ "timezone": "UTC"
+ },
+ "enabled": false
+ }
```
- ---
-
- ## 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.
+ The authoritative config schema is in `plugins/slack-channel-monitor/.plugin/plugin.json` under `automation.config_schema`. Use that schema to validate any user-provided values before creating the automation.
- ---
+ ## Runtime notes
- ## Troubleshooting
+ - Entrypoint: `python3 plugin_runner.py main.py` in `scripts`.
+ - Agent profile: `default`.
+ - Secondary plugins: `[]`.
- | 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` |
+ The old skill-local script packaging flow is deprecated. Use the plugin spec directly so OpenHands/automation resolves the plugin, writes `config.json`, and derives the runtime entrypoint from the plugin manifest.