Looped Docs

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 works
configs:
  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.

KeyDefaultWhat it does
channelsall channelsChannel 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_mentionfalseOnly respond when the bot is @-mentioned. A DM always addresses the bot, so it passes.
from_usersanyoneOnly 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_envDISCORD_BOT_TOKENThe env var holding the bot token.
reply_channelthe source channelChannel id to post replies into. Out-of-channel replies quote the triggering message and link back to it.
allow_silencefalsePost nothing when the agent replies with exactly __NO_REPLY__, or with nothing at all. Tell the agent about the sentinel in purpose.
show_typingfalseShow the typing indicator in the source channel while a run is in flight.

Setup

Setting up a Discord bot takes about 15 minutes:

  1. In the Discord Developer Portal, create a New Application with a Bot.
  2. Enable the Message Content Intent under Privileged Gateway Intents. Without it, messages arrive empty — this is the most common setup failure.
  3. Copy the bot token and export it: export DISCORD_BOT_TOKEN=...
  4. Run af discord-invite agent.yaml to print a ready-made invite URL with the correct scopes and permissions, then open it and invite the bot to your server.
  5. 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 block

Voice 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.

On this page