Channels
Twist
Twist (Doist) channel plugin for OpenCLAW โ poll-based DM and @mention routing.
Install
openclaw plugins install --link
README
# openclaw-twist
A [Twist](https://twist.com) (Doist) channel plugin for [OpenCLAW](https://openclaw.ai). It lets an OpenCLAW agent act as a Twist bot account โ answering direct messages and replying when it's `@mentioned` in group DMs and channel threads โ the same way OpenCLAW integrates with Slack, Telegram, and friends.
Built and used in production at [Sanctuary Computer](https://sanctuary.computer) for our "Stacksbot" assistant.
## What it does
| Context | Behavior | Session |
|---|---|---|
| **1:1 DM** | Replies to every message | Persistent per-DM |
| **Group DM** (multi-person) | Replies **only** when the bot is `@mentioned` | Per-conversation |
| **Channel thread** | Replies **only** when the bot is `@mentioned` | Separate per thread |
While a turn runs, the triggering message gets an **โณ** reaction, which becomes **โ
** on success or **โ** on error โ so you can see at a glance that the bot picked your message up and whether it's settled.
The agent receives full Twist context, not just the bare mention: the **thread title**, **channel name**, and a **transcript** of the surrounding discussion (Twist `[Name](twist-mention://id)` markup is cleaned to readable `@Name`).
## How it works
Twist's API can't deliver webhooks to a loopback-bound gateway, so this channel **polls** Twist's unread endpoints on an interval (default 15s) from **inside the gateway process** โ there's no separate service to run or supervise. It registers via the channel `gateway.startAccount` lifecycle (`runStoppablePassiveMonitor`), filters channel threads to mention-only before fetching (so it ignores the noise of every unread thread), dedups via a per-thread/conversation cursor, and **baselines on first sight** so it never replies to pre-existing backlog. Dispatch is non-blocking, so one slow agent turn never stalls polling.
Cursors persist to `./.state/cursors.json`; Twist's own read state is never mutated.
## Requirements
- OpenCLAW **โฅ 2026.6.6**
- A dedicated **Twist bot account** and an OAuth token for it, with scopes:
`user:read`, `workspaces:read`, `channels:read`, `threads:read`/`write`,
`comments:read`/`write`, `messages:read`/`write`, `reactions:write`
(add `search:read` if you also use the optional MCP server below).
- The bot account's numeric **user id** (for `@mention` detection). You can find it with:
```bash
curl -s -H "Authorization: Bearer $TWIST_TOKEN" \
https://api.twist.com/api/v3/users/get_session_user | jq '{id, name}'
```
> **Note:** the Twist bearer value includes the literal `oauth2:` prefix
> (e.g. `oauth2:abc123โฆ`). Sending the bare token returns `403 Invalid token`.
## Install
```bash
# from a local checkout (recommended for now)
openclaw plugins install --link /path/to/openclaw-twist
```
The host resolves `openclaw/plugin-sdk/*` itself โ you do **not** need to vendor or symlink `openclaw` into the plugin.
## Configuration
Provide credentials via **environment variables** (preferred โ nothing secret in your config):
```bash
export TWIST_TOKEN="oauth2:โฆ" # bot account token (incl. oauth2: prefix)
export TWIST_WORKSPACE_ID="133876"
export TWIST_BOT_USER_ID="634870"
```
Then enable the channel in `~/.openclaw/openclaw.json`:
```json5
{
channels: {
twist: {
enabled: true,
dmPolicy: "open", // any workspace member can DM the bot
allowFrom: ["*"], // required when dmPolicy is "open"
groupPolicy: "open",
groups: { "*": { requireMention: true } },
},
},
plugins: {
load: { paths: ["/path/to/openclaw-twist"] },
entries: { twist: { enabled: true } },
},
}
```
See [`openclaw.twist.example.json5`](./openclaw.twist.example.json5) for a fully-annotated example.
**Secret resolution order** for the token: `channels.twist.token` โ `channels.twist.tokenFile` โ `TWIST_TOKEN`. If you must put it in config, use `"${TWIST_TOKEN}"` substitution rather than a literal. `workspaceId`/`botUserId` fall back to `TWIST_WORKSPACE_ID`/`TWIST_BOT_USER_ID`.
### Config reference
| Key | Type | Default | Notes |
|---|---|---|---|
| `enabled` | boolean | `false` | Turn the channel on |
| `token` / `tokenFile` | string | โ | Bot token (incl. `oauth2:`), or a file path |
| `workspaceId` | int/string | โ | Twist workspace id |
| `botUserId` | int/string | โ | Bot account user id (mention target) |
| `pollIntervalMs` | int | `15000` | Poll cadence (2000โ600000) |
| `dmPolicy` | enum | `open` | `pairing` \| `allowlist` \| `open` \| `disabled` |
| `allowFrom` | string[] | โ | DM allowlist (`["*"]` for open) |
| `groupPolicy` | enum | `open` | `open` \| `allowlist` \| `disabled` |
| `groups."*".requireMention` | boolean | `true` | Require `@mention` in groups/threads |
## Optional: richer Twist tools via MCP
Register Doist's official [`@doist/twist-ai`](https://github.com/Doist/twist-ai) MCP server to give the agent extra Twist tools (search, inbox, react, mark-done, build-link) inside its sessions:
```bash
openclaw mcp add twist-ai --command npx --arg -y --arg @doist/twist-ai \
--env TWIST_API_KEY="$TWIST_TOKEN" --parallel
```
This channel does **not** require the MCP server โ it handles inbound routing and replies on its own. The MCP is purely additive agent capability.
## Development
```bash
npm test # routing/parsing/markup unit tests (no network, no SDK needed)
```
The pure logic (routing rules, mention/self-filtering, cursor advancement, target parsing, markup cleaning) lives in `src/routing.js` and is fully unit-tested. SDK-coupled code (`src/channel.js`, `src/inbound.js`, `src/monitor.js`) is validated by loading in a running gateway.
For local iteration against your own gateway, you can symlink the host SDK so standalone `node` imports resolve (gitignored):
```bash
mkdir -p node_modules && ln -s "$(npm root -g)/openclaw" node_modules/openclaw
```
## Limitations (v1)
- Single account per workspace.
- Text only โ no inbound/outbound attachments or media.
- No streaming (replies post as a whole message).
- Reactions are status indicators only (not used as triggers).
- Group **group-mentions** (`twist-group-mention://`) aren't treated as a direct mention.
## License
[MIT](./LICENSE) ยฉ Sanctuary Computer
channels
Comments
Sign in to leave a comment