Every action, in detail
One entry per action, generated from the bot itself, so what you read here is what the bot runs. Actions is the friendlier introduction; this page is the reference.
Every entry lists the values the action hands to later steps. You read one
with {steps.<step number>.<field>}, using the number shown on the step in
the editor. On top of what is listed, every step also exposes:
{steps.N.status}(success or permission_error or discord_error or …): How the step ended. Pair it with on_error: continue to react to a failure instead of stopping.{steps.N.error_code}(empty on success): The machine-readable reason a step failed, empty when it succeeded.
A field is always present, so a token you copy from here always resolves. An action with nothing listed returns nothing you can read.
Flow
Cancel a pending wait
control.cancel
Calls off every pending cancellable Wait parked under the same key for this server and bot, and reports how many it found. Finding nothing pending is a success, not an error, which is the whole point: with a per-user key such as honeypot:{user.id} a count of zero is the workflow's way of saying "nothing of yours was pending", and a later step can branch on that instead of aborting. A scheduler it cannot reach does NOT read as zero: the cancel request has a 5 second budget and a timeout or connection failure fails the step, which by default aborts the rest, so the "nothing was pending" branch is never taken during an outage. It needs no Discord permission and only ever matches keys belonging to the same server and bot, and on a host with no scheduler bridge at all it fails with scheduler_unavailable.
Bot permission needed: none
Settings: cancel_key
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
cancel_key | string | The key that was searched, exactly as it resolved after placeholders. |
cancelled | string | The text true or false: true when at least one pending wait was called off. |
cancelled_count | string | How many pending waits were cancelled under this key, as text (0 means nothing was pending). |
Wait (cancellable)
control.defer
Parks every step that comes AFTER this one so they run later instead of now, under a cancel key that another workflow can use to call them off. The delay is stored as a one-shot scheduler task, so it survives a worker restart and can be anywhere from 1 second to 30 days, unlike control.wait which is capped at 60 seconds and dies with the process. The trigger variables (including any {vars.*} produced by earlier steps), the trigger user, channel and message ids, and the results of the earlier steps your parked steps actually read are all snapshotted at park time, so a {steps.
Bot permission needed: none
Settings: seconds, cancel_key
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
cancel_key | string | The key this defer was parked under, exactly as it resolved after placeholders, to reuse in a Cancel step. |
deferred_seconds | string | How many seconds the parked steps will wait before running, as text. |
deferred_steps | string | How many following steps were parked, as text. |
task_id | id | Id of the parked scheduler task, useful for tracing which pending job this run created. |
Wait
control.wait
Pauses the workflow in place for 1 to 60 seconds before the next step runs. The pause is held in the running process, so the step gets an extended 65 second timeout, but a bot restart during the pause loses the rest of the workflow and nothing can call it off once started. Use it for small pacing tricks such as letting a welcome message land before a follow-up, and reach for Wait (cancellable) instead when the delay must survive a restart, exceed 60 seconds, or be cancellable. It needs no Discord permission and is not available from ticket button, webhook, economy item or internal triggers.
Bot permission needed: none
Gives up after: 65s
Settings: seconds
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
waited_seconds | number | How many seconds the workflow actually paused for, echoing what was configured. |
Emit event
workflow.emit
Fires a named custom event that any enabled custom event rule of the SAME server can listen to, which is how one shared sub-workflow gets reused from several places without copy-pasting its steps. The event name must be a lowercase slug, and an optional flat map of up to 10 keys (values up to 1000 characters, placeholders allowed) reaches the listener as {event.
Bot permission needed: none
Settings: event, payload
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
chain_depth | depth after this emit | How deep this emit sits in the chain, as text: the depth the listener workflows will run at. |
event | the emitted name | The event name that was emitted. |
Discord
Add reactions
discord.add_reactions
Reacts to the trigger message, or to a message you address by channel id and message id, with between 1 and 20 emojis added one at a time in the order you listed them. Both custom emoji forms are accepted, the raw mention you copy out of Discord and the bare name and id pair, as well as plain unicode emoji. The bot needs Add Reactions in that channel, and a channel id you supply yourself is verified to belong to this server, both before the first reaction is added. Only entries that are empty after trimming are skipped; every other value is sent to Discord exactly as typed, so a typo or an unresolved placeholder fails the whole step and every emoji still queued behind it is abandoned, while the reactions already added stay on the message. The reactions output counts what actually landed, and requested counts what you listed, so the two differ when an entry was blank.
Bot permission needed: Add reactions
Settings: target, emojis
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel holding the message that was reacted to. |
message_id | id | Id of the message the reactions were added to. |
reactions | count | How many reactions were actually added to the message. |
requested | count | How many entries your emoji list held. Higher than reactions when some entries were blank. |
Add role
discord.add_role
Gives a role to the user who triggered the automation, or to a specific user id you supply, with an optional audit log reason. The bot needs Manage Roles (the step fails with missing_bot_permission, or with permission_cache_unavailable while the server is not in the gateway cache yet), and the role has to exist in this server, although that existence check steps aside rather than failing when the role list cannot be resolved, so an outage never blocks the grant. Ordinary autoresponder triggers do carry the member's current roles (message, message edit, reaction add and remove, member join and leave, boost, role change, nickname, timeout, voice, level up), as do button clicks, ticket buttons and custom commands, so when the trigger user already holds the role the grant is skipped, the step returns skipped: true, and only the optional already has message is posted in the channel the automation fired in. Roles are unknown for events that carry no member and for steps parked by a cancellable wait, and the skip never applies when you target a specific user id, so in those cases the grant is simply sent; if no user can be resolved at all the step fails with missing_user.
Bot permission needed: Manage roles
Settings: role_id, target, reason, confirmation
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
role_id | id | The id of the role involved, echoed from what you configured. |
skipped | true | false | True when the grant was skipped because the trigger user already had the role, false when the role was really assigned. |
user_id | id | The member the role was granted to, or would have been granted to. |
Create channel
discord.create_channel
Creates a brand new channel in the server (text, voice, category, announcement, stage, or forum): only the name and the type are required, and everything else falls back to Discord's defaults, including topic, parent category, NSFW flag, slowmode, bitrate, user limit, position, voice region, the forum-specific settings, and up to 25 starting permission overwrites that Discord applies as part of the create so the channel is never briefly visible with default permissions. The bot needs Manage Channels, plus Manage Roles as soon as you add overwrites or turn on sync_permissions_with_parent, it must also be allowed to manage the parent category when you pick one, and the overwrites you write by hand are limited to a safe allowlist that excludes Administrator, Manage Roles, Mention Everyone and the moderation permissions. Turning on sync_permissions_with_parent copies the category's overwrites once at creation time and trims them to what the bot may actually write, so a permission the bot lacks is dropped and can leave the new channel more open than its category, which is reported back in dropped_deny_bits. Manage Roles is stripped from a copied overwrite on every sync, because Discord only accepts that overwrite from an administrator bot, and that is reported on its own as manage_roles_stripped rather than mixed into the dropped bitfields, which now only ever name permissions the bot genuinely lacks; a bot that does hold Administrator skips the trimming entirely and copies the category verbatim; the step fails outright when the parent cannot be read, is not a category, or its permissions are not cached yet.
Bot permission needed: Manage channels
Gives up after: 15s
Settings: name, type, topic, parent_id, nsfw, rate_limit_per_user, bitrate, user_limit, position, rtc_region, video_quality_mode, default_auto_archive_duration, default_thread_rate_limit_per_user, default_sort_order, default_forum_layout, default_reaction_emoji, available_tags, permission_overwrites, sync_permissions_with_parent
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_bitrate | number, empty on text channels | Voice bitrate in bits per second. Empty for text-like channels, which have no bitrate. |
channel_id | id | The id of the channel that was just created. |
channel_mention | mention | The channel as a clickable #mention, ready to paste into a message. |
channel_name | text | The channel name Discord actually saved, which for text channels is lowercased and dash-joined. |
channel_nsfw | true | false | Whether the channel is marked age restricted. |
channel_parent_id | id, empty if none | Id of the category the channel sits in, or an empty string when it has no category. |
channel_position | number | Sort position in the channel list, where a smaller number sits higher. |
channel_slowmode | seconds | Slowmode in seconds between messages, 0 when slowmode is off. |
channel_topic | text | The channel topic, or an empty string when none was set. |
channel_type | number | Discord's numeric channel type: 0 text, 2 voice, 4 category, 5 announcement, 13 stage, 15 forum. |
channel_user_limit | number, empty on text channels | Maximum members allowed in the voice channel, 0 meaning unlimited. Empty for text-like channels. |
dropped_allow_bits | string | Decimal permission bitfield of allow permissions that were not copied from the category, leaving the channel less permissive than it. "0" when nothing was dropped. Manage Roles is always stripped from a copied overwrite even though the bot holds it, so it shows up here whenever the category grants it. |
dropped_deny_bits | string | Decimal permission bitfield of deny permissions that were not copied from the category, leaving the channel more open than it. "0" when nothing was dropped. Manage Roles is always stripped, so a category that denies it shows up here. |
manage_roles_stripped | true | false | True when a copied category overwrite touched Manage Roles, which Discord never lets a non-administrator bot write. Not a missing permission on your side. |
overwrite_count | number | How many permission overwrite rows the created channel ended up with, as echoed back by Discord. |
synced_from_parent | true | false | True when the category sync actually ran, meaning sync_permissions_with_parent was on and a parent category was given. |
synced_overwrite_count | number | How many overwrite rows were sent to Discord, counting your own rows merged on top of the copied category rows. |
Create role
discord.create_role
Creates a brand new role in the server with the name you give it, plus optional color, hoist (show separately in the member list), mentionable flag, permission list and unicode emoji icon. Only a curated safe subset of permissions can be granted (SAFE_ROLE_PERMISSIONS: things like Send Messages, Connect, Add Reactions, never Administrator or Manage Roles), and Discord creates the role at the bottom of the hierarchy, so you normally pair this with a later step that assigns it. The bot needs Manage Roles, the emoji icon only works on servers that have the role icons feature, and the step fails with create_role_failed if Discord does not return the created role. Every property of the new role is echoed to later steps, and the creation is marked internally so it does not retrigger your own role created automations.
Bot permission needed: Manage roles
Gives up after: 15s
Settings: name, color, hoist, mentionable, permissions, unicode_emoji
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
role_color | number | The role color as a decimal number, 0 when no color was set. |
role_emoji | emoji, empty if none | The role's unicode emoji icon, or an empty string when it has none. |
role_hoist | true | false | True when the role is displayed separately in the member list. |
role_id | id | The id of the role that was just created, ready to feed an add role step. |
role_mention | mention | The role written as a clickable mention, ready to drop into a message. |
role_mentionable | true | false | True when anyone can ping the role. |
role_name | text | The name Discord stored for the new role. |
role_permissions | bitfield | The role's permission bitfield as a decimal string. |
role_position | number | Where the role sits in the server's role list. |
Create thread
discord.create_thread
Starts a thread, either hanging off the message that triggered the workflow or on a channel and message you name yourself, with an optional auto archive of 1 hour, 1 day, 3 days, or 7 days. When a message is resolved the thread is anchored to that message, and when there is none it creates a standalone thread directly on the channel, which forum and media channels refuse because they always need a starter message. Picking the trigger message on a trigger that carries no message, such as a member join, therefore quietly falls back to a standalone thread in the trigger channel, and the step fails with missing_channel when there is no channel to work with at all. The bot needs Create Public Threads in the target channel, and a channel you name yourself must belong to the same server.
Bot permission needed: Create public threads
Gives up after: 15s
Settings: target, name, auto_archive_duration
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
thread_id | id | The id of the thread that was created, usable as a channel id in later steps such as sending a message into it. |
thread_mention | mention | The thread as a clickable #mention, ready to paste into a message. |
Delete channel
discord.delete_channel
Permanently deletes the channel whose id you give it, along with everything inside it. There is no confirmation and no undo in the action itself, the workflow editor is expected to be the place where a human confirms, exactly like the ban and kick actions. The bot needs Manage Channels in that channel and the channel must belong to the same server as the trigger, otherwise the step fails before anything is deleted; an optional reason is written to the server audit log. The output simply echoes the id you passed rather than anything fetched from Discord, so later steps can still name the channel even though it no longer exists.
Bot permission needed: Manage channels
Gives up after: 15s
Settings: channel_id, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The id of the channel that was deleted, echoed straight back from the action's own settings. |
Delete message
discord.delete_message
Deletes the message that triggered the workflow, or a specific message addressed by channel id and message id. The bot needs Manage Messages in that channel and the channel has to belong to this server, both of which are checked before any delete is attempted. You may attach a reason of up to 512 characters, which is what shows up in the server audit log. Nothing special happens when the message is already gone, so Discord's refusal is reported as a failed step in the run history rather than quietly treated as a success.
Bot permission needed: Manage messages
Settings: target, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel the deleted message was in. |
message_id | id | Id of the message that was deleted, still usable by later steps as a reference. |
Delete role
discord.delete_role
Permanently deletes the role you point it at, with an optional reason (up to 512 characters) that lands in the server audit log. This is destructive and carries no confirmation of its own, so the workflow editor is what gates it. The same hierarchy guard as edit role applies and fails closed: a Discord managed role, a role at or above the bot's own highest role, or a role list that cannot be resolved all refuse the delete instead of trying it. The output only carries back the role id you supplied, since the role no longer exists to read properties from, and the deletion is marked so it does not retrigger your own role deleted automations.
Bot permission needed: Manage roles
Gives up after: 15s
Settings: role_id, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
role_id | id | The id of the role that was deleted, echoed straight from what you configured. |
Edit channel
discord.edit_channel
Edits an existing channel that you point at by id. Every field except channel_id is optional and only what you supply is changed, with two exceptions worth knowing: permission_overwrites is a full replacement of the channel's overwrite list, so leaving it out keeps the current overwrites while sending an empty list wipes all of them, and setting rtc_region to an empty string resets the voice region back to automatic. The bot needs Manage Channels in that specific channel, plus Manage Roles when you send overwrites, and when you move the channel with parent_id it must also be allowed to manage the destination category, because permission on the channel says nothing about permission on the category. The channel must belong to the same server as the trigger, and the step fails when Discord does not echo the updated channel back.
Bot permission needed: Manage channels
Gives up after: 15s
Settings: channel_id, name, topic, nsfw, rate_limit_per_user, parent_id, bitrate, user_limit, position, rtc_region, video_quality_mode, default_auto_archive_duration, default_thread_rate_limit_per_user, permission_overwrites
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_bitrate | number, empty on text channels | Voice bitrate in bits per second. Empty for text-like channels, which have no bitrate. |
channel_id | id | Id of the channel that was edited. |
channel_mention | mention | The channel as a clickable #mention. |
channel_name | text | The channel name after the edit, as saved by Discord. |
channel_nsfw | true | false | Whether the channel is marked age restricted after the edit. |
channel_parent_id | id, empty if none | Id of the category the channel now sits in, or an empty string when it has no category. |
channel_position | number | Sort position in the channel list after the edit, where a smaller number sits higher. |
channel_slowmode | seconds | Slowmode in seconds after the edit, 0 when slowmode is off. |
channel_topic | text | The channel topic after the edit, or an empty string when none is set. |
channel_type | number | Discord's numeric channel type: 0 text, 2 voice, 4 category, 5 announcement, 13 stage, 15 forum. |
channel_user_limit | number, empty on text channels | Maximum members allowed in the voice channel, 0 meaning unlimited. Empty for text-like channels. |
overwrite_count | number | How many permission overwrite rows the channel has after the edit. |
Edit message
discord.edit_message
Rewrites a message the bot itself posted, replacing its content, embeds or components. By default it edits the message the trigger happened on, which for a button click is the clicked message, and that is what makes self updating panels and role menus work; you can instead address any bot message by channel id and message id. There is no permission pre-check because Discord only ever lets a bot edit its own messages, so pointing this at somebody else's message simply comes back as a readable 403 in the run history. A Components V2 payload has its content and embeds stripped, and a specific target in another server is refused before the edit is attempted.
Bot permission needed: none
Settings: target, content, embeds, components, flags
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel holding the message that was edited. |
message_id | id | Id of the edited message, which is the same message you targeted. |
Edit role
discord.edit_role
Edits an existing role in place: only the fields you fill in are changed and everything you leave blank keeps its current value. Before touching anything it verifies the bot's highest role sits above the target and that the target is not a Discord managed role (a bot or integration role), and that check fails closed, so the step is refused when the server's role list or the bot's own member cannot be resolved. Sending an empty value for the emoji clears the role icon, and the permission field is limited to the same safe subset as create role. It returns the role exactly as it looks after the edit, fails with edit_role_failed if Discord returns nothing, and marks the change so it does not retrigger your own role updated automations.
Bot permission needed: Manage roles
Gives up after: 15s
Settings: role_id, name, color, hoist, mentionable, permissions, unicode_emoji
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
role_color | number | The role color after the edit, as a decimal number. |
role_emoji | emoji, empty if none | The role's unicode emoji icon after the edit, or an empty string when it has none. |
role_hoist | true | false | True when the role is displayed separately in the member list after the edit. |
role_id | id | The id of the role that was edited. |
role_mention | mention | The role written as a clickable mention. |
role_mentionable | true | false | True when anyone can ping the role after the edit. |
role_name | text | The role name after the edit. |
role_permissions | bitfield | The role's permission bitfield after the edit, as a decimal string. |
role_position | number | Where the role sits in the server's role list after the edit. |
Ephemeral reply
discord.ephemeral_reply
Sends a private reply that only the member who clicked the button can see, which is the normal way to confirm an action without posting in the channel. It is offered on button and ticket button triggers only, because it rides the interaction webhook, and that token stops working 15 minutes after the click. No Discord permission is required since the webhook path bypasses channel permissions, and the ephemeral flag is always added on top of any flags you set. Used from any other trigger the step fails with missing_interaction_token, and a Components V2 payload has its content and embeds stripped.
Bot permission needed: none
Settings: content, embeds, components, flags
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
message_id | id | Id of the private reply, usable by a later step that wants to edit it. |
Reply to the click
discord.interaction_reply
Posts a public reply attached to the button click so that everyone in the channel sees it, the visible counterpart of the ephemeral reply. Like the ephemeral version it goes through the interaction webhook, needs no channel permission and is restricted to button and ticket button triggers, failing with missing_interaction_token anywhere else. If you supply components the message is flagged as Components V2, and Discord refuses that flag alongside content or embeds, so send one or the other rather than both. When Discord returns nothing for the followup the step fails outright with followup_failed instead of reporting a success with no message.
Bot permission needed: none
Settings: content, embeds, components, flags
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
message_id | id | Id of the public reply that was posted, usable by a later step that wants to edit or delete it. |
Lock channel
discord.lock_channel
Locks a channel by writing a permission overwrite that denies Send Messages to the @everyone role. With no channel setting, or with the channel set to trigger, it acts on the channel the workflow was triggered in, and it fails with missing_channel when that trigger has no channel at all, for example a member join; pick a specific channel to lock somewhere else. The bot needs both Manage Channels and Manage Roles in the target channel, and a specific channel must belong to the same server. Be aware that this replaces the entire @everyone overwrite on that channel rather than editing one line of it, so any other allow or deny you had set for @everyone there is cleared; an optional reason is written to the audit log.
Bot permission needed: Manage channels, Manage roles
Gives up after: 15s
Settings: channel, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The id of the channel that was locked, either the specific one you chose or the trigger channel. |
Pin message
discord.pin_message
Pins the message that triggered the workflow, or any message you address by channel id and message id, to the pin list of its channel. The bot needs Manage Messages in that channel, and a channel id you supply yourself is verified to belong to this server, both before the pin is attempted. Pinning does not produce a gateway event the autoresponder matches on, so there is no anti loop marker to think about here. Any refusal from Discord, such as a message id that no longer exists, is reported as a failed step carrying a discord_
Bot permission needed: Manage messages
Gives up after: 15s
Settings: target
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel the message was pinned in. |
message_id | id | Id of the message that was pinned. |
Publish message
discord.publish_message
Crossposts an existing message out of an announcement channel so every server following that channel receives it. It works on the trigger message by default, or on any message you address by channel id and message id, in which case the channel is first checked to belong to this server, and the bot needs Manage Messages in that channel. The channel type is then read from the gateway cache and compared to announcement, so aiming this at a normal text channel fails fast with not_announcement_channel instead of the opaque error Discord would return, and a channel the bot has not cached yet fails with channel_not_cached rather than being mislabelled as the wrong type. This step publishes a message that already exists and never creates one, and its 15 second budget is shorter than the 60 second executor default, so it is among the first actions to give up with handler_timeout.
Bot permission needed: Manage messages
Gives up after: 15s
Settings: target
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The announcement channel the message was published from. |
message_id | id | Id of the message that was published to the following servers. |
Purge messages
discord.purge_messages
Bulk deletes the most recent messages of a channel, between 2 and 100 of them, in the channel the automation fired in unless you point it at another one. Discord refuses to bulk delete anything older than 14 days, so those messages are filtered out first and the number actually deleted can be lower than what you asked for, or even zero, without the step being reported as a failure; when exactly one message qualifies it falls back to a single delete. The bot needs Manage Messages in the target channel, and a channel you name explicitly is verified to belong to this server first. This is destructive with no confirmation inside the action itself, and the deletions are marked so they do not retrigger your own message deleted automations.
Bot permission needed: Manage messages
Gives up after: 15s
Settings: channel, count
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel that was purged. |
deleted_count | number | How many messages were actually deleted, which can be 0 when nothing was recent enough to qualify. |
Remove reaction
discord.remove_reaction
Takes one member's reaction off a message, which is what you pair with a reaction added trigger to enforce an allow list or a block list. By default it removes the emoji carried by the trigger from the member who reacted, and both halves can be overridden with an explicit emoji and an explicit user id, just as the message can be the trigger message or one you address by channel id and message id. The bot needs Manage Messages in that channel because removing somebody else's reaction requires it, and a channel belonging to another server is refused. Choosing the trigger emoji from a trigger that carries no reaction leaves nothing to work with and the step fails with missing_emoji; this action is only offered on autoresponder and manual runs.
Bot permission needed: Manage messages
Settings: emoji, target, user
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel holding the message the reaction was removed from. |
emoji | string | The emoji that was removed, normalised to the plain unicode character or the name and id pair. |
message_id | id | Id of the message the reaction was removed from. |
user_id | id | The member whose reaction was removed, the trigger user unless you named someone else. |
Remove role
discord.remove_role
Takes a role away from the user who triggered the automation, or from a specific user id you supply, with an optional audit log reason. The bot needs Manage Roles (missing_bot_permission, or permission_cache_unavailable while the server is not cached yet) and the role has to exist in this server, though that existence check steps aside instead of failing when the role list cannot be resolved. For the many triggers that do report the member's roles (message, message edit, reaction add and remove, member join and leave, boost, role change, nickname, timeout, voice, level up, plus button clicks, ticket buttons and custom commands) the removal is skipped when the trigger user does not have the role: no Discord call is made, the step returns skipped: true, and the optional did not have it message is posted instead of the removed one. Only when the roles are genuinely unknown (events with no member, or steps resumed after a cancellable wait, which deliberately drops the role snapshot) or when you target a specific user id is the removal sent unconditionally, and the step fails with missing_user when no user could be resolved.
Bot permission needed: Manage roles
Settings: role_id, target, reason, confirmation
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
role_id | id | The id of the role involved, echoed from what you configured. |
skipped | true | false | True when the removal was skipped because the trigger user was known not to have the role, false when the role was really removed. |
user_id | id | The member the role was removed from, or would have been removed from. |
Send DM
discord.send_dm
Sends a direct message to the member who triggered the workflow, and only to them, since there is deliberately no field for choosing another recipient. A cap of 5 per member per server applies in a fixed one hour window that starts at the first attempt rather than sliding, The slot is taken before anything else runs, so a looping rule cannot outrun the cap, and it is handed straight back when the step stops at the member gate without ever reaching Discord, so a rule pointed at people who have left does not eat a real member's allowance. A slot is kept once a DM channel has been opened, so dm_closed and dm_open_failed do count, and anything past the fifth fails with dm_rate_limited. It then refuses anyone who is not currently a member of the server with not_a_member, and every DM goes out as a Components V2 message with a disabled Sent from your server name button appended that cannot be turned off, so plain text is wrapped into a text component next to it. No Discord permission is needed, a member with DMs closed or who blocked the bot only surfaces at send time as dm_closed while a DM channel that cannot be opened at all is dm_open_failed, and the step is capped at 15 seconds, which is shorter than the 60 second executor default, so it gives up earlier than most actions with handler_timeout.
Bot permission needed: none
Gives up after: 15s
Settings: content, components, flags
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
dm_channel_source | string | Whether the bot reused a stored DM channel or opened a fresh one. Empty when the host does not report it. Delivery telemetry rather than something to build on. |
message_id | id | Id of the DM that was sent, inside the private channel with that member. |
user_id | id | The member who received the DM, always the workflow's trigger user. |
Send message
discord.send_message
Posts a message into a channel, either the channel that fired the workflow or a specific channel you pick. The bot needs Send Messages there, plus Send Messages in Threads when the target is a thread, and a channel id belonging to another server is refused before anything is sent. If the target turns out to be a forum or media channel the step cannot post a normal message, so it opens a new forum post instead: that path requires a thread name (up to 5 forum tag ids may be applied), needs Create Public Threads, and fails with forum_thread_name_required when the name is missing. Replying to the trigger only attaches a reply when the trigger actually carried a message, and a Components V2 payload has its content and embeds stripped because Discord refuses them alongside that flag.
Bot permission needed: Send messages
Settings: channel, content, embeds, components, flags, reply_to_trigger, forum_thread_name, forum_applied_tags
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel the message went to, whether you picked it or it came from the trigger. |
message_id | id | Id of the message that was posted, for later steps that want to edit, pin or react to it. |
thread_id | id | Id of the forum post that was opened. Empty unless the target was a forum or media channel. |
Set nickname
discord.set_nickname
Sets the server nickname of the user who triggered the automation, or of a specific user id you supply, and an empty value clears the nickname back to their normal username. Discord caps a nickname at 32 characters, which the action enforces before calling out. The bot needs Manage Nicknames, and there is no hierarchy pre check in this action, so a member Discord refuses to rename (the server owner, or anyone whose top role is above the bot) simply comes back as a failed step. The rename is marked internally so it does not retrigger your own nickname changed automations, and the step fails when no user could be resolved from the trigger.
Bot permission needed: Manage nicknames
Gives up after: 15s
Settings: target, nickname
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
user_id | id | The member whose nickname was set or cleared. |
Set roles (exclusive swap)
discord.set_roles
Replaces a member's roles inside a pool you define in one shot: every pool role they hold is dropped, the roles you grant are added, and any role outside the pool is left alone, which is what makes exclusive menus like pick one color safe against fast double clicks. Give it the pool (1 to 25 roles) and the roles the member should end up with (0 to 25, empty clears the pool). A pure add or a pure remove is done with per role calls, while a true swap writes the whole role list at once from the cached member, so a role someone else changed in that split second can be clobbered. The bot needs Manage Roles, Discord managed roles are filtered out of both lists, a grant role that does not exist in the server fails the step, and so does a member the bot cannot resolve or a role id that is still an unresolved placeholder at run time.
Bot permission needed: Manage roles
Settings: target, pool_role_ids, grant_role_ids, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
added_role_ids | comma-separated ids | Comma separated ids of the roles that were added, empty when none were. |
changed | true | false | The text "true" when something actually changed, "false" when the member already had exactly the requested roles. |
removed_role_ids | comma-separated ids | Comma separated ids of the roles that were taken away, empty when none were. |
role_count | number | How many roles the member holds after the step, as text. |
user_id | id | The member whose roles were recomputed. |
Toggle role
discord.toggle_role
Adds the role when the target does not have it and removes it when they do, which makes it the natural action behind a self assign button. The direction is read from the roles the trigger carried when it has them, which ordinary autoresponder events do (message, reaction, member join, voice, level up and the other events with a member object); otherwise, and always when you point it at a specific user id, the member is looked up first, and the step fails with member_not_resolved rather than guessing, because guessing would add a role to somebody who already has it. The bot needs Manage Roles, the role is checked against this server but that check steps aside instead of failing when the role list cannot be resolved (the same fail open as add role and remove role), you can set a different confirmation message for the added and the removed case, and the output's toggled field says which way it went so a later step can branch on it.
Bot permission needed: Manage roles
Settings: role_id, target, reason, confirmation
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
role_id | id | The id of the role that was toggled, echoed from what you configured. |
toggled | added | removed | Which direction the toggle went, either "added" or "removed". |
user_id | id | The member whose roles were changed. |
Trigger typing
discord.trigger_typing
Makes the bot show the typing indicator in a channel, which Discord clears on its own after roughly ten seconds or as soon as the bot posts a message, so it is normally used right before a slower step to make the automation feel alive. It uses the channel the automation fired in unless you point it at a specific one, and a specific channel is checked to belong to this server before anything is sent. The bot needs Send Messages in that channel. Nothing is created or changed by this action, and the only way it fails is when no channel could be resolved, the channel is not in this server, or the permission check refuses.
Bot permission needed: Send messages
Settings: channel
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel the typing indicator was shown in. |
Unlock channel
discord.unlock_channel
Unlocks a channel by clearing the @everyone permission overwrite so members can post again, the reverse of the lock action. With no channel setting, or with the channel set to trigger, it acts on the channel the workflow was triggered in, and it fails with missing_channel when that trigger has no channel; pick a specific channel to unlock somewhere else. The bot needs both Manage Channels and Manage Roles in the target channel, and a specific channel must belong to the same server. Note that it writes an empty allow and an empty deny, so it wipes the whole @everyone overwrite on that channel rather than only lifting the Send Messages deny, which means any other @everyone allow or deny set there is also cleared; an optional reason is written to the audit log.
Bot permission needed: Manage channels, Manage roles
Gives up after: 15s
Settings: channel, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The id of the channel that was unlocked, either the specific one you chose or the trigger channel. |
Unpin message
discord.unpin_message
Removes the message that triggered the workflow, or any message you address by channel id and message id, from its channel's pin list. The bot needs Manage Messages in that channel, and a channel id you supply yourself is verified to belong to this server, both before the unpin is attempted. Unpinning does not produce a gateway event the autoresponder matches on, so there is no anti loop marker involved. Any refusal from Discord, such as a message that was never pinned or no longer exists, is reported as a failed step carrying a discord_
Bot permission needed: Manage messages
Gives up after: 15s
Settings: target
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | The channel the message was unpinned from. |
message_id | id | Id of the message that was unpinned. |
Server-deafen member
discord.voice_deafen
Server-deafens or undeafens a member in voice, the moderator-applied deafen that the member cannot lift on their own. By default it acts on the user who triggered the workflow, and you can point it at a specific member instead by giving a user id; a single enabled switch decides whether it deafens or undeafens. The bot needs Deafen Members, checked at server level rather than on any one voice channel. It fails with missing_user when the trigger has no user behind it, and Discord refuses the change with not_in_voice when the member is not connected to a voice channel at that moment.
Bot permission needed: Deafen members
Gives up after: 15s
Settings: target, enabled
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
user_id | id | Id of the member that was deafened or undeafened, either the trigger user or the specific member you chose. |
Disconnect member (voice)
discord.voice_disconnect
Kicks a member out of whatever voice channel they are currently in, without removing them from the server. By default it disconnects the user who triggered the workflow, and you can point it at a specific member instead by giving a user id; there is nothing else to configure. The bot needs Move Members, checked at server level here rather than on the channel the member happens to be sitting in. It fails with missing_user when the trigger has no user behind it, and Discord refuses with not_in_voice when the member is not connected to voice at that moment.
Bot permission needed: Move members
Gives up after: 15s
Settings: target
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
user_id | id | Id of the member that was disconnected, either the trigger user or the specific member you chose. |
Move member (voice)
discord.voice_move
Drags a member who is already connected to voice into another voice or stage channel; by default it moves the user who triggered the workflow, and you can point it at a specific member instead by giving a user id, while the destination channel id is always required. The destination must be in the same server and must really be a voice or stage channel, otherwise the step fails with not_voice_channel before anything happens, and a destination the bot has not cached yet fails with channel_not_cached even when the id is perfectly valid. The bot's Move Members permission is checked on that destination channel rather than server-wide, so a voice channel whose overwrites deny it blocks the move for an ordinary bot, but a bot holding server-wide Administrator passes that check whatever the channel overwrites say, because Administrator resolves to every permission before overwrites are applied. It fails with missing_user when the trigger has no user behind it, and Discord refuses with not_in_voice when the member is not currently connected to voice anywhere in the server.
Bot permission needed: Move members
Gives up after: 15s
Settings: target, channel_id
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id | Id of the voice or stage channel the member was moved into, echoed from the action's own settings. |
user_id | id | Id of the member that was moved, either the trigger user or the specific member you chose. |
Server-mute member
discord.voice_mute
Server-mutes or unmutes a member in voice, which is the same mute a moderator applies by right-clicking someone in a voice channel, not a mute the member can lift themselves. By default it acts on the user who triggered the workflow, and you can point it at a specific member instead by giving a user id; a single enabled switch decides whether it mutes or unmutes. The bot needs Mute Members, checked at server level rather than on any one voice channel. It fails with missing_user when the trigger has no user behind it, such as a scheduled run, and Discord refuses the change with not_in_voice when the member is not connected to a voice channel at that moment.
Bot permission needed: Mute members
Gives up after: 15s
Settings: target, enabled
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
user_id | id | Id of the member that was muted or unmuted, either the trigger user or the specific member you chose. |
Economy
Add coins
economy.add_coins
Credits coins into a member's economy wallet, for quest rewards, event prizes or level milestones. It acts on the workflow's trigger user unless the target is switched to a specific member id, and the amount is a fixed integer between 1 and 1,000,000,000 set in the step itself. No Discord permission is needed, but the server economy must be enabled or the step fails with economy_disabled, and a wallet already at the server's maximum balance fails with max_balance instead of being partially credited. The credit is silent (it never fires economy autoresponder triggers) and each step carries a ledger idempotency key, so a redelivered workflow cannot double-credit.
Bot permission needed: none
Settings: target, amount
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
amount | number | The number of coins this step asked to credit. |
new_balance | number | The member's wallet balance after the credit, as a numeric string. |
user_id | id | The member whose wallet was credited, ready to mention in a later step. |
Give item
economy.give_item
Grants a shop item straight into a member's inventory free of charge, for giveaway prizes or quest drops. It targets the trigger user by default or a specific member id, with a quantity from 1 to 100 that defaults to 1 when left out. The item id must exist in this server's shop or the step fails with unknown_item, and the whole action fails with economy_disabled when the server economy is off. No coins move and no purchase effects run: this is a pure inventory grant, and the quantity it reports back is the member's new total for that item rather than the amount just added.
Bot permission needed: none
Settings: target, item_id, quantity
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
item_id | id | The shop item that was granted, exactly as given in the step. |
quantity | number | How many of that item the member owns now, after the grant. |
user_id | id | The member who received the item. |
Remove coins
economy.remove_coins
Debits coins from a member's economy wallet, for entry fees, fines or buy-ins. It targets the trigger user by default or a specific member id, with a fixed amount between 1 and 1,000,000,000. A wallet can never go negative: when the member cannot cover the amount the whole step fails with insufficient_funds and nothing is taken, and the step fails with economy_disabled when the server has no economy enabled. The debit is silent (no economy autoresponder triggers fire from it) and is idempotent per step, so a redelivered workflow cannot charge twice.
Bot permission needed: none
Settings: target, amount
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
amount | number | The number of coins this step asked to remove. |
new_balance | number | The member's wallet balance after the debit, as a numeric string. |
user_id | id | The member whose wallet was debited. |
Set balance
economy.set_balance
Overwrites a member's wallet with an absolute amount from 0 to 1,000,000,000 instead of adding or removing, targeting the trigger user by default or a specific member id. It is destructive because it can wipe a balance to zero, which is why presets that carry it require curation before they can be published. The movement is recorded as one autoresponder.set ledger row for the difference, and the step fails with economy_disabled when the server economy is off. Like the other economy actions it is silent: setting a balance never fires economy autoresponder triggers.
Bot permission needed: none
Settings: target, amount
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
amount | number | The absolute value the wallet was set to, as requested by this step. |
new_balance | number | The member's wallet balance after the set, as a numeric string. |
user_id | id | The member whose wallet was overwritten. |
Take item
economy.take_item
Consumes a shop item from a member's inventory, the usual second half of a redeem-a-token flow. It targets the trigger user by default or a specific member id, taking 1 to 100 units per step (1 when the quantity is left out). A bad item id fails the step with unknown_item and a member who does not own enough fails with item_not_owned, in both cases without consuming anything partially, and an economy that is switched off fails with economy_disabled. The quantity it reports back is what the member has left after the take, so a follow-up message can tell them how many remain.
Bot permission needed: none
Settings: target, item_id, quantity
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
item_id | id | The shop item that was consumed, exactly as given in the step. |
quantity | number | How many of that item the member still owns after the take. |
user_id | id | The member the item was taken from. |
Music and levels
Give XP
leveling.give_xp
Grants leveling XP to a member, either the trigger user or a specific user id, between 1 and 10000 per step. The grant is deliberately silent: no level-up announcement, no reward roles and no level_up autoresponder is fired by it, so an XP reward can never cascade into more automations. If the step is left on the default trigger user target and the trigger carries no user, which is exactly what a scheduled rule looks like, it fails with missing_context before anything else is checked; past that, leveling must be enabled on the server and handled by this bot and the target must really be a member, otherwise you get leveling_disabled, leveling_other_bot or not_a_member. The reported XP is the amount inside the member's new level, not a lifetime total, and the gain is recorded in the XP analytics as an automation grant. The new_xp output is the XP inside the resulting level, not the running total, so quote new_total_xp when a later step needs the lifetime figure.
Bot permission needed: none
Settings: target, amount
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
amount | number | How much XP was requested, echoed from the payload. |
leveled_up | true | false | True when this grant pushed the member up at least one level. |
new_level | number | The member's level after the grant. |
new_xp | number | The member's XP inside their current level after the grant, not their lifetime total. |
new_total_xp | number | Lifetime XP after the change. Quote this rather than new_xp when you mean the running total. |
user_id | id | The member who received the XP: the trigger user, or the specific user id you set. |
Remove XP
leveling.remove_xp
Removes leveling XP from a member, either the trigger user or a specific user id, between 1 and 10000 per step. XP is floored at 0 and the member never drops a level, so this can only eat into the XP they have accumulated inside their current level. On the default trigger user target with a trigger that carries no user, such as a scheduled rule, the step fails with missing_context before any other check; past that, leveling must be enabled on the server and handled by this bot and the target must really be a member, otherwise you get leveling_disabled, leveling_other_bot or not_a_member. The removal is silent (no announcement, no reward roles, no level_up autoresponder) and is recorded in the XP analytics as an automation change. The new_xp output is the XP inside the resulting level, not the running total, so quote new_total_xp when a later step needs the lifetime figure.
Bot permission needed: none
Settings: target, amount
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
amount | number | How much XP was requested for removal, echoed from the payload. |
new_level | number | The member's level after the removal; it never goes down from this step. |
new_xp | number | The member's XP inside their current level after the removal, floored at 0. |
new_total_xp | number | Lifetime XP after the change. Quote this rather than new_xp when you mean the running total. |
user_id | id | The member the XP was taken from: the trigger user, or the specific user id you set. |
Set level
leveling.set_level
Sets a member to an exact level between 0 and 1000 and resets their in-level XP to 0. Unlike remove_xp it can bring someone down a level, which is the point of it: on a level_up trigger filtered to the level you want to block, set the member back to the level below. On the default trigger user target with a trigger that carries no user, such as a scheduled rule, the step fails with missing_context before any other check; past that, leveling must be enabled on the server and handled by this bot and the target must really be a member, otherwise you get leveling_disabled, leveling_other_bot or not_a_member. The level you ask for is clamped to the server's max level when one is configured, and the change is silent: no announcement, no reward roles, no level_up autoresponder. The new_xp output is the XP inside the resulting level, not the running total, so quote new_total_xp when a later step needs the lifetime figure.
Bot permission needed: none
Settings: target, level
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
new_level | number | The level the member is now at, after clamping to the server maximum level when one is configured. |
new_xp | number | XP inside the new level after the change, which this step resets to 0. |
new_total_xp | number | Lifetime XP after the change. Quote this rather than new_xp when you mean the running total. |
user_id | id | The member whose level was set: the trigger user, or the specific user id you set. |
Set XP
leveling.set_xp
Sets a member to an exact TOTAL cumulative XP, between 0 and 100000000, and the level is recomputed from that total, so it can move someone down as well as up. On the default trigger user target with a trigger that carries no user, such as a scheduled rule, the step fails with missing_context before any other check; past that, leveling must be enabled on the server and handled by this bot and the target must really be a member, otherwise you get leveling_disabled, leveling_other_bot or not_a_member. Mind the asymmetry: the value you pass is a lifetime total, while the new_xp it reports back is only the XP inside the resulting level, and a server max level clamps the result to that level with 0 XP inside it. The change is silent: no announcement, no reward roles and no level_up autoresponder. The new_xp output is the XP inside the resulting level, not the running total, so quote new_total_xp when a later step needs the lifetime figure.
Bot permission needed: none
Settings: target, total_xp
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
new_level | number | The level the member ended up at once the total XP was settled. |
new_xp | number | XP left inside the member's new level after the total was settled, not the total you passed. |
new_total_xp | number | Lifetime XP after the change. Quote this rather than new_xp when you mean the running total. |
user_id | id | The member whose XP was set: the trigger user, or the specific user id you set. |
24/7 mode
music.247
Turns 24/7 mode on or off, which keeps the bot camped in a voice channel instead of leaving when it goes idle. Enabling uses voice_channel_id when set and otherwise the voice channel the trigger user is in, and it fails with no_voice_target when there is neither; disabling needs no channel at all. The channel_id output names the channel the bot is now camped in, and is empty after a disable or when the engine reports no channel, so it is always safe to reference from a later step. Anything the engine refuses comes back as a failed step with a music_* error code.
Bot permission needed: none
Gives up after: 65s
Settings: action, voice_channel_id
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
channel_id | id, empty when disabling | Voice channel the bot is now camping in. Empty when the step turned 24/7 off, or when the engine reported no channel. |
enabled | true | false | True when the step enabled 24/7 mode, false when it disabled it; derived from your payload, not from the engine. |
Control music
music.control
Sends a single transport command to the server's live player: pause, resume, skip, previous, stop, shuffle, clear_queue, remove_dupes or disconnect. There is nothing to target, it always acts on the guild the workflow runs in, and it runs with automation semantics so nobody has to be in the voice channel, DJ mode is bypassed and a skip is always a direct skip rather than a vote. When there is no player, or the engine is unreachable, the step fails with a music_* error code, so set the step to continue on error if the rest of the automation should still run. No Discord permission and no premium tier are required on the step itself.
Bot permission needed: none
Gives up after: 65s
Settings: action
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
action | text | The transport action you asked for, echoed back (pause, skip, stop, ...). |
result | engine result code | Outcome code the engine reported (for example "paused"), falling back to the requested action when the engine sent none. |
Audio filter
music.filter
Applies one audio filter to the server's player: bassboost, 8d, demon, karaoke, nightcore, vaporwave and vibrato toggle on and off, speed and pitch take a value between 0.25 and 3, and "clear" removes every filter at once. It acts on the current player only, with automation semantics (no in-voice requirement, DJ gate bypassed). The output always echoes the filter you asked for plus an enabled flag, which is how you tell a toggle-on from a toggle-off; a clear answers with enabled false. With no active player the engine rejects the request and the step fails with a music_* code.
Bot permission needed: none
Gives up after: 65s
Settings: filter, value
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
enabled | true | false | Whether the filter ended up on; false after a clear, and defaults to true when the engine sends no explicit state. |
filter | text | The filter name you asked for, echoed back verbatim from the payload. |
Join voice channel
music.join
Connects the bot to a voice channel without queueing anything, which is handy to have it present before a later step plays a track or speaks. It targets voice_channel_id when set, otherwise the voice channel the trigger user is in, and fails with no_voice_target when neither is available. It only actually connects when the bot has no player in that server yet: if the bot is already sitting in any voice channel there, the step succeeds with already_connected true and the bot does not move, and channel_id then reports the channel the bot is currently in rather than the one you targeted, so a follow-up {steps.N.channel_id} can name a channel your automation never asked for. If the engine cannot connect, the step fails with a music_* error code such as music_player_creation_failed or music_no_voice.
Bot permission needed: none
Gives up after: 65s
Settings: voice_channel_id
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
already_connected | true | false | True when the bot was already in that channel and nothing changed, false when it joined as part of this step. |
channel_id | id | Voice channel the bot is connected to, from the engine, falling back to the voice_channel_id you passed, or an empty string. |
Set loop mode
music.loop
Sets the loop mode of the server's player: "track" repeats the current song, "queue" repeats the whole queue and "disable" turns looping off. It always applies to the guild the workflow runs in, there is no channel or member to pick, and it uses automation semantics so no one needs to be in the voice channel and the DJ gate does not apply. The engine does not always echo the new state back, in which case the output falls back to the mode you asked for, so the two output fields are always populated but may reflect your request rather than a confirmation. With no active player the engine rejects the call and the step fails with a music_* code.
Bot permission needed: none
Gives up after: 65s
Settings: mode
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
enabled | true | false | Whether looping ended up on; when the engine sends no confirmation this is derived as true for any mode other than disable. |
mode | track | queue | disable | Loop mode in effect (track, queue or disable), echoed by the engine when it answers, otherwise the mode you requested. |
Play music
music.play
Queues a track or a playlist through the music engine exactly like the /play command: the query can be a link or a search term, and insert_first puts it at the front of the queue instead of the back. It plays in the voice channel the trigger user is sitting in unless you set voice_channel_id, which outranks the user's presence and is what lets a scheduled or webhook trigger start a radio with nobody around, but that only decides the channel when the bot has no player in the server yet: if the bot is already connected there, the track simply joins the existing queue in the channel it is already in. With neither a trigger user nor a voice_channel_id the step fails with missing_context, it always refuses to run from a music_track_start trigger (code music_play_loop_guard) so a starting track cannot queue another one forever, and a server is capped at 10 workflow plays per 5 minutes (music_rate_limited); anything else the engine refuses comes back as a failed step with a music_* code, and the step waits up to 65 seconds because the engine itself waits 60. For history and analytics the step is filed against voice_channel_id when you set one and against the trigger user otherwise, so the channel a rule names is what the history shows even when a member triggered it; that recorded channel is the one the rule asked for, which is not always where the track landed if the bot was already connected somewhere else.
Bot permission needed: none
Gives up after: 65s
Settings: query, insert_first, voice_channel_id
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
track_title | text | Title of the track the engine queued, or the query you typed when the engine returned no title. |
Seek
music.seek
Acts on the playhead of the track playing right now, on the server's current player only, with automation semantics so the trigger user does not need to be in the voice channel and the DJ gate is bypassed. Mode "to" jumps to an absolute position counted from the start of the track, while "forward" and "rewind" move the playhead by that many seconds relative to where it is, and a rewind past the start lands at 0 rather than failing. The position_seconds output is the resulting playhead in whole seconds for all three modes, so it is safe to feed into a follow-up message. Seconds are capped at 86400, and an idle player, a live stream or an unreachable engine fails the step with a music_* code such as music_not_playing, music_seek_live or music_engine_unreachable.
Bot permission needed: none
Gives up after: 65s
Settings: mode, seconds
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
position_seconds | number | Resulting playback position in seconds as reported by the engine, falling back to the seconds value you requested. |
Speak (TTS)
music.tts
Speaks a short text-to-speech message, up to 300 characters, in a voice channel; placeholders such as {user.username} are resolved before the step runs. It speaks in voice_channel_id when you set one, otherwise in the voice channel the trigger user currently occupies, and it fails with no_voice_target when there is neither, which is the usual trap on scheduled triggers. The output only echoes the text, not a channel id, so a following step cannot read back where the message was spoken. Engine side refusals (no node, the user is not in voice, a restricted channel) surface as a failed step with a music_* error code.
Bot permission needed: none
Gives up after: 65s
Settings: text, voice_channel_id
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
text | text | The text that was sent to the engine to be spoken, after placeholder resolution. |
Set volume
music.volume
Sets the player volume to an exact value, or nudges it one step up or down, mirroring the /volume command. Mode "set" requires a value between 0 and 200 and the step fails up front with missing_volume_value when it is missing; modes "up" and "down" ignore any value you pass. It acts on the server's current player with automation semantics, so the trigger user does not need to be in voice and the DJ gate is bypassed. With nothing playing, or when the engine cannot be reached, the step fails with a music_* code.
Bot permission needed: none
Gives up after: 65s
Settings: mode, value
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
volume | number | Volume after the change as reported by the engine, falling back to the value you requested, or 0 when neither exists. |
Moderation
Ban member
moderation.ban
Bans the trigger user, or a specific member id when the target is switched, producing a real mod-log case with DM and auto-escalation credit rather than a bare REST ban. The bot needs the Ban Members permission: it is checked before the ban and fails closed when the permission cache is cold, and a target above the bot in the role hierarchy surfaces as a permission error. Set the optional delete_message_seconds (0 to 604800, Discord's seven day cap) to also purge that much of the member's recent message history; leave it out and no history is pruned. The reason is optional, up to 512 characters, and is stored on the case and sent to Discord's audit log.
Bot permission needed: Ban members
Settings: target, reason, delete_message_seconds
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
case_number | number | The mod-log case number of the ban, for quoting in a follow-up message. Empty when no case was recorded. |
user_id | id | The member who was banned. |
Kick member
moderation.kick
Kicks the trigger user, or a specific member id when the target is switched, out of the server, producing a real mod-log case with DM and auto-escalation credit rather than a bare REST kick. The bot needs the Kick Members permission: it is checked before the kick and fails closed when the permission cache is cold, and a target that sits above the bot in the role hierarchy comes back as a permission error too. The optional reason, up to 512 characters, is kept on the case and passed to Discord's audit log. The step fails with missing_user when nothing resolves a target member, for example on a scheduled run with the target left as the trigger user.
Bot permission needed: Kick members
Settings: target, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
case_number | number | The mod-log case number of the kick, for quoting in a follow-up message. Empty when no case was recorded. |
user_id | id | The member who was kicked. |
Soft-ban member
moderation.soft_ban
Bans and then immediately unbans the trigger user, or a specific member id when the target is switched, which purges their recent messages without leaving them banned, and still records a real mod-log case. The bot needs the Ban Members permission, checked before the action and failing closed on a cold permission cache. delete_message_seconds sets the purge window from 0 to 604800 seconds; when you leave it unset the bot prunes the last seven days, because that purge is the whole point of a soft-ban. The member can rejoin immediately afterwards, so this is the tool for cleaning up a spam burst rather than for removing someone permanently.
Bot permission needed: Ban members
Settings: target, reason, delete_message_seconds
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
case_number | number | The mod-log case number of the soft-ban, for quoting in a follow-up message. Empty when no case was recorded. |
user_id | id | The member who was soft-banned. |
Timeout member
moderation.timeout
Applies a native Discord timeout to the trigger user, or to a specific member id when the target is switched, so they cannot send messages or talk in voice until it expires, with a real mod-log case and DM. duration_seconds is required and must be between 1 second and 2,419,200 seconds, which is Discord's 28 day maximum. The bot needs the Moderate Members permission: it is checked before the call and fails closed on a cold permission cache, and a target above the bot in the role hierarchy comes back as a permission error. The reason is optional, up to 512 characters, and is stored on the case and sent to Discord's audit log.
Bot permission needed: Moderate members
Settings: target, duration_seconds, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
case_number | number | The mod-log case number of the timeout, for quoting in a follow-up message. Empty when no case was recorded. |
user_id | id | The member who was timed out. |
Unban member
moderation.unban
Lifts an existing ban on the trigger user, or on a specific user id when the target is switched, so the person can be invited back. It is a pure revert: no new infraction is minted, which is why the step exposes no case number at all, and a pending automatic unban attached to the original ban is cancelled. The bot needs the Ban Members permission, checked before the call and failing closed on a cold permission cache. Because a banned person is not in the server, you almost always want an explicit user id here rather than the trigger user, and an optional reason of up to 512 characters is recorded with the revert.
Bot permission needed: Ban members
Settings: target, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
user_id | id | The user whose ban was lifted. |
Remove timeout
moderation.untimeout
Clears a Discord timeout on the trigger user, or on a specific member id when the target is switched, before it would have expired on its own. It is a pure revert: no new infraction is minted, so the step exposes no case number. The bot needs the Moderate Members permission, checked before the call and failing closed on a cold permission cache. The optional reason, up to 512 characters, is recorded with the revert, and the step fails with missing_user when nothing resolves a target member.
Bot permission needed: Moderate members
Settings: target, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
user_id | id | The member whose timeout was removed. |
Warn member
moderation.warn
Records a formal warning against the trigger user, or against a specific member id when the target is switched, exactly as if a moderator had warned them by hand: a real infraction with a case number, the standard DM, a mod-log entry and credit towards auto-escalation. This is the only moderation action that needs no Discord permission at all, because nothing is changed on Discord's side. The reason is optional and capped at 512 characters, and it is stored on the case like a manual reason. The step fails with missing_user when the trigger carries no user and no specific target was set, which is what happens on scheduled runs.
Bot permission needed: none
Settings: target, reason
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
case_number | number | The mod-log case number of the warning, for quoting in a follow-up message. Empty when no case was recorded. |
user_id | id | The member who was warned. |
Variables and stored data
Delete data
data.delete
Removes a stored variable entirely, which is how you reset a counter, clear a flag or drop a list in one step. Deleting something that was never there is a success and simply reports deleted as false, so a cleanup step never aborts a workflow. Scope targeting behaves like the other data actions: server scope needs nothing, member and channel scope default to the trigger member or channel and fail with missing_scope_target when the trigger has neither. Only the variable named in this step and this exact scope target is removed, so a member scoped delete never wipes the server wide value of the same name.
Bot permission needed: none
Gives up after: 10s
Settings: scope, user_id, channel_id, name
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
deleted | true | false | True when a stored variable was actually removed, false when there was nothing to delete. |
name | string | The variable name that was targeted. |
scope | string | Which scope was targeted: guild, member or channel. |
Read data
data.get
Reads a stored variable and publishes it under the short alias you choose, so later steps and conditions can use {vars.
Bot permission needed: none
Gives up after: 10s
Settings: scope, user_id, channel_id, name, variable_name, default
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
exists | true | false - branch on this | Whether the variable was found, as a true or false value for step conditions. |
length | number, 0 when not a list | Number of entries in the list, as text. "0" when the variable does not exist yet or is not a list, so a numeric condition still works before the first push. |
type | text | number | boolean | list | json | The stored type (text, number, boolean, list or json), empty text when the variable does not exist. |
value | the stored value (or the default) | The stored value as text, or the configured default (empty when none) if the variable does not exist. |
Add to counter
data.increment
Adds a signed amount to a persistent counter in one atomic step, which is the safe way to keep strikes, points, quotas or gauges that several events touch at once. A counter that does not exist yet is seeded from the configured starting value before the step is applied, and optional minimum and maximum bounds clamp the result (the store also hard-clamps at plus or minus 1e15). Scope targeting matches the other data actions, defaulting to the trigger member or channel and failing with missing_scope_target when neither exists; a variable already holding something other than a number fails with type_mismatch instead of being silently coerced. Leaving the TTL empty keeps whatever expiry the counter already had, and a repeat delivery of the same workflow step is de-duplicated for an hour so a retried deferred run cannot double count.
Bot permission needed: none
Gives up after: 10s
Settings: scope, user_id, channel_id, name, by, default, min, max, expire_after_seconds
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
name | string | The counter name that was updated. |
previous | number | The value before the change, or the configured starting value when the counter did not exist yet. |
scope | string | Which scope was updated: guild, member or channel. |
value | number (new value) | The counter value after the change, already clamped, ready for a numeric condition. |
Add to list
data.list_push
Appends a value to a persistent list, creating the list on the first push (an expired list is recreated holding just that value), which covers opt-ins, waiting queues, winners and seen-message ids. Turning on the unique option skips the push when the exact value is already present, and that skip is still a success reporting added as false, so you can branch on "already signed up". The list has a configurable maximum (default and hard ceiling 100 entries): pushing into a full list fails with list_full, a variable already holding a non-list type fails with type_mismatch, and creating a brand new variable name once the server has used up its plan's allowance of variable names fails with variable_limit. Scope targeting is the usual guild, member or channel with missing_scope_target when the trigger has no member or channel to default to, leaving the TTL empty keeps an existing list's current expiry, and a replay of the same workflow and step id within an hour returns the first outcome instead of pushing again (best effort: the push goes through anyway when Redis is unreachable).
Bot permission needed: none
Gives up after: 10s
Settings: scope, user_id, channel_id, name, value, unique, max_items, expire_after_seconds
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
added | true | false | True when the value was appended, false when the unique option skipped an already present value. |
length | number | How many entries the list holds after the step, ideal for a quota condition. |
name | string | The list variable name that was targeted. |
scope | string | Which scope was targeted: guild, member or channel. |
Remove from list
data.list_remove
Removes EVERY exact occurrence of a value from a persistent list, which is how a member opts out or frees a quota slot. Matching is exact text, so the value must be written the same way it was pushed, and removing something that is not in the list is a success reporting removed as 0. A list that does not exist or has expired is also a success and reports a length of 0, while a variable holding a non-list type fails with type_mismatch, and a removal never changes the list's expiry. Scope targeting is the usual guild, member or channel, defaulting to the trigger member or channel and failing with missing_scope_target when neither is available, and a replay of the same workflow and step id within an hour returns the first outcome instead of removing again (the removal simply runs again when Redis is unreachable, which is harmless since removing twice is a no-op).
Bot permission needed: none
Gives up after: 10s
Settings: scope, user_id, channel_id, name, value
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
length | number | How many entries remain in the list after the removal (0 when the list did not exist). |
name | string | The list variable name that was targeted. |
removed | number | How many matching entries were removed, 0 when the value was not in the list. |
scope | string | Which scope was targeted: guild, member or channel. |
Store data
data.set
Stores or fully replaces a persistent variable attached to the whole server, to one member, or to one channel, holding text, a number, a boolean, a list or free JSON. Member scope defaults to the member who triggered the workflow and channel scope to the channel it fired in, but either can be pointed at a specific id; a scope with no target at all fails with missing_scope_target rather than quietly falling back to a single shared server value. The value is written as text and then converted to the chosen type, so a value that is not a valid number, boolean or JSON fails with invalid_value, and a list is capped at 100 entries. Unlike the counter and list actions, a set replaces the row wholesale including its expiry, so leaving the TTL empty makes the variable permanent again; a server that has run out of its plan's distinct variable names gets variable_limit.
Bot permission needed: none
Gives up after: 10s
Settings: scope, user_id, channel_id, name, type, value, expire_after_seconds
Hands to later steps:
| Field | Shape | What it is |
|---|---|---|
created | true | false | True when this call created the variable, false when it overwrote an existing one. |
name | text | The variable name that was written. |
scope | string | Which scope it was written to: guild, member or channel. |
type | string | The stored value type: text, number, boolean, list or json. |
Get music player status
flavibot.get_player
Reads the server's music player and publishes it as a named set of variables under {vars.<variable_name>.*}, so later steps and "run only if" conditions can branch on what is playing. It always looks at the guild the workflow runs in, there is nothing else to target, and the data is the public-safe subset that /debug music shows, with no infrastructure identifiers. When there is no player at all, or the engine cannot be reached, the step does not fail: it publishes a neutral "not playing" snapshot with everything zeroed or false, so branch on the playing variable rather than expecting an error. Every value is published as a string, so booleans read as "true" and "false", and the bare {vars.
Bot permission needed: none
Gives up after: 10s
Settings: variable_name
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
always_connected | true | false - 24/7 mode | "true" or "false": whether 24/7 mode is enabled on this server. |
autoplay | true | false | "true" or "false": whether autoplay keeps the music going when the queue empties. |
connected | true | false | "true" or "false": whether the bot is actually connected to voice. |
dj_enabled | true | false | "true" or "false": whether DJ mode restrictions are enabled on this server. |
loop_queue | true | false | "true" or "false": whether the whole queue is set to repeat. |
loop_track | true | false | "true" or "false": whether the current track is set to repeat. |
no_sound | true | false - playing but not connected to voice | "true" only when the player thinks it is playing while not connected, the classic silent-bot symptom. |
node | text | Name of the audio node serving this server, empty string when there is no player. |
paused | true | false | "true" or "false": whether playback is paused. |
ping | number (ms) | Audio node latency in milliseconds as a string; "0" when there is no player. |
playing | true | false | "true" or "false": whether the player is currently playing something. |
queue_length | number | Number of tracks waiting in the queue, as a string. |
region | text | Voice region the player is on, empty string when there is no player. |
track_title | text, empty when nothing is playing | Title of the current track, empty string when nothing is playing. |
track_url | url, empty when nothing is playing | Link to the current track, empty string when nothing is playing. |
value | track title when playing, else "not playing" | Headline value used by a bare {vars. |
volume | number | Current player volume as a string; "0" when there is no player. |
HTTP GET request
http.get
Fetches a URL with a plain GET and publishes the response under the short alias you choose, so later steps can read {vars.
Bot permission needed: none
Gives up after: 13s
Settings: variable_name, url
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
ok | true when the status is 2xx | The text true or false saying whether the response status was a success code. |
status | HTTP status code, e.g. "200" | The HTTP status code as text, for example 200 or 404. |
value | the body's own value field, else the HTTP status | The response body's own top-level value field when it has one, otherwise the HTTP status code as text. Also read as the bare {vars. |
Get channel variables
variable.get_channel
Resolves a channel of this server and publishes {vars.
Bot permission needed: none
Gives up after: 15s
Settings: variable_name, channel_id
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
id | id | The channel's Discord id. |
mention | mention | A clickable #channel mention. |
name | text | The channel name without the hash; empty when the channel has none. |
parent_id | category id, empty if none | The parent category id; empty when the channel sits outside any category. |
type | numeric channel type | Discord's numeric channel type, for example 0 for text and 2 for voice. |
value | mention | The channel mention, also readable as the bare {vars. |
Get role variables
variable.get_role
Resolves a role of this server and publishes {vars.
Bot permission needed: none
Gives up after: 15s
Settings: variable_name, role_id
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
id | id | The role's Discord id. |
managed | true | false | The text "true" or "false", true for roles owned by a bot or integration and not assignable by hand. |
mention | mention | A clickable role mention, which pings the role when posted. |
name | text | The role name without the at sign. |
position | number | The role's position in the hierarchy, higher meaning further up the list. |
value | mention | The role mention, also readable as the bare {vars. |
Get user variables
variable.get_user
Looks a person up and publishes a whole set of {vars.
Bot permission needed: none
Gives up after: 15s
Settings: variable_name, target
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
account_age_days | number | Whole days since the account was created, handy for anti-scam conditions. |
avatar_url | url | Direct image URL, preferring the server avatar and falling back to Discord's default. |
bot | true | false | The text "true" or "false" telling you whether the account is a bot. |
created_at | relative timestamp | Account creation as Discord relative-time markup that renders as "3 years ago". |
display_name | global name or username | The global display name, falling back to the username. |
id | id | The user's Discord id. |
joined_at | relative timestamp, empty if not a member | Server join date as relative-time markup; empty when it is unknown or they are not a member. |
member_found | true | false | The text "true" or "false" telling you whether they are actually in this server. |
mention | mention | A clickable mention of the user. |
nick | server nickname, empty if none | The server nickname; empty when there is none or the person is not a member. |
username | text | The Discord username; empty if the lookup carried none. |
value | mention | The user's mention, also readable as the bare {vars. |
Pick at random
variable.random
Draws one of up to 25 listed choices at random and publishes it as {vars.
Bot permission needed: none
Gives up after: 5s
Settings: variable_name, choices
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
index | which choice was drawn (0-based) - branch on this | Which choice was drawn as a zero-based position, ideal for a run condition. |
value | the drawn choice | The choice that was drawn, also readable as {vars. |
Set variable
variable.set
Builds a named text variable from a source value plus an ordered list of up to 10 transforms (trim, lowercase, uppercase, regex extract, replace, default, map), and publishes it for the rest of the workflow as {vars.
Bot permission needed: none
Gives up after: 5s
Settings: variable_name, source, transforms
Publishes under the name you choose, read as {vars.<name>.<field>} (or the bare {vars.<name>} for value):
| Field | Shape | What it is |
|---|---|---|
value | the final transformed text | The final text after every transform, also readable as {vars. |
