Discord
Connect an agent to Discord: setup, in-channel replies and slash commands.
Triggers turn an agent into a long-lived service that waits for events, acts on them, delivers the result, and goes idle. The discord trigger connects that loop to a Discord server: the agent watches channels and replies in-channel to the messages that wake it.
triggers:
- type: discord
channels: ["issues"] # names or ids; omit for all channels
# require_mention: true # only respond when @-mentioned
# token_env: DISCORD_BOT_TOKEN (default)
# from_users: ["amin", "ratul"] # only handle these authors (user ids or usernames)
# reply_channel: "1522..." # post replies here instead of the source channel
# allow_silence: true # a reply of exactly __NO_REPLY__ posts nothing
# show_typing: true # show "typing…" in the channel while the agent worksconfigs:
agent-yaml:
content: |
handle: issue-bot
description: Turns team Discord messages into GitHub issues.
model:
provider: openai-compatible
id: gpt-5.4-mini
purpose: |
You turn Discord messages in the issues channel into well-formed
GitHub issues and reply with the issue link.
triggers:
- type: discord
channels: ["issues"] # names or ids; omit for all channels
memory:
scope: thread
services:
issue-bot:
image: ghcr.io/loopedautomation/agent:latest
configs:
- source: agent-yaml
target: /agent/agent.yaml
env_file: .env # DISCORD_BOT_TOKEN and the model's API key
volumes:
- issue-bot-data:/data
restart: unless-stopped
volumes:
issue-bot-data:Options
Every key is optional; the defaults give you a bot that listens everywhere it's invited and answers everyone.
| Key | Default | What it does |
|---|---|---|
channels | all channels | Channel names or ids to listen in. A thread counts as the channel it lives under, so listing general also covers its threads. DMs skip this filter. |
require_mention | false | Only respond when the bot is @-mentioned. A DM always addresses the bot, so it passes. |
from_users | anyone | Only handle messages from these authors (user ids or usernames). The filter runs before the model is called, so everyone else's messages cost no tokens. |
token_env | DISCORD_BOT_TOKEN | The env var holding the bot token. |
reply_channel | the source channel | Channel id to post replies into. Out-of-channel replies quote the triggering message and link back to it. |
allow_silence | false | Post nothing when the agent replies with exactly __NO_REPLY__, or with nothing at all. Tell the agent about the sentinel in purpose. |
show_typing | false | Show the typing indicator in the source channel while a run is in flight. |
Setup
Setting up a Discord bot takes about 15 minutes:
- In the Discord Developer Portal, create a New Application with a Bot.
- Enable the Message Content Intent under Privileged Gateway Intents. Without it, messages arrive empty — this is the most common setup failure.
- Copy the bot token and export it:
export DISCORD_BOT_TOKEN=... - Run
af discord-invite agent.yamlto print a ready-made invite URL with the correct scopes and permissions, then open it and invite the bot to your server. - Run the agent:
af run agent.yaml
The agent replies in-channel to the triggering message; conversations are keyed per channel or thread, and memory.scope: thread continues them (Memory). It ignores bots, itself, and empty messages; long replies split at Discord's 2000-char limit. While connected the bot shows as online, and with show_typing: true it also shows the "typing…" indicator in the source channel for as long as a run is in flight. A run can take a while on a slow model, and the indicator is what tells the person who asked that something is happening.
Direct messages
The bot answers DMs too. A DM addresses the bot by definition, so the channels filter and require_mention don't apply there. from_users still does, and it's the knob to reach for if you don't want the bot talking to everyone who can see it in a server. Each DM is its own conversation, with history kept separate from every channel the bot watches.
Voice
With a top-level voice block in the agent file, the trigger handles Discord voice messages: it downloads the audio, transcribes it and runs the transcript through the normal loop, and with tts configured the reply comes back as a voice message too.
Add voice.live and a voice_channels filter and the agent joins a voice channel and holds a spoken conversation in it:
triggers:
- type: discord
channels: ["issues"]
voice_channels: ["standup"] # names or ids; needs the top-level voice.live blockVoice has the setup, the costs and the fallbacks for both. For a bot invited before you turned voice on, run af discord-invite again and open the URL; the invite now carries the permissions to send voice messages, connect to a voice channel and speak in it.
Observer agents
from_users, reply_channel and allow_silence together turn the trigger from a chatbot into an observer: an agent that watches channels, reacts to specific people and reports elsewhere. Observer agents covers the pattern, with a full example.
Slash commands
The trigger registers the agent's slash commands as Discord application commands at startup, so typing / in a channel pops Discord's native picker with each command's description. Picking one arrives as an interaction; the agent acknowledges it with the "thinking…" state and fills in the reply when the run finishes. Typing a command as plain text works the same way.
Are you an AI? Visit llms.txt — these docs as plain markdown.