Distinct Identities
Each agent appears as a separate bot with its own name, avatar, and presence status
Connect your herdctl agents to Discord, allowing users to interact with AI agents through chat messages and slash commands. Each agent appears as a distinct bot with its own identity, presence, and configuration.
herdctl uses a per-agent bot architecture where each Discord-enabled agent has its own Discord Application and bot token. This design provides several benefits:
Distinct Identities
Each agent appears as a separate bot with its own name, avatar, and presence status
Independent Configuration
Different agents can monitor different channels with different response modes
Scalable Architecture
Add new agents without affecting existing bot configurations
Clear User Experience
Users always know which agent they’re talking to by the bot’s identity
Discord Server├── #support│ └── @SupportBot responds to mentions (support-agent)├── #dev-chat│ └── @CoderBot responds to all messages (coder-agent)└── DMs └── @SupportBot auto-responds (support-agent)Each agent connects to Discord with its own bot account. When a user messages or mentions the bot, herdctl triggers a Claude session to respond.
| Mode | Behavior | Best For |
|---|---|---|
mention | Responds only when @mentioned | Shared channels where multiple bots exist |
auto | Responds to all messages | Dedicated support channels, DMs |
herdctl supports two different Discord integrations:
| Integration | Type | Purpose | Configuration |
|---|---|---|---|
| Chat | Two-way | Interactive conversations | chat.discord |
| Notification Hooks | One-way | Job completion alerts | hooks.after_run |
The chat integration documented on this page enables interactive, two-way conversations:
chat.discord section of agent configNotification hooks send one-way alerts when jobs complete:
hooks.after_run sectionExample notification hook:
hooks: after_run: - type: discord channel_id: "${DISCORD_CHANNEL_ID}" bot_token_env: DISCORD_BOT_TOKEN when: "metadata.shouldNotify"Before setting up Discord integration, ensure you have:
Open the Discord Developer Portal
Navigate to https://discord.com/developers/applications and sign in with your Discord account.
Create a New Application
Click the New Application button in the top-right corner.
Enter a name for your application. This name appears in OAuth authorization screens but not as the bot’s display name.
Navigate to the Bot Section
In the left sidebar, click Bot.
Configure the Bot Username
Click Edit next to the bot’s username. This is the name users will see in Discord.
Example names:
Support Bot for a customer support agentCode Assistant for a development agentMarketing Bot for a content agentSet the Bot Avatar (Optional)
Click on the bot’s avatar to upload a custom image. This helps users identify the bot.
Copy the Bot Token
Click Reset Token to generate a new token (or Copy if one exists).
Save the Token Securely
Add the token to your environment:
# Add to your shell profile or .env fileexport SUPPORT_DISCORD_TOKEN="your-bot-token-here"herdctl requires these Privileged Gateway Intents to function properly:
In your Discord Application, go to Bot settings
Scroll down to Privileged Gateway Intents
Enable the following intents:
| Intent | Required | Purpose |
|---|---|---|
| Message Content Intent | Yes | Read message text to respond to users |
| Server Members Intent | No | Not required for basic functionality |
| Presence Intent | No | Not required for basic functionality |
Click Save Changes
When inviting the bot, herdctl needs these permissions:
| Permission | Purpose |
|---|---|
Send Messages | Reply to user messages |
Read Message History | Build conversation context |
Use Slash Commands | Handle /help, /reset, /status commands |
View Channels | See channels the bot has access to |
The combined permission integer is: 2147551232
In your Discord Application, go to OAuth2 > URL Generator
Under Scopes, select:
botapplications.commandsUnder Bot Permissions, select:
Or use permission integer: 2147551232
Copy the generated URL at the bottom of the page
Open the URL in your browser
Select the server where you want to add the bot
Click Authorize
Complete the CAPTCHA if prompted
The bot will now appear in your server’s member list (offline until you start herdctl).
Configure Discord in your agent’s YAML file under the chat.discord section:
name: support-agentdescription: "Customer support agent"
chat: discord: bot_token_env: SUPPORT_DISCORD_TOKEN guilds: - id: "123456789012345678" channels: - id: "987654321098765432" name: "#support" mode: mentionchat: discord: # Environment variable containing the bot token (required) bot_token_env: SUPPORT_DISCORD_TOKEN
# Session expiry in hours (default: 24) session_expiry_hours: 24
# Log level: minimal, standard, verbose (default: standard) log_level: standard
# Optional: slash command registration mode # global = available everywhere (slower to propagate) # guild = faster updates for one guild (recommended for local dev) command_registration: scope: global # global | guild # guild_id: "123456789012345678" # required when scope: guild
# Output configuration - control what SDK messages appear in Discord output: tool_results: true # Show tool result embeds (default: true) tool_result_max_length: 900 # Max chars in tool output (default: 900, max: 1000) system_status: true # Show system status embeds (default: true) result_summary: true # Show completion summary embed (default: true) errors: true # Show error embeds (default: true) typing_indicator: true # Show typing indicator while processing (default: true) acknowledge_emoji: "👀" # Emoji reaction on receipt (default: "👀", empty to disable) assistant_messages: answers # Which assistant turns to send: "answers" or "all" (default: answers) progress_indicator: true # Show progress embed with tool names (default: true)
# Bot presence/activity (optional) presence: activity_type: watching # playing, watching, listening, competing activity_message: "for support requests"
# Voice message transcription (optional) voice: enabled: true # Enable voice message transcription (default: false) provider: openai # Transcription provider (default: openai) api_key_env: OPENAI_API_KEY # Environment variable with API key (default: OPENAI_API_KEY) model: whisper-1 # Whisper model to use (default: whisper-1) language: en # Language hint for accuracy (ISO 639-1, optional)
# File attachments (optional) attachments: enabled: true # Enable file attachment processing (default: false) max_file_size_mb: 10 # Maximum file size in MB (default: 10) max_files_per_message: 5 # Maximum files per message (default: 5) allowed_types: # Allowed MIME types (default: image/*, pdf, text/*) - "image/*" - "application/pdf" - "text/*" download_dir: ".discord-attachments" # Directory for downloaded files (default: .discord-attachments) cleanup_after_processing: true # Delete files after processing (default: true)
# Skill discovery (optional, recommended in containerized deployments) skills: - name: "security-audit" description: "Run a comprehensive security audit" - name: "docs-update" description: "Update documentation"
# Guild (server) configurations guilds: - id: "123456789012345678" # Discord server ID # Default channel mode for unlisted channels (optional) # When set, bot responds to @mentions (or all messages if "auto") in any channel default_channel_mode: mention # mention or auto (optional)
channels: - id: "987654321098765432" # Channel ID name: "#support" # For documentation (optional) mode: mention # mention or auto context_messages: 10 # Messages to include as context
- id: "111222333444555666" name: "#general" mode: auto context_messages: 5
# DM settings for this guild's members (optional) dm: enabled: true mode: auto allowlist: ["user-id-1", "user-id-2"] # Only these users (optional) blocklist: ["spam-user-id"] # Block specific users (optional)
# Global DM settings (applies to all DMs) dm: enabled: true mode: auto| Field | Type | Default | Description |
|---|---|---|---|
bot_token_env | string | — | Required. Environment variable name containing the bot token |
session_expiry_hours | number | 24 | Hours before a conversation session expires |
log_level | string | standard | Logging verbosity: minimal, standard, verbose |
command_registration | object | { scope: global } | Slash command registration mode (global or guild) |
presence | object | — | Bot presence/activity configuration |
guilds | array | — | List of Discord servers to operate in |
dm | object | — | Global DM configuration |
voice | object | — | Voice message transcription configuration |
attachments | object | — | File attachment handling configuration |
skills | array | — | Explicit skill list for /skills and /skill commands |
| Field | Type | Description |
|---|---|---|
activity_type | string | Activity type: playing, watching, listening, competing |
activity_message | string | Activity text shown in the bot’s status |
| Field | Type | Default | Description |
|---|---|---|---|
scope | string | global | global registers app commands globally; guild registers to one guild for faster propagation |
guild_id | string | — | Required when scope: guild; target guild for registration |
| Field | Type | Description |
|---|---|---|
id | string | Required. Discord server (guild) ID |
channels | array | Channels to monitor in this guild |
default_channel_mode | string | Default mode for channels not explicitly listed. When set, bot responds to @mentions (or all messages if auto) in any channel in this guild |
dm | object | DM settings for members of this guild |
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Required. Discord channel ID |
name | string | — | Human-readable name (for documentation) |
mode | string | mention | Response mode: mention or auto |
context_messages | number | 10 | Number of recent messages to include as context |
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Allow direct messages |
mode | string | auto | Response mode for DMs |
allowlist | string[] | — | Only allow DMs from these user IDs |
blocklist | string[] | — | Block DMs from these user IDs |
Control which SDK messages are surfaced in Discord. When your agent uses tools (Bash, Read, Write, etc.) or the SDK emits system/error messages, these settings determine what appears in the channel.
| Field | Type | Default | Description |
|---|---|---|---|
output.tool_results | boolean | true | Show tool result embeds when the agent uses tools |
output.tool_result_max_length | number | 900 | Maximum characters shown in tool output (max: 1000) |
output.system_status | boolean | true | Show system status embeds (e.g., “Compacting context…”) |
output.result_summary | boolean | true | Show a summary embed when the agent finishes (duration, cost, tokens) |
output.errors | boolean | true | Show error embeds when the SDK reports errors |
output.typing_indicator | boolean | true | Show typing indicator while processing. Set to false on long-running jobs to prevent Discord timeout errors |
output.acknowledge_emoji | string | "👀" | Emoji reaction added when message is received. Removed automatically after response is sent. Set to empty string to disable |
output.assistant_messages | string | "answers" | Which assistant turns to send to Discord: "answers" (only text responses) or "all" (includes tool_use blocks) |
output.progress_indicator | boolean | true | Show a “Working…” embed that updates in real-time with tool names as the agent works |
All output types appear as compact Discord embeds with color coding:
| Message Type | Embed Color | Example Content |
|---|---|---|
| Tool result (success) | Blurple | 🔧 Bash — > git status with output preview |
| Tool result (error) | Red | 🔧 Bash — command output with error |
| System status | Gray | ⚙️ System — “Compacting context…” |
| Result summary | Green/Red | ✅ Task Complete — duration, turns, cost, token usage |
| Error | Red | ❌ Error — error description |
Tool result embeds include the tool name with an emoji, the input summary (command, file path, or search pattern), execution duration, output length, and a truncated preview of the output in a code block.
Minimal output (text responses only):
chat: discord: bot_token_env: DISCORD_BOT_TOKEN output: tool_results: false system_status: false errors: falseFull visibility (all message types):
chat: discord: bot_token_env: DISCORD_BOT_TOKEN output: tool_results: true tool_result_max_length: 500 system_status: true result_summary: true errors: trueherdctl can automatically transcribe voice messages sent in Discord text channels using OpenAI’s Whisper API. When enabled, users can send voice recordings and the agent receives the transcribed text as the prompt.
| Field | Type | Default | Description |
|---|---|---|---|
voice.enabled | boolean | false | Enable voice message transcription |
voice.provider | string | "openai" | Transcription provider (currently only openai supported) |
voice.api_key_env | string | "OPENAI_API_KEY" | Environment variable containing the OpenAI API key |
voice.model | string | "whisper-1" | Whisper model to use for transcription |
voice.language | string | — | Language hint for better accuracy (ISO 639-1 code like "en", "es", "fr") |
chat: discord: bot_token_env: DISCORD_BOT_TOKEN voice: enabled: true provider: openai api_key_env: OPENAI_API_KEY model: whisper-1 language: en # Improves accuracy for EnglishGet an OpenAI API key
Sign up at platform.openai.com and create an API key with access to the Whisper API.
Add the API key to your environment
export OPENAI_API_KEY="sk-..."Enable voice in your agent config
chat: discord: bot_token_env: DISCORD_BOT_TOKEN voice: enabled: trueStart herdctl
herdctl startTest with a voice message
In Discord, hold the microphone button to record a voice message and send it in a channel where the bot is active.
herdctl can process file attachments (images, PDFs, text files) uploaded by users alongside their messages. When enabled, agents can view and analyze files, and agents can also send files back to users.
User uploads to agent:
Agent sends to user:
upload_file operation| Field | Type | Default | Description |
|---|---|---|---|
attachments.enabled | boolean | false | Enable file attachment processing |
attachments.max_file_size_mb | number | 10 | Maximum file size in megabytes |
attachments.max_files_per_message | number | 5 | Maximum number of files per message |
attachments.allowed_types | array | ["image/*", "application/pdf", "text/*"] | Allowed MIME type patterns (supports wildcards) |
attachments.download_dir | string | ".discord-attachments" | Directory for downloaded files (relative to agent’s working_directory) |
attachments.cleanup_after_processing | boolean | true | Delete downloaded files after agent run completes |
chat: discord: bot_token_env: DISCORD_BOT_TOKEN attachments: enabled: true max_file_size_mb: 10 max_files_per_message: 5 allowed_types: - "image/*" # PNG, JPG, GIF, etc. - "application/pdf" - "text/*" # .txt, .md, .json, .yaml, etc. - "text/x-python" # Specific MIME type for .py files download_dir: ".discord-attachments" cleanup_after_processing: true| Category | MIME Pattern | Examples | Processing |
|---|---|---|---|
| Images | image/* | PNG, JPG, GIF, WebP | Saved to download directory, accessible via Read tool |
| PDFs | application/pdf | PDF documents | Saved to download directory, accessible via Read tool |
| Text | text/* | .txt, .md, .json, .yaml, .py, .js | Content inlined directly in prompt |
When running in Docker, herdctl automatically translates file paths:
chat: discord: bot_token_env: DISCORD_BOT_TOKEN attachments: enabled: true allowed_types: ["image/*"]User sends:
Check this screenshot for errors[screenshot.png]Agent receives prompt with file metadata:
Check this screenshot for errors
Files attached:- screenshot.png (image/png, 245 KB) Path: /agent-working-dir/.discord-attachments/screenshot.pngAgent can then use the Read tool to view the image.
herdctl can discover and expose skills via the /skills and /skill slash commands. By default, skills are discovered from the filesystem, but you can override this with an explicit list.
Skills are discovered from these directories (in order):
.claude/skills/ (recommended location).codex/skills/ (legacy location)skills/ (alternative location)Each skill is a subdirectory containing a skill.json or skill.yaml file with metadata.
Override filesystem discovery by providing an explicit skills array:
chat: discord: bot_token_env: DISCORD_BOT_TOKEN skills: - name: "security-audit" description: "Run a comprehensive security audit" - name: "docs-update" description: "Update documentation with latest changes" - name: "review-pr" description: "Review a GitHub pull request"| Scenario | Recommendation |
|---|---|
| Containerized deployment | Use explicit skills list (filesystem may not be available) |
| Disable skill discovery | Set skills: [] to disable /skills and /skill commands |
| Curated skill list | List only the skills you want exposed in Discord |
| Development | Omit skills field to auto-discover from filesystem |
To disable skill commands entirely:
chat: discord: bot_token_env: DISCORD_BOT_TOKEN skills: [] # Empty array disables skill discoveryherdctl uses environment variables for Discord tokens to keep secrets out of configuration files.
Use a consistent naming pattern for your bot tokens:
# Pattern: {AGENT_NAME}_DISCORD_TOKENexport SUPPORT_DISCORD_TOKEN="your-support-bot-token"export CODER_DISCORD_TOKEN="your-coder-bot-token"export MARKETING_DISCORD_TOKEN="your-marketing-bot-token"Add to ~/.bashrc, ~/.zshrc, or equivalent:
export SUPPORT_DISCORD_TOKEN="your-token-here"Then reload:
source ~/.bashrc # or ~/.zshrcCreate a .env file in your project root:
SUPPORT_DISCORD_TOKEN=your-token-hereCODER_DISCORD_TOKEN=another-token-hereLoad with dotenv or similar tooling.
On macOS/Linux:
export SUPPORT_DISCORD_TOKEN="your-token-here"On Windows (PowerShell):
$env:SUPPORT_DISCORD_TOKEN="your-token-here"In your agent YAML, reference the environment variable name (not the value):
chat: discord: bot_token_env: SUPPORT_DISCORD_TOKEN # The variable NAMEherdctl reads the token from process.env.SUPPORT_DISCORD_TOKEN at runtime.
Discord IDs are unique 17-19 digit numbers. You’ll need IDs for servers (guilds), channels, and users.
Open Discord (desktop or web app)
Go to User Settings (gear icon near your username)
Navigate to App Settings > Advanced
Enable Developer Mode
With Developer Mode enabled, right-click on any server, channel, or user to see a Copy ID option:
| Item | How to Copy |
|---|---|
| Server ID | Right-click the server icon in the sidebar > Copy Server ID |
| Channel ID | Right-click the channel name > Copy Channel ID |
| User ID | Right-click a user’s name > Copy User ID |
| Message ID | Right-click a message > Copy Message ID |
guilds: - id: "1234567890123456789" # Server ID (18 digits) channels: - id: "9876543210987654321" # Channel ID (19 digits)herdctl startLook for connection messages in the logs:
[support-agent] Connecting to Discord...[support-agent] Connected to Discord: SupportBot#1234[support-agent] Slash commands registered successfullyThe bot should appear online in your Discord server with the configured presence:
SupportBot - Watching for support requestsIn a channel configured with mode: mention:
You: @SupportBot How do I reset my password?SupportBot: To reset your password, follow these steps...In a channel configured with mode: auto:
You: How do I reset my password?SupportBot: To reset your password, follow these steps...herdctl automatically registers slash commands for every Discord-enabled agent:
| Command | Description |
|---|---|
/help | Show available commands and usage |
/ping | Quick health check |
/config | Show runtime-relevant agent configuration |
/tools | Show allowed/denied tools and MCP servers |
/usage | Show last run stats and cumulative session totals |
/skills | List discovered skills for this agent |
/skill | Trigger a skill (with autocomplete) |
/status | Show bot connection status and session info |
/session | Show current session and run state for this channel |
/reset | Clear conversation context and start fresh |
/new | Alias for starting a fresh conversation |
/stop | Stop the active run in this channel |
/cancel | Alias for /stop |
/retry | Retry the last prompt in this channel |
Discord slash commands are not regular chat messages. Type / in the message box, then pick the command under your bot’s app name in Discord’s command picker. Discord sends this as an interaction event (not a normal message), which herdctl handles via the command manager.
Try them in any channel where the bot is active:
/statusShows available commands and basic usage information:
/helpExample output:
🤖 Support Bot Commands
/help - Show this message/usage - Show latest run usage/skills - List discovered skills/status - Show bot status and stats/reset - Clear conversation context
Chat with me:• @SupportBot your question - In channels• Just type in DMs - Direct messagesShows detailed connection status and statistics:
/statusExample output:
🟢 Support Bot Status
Connected: YesUptime: 2h 34mSession: Active (expires in 21h)
Stats:• Messages received: 47• Responses sent: 43• Current channel: #supportClears the conversation context for the current channel:
/resetExample output:
✨ Conversation reset! Starting fresh.Use /reset when:
If DMs are enabled, send a direct message to the bot:
You (DM): Hello!SupportBot: Hello! How can I help you today?You can have multiple agents (each with their own bot) in the same Discord server. This is useful for specialized agents:
name: support-agentchat: discord: bot_token_env: SUPPORT_DISCORD_TOKEN guilds: - id: "123456789012345678" channels: - id: "support-channel-id" name: "#support" mode: mention
# agents/coder-agent.yamlname: coder-agentchat: discord: bot_token_env: CODER_DISCORD_TOKEN guilds: - id: "123456789012345678" # Same server channels: - id: "dev-channel-id" name: "#dev-chat" mode: autoYou can configure a bot to respond to @mentions in any channel in a guild, without explicitly listing each channel:
chat: discord: bot_token_env: DISCORD_BOT_TOKEN guilds: - id: "123456789012345678" # Respond to @mentions in any channel (no explicit channel list needed) default_channel_mode: mention
# Optionally, override mode for specific channels channels: - id: "987654321098765432" name: "#support" mode: auto # Auto mode in this specific channelThis is useful for:
How it works:
channels, that configuration takes precedencedefault_channel_mode appliesdefault_channel_mode is not set, the bot ignores unlisted channelsUse distinct bot names and avatars - Help users identify which bot they’re addressing
Use mention mode in shared channels - Prevents bots from responding to each other or creating confusion
Dedicate channels when possible - Assign specific channels to specific bots with auto mode
Document bot purposes - Add channel topics explaining which bot handles what
| Scenario | Recommendation |
|---|---|
| Two bots in same channel | Use mode: mention for both |
| Bot in dedicated channel | Use mode: auto |
| Support + Dev bots | Separate channels or mention mode |
Discord chat integration maintains conversation context so your agent can have multi-turn conversations and “remember” what was discussed.
session_expiry_hours (default: 24 hours)When a user sends a message, the agent receives:
This allows natural conversations:
User: What's the current price of the Hyken chair?Bot: The Hyken chair is currently $189 at Staples.
User: When did you last check?Bot: I checked prices 2 hours ago at 10:30 AM.
User: Is that below my target?Bot: Yes! Your target is $200, so $189 is $11 below target.The bot remembers the chair and target price from earlier in the conversation.
chat: discord: bot_token_env: DISCORD_BOT_TOKEN session_expiry_hours: 24 # Default: 24 hoursChoose expiry based on your use case:
| Use Case | Recommended Expiry |
|---|---|
| Support bot | 24-48 hours |
| Quick Q&A | 1-4 hours |
| Long-running projects | 72+ hours |
| Stateless responses | 1 hour |
Users can clear their session context using /reset:
/resetThis is useful when:
Control how many recent messages are included as context:
guilds: - id: "123456789012345678" channels: - id: "987654321098765432" mode: mention context_messages: 10 # Include last 10 messagesMore context = better continuity but higher token usage.
| Scenario | Recommended Mode |
|---|---|
| Shared channel with humans | mention |
| Shared channel with other bots | mention |
| Dedicated support channel | auto |
| Direct messages | auto |
| High-traffic general channel | mention |
For multiple agents in the same server:
Dedicated channels: Give each bot its own channel with auto mode
#support → SupportBot (auto)#dev-help → CoderBot (auto)Shared channels: Use mention mode so users choose which bot to address
#general → SupportBot (mention) + CoderBot (mention)Open DMs (friendly bot):
dm: enabled: true mode: autoRestricted DMs (team only):
dm: enabled: true mode: auto allowlist: - "user-id-1" - "user-id-2"No DMs (channel-only):
dm: enabled: falseBlock specific users:
dm: enabled: true mode: auto blocklist: - "spam-user-id"Discord has rate limits on message sending. To avoid issues:
If you see rate limit warnings in logs, consider:
Cause: The environment variable specified in bot_token_env is not set or is empty.
Solution:
# Check if the variable is setecho $SUPPORT_DISCORD_TOKEN
# Set it if missingexport SUPPORT_DISCORD_TOKEN="your-token-here"Cause: Message Content Intent is not enabled in the Discord Developer Portal.
Solution:
Cause: The bot lacks required permissions in the channel.
Solution:
Cause: herdctl isn’t running or connection failed.
Solution:
herdctl startCause: Various configuration issues.
Checklist:
mention requires @mention)Cause: The bot token is incorrect or has been reset.
Solution:
Enable verbose logging to troubleshoot issues:
chat: discord: bot_token_env: SUPPORT_DISCORD_TOKEN log_level: verbose # Shows detailed debug informationVerbose logs include:
Use the /status slash command in Discord to check the bot’s connection status and statistics.
Discord rate limits are handled automatically by the connector. If you see frequent rate limit warnings:
context_messages)Rate limit events are logged at the standard level:
[support-agent] Rate limited by Discord API: { route: '/channels/123/messages', timeToReset: 5000 }