Discord Action — v1 Concept
Integration of Discord into the Hercules automation platform, built on
discord.js. This document defines the scope for the
first version: which triggers/events and which functions should be implemented.
Architecture
Unlike the Shopware/Shopify actions, Discord does not push events via plain
HTTP webhooks. Events arrive over the Discord Gateway (a persistent
WebSocket connection) that discord.js manages through its Client.
This maps onto Hercules as follows:
- The action process runs a single discord.js
Client (one bot) next to the
Aquila connection.
- Event classes are registered as runtime events (like the existing actions),
but instead of extending the Rest flow type, the action listens to
discord.js client events and pushes them into flows via
action.fire(eventClass, projectId, payload).
- Functions call the Discord REST API through the same discord.js client
(client.channels, guild.members, etc.), so no separate HTTP layer is
needed.
Discord Gateway ──ws──> discord.js Client ──action.fire()──> Aquila ──> Flows
Flows ──function call──> discord.js Client ──REST──> Discord API
Client lifecycle
- Log in on action start (
client.login(token)), reuse the connected client
for all function calls.
- discord.js handles gateway reconnects itself; on fatal auth errors, surface
them via CodeZeroEvent.error like the reconnect loop in the existing
actions.
Gateway intents
Discord requires intents to be declared for the events a bot receives. v1
needs:
| Intent |
Required for |
Privileged |
Guilds |
base guild/channel cache |
no |
GuildMessages |
message events |
no |
MessageContent |
reading message content |
yes — must be enabled in the Discord developer portal |
GuildMembers |
member join/leave events |
yes |
GuildMessageReactions |
reaction events |
no |
The README of the action must document how to enable the two privileged
intents in the developer portal.
Configuration
Defined as ConfigurationDefinitions on the Action (like client_id /
client_secret in the GLS action):
| Identifier |
Type |
Description |
bot_token |
TEXT |
Bot token from the Discord developer portal. Used for client.login(). |
application_id |
TEXT |
Application ID; needed for registering slash commands. |
guild_id |
TEXT (optional) |
If set, slash commands are registered guild-scoped (instant updates, ideal for development). Otherwise global. |
Events (Triggers) — v1
| Identifier |
discord.js event |
Payload data type |
Notes |
DiscordMessageCreated |
messageCreate |
DiscordMessage |
Ignore messages from bots (incl. self) by default to avoid feedback loops. |
DiscordSlashCommandInvoked |
interactionCreate (ChatInputCommand) |
DiscordCommandInteraction |
Event setting: commandName (unique per flow) — the action registers the slash command at Discord on flow deployment. |
DiscordMemberJoined |
guildMemberAdd |
DiscordGuildMember |
Classic onboarding trigger (welcome message, auto-role). |
DiscordMemberLeft |
guildMemberRemove |
DiscordGuildMember |
|
DiscordReactionAdded |
messageReactionAdd |
DiscordReaction |
Enables reaction-role and approval flows. |
Deliberately not in v1: messageUpdate/messageDelete (partials handling),
buttons/select menus (interactionCreate component types), voice events,
scheduled events, bans. See Out of scope.
Event settings
Where useful, events get optional settings to filter at the source instead of
in the flow:
channelId (optional) on DiscordMessageCreated / DiscordReactionAdded —
only fire for a specific channel.
commandName (+ commandDescription, parameter definitions later) on
DiscordSlashCommandInvoked.
Functions — v1
Grouped like the GLS action (functions/, functions/utils/).
Messaging
| Identifier |
Signature (sketch) |
Description |
discordSendMessage |
(channelId: string, content: string, embed?: DISCORD_EMBED): DISCORD_MESSAGE |
Send a message to a channel. |
discordSendDirectMessage |
(userId: string, content: string, embed?: DISCORD_EMBED): DISCORD_MESSAGE |
Open a DM channel and send a message. |
discordReplyToMessage |
(channelId: string, messageId: string, content: string, embed?: DISCORD_EMBED): DISCORD_MESSAGE |
Reply with message reference. |
discordEditMessage |
(channelId: string, messageId: string, content: string): DISCORD_MESSAGE |
Edit a message previously sent by the bot. |
discordDeleteMessage |
(channelId: string, messageId: string): BOOLEAN |
Delete a message. |
discordAddReaction |
(channelId: string, messageId: string, emoji: string): BOOLEAN |
React with a unicode or custom emoji. |
Members & roles (moderation basics)
| Identifier |
Signature (sketch) |
Description |
discordAddRoleToMember |
(guildId: string, userId: string, roleId: string): DISCORD_GUILD_MEMBER |
Assign a role. |
discordRemoveRoleFromMember |
(guildId: string, userId: string, roleId: string): DISCORD_GUILD_MEMBER |
Remove a role. |
discordKickMember |
(guildId: string, userId: string, reason?: string): BOOLEAN |
Kick a member. |
discordBanMember |
(guildId: string, userId: string, reason?: string, deleteMessageSeconds?: number): BOOLEAN |
Ban a member. |
discordSetMemberNickname |
(guildId: string, userId: string, nickname: string): DISCORD_GUILD_MEMBER |
Set/clear a nickname. |
Lookups
| Identifier |
Signature (sketch) |
Description |
discordGetUser |
(userId: string): DISCORD_USER |
Fetch a user. |
discordGetMember |
(guildId: string, userId: string): DISCORD_GUILD_MEMBER |
Fetch a guild member (roles, join date). |
discordGetChannel |
(channelId: string): DISCORD_CHANNEL |
Fetch channel metadata. |
discordGetGuild |
(guildId: string): DISCORD_GUILD |
Fetch guild metadata (member count, name). |
Utils (data type builders)
Like the GLS create* util functions — pure builders so flows can assemble
nested payloads:
| Identifier |
Signature (sketch) |
Description |
discordCreateEmbed |
(title?: string, description?: string, color?: number, url?: string, fields?: Array<DISCORD_EMBED_FIELD>, footerText?: string, imageUrl?: string, thumbnailUrl?: string): DISCORD_EMBED |
Build an embed. |
discordCreateEmbedField |
(name: string, value: string, inline?: boolean): DISCORD_EMBED_FIELD |
Build an embed field. |
All functions wrap discord.js errors in RuntimeError with stable error codes
(e.g. DISCORD_UNKNOWN_CHANNEL, DISCORD_MISSING_PERMISSIONS,
DISCORD_MISSING_ACCESS), mapped from DiscordAPIError.code.
Data types — v1
One class per file in src/data_types/, zod schema + @Schema decorator,
following the existing actions:
DiscordUser — id, username, globalName, avatar URL, bot flag
DiscordGuildMember — user, nickname, role ids, joinedAt, guild id
DiscordMessage — id, channelId, guildId, author (DiscordUser), content,
embeds, attachments, mentions, referenced message id, createdAt
DiscordChannel — id, guildId, name, type (DiscordChannelType), topic,
parent id
DiscordChannelType — enum registered as its own data type (text, voice,
category, announcement, thread, forum, dm)
DiscordGuild — id, name, icon URL, ownerId, memberCount
DiscordRole — id, guildId, name, color, position
DiscordEmbed, DiscordEmbedField, DiscordEmbedFooter,
DiscordEmbedAuthor
DiscordAttachment — id, filename, contentType, size, url
DiscordReaction — emoji (name/id), messageId, channelId, user
(DiscordUser)
DiscordCommandInteraction — id, commandName, options (name/value pairs),
user, channelId, guildId
Known pitfalls from the other actions apply here:
@Signature values are capped at 500 characters — register enums/unions as
data types instead of inlining them.
- Optional parameters need
optional: true on @Parameter; ?: in the
signature alone is not enough.
DiscordMessage references itself (replied-to message) — keep cyclic
references as plain id fields in v1 to avoid recursive schema handling, or
register each cyclic schema as its own data type with a deferred getter if
the full object is needed.
Out of scope — v2 candidates
- Components: buttons, select menus, modals (
interactionCreate component
events + a discordSendMessageWithComponents function)
- Message events:
messageUpdate, messageDelete (require partials
handling and cache tuning)
- Channel management: create/delete/rename channels, threads
- Moderation events:
guildBanAdd, guildAuditLogEntryCreate, timeouts
(discordTimeoutMember)
- Voice: voice state events
- Scheduled events, webhooks management, polls
- Multi-bot support (one client per config scope) — v1 assumes a single
bot token per action instance
Suggested project layout
actions/discord-action/
├── src/
│ ├── index.ts # Action setup, discord.js client, event wiring
│ ├── client.ts # discord.js client factory + login/reconnect
│ ├── helpers.ts # error mapping (DiscordAPIError -> RuntimeError)
│ ├── data_types/ # one data type class per file
│ ├── events/ # one event class per file
│ └── functions/
│ ├── messaging/
│ ├── members/
│ ├── lookups/
│ └── utils/
├── module.json
├── package.json # deps: @code0-tech/hercules, discord.js, zod
└── README.md # bot setup, privileged intents, invite URL
Discord Action — v1 Concept
Integration of Discord into the Hercules automation platform, built on
discord.js. This document defines the scope for the
first version: which triggers/events and which functions should be implemented.
Architecture
Unlike the Shopware/Shopify actions, Discord does not push events via plain
HTTP webhooks. Events arrive over the Discord Gateway (a persistent
WebSocket connection) that discord.js manages through its
Client.This maps onto Hercules as follows:
Client(one bot) next to theAquila connection.
but instead of extending the
Restflow type, the action listens todiscord.js client events and pushes them into flows via
action.fire(eventClass, projectId, payload).(
client.channels,guild.members, etc.), so no separate HTTP layer isneeded.
Client lifecycle
client.login(token)), reuse the connected clientfor all function calls.
them via
CodeZeroEvent.errorlike the reconnect loop in the existingactions.
Gateway intents
Discord requires intents to be declared for the events a bot receives. v1
needs:
GuildsGuildMessagesMessageContentGuildMembersGuildMessageReactionsThe README of the action must document how to enable the two privileged
intents in the developer portal.
Configuration
Defined as
ConfigurationDefinitions on theAction(likeclient_id/client_secretin the GLS action):bot_tokenclient.login().application_idguild_idEvents (Triggers) — v1
DiscordMessageCreatedmessageCreateDiscordMessageDiscordSlashCommandInvokedinteractionCreate(ChatInputCommand)DiscordCommandInteractioncommandName(unique per flow) — the action registers the slash command at Discord on flow deployment.DiscordMemberJoinedguildMemberAddDiscordGuildMemberDiscordMemberLeftguildMemberRemoveDiscordGuildMemberDiscordReactionAddedmessageReactionAddDiscordReactionDeliberately not in v1:
messageUpdate/messageDelete(partials handling),buttons/select menus (
interactionCreatecomponent types), voice events,scheduled events, bans. See Out of scope.
Event settings
Where useful, events get optional settings to filter at the source instead of
in the flow:
channelId(optional) onDiscordMessageCreated/DiscordReactionAdded—only fire for a specific channel.
commandName(+commandDescription, parameter definitions later) onDiscordSlashCommandInvoked.Functions — v1
Grouped like the GLS action (
functions/,functions/utils/).Messaging
discordSendMessage(channelId: string, content: string, embed?: DISCORD_EMBED): DISCORD_MESSAGEdiscordSendDirectMessage(userId: string, content: string, embed?: DISCORD_EMBED): DISCORD_MESSAGEdiscordReplyToMessage(channelId: string, messageId: string, content: string, embed?: DISCORD_EMBED): DISCORD_MESSAGEdiscordEditMessage(channelId: string, messageId: string, content: string): DISCORD_MESSAGEdiscordDeleteMessage(channelId: string, messageId: string): BOOLEANdiscordAddReaction(channelId: string, messageId: string, emoji: string): BOOLEANMembers & roles (moderation basics)
discordAddRoleToMember(guildId: string, userId: string, roleId: string): DISCORD_GUILD_MEMBERdiscordRemoveRoleFromMember(guildId: string, userId: string, roleId: string): DISCORD_GUILD_MEMBERdiscordKickMember(guildId: string, userId: string, reason?: string): BOOLEANdiscordBanMember(guildId: string, userId: string, reason?: string, deleteMessageSeconds?: number): BOOLEANdiscordSetMemberNickname(guildId: string, userId: string, nickname: string): DISCORD_GUILD_MEMBERLookups
discordGetUser(userId: string): DISCORD_USERdiscordGetMember(guildId: string, userId: string): DISCORD_GUILD_MEMBERdiscordGetChannel(channelId: string): DISCORD_CHANNELdiscordGetGuild(guildId: string): DISCORD_GUILDUtils (data type builders)
Like the GLS
create*util functions — pure builders so flows can assemblenested payloads:
discordCreateEmbed(title?: string, description?: string, color?: number, url?: string, fields?: Array<DISCORD_EMBED_FIELD>, footerText?: string, imageUrl?: string, thumbnailUrl?: string): DISCORD_EMBEDdiscordCreateEmbedField(name: string, value: string, inline?: boolean): DISCORD_EMBED_FIELDAll functions wrap discord.js errors in
RuntimeErrorwith stable error codes(e.g.
DISCORD_UNKNOWN_CHANNEL,DISCORD_MISSING_PERMISSIONS,DISCORD_MISSING_ACCESS), mapped fromDiscordAPIError.code.Data types — v1
One class per file in
src/data_types/, zod schema +@Schemadecorator,following the existing actions:
DiscordUser— id, username, globalName, avatar URL, bot flagDiscordGuildMember— user, nickname, role ids, joinedAt, guild idDiscordMessage— id, channelId, guildId, author (DiscordUser), content,embeds, attachments, mentions, referenced message id, createdAt
DiscordChannel— id, guildId, name, type (DiscordChannelType), topic,parent id
DiscordChannelType— enum registered as its own data type (text, voice,category, announcement, thread, forum, dm)
DiscordGuild— id, name, icon URL, ownerId, memberCountDiscordRole— id, guildId, name, color, positionDiscordEmbed,DiscordEmbedField,DiscordEmbedFooter,DiscordEmbedAuthorDiscordAttachment— id, filename, contentType, size, urlDiscordReaction— emoji (name/id), messageId, channelId, user(
DiscordUser)DiscordCommandInteraction— id, commandName, options (name/value pairs),user, channelId, guildId
Known pitfalls from the other actions apply here:
@Signaturevalues are capped at 500 characters — register enums/unions asdata types instead of inlining them.
optional: trueon@Parameter;?:in thesignature alone is not enough.
DiscordMessagereferences itself (replied-to message) — keep cyclicreferences as plain id fields in v1 to avoid recursive schema handling, or
register each cyclic schema as its own data type with a deferred getter if
the full object is needed.
Out of scope — v2 candidates
interactionCreatecomponentevents + a
discordSendMessageWithComponentsfunction)messageUpdate,messageDelete(require partialshandling and cache tuning)
guildBanAdd,guildAuditLogEntryCreate, timeouts(
discordTimeoutMember)bot token per action instance
Suggested project layout