Workflow Operator on Discord
Workflow Operator on Discord uses a Discord bot to map each Discord channel to a persistent Dagu AI agent session over Discord's gateway (WebSocket, no public HTTP endpoint required). Messages sent in Discord are forwarded to the built-in AI agent, and agent responses are posted back. When a DAG run completes, Workflow Operator can also send AI-generated notifications so follow-up stays in the same conversation.
Prerequisites
Before setting up Workflow Operator on Discord, configure the AI agent in the Web UI. Go to Agent Settings (/agent-settings) and set up the agent model, tool policy, and other defaults first. The Discord bot forwards messages to the built-in agent, so it must be configured before Discord can use it. Start with Agent Settings, then use Models & Providers and Tool Permissions & Bash Policy for the concrete setup pieces.
Creating a Discord App
Before configuring Dagu, you need to create a Discord application and bot user.
1. Create the application
- Go to https://discord.com/developers/applications and click New Application.
- Name it (e.g.,
Dagu) and click Create.
2. Create the bot user
- In the left sidebar, go to Bot.
- Click Reset Token (or Add Bot on older-style applications) and copy the token. This is the value for
bots.discord.tokenin Dagu config, or theDAGU_BOTS_DISCORD_TOKENenvironment variable. Treat it like a password — it cannot be retrieved again once you leave the page.
3. Enable privileged intents
The bot needs to read message content. In the Bot tab:
- Scroll to Privileged Gateway Intents.
- Toggle MESSAGE CONTENT INTENT to ON.
- Click Save Changes.
Without this intent, the bot will receive message events but Content will be empty for messages that do not @mention it, and respond_to_all will not work.
4. Invite the bot to your server
- In the left sidebar, go to OAuth2 → URL Generator.
- Under Scopes, check bot and (optionally) applications.commands.
- Under Bot Permissions, check at least:
View ChannelsSend MessagesSend Messages in ThreadsRead Message HistoryUse Slash Commands(if you plan to add slash commands)
- Copy the generated URL, open it in a browser, pick the server, and authorize.
5. Finding your Channel ID
- In Discord, enable Developer Mode under User Settings → Advanced.
- Right-click the channel name and choose Copy Channel ID. The ID is a numeric string (e.g.,
1234567890123456789). This is what you put inallowed_channel_ids.
Running
The Discord connector for Workflow Operator starts automatically when bots.provider is set to discord and you run either:
dagu serveror
dagu start-allIn both modes, the connector shares the server's agent API instance.
Configuration
Set provider: discord under bots and configure the Discord-specific fields. Only one connector can be active at a time.
bots:
provider: discord
safe_mode: true
discord:
token: "your-discord-bot-token"
allowed_channel_ids:
- "1234567890123456789"
- "9876543210987654321"
respond_to_all: truebots fields
| Field | Type | Default | Description |
|---|---|---|---|
provider | string | "" (disabled) | Which connector to run. Set to "discord" for Discord. If empty, no bot starts. |
safe_mode | bool | true | Passed to the agent's ChatRequest.SafeMode field. Applies to all bot connectors. |
bots.discord fields
| Field | Type | Default | Description |
|---|---|---|---|
token | string | (required) | Bot token from the Discord Developer Portal. |
allowed_channel_ids | []string | (required) | Discord channel IDs authorized to use the bot. Messages from other guild channels are silently ignored. Direct messages to the bot are always handled regardless of this list. |
respond_to_all | bool | true | When true, the bot responds to every message in allowed guild channels. When false, the bot only responds to messages that @mention it. DMs are always handled regardless of this setting. |
Environment variables
| Variable | Config equivalent |
|---|---|
DAGU_BOTS_PROVIDER | bots.provider |
DAGU_BOTS_SAFE_MODE | bots.safe_mode |
DAGU_BOTS_DISCORD_TOKEN | bots.discord.token |
DAGU_BOTS_DISCORD_ALLOWED_CHANNEL_IDS | bots.discord.allowed_channel_ids |
DAGU_BOTS_DISCORD_RESPOND_TO_ALL | bots.discord.respond_to_all |
Environment variables take precedence over the config file for token. For allowed_channel_ids, use a comma-separated string:
export DAGU_BOTS_DISCORD_ALLOWED_CHANNEL_IDS=1234567890123456789,9876543210987654321Event Handling
The bot subscribes to Discord's gateway and handles two types of events:
Messages (MessageCreate)
- DMs (
GuildID == ""): always handled. - Guild channel messages: only handled when the channel ID is in
allowed_channel_ids. Withrespond_to_all: true, every message is forwarded to the agent. Withrespond_to_all: false, only messages that @mention the bot are handled — the<@BOT_ID>prefix is stripped from the text before it reaches the agent.
Messages authored by bots (including this bot itself) are ignored to avoid loops.
Interactive Components (Button Presses)
When the agent emits a UserPrompt with options, the bot renders it as a message with one or more ActionsRow components containing Button elements. Each button's custom_id is formatted as prompt:<promptID>:<optionID>.
When a user clicks a button, the bot submits the selected option to the agent and updates the original message in-place (via InteractionResponseUpdateMessage) to show the selection and remove the now-consumed buttons.
Button labels are truncated to 80 characters (Discord's limit for button labels). Up to 5 buttons are shown per row, and up to 5 rows per message — prompts with more than 25 options have the extras silently dropped.
If the prompt allows free text (AllowFreeText: true), the user can also reply with a regular text message instead of clicking a button.
Text Commands
Type these as plain messages (or after @mentioning the bot when respond_to_all is false):
| Text | Behavior |
|---|---|
new | Resets the channel's session state and clears the current agent session |
cancel | Cancels the currently active agent session |
Any text starting with new or cancel is treated as a command. All other text is forwarded to the agent.
Session Rotation
The bot tracks token usage across all messages in a session. When total tokens exceed 50% of the assumed context limit (200,000 tokens), the bot automatically:
- Collects the last 3 user/assistant exchanges from the old session (each truncated to 200/300 characters respectively).
- Resets the chat state.
- Creates a new session with the summary prepended to the user's message.
- Sends a notice:
(Session context limit reached — continuing with recent context carried forward)
DAG Run Notifications
When the centralized event store is available (the default in both server and start-all modes), the bot starts a DAG run monitor that reads persisted DAG-run events and sends notifications. If the event store is disabled or unavailable, Discord chat works normally but DAG-run notifications stay disabled.
Monitored statuses
Notifications are sent for these DAG run statuses:
succeededfailedabortedpartially_succeededrejectedwaiting(for human approval requests)
How notifications work
- The monitor polls the event store every 10 seconds and reads only new DAG-run events, using durable on-disk state so restarts do not lose pending notifications.
- On first startup, it seeds its cursor at the current event-store head, so existing channels only receive future events.
- For each new completion, it creates a dedicated agent session per allowed channel, sends a structured prompt with the run details (DAG name, status, error, start/finish times, step results), and waits for the agent to generate a notification message (up to 10 minutes).
- The notification session is adopted as the channel's active session, so users can send follow-up messages like "show me the logs" or "retry it".
- Delivered entries are retained for 2 hours to suppress duplicate event replays, while failed deliveries remain pending and are retried.
Fallback
If the agent API is unavailable or times out, the bot sends a plain text fallback:
<emoji> DAG '<name>' <status>
Error: <error message if any>Where the emoji is: ✅ succeeded/partial, ❌ failed/rejected, ⚠️ aborted, ⏳ waiting.
Message Length
Messages longer than 2,000 characters are split at paragraph boundaries (\n\n), falling back to line boundaries (\n) if no paragraph break exists in the second half of the text. Discord's message limit for bot content is 2,000 characters per message.
User Identity
All Discord users are mapped to agent sessions with auth.RoleAdmin. The user ID format is discord:<channel_id> — the session is scoped to the channel, not to an individual user, so everyone posting in an allowed channel shares the same agent session.
