paperclip-plugin-discord
v0.11.0
Published
[](https://www.npmjs.com/package/paperclip-plugin-discord) [](https://opensource.org/licenses/MIT)
Readme
paperclip-plugin-discord
Bidirectional Discord integration for Paperclip. Push agent notifications to Discord, receive slash commands, approve requests with interactive buttons, gather community intelligence, run multi-agent sessions in threads, process media attachments, register custom commands, and deploy proactive agent suggestions.
Built on the Paperclip plugin SDK and the domain event bridge (PR #909).
Why this exists
Multiple Paperclip users asked for notifications on the same day the plugin system shipped (2026-03-14):
"is there a way to have codex/claude check paperclip to see when tasks are done without me prompting it?" - @Choose Liberty, Discord #dev
"basically to have it 'let me know when its done'" - @Choose Liberty, Discord #dev
"can claude code check paperclip to see when tasks are done" - @Nascozz, Discord #dev
@dotta (maintainer) responded: "we're also adding issue-changed hooks for plugins so when that lands someone could [make notifications]." @Ryze said "Really excited by the plugins. I had developed a custom plugin bridge that I will now deprecate and migrate over to the new supported plugin system."
This is that plugin.
What it does
Notifications (rich embeds with color coding)
- Issue created - Blue embed with title, description, status, priority, assignee, project fields, and a "View Issue" link button
- Issue done - Green embed with completion confirmation
- Approval requested - Yellow embed with interactive Approve, Reject, and View buttons. Click to act without leaving Discord.
- Agent error - Red embed with error message (truncated to 1024 chars)
- Agent run started/finished - Blue/green lifecycle embeds
Interactive approvals
- Approve/reject buttons on every approval notification
- Works via Discord Gateway (WebSocket) so buttons work in local deployments without a public URL
- Clicking a button calls the Paperclip API and updates the Discord message inline
- Identifies which Discord user acted (logged as
discord:{username})
Per-type channel routing
approvalsChannelId- Dedicated channel for approval notificationserrorsChannelId- Dedicated channel for agent errorsbdPipelineChannelId- Dedicated channel for agent run lifecycleescalationChannelId- Dedicated channel for agent escalations- Falls back to
defaultChannelIdwhen per-type channels aren't configured
Slash commands
/clip status- Show active agents and recent completions/clip approve <id>- Approve a pending approval/clip budget <agent>- Check an agent's remaining budget/clip issues [project]- List open issues with optional project filter/clip agents- Show all agents with status indicators/clip help- Display all available commands/clip connect [company]- Link a Discord channel to a Paperclip company/clip connect-channel <project>- Map a Discord channel to a project for notification routing/clip digest <on|off|status> [mode]- Configure daily digest (daily/bidaily/tridaily)/clip commands import <json>- Import a workflow command from JSON/clip commands list- List registered workflow commands/clip commands run <name> [args]- Execute a workflow command/clip commands delete <name>- Delete a workflow command/acp spawn agent:<name> task:<description>- Start a new agent session in a Discord thread/acp status session:<id>- Check ACP session status/acp cancel session:<id>- Cancel a running ACP session/acp close session:<id>- Close a completed ACP session and archive the thread
Community intelligence
- Role-weighted signal extraction from Discord channels (every 6 hours)
- Classifies messages into feature wishes, pain points, maintainer directives, and sentiment
- Author roles weighted: admin/mod (5x), contributor (3x), member (1x)
- Historical backfill on first install (configurable, default 90 days)
- Agents can query signals via the
discord_signalstool - On-demand re-backfill via the
trigger-backfillaction
Phase 1: HITL Escalation
- Agents that get stuck can escalate to a dedicated Discord channel with full conversation context
- Rich embed formatting (yellow for pending, green for resolved, red for timed out)
- "Use Suggested Reply" button when the agent has a best-guess response
- "Reply to Customer", "Override Agent", and "Dismiss" component buttons
- Configurable timeout (default 30 min) with automatic timeout marking
escalate_to_humantool - agents can call directly with reason, confidence score, conversation history, and suggested reply- Resolved escalations emit
escalation-resolvedevents; timed-out escalations emitescalation-timed-outevents - Works via Gateway WebSocket like approval buttons - no public URL needed
Phase 2: Multi-Agent Group Threads
- Spawn multiple agents in a single Discord thread (up to
maxAgentsPerThread, default 5) - @mention routing -
@agentnamein a thread message routes to that specific agent - Reply-to routing - reply to a specific agent's message to route back to that session
- Most-recently-active fallback - if no mention or reply, routes to the agent with the most recent activity
- Agent handoff - one agent can hand off to another via the
handoff_to_agenttool; requires human approval via Approve/Reject buttons - Discussion loops - two agents can have a multi-turn back-and-forth via the
discuss_with_agenttool- Configurable max turns (2-50) and human checkpoint intervals
- "Continue Discussion" / "End Discussion" buttons at each checkpoint
- Automatic stale detection (5 min inactivity)
- Dual transport - native Paperclip sessions with ACP (Agent Client Protocol) fallback
- Output sequencing - queued output with 500ms flush delay to prevent interleaving in multi-agent threads
- Per-agent join/leave/complete/fail embeds in-thread
Phase 3: Media-to-Task Pipeline
- Detects audio, video, and image attachments in Discord messages
- Audio/video files are sent to a Whisper transcription agent, then routed to the Brief Agent for summarization
- Images are routed directly to the Brief Agent for analysis
- Supports common formats: mp3, wav, ogg, flac, mp4, webm, mov, png, jpg, gif, webp, and more
- Content-type and file-extension detection
- Configure which channels to monitor via
mediaChannelIds - Enable with
enableMediaPipeline: true
Reply routing
- Reply to any bot notification to route your message back to Paperclip
- Replies to issue notifications create issue comments automatically
- Replies to escalation notifications resolve the escalation as a human reply
- Message mappings stored per-channel/message for accurate routing
- Enable/disable with
enableInboundconfig toggle (default: true)
Daily digest
- Configurable digest summaries posted to your Discord channels
- Modes:
daily(once),bidaily(twice),tridaily(three times per day) - Configure via
/clip digest on <mode>or thedigestModeconfig setting - Includes: tasks completed/created today, active agents, in-progress/review/blocked issues
- Per-company routing to mapped channels
Workflow commands
- Define multi-step workflows as JSON and execute them via
/clip commands run - Seven step types:
fetch_issue,invoke_agent,http_request,send_message,create_issue,wait_approval,set_state - Template interpolation:
{{arg0}},{{args}},{{prev.result}},{{step_id.result}} wait_approvalsteps suspend execution and show Approve/Reject buttons- Import workflows with
/clip commands import, list with/clip commands list - Built-in command names are protected and cannot be overridden
Phase 4: Custom Workflow Commands
- Agents register
!commandstyle commands via theregister_custom_commandtool - Discord users invoke commands by typing
!commandname <args>in any monitored channel - Commands are routed to the registering agent with the parsed arguments
- Command registry persisted per-company in plugin state
- Duplicate command names update the existing registration (upsert)
- Rich embed feedback: "Running" embed on invocation, "Failed" embed on error
- List all registered commands via
listCommands() - Enable with
enableCustomCommands: true
Phase 5: Proactive Agent Suggestions
- Agents register watch conditions via the
register_watchtool - Watches define regex patterns, target channels, a response template, and a cooldown period
- The
check-watchesjob runs on a configurable interval (default 15 min) and scans recent messages (20 min window) - When a pattern matches, the plugin posts a suggestion embed and invokes the agent for deeper analysis
- Response templates support
{{author}},{{content}}, and{{channel}}interpolation - Per-watch cooldown prevents duplicate triggers (default 60 min)
- Bot messages are excluded from pattern matching
- Enable with
enableProactiveSuggestions: true
Gateway WebSocket
- Persistent WebSocket connection to Discord Gateway for real-time interaction handling
- Automatic heartbeat with jitter
- Session resume on reconnect
- Exponential backoff with max 5 consecutive failures before long backoff (60s)
- Handles op 7 (reconnect), op 9 (invalid session), heartbeat ACK timeouts
Voice transcription (optional, opt-in)
- The bot joins one voice channel and transcribes what is said there (inbound only — it never speaks)
- Each utterance goes to Paperclip through the same inbound ingress a typed reply uses — routed to whatever its channel is currently about, or to a configured default — and is also posted into a text channel by webhook so the room can see what was heard
- The webhook post is display only — nothing reads it back, so a transcript cannot loop and no webhook is ever a trusted input source
- Speech-to-text via Deepgram; end-of-utterance is 800 ms of silence
- Off unless four environment variables are set, and it rides the plugin's existing gateway connection — no second Discord connection
- Joins on the gateway's own READY/RESUMED boundary, and rejoins there after a reconnect
- Requires optional dependencies that are not installed by default
Install
npm install paperclip-plugin-discordOr register with your Paperclip instance directly:
curl -X POST http://127.0.0.1:3100/api/plugins/install \
-H "Content-Type: application/json" \
-d '{"packageName":"paperclip-plugin-discord"}'Note —
paperclipaimaster, post #5429 (2026-05-09): The new Secrets Manager ships with a temporary kill switch on plugin secret-ref UUIDs while a company-scopedplugin_configfollow-up lands. If you're running paperclipai master, plugin activation will fail withPlugin secret references are disabled until company-scoped plugin config lands, andPOST /api/plugins/:id/configreturns HTTP 422 for configs containing secret-ref UUIDs (e.g.discordBotTokenRef). This is intentional fail-closed mitigation (PAP-2394 — see the upstream plan doc). Until the follow-up lands, pin to the last paperclipai release before #5429. This callout will be removed once secret-ref resolution is restored.
Troubleshooting: confirm your Paperclip host
Before filing a bug, confirm which Paperclip host this plugin is actually talking to. Run paperclipai plugin target (#8575) — it prints the resolved API URL plus the server's status, version, deploymentMode, and deploymentExposure before anything is installed. A server version older than this plugin expects is the most common cause of activation failures and secret-resolution errors (e.g. discordBotTokenRef not resolving) that look like plugin bugs but aren't. If the URL or version is wrong, point Paperclip at the right host — or update the server — before opening an issue.
Setup
Create a Discord application at https://discord.com/developers/applications
Add a bot to the application and copy the bot token
Enable the MESSAGE CONTENT privileged intent (for intelligence scanning)
Invite the bot to your server with
applications.commandsandbotscopesIn Paperclip, create a company secret holding your bot token, by either:
- UI: Open any agent's Configuration → Environment variables, enter a name (e.g.
discord-bot-token) and the bot token as the value, then click Create / Seal. The secret is created at the company level (not bound to that agent — despite the agent-context UI) and the returned UUID can be used from any plugin in the company. - REST API:
POST /api/companies/{companyId}/secretswith body{"name": "discord-bot-token", "value": "<your-bot-token>", "provider": "local_encrypted"}. The response contains the secret's UUID.
Copy the resulting secret UUID — you'll paste it into
discordBotTokenRefin the next step.- UI: Open any agent's Configuration → Environment variables, enter a name (e.g.
Configure the plugin with the secret UUID in
discordBotTokenRef, your guild ID, and channel ID
Configuration
| Setting | Required | Description |
|---------|----------|-------------|
| discordBotTokenRef | Yes | Secret reference to your Discord bot token |
| defaultChannelId | Yes | Default channel for notifications |
| defaultGuildId | No | Server ID (required for slash commands and intelligence) |
| approvalsChannelId | No | Dedicated channel for approvals |
| errorsChannelId | No | Dedicated channel for agent errors |
| bdPipelineChannelId | No | Dedicated channel for agent run lifecycle |
| escalationChannelId | No | Dedicated channel for agent escalations |
| notifyOnIssueCreated | No | Post when issues are created (default: true) |
| notifyOnIssueDone | No | Post when issues complete (default: true) |
| notifyOnApprovalCreated | No | Post when approvals are needed (default: true) |
| notifyOnAgentError | No | Post when agents error (default: true) |
| enableEscalations | No | Enable escalation features (default: true) |
| escalationTimeoutMinutes | No | Timeout before marking escalation timed out (default: 30, min: 5, max: 1440) |
| enableIntelligence | No | Enable community signal scanning (default: false) |
| intelligenceChannelIds | No | Channel IDs to scan for signals |
| backfillDays | No | Days of history to scan on first install (default: 90, max: 365) |
| intelligenceRetentionDays | No | Days to retain intelligence signals (default: 30, max: 365) |
| maxAgentsPerThread | No | Max concurrent agents per Discord thread (default: 5, max: 10) |
| enableCommands | No | Enable slash command handling (default: true) |
| enableInbound | No | Enable reply routing to Paperclip (default: true) |
| topicRouting | No | Route notifications by project-to-channel mappings (default: false) |
| digestMode | No | Digest frequency: off, daily, bidaily, tridaily (default: off) |
| dailyDigestTime | No | UTC time for daily digest, HH:MM (default: 09:00) |
| bidailySecondTime | No | Second digest time for bidaily mode (default: 17:00) |
| tridailyTimes | No | Comma-separated HH:MM times for tridaily (default: 07:00,13:00,19:00) |
| enableMediaPipeline | No | Detect and process media attachments (default: false) |
| mediaChannelIds | No | Channel IDs to monitor for media (empty = all) |
| enableCustomCommands | No | Allow agents to register !commands (default: false) |
| enableProactiveSuggestions | No | Allow agents to register watch conditions (default: false) |
| proactiveScanIntervalMinutes | No | How often to check watches (default: 15, min: 5, max: 60) |
| paperclipBaseUrl | No | Base URL for Paperclip API calls (default: http://localhost:3100) |
Voice transcription setup
Voice is opt-in and adds no weight to a normal install. Its dependencies are declared as optional peer dependencies, so npm install paperclip-plugin-discord does not pull them in and nothing about the text path changes.
To enable it, install the voice dependencies alongside the plugin:
npm install @discordjs/voice prism-media [email protected] libsodium-wrappers ws
opusscriptis pinned to0.0.8on purpose:[email protected]declares it aspeerOptional opusscript@^0.0.8, so installing a neweropusscriptfails withERESOLVE.
prism-media ships no Opus codec of its own — it needs one of opusscript, @discordjs/opus, node-opus or ffmpeg-static to decode incoming audio. opusscript is pure JavaScript and needs no native build, so it is the recommended default; @discordjs/opus is faster but compiles a native module. Without an engine the plugin refuses to start voice and logs the install command, rather than joining the channel and silently transcribing nothing.
Then set these environment variables on the plugin worker:
| Variable | Required | Description |
|----------|----------|-------------|
| DISCORD_VOICE_GUILD_ID | Yes | Guild (server) ID containing the voice channel |
| DISCORD_VOICE_CHANNEL_ID | Yes | Voice channel the bot joins on startup |
| DISCORD_VOICE_WEBHOOK_URL | Yes | Webhook URL of the text channel transcripts are posted to |
| DEEPGRAM_API_KEY | Yes | Deepgram API key for streaming speech-to-text |
| DISCORD_VOICE_USERNAME | No | Webhook display name for transcripts (default: Voice) |
| DISCORD_VOICE_DEFAULT_ISSUE_ID | No | Fallback Paperclip issue for spoken utterances, used when the transcript channel has no target of its own. |
If any of the four required variables is missing, voice initializes nothing and the plugin runs exactly as before. Voice startup and shutdown are both wrapped: a voice failure is logged, reported through plugin health, and never crashes the plugin or interrupts text routing.
Where a spoken utterance goes
A voice utterance is not a reply to anything, so it has no message to inherit a destination from. Two destinations are tried, in order:
- What the transcript's channel is currently about. Every time the plugin posts a notification into a channel it records what that notification was about, and a spoken utterance follows that pointer.
DISCORD_VOICE_DEFAULT_ISSUE_ID, when the channel has no pointer yet.
Either way the utterance is commented on the resolved issue under the company
that owns this install, through the same ingress and the same API call a typed
reply uses, authored discord:voice:<userId>.
With neither available — no notification has been posted in that channel and no default configured — voice runs display-only: it still transcribes and still posts to the text channel, but sends nothing to Paperclip, and plugin health says so. The same is true for an utterance whose destination Paperclip rejects (the issue was deleted, say): the transcript still shows, health reports it, and nothing else in the plugin is affected.
Known phase-1 property: the channel pointer is last-notification-wins. It
follows the most recent thing the plugin announced in that channel, not what the
room is actually discussing, so a notification arriving mid-conversation moves
where the next utterance lands. Set DISCORD_VOICE_DEFAULT_ISSUE_ID and use a
channel the plugin does not post into if you want a fixed destination. A
genuinely conversation-scoped target needs state voice does not have yet.
A pointer is only ever followed for the company that currently owns the install. Ownership can move between installs, and a pointer left behind by a previous owner is ignored rather than allowed to file a transcript under the wrong company. The same rule covers slow work: an utterance still being transcribed when ownership moves is dropped rather than filed under whoever owns the install by the time it finishes.
To find the pointer, the plugin has to know which channel the webhook posts into, which a webhook URL does not carry — so it asks Discord for the webhook object once. At most one such lookup is in flight at a time and every concurrent utterance shares its result; a resolved channel is cached, and a failed lookup is retried after a five-minute cooldown. While it is unresolved, utterances use the default issue.
Plugin health tracks two independent things about voice: whether it can connect, and whether what it hears can be routed. Neither is evidence about the other, so reconnecting does not clear a routing problem and saving the configuration does not either — only an utterance actually reaching Paperclip does.
The webhook transcript is never the transport. The inbound router refuses bot authors and refuses anything that is not a reply to a message this plugin posted, and voice does not ask it to make an exception: it calls the ingress directly. Nothing this plugin writes to Discord can be read back in as input.
Notes:
- The bot joins self-muted and un-deafened — Phase 1 listens, it does not speak.
- Enabling voice adds the
GUILD_VOICE_STATESintent to the gateway IDENTIFY. Intents are fixed when the socket identifies, so turning voice on or off reconnects the gateway. - Voice joins when the gateway reaches READY or RESUMED, never before: a voice-state update sent to a socket that has not identified is not deliverable, and a join that could not be sent is not retried by
@discordjs/voice. - Transcripts are posted with
allowed_mentions: { parse: [] }. Speech recognition will happily render "at everyone" as@everyone, so no transcript can ever ping anyone — the text is preserved verbatim, but Discord resolves no mentions in it. - Audio is sent to Deepgram for transcription. Tell the people in the channel.
Not in this phase: text-to-speech output, per-agent voices, cost guards.
Agent tools
| Tool | Phase | Description |
|------|-------|-------------|
| escalate_to_human | 1 | Escalate a conversation to a human via Discord |
| discord_signals | - | Query community intelligence signals |
| handoff_to_agent | 2 | Hand off a thread to another agent (requires human approval) |
| discuss_with_agent | 2 | Start a multi-turn agent-to-agent discussion |
| register_custom_command | 4 | Register a !command for Discord users |
| register_watch | 5 | Register a watch condition for proactive suggestions |
Credits
@MatB57 - Escalation channel concept, "Chat OS" vision for turning chat plugins into bidirectional agent command centers, and the HITL suggested-reply flow.
@leeknowsai - Worker bootstrap and packaging fix (#1), rich notification embeds, approval button UX, and per-type channel routing (#4). Most of the notification formatting and interactive approval flow is their work.
Notification event handler patterns adapted from PR #398 by @StartupBros.
Changelog
v0.3.0 - Telegram Feature Parity
Brings the Discord plugin to full parity with the Telegram plugin across 14 feature gaps.
New slash commands: /clip issues, /clip agents, /clip help, /clip connect, /clip connect-channel, /clip digest, /clip commands import/list/run/delete
Reply routing: Replying to bot notifications now routes messages back to Paperclip as issue comments or escalation responses. Controlled by the enableInbound toggle.
Daily digest: Configurable summary digests (daily/bidaily/tridaily) with tasks completed, active agents, and blocked issues. Configure via /clip digest on <mode> or the digestMode config.
Workflow engine: Define multi-step workflows with 7 step types (fetch_issue, invoke_agent, http_request, send_message, create_issue, wait_approval, set_state). Supports template interpolation and approval-gated execution.
Config toggles: enableCommands, enableInbound, topicRouting, digest scheduling options.
Migration
v0.2.1
The discordBotTokenRef field now requires a Paperclip secret reference (a UUID), not the raw token value. If you previously entered a raw bot token in the field, follow these steps to migrate:
- Create a company secret holding your bot token using one of the paths in the Setup section above (UI or REST API).
- Copy the returned secret UUID.
- Open Plugin Settings for Discord Bot and paste the UUID into "Discord Bot Token".
- Save and restart the plugin.
The plugin will fail to activate if a raw token (non-UUID) is entered in the field.
Development
pnpm install
pnpm typecheck
pnpm test
pnpm build323 tests covering formatters, commands, intelligence, session registry, media pipeline, custom commands, proactive suggestions, retry logic, workflow engine, and Telegram-parity features.
Contributing
Issues and PRs welcome at github.com/mvanhorn/paperclip-plugin-discord.
Auto-publishes to npm on push to main via OIDC trusted publishing.
License
MIT
