# FlaviBot

> FlaviBot is a free Discord music and multipurpose bot: it supports Spotify, Apple Music, Deezer, Tidal, Amazon Music and radio, plus moderation, AutoMod, ticketing, leveling, welcome messages and autoresponder automations.

This is the long form of https://flavibot.xyz/llms.txt: the product written out module by module, then the whole documentation inline, each page under its canonical URL. The short index, with a link per page and nothing else, is at https://flavibot.xyz/llms.txt.

Shorter than either: https://flavibot.xyz/ai-instructions is a single page stating what FlaviBot is, what it supports, what it costs and what it is not. Same facts, small enough to quote whole.

## What FlaviBot is

FlaviBot is a free Discord bot, created in March 2019, that combines a music player with the server-management modules a community usually spreads across three or four separate bots. It runs on 2,015,533 Discord servers. Everything is usable for free: a free server gets a 200-track queue and reads up to 500 tracks from an imported playlist link, which Premium raises to 100,000 and 10,000. It is driven from Discord with slash commands, from the buttons it posts, and from a web dashboard at https://flavibot.xyz/dashboard where every module has a settings page; opening the dashboard needs a Discord login and the **Manage Server** permission on the server you want to configure, or an explicit grant from someone who has it.

Beyond music it covers moderation with a numbered case history, Discord AutoMod, ticketing, welcome and goodbye messages, levels and rank cards, temporary voice channels, starboard, suggestions, custom commands, invite tracking, birthdays, embed messages, server logs and autoresponder automations. Support is on Discord at https://discord.gg/flavibot, and the project's GitHub organisation is https://github.com/flavibot.

## Music

For music it supports Spotify, Apple Music, Deezer, Tidal and Amazon Music, plus direct audio file URLs, radio stations and audio files uploaded with `/play-file`; searches default to Spotify. YouTube is not a supported music source. You join a voice channel and run `/play` with a song name or a link: the bot joins, queues the track and announces it, and Discord autocompletes matching tracks as you type. An album, artist or playlist link queues everything behind it in one go — up to 500 tracks on a free server, 10,000 on a Premium one — and `insert-first` puts your track at the top of the queue instead of the end. `/play-file` plays an audio file you attach, and `/radios` browses the radio stations curated by the community.

The queue holds 200 tracks on a free server and 100,000 on a Premium one. `/queue` reads it; `/skip`, `/previous`, `/pause`, `/resume`, `/stop`, `/seek`, `/fastforward`, `/rewind`, `/jump`, `/shuffle`, `/loop`, `/volume`, `/remove`, `/clearqueue` and `/removedupes` drive it; `/nowplaying` reposts the current track with its buttons, `/lyrics` fetches the lyrics of what is playing, and `/join` and `/disconnect` move the bot in and out of a channel. `/autoplay` builds a fresh queue from the current track once yours runs dry, and `/filter` applies one of nine audio effects.

Playlists are saved and replayed with `/playlist`, `/music-info` looks a song up without playing it, and `/grab` sends the current track's details to your DMs. `/djmode` decides who may touch the player, `/controller` turns a channel into a place where typing a song name is enough to queue it, `/announcechannel` sets where the bot announces tracks, and `/setvc` restricts which voice channels it may join. There is also a web player — search, a drag-and-drop queue and the filters, in a browser — opened with `/music-panel`, from the **Now playing** title, or from the dashboard.

A few music commands ask a member on a free server to vote for FlaviBot on top.gg first: `/filter`, `/volume`, `/loop`, `/shuffle`, `/jump`, `/seek`, `/fastforward`, `/rewind`, `/removedupes`, `/lyrics`, `/tts` and `/announcechannel`. A vote is free and counts for twelve hours, and a Premium server removes the prompt for everyone. Premium also unlocks the playback defaults a free server cannot save at all: 24/7 mode (`/24-7`), a default volume, default songs, a default playlist (`/defaultplaylist`), default autoplay, a default queue loop, auto shuffle, a shorter inactive-disconnect timeout and a custom controller image.

## Moderation

Nothing happens without a record: every sanction leaves a numbered **case** in the server's history, whether a moderator typed it, a Discord AutoMod rule handed it out, or the bot applied it automatically. `/warn`, `/mute`, `/kick`, `/ban`, `/tempban` and `/softban` are the sanctions, `/note` writes a private record with no punishment, and `/unmute` and `/unban` undo without erasing — a reversal is recorded as a reversal, not as a judgment. `/case` looks up one case and `/modhistory` reads a member's whole record; `/clear` bulk-deletes messages and `/mass-role` adds or removes a role across the server.

Repeated warns can escalate on their own: the dashboard's moderation settings decide how many warns turn into a timeout, a kick or a ban, where moderation events are posted, and what the sanctioned member is told in DM.

## Discord AutoMod and server logs

FlaviBot manages Discord's own AutoMod rules from the dashboard, so keyword filters, spam and mention limits are edited in the same place as everything else. A rule can go further than Discord allows on its own and hand out a real FlaviBot warn, which lands in the case history and counts toward auto-escalation like any other.

Server logs are a written record of what happens on the server — message edits and deletions, joins and leaves, role and channel changes, voice activity — posted into the channels you pick, event by event. A free server has one log channel; Silver raises that to five, Gold to ten and Platinum to fifteen. Both are dashboard features with no slash command of their own.

## Tickets

A ticket is a private channel between one member and the support team. You build a **panel** on the dashboard — its channel, its category, the roles that answer it, the message members see — and every save posts or updates that panel message. A member clicks **Create a ticket**, the bot checks the blocks (blacklist, business hours, one ticket per panel, a price in coins if you set one), creates the channel and posts the staff buttons: **Close Ticket**, **Claim Ticket**, **Escalate**.

Members need no command. Staff have `/ticket` (with `add`, `remove`, `blacklist` and `request-close` among its subcommands) and a **Create Ticket** message command for opening one on someone's behalf. Closing archives or deletes the channel depending on the panel, and can send a transcript to the member and to a log channel. Tickets can also be answered from the dashboard, which lets a support team work without being given the run of the server.

## Levels, rank cards and the economy

Levels turn activity into progress: members earn XP for talking and for sitting in voice channels, XP becomes levels, and levels hand out roles, post an announcement and fill a rank card. `/rank` shows a member's card, `/leaderboard` the public ranking, and `/xp add|remove|set|reset` is the Manage Server tool for editing a member's XP directly. The card is redesignable on the dashboard, the XP rate and per-role multipliers are Premium, and role rewards run from 3 on a free server to 50 on Platinum.

The economy is a separate, per-server currency: members hold a balance, earn coins from the sources you switch on, and spend them in a shop you write yourself. `/economy` and `/casino` are its commands. Neither system needs the other, both are off until you turn them on, and the economy is still being rolled out server by server — where it is not enabled, `/economy` says so instead of running.

## Engagement

Welcome and goodbye messages greet joiners and say goodbye to leavers, optionally with a generated card and a role handed out on join; boost messages do the same for a server boost. The starboard reposts messages that collect enough of a chosen reaction, so members pin the best of the server themselves. Suggestions (`/suggestion`) collect ideas in one channel with voting and an approved/rejected status. Birthdays (`/birthday`) announce a member's day, optionally with a role for the day (24 hours by default, anywhere from 1 to 168), and `/recap` posts a snapshot of your last seven days of FlaviBot activity.

Giveaways (`/giveaway`) run prize draws with entry requirements, automatic winner picking and rerolls. Events (`/event`) schedule what a server does together: FlaviBot posts an RSVP panel where members answer Going, Maybe or Can't make it, keeps a waitlist once an event with a capacity is full, DMs reminders before the start, repeats an event daily, weekly, monthly or yearly, and can mirror it into the server's Discord Events tab. Events are being rolled out gradually, so where a server is not in the rollout yet, `/event` says it is not available and points to the support server. Feed announcements post new YouTube videos, Twitch and Kick go-lives and Reddit posts into the channels you choose — notifications about a creator, not a music source. The invite tracker (`/invites`) records who invited each member and ranks the top inviters, and the counting game keeps a channel where members count together, with `/counting` for the saves that rescue a broken streak.

## Temporary voice channels

One channel hands out voice rooms instead of ten static channels sitting half empty. A member joins the **creator channel**, FlaviBot builds a room named after them, moves them into it and posts a control panel in the room's text chat; when the last person leaves, the room is deleted. The member who created the room owns it, which means renaming it, capping it, locking it, hiding it and deciding who may come in — all from the panel's buttons.

There are no commands for this module: members drive it entirely from the creator channel and the panel, and everything you configure lives on the dashboard under Server Management.

## Autoresponder automations

An autoresponder is one rule that says *when this happens, do that*. Four pieces make every rule: a **trigger** (a message, a reaction, a join, a schedule, a button click), **filters** that decide whether to react at all, **actions** that do the work, and **variables** that fill in the values. Steps run top to bottom and a failed step stops the rest by default, so a rule never half-runs silently; every fire is recorded, with the step that failed and why.

It is deliberately not a scripting language — no loops, no arbitrary code — which is what makes a rule readable by whoever inherits the server and safe to install from someone else. That is the basis of the preset marketplace: an automation someone built can be shared and installed on another server as-is. Reaction-role menus, anti-raid gates, strike systems, ticket panels and scheduled announcements are all the same four pieces in a different order. The module is configured entirely on the dashboard.

## Everyday utilities

Embed messages are rich messages built in a visual editor and posted or updated from the dashboard; custom commands are your own commands with your own responses; sticky messages keep an important message at the bottom of a channel; automated messages post on a schedule or once at a chosen time; channel stats are voice channels whose names show live server numbers; persistent roles give a member their roles back when they rejoin. `/reminders` manages your personal reminders, `/server-stats` opens the server activity statistics, and `/say`, `/avatar`, `/info` (server, user, role, channel and more) and `/members` are the small ones.

`/help` lists the commands, `/settings` and `/prefix` and `/language` set the server up, `/premium` shows and activates a subscription, `/dashboard` and `/support` hand out the links, `/stats` and `/ping` report on the bot, and `/privacy` covers what a member can opt out of. A module or a single command can be switched off entirely from the dashboard.

## Premium

Premium is sold as server subscriptions — Silver €3.99, Gold €5.99 and Platinum €9.99 per month (€39.90, €59.90 and €99.90 per year), in EUR — covering 2, 4 and 6 premium servers per subscription; Platinum adds one custom bot, a Discord application of your own running FlaviBot. Extra custom bots are €4.99 per month (€49.99 per year) each, on top of Platinum. A subscription belongs to your account; premium on a server is a slot you spend from it, activated with `/premium activate` on that server, and moved to another server whenever you like. Everything is on https://flavibot.xyz/premium.

| Plan | Per month | Per year | Premium servers |
| --- | --- | --- | --- |
| Free | — | — | 0 |
| Silver | €3.99 | €39.90 | 2 |
| Gold | €5.99 | €59.90 | 4 |
| Platinum | €9.99 | €99.90 | 6 |

The caps, plan by plan (the full table is at the end of this file, under Limits by tier):

| Limit | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Premium servers per subscription | 0 | 2 | 4 | 6 |
| Custom bots included | 0 | 0 | 0 | 1 |
| Songs in the queue | 200 | 100,000 | 100,000 | 100,000 |
| Songs read from an imported playlist | 500 | 10,000 | 10,000 | 10,000 |
| Saved playlists (per account) | 3 | Unlimited | Unlimited | Unlimited |
| Autoresponders | 3 | 15 | 25 | 50 |
| Automated messages | 2 | 10 | 15 | 25 |
| Embed messages | 3 | 10 | 15 | 500 |
| Ticket panels | 1 | 3 | 5 | 20 |
| Level role rewards | 3 | 10 | 25 | 50 |
| Server log channels | 1 | 5 | 10 | 15 |
| Server Stats history | 14 days | Full | Full | Full |

An older **Bronze** tier still exists for the people who bought it, but it is no longer sold.

## Custom bots

A custom bot is your own Discord application running FlaviBot: members see your name, your avatar, your banner and your status, while the features, the commands and the dashboard are FlaviBot's. It is not a second copy of your settings — the bot joins the server like any other, and you configure it on the same dashboard by switching the bot selector to it. Platinum includes one, and extra ones are sold as an add-on on top of a Platinum subscription.

Setting one up means creating an application on Discord's developer portal with the three privileged intents enabled, then pasting its token into the Custom Bot page of the dashboard. Gold adds a lighter version of the same idea, **Server Bot Profile**: a nickname, avatar, banner and embed colour for the official FlaviBot that apply to your server only.

## Free tools

The site also hosts free tools that work for any Discord bot, not only FlaviBot. The Discord Bot Invite Link Generator at https://flavibot.xyz/tools/discord-bot-invite-generator builds an OAuth2 invite link from an application ID: it offers the `bot` and `applications.commands` scopes, a server or user install, an optional server ID to pre-select or lock, presets, and every Discord permission as a checkbox, kept in sync with the permissions integer both ways. The link is built in the browser. All tools are listed at https://flavibot.xyz/tools.

## Documentation

The 89 pages below are the complete documentation, in the order the site lists them. Each one is printed under its canonical URL; the URLs keep their two-digit filename prefix (`/docs/premium/03-tiers`, not `/docs/premium/tiers`).

### Adding FlaviBot

Source: https://flavibot.xyz/docs/getting-started/01-add-flavibot
Summary: Where to find the FlaviBot invite link, what the invite asks Discord for, and what happens in your server the moment the bot joins.

FlaviBot is one bot with many modules: music, moderation, levels, tickets,
welcome messages, giveaways, starboard, automations. Almost all of it is
configured from a web dashboard rather than from chat, so there are only two
things to do to get started: add the bot, then open the dashboard.

#### Where is the invite link?

Three places, all of them granting the same permissions: the site button and the dashboard's *Add FlaviBot* button both add the main FlaviBot (the dashboard one straight into the server whose card you clicked), while `/invite` returns an invite for the bot that answered the command.

- **flavibot.xyz**, the *Add FlaviBot* button.
- **The dashboard's server list.** Servers that already have FlaviBot show a
  *Configure Dashboard* button; servers that do not show *Add FlaviBot*
  instead.
- **From inside a server that already has it**: run `/invite` and the bot
  replies with an *Add FlaviBot* button.

Adding a bot to a server is something Discord only lets you do where you have
the **Manage Server** permission, so that is the short answer to "which of my
servers can I add it to".

#### What does the invite ask for?

FlaviBot's invite link asks for **Administrator**, on top of the message,
channel and voice permissions its modules use. Discord shows you the full list
on the authorisation screen before you accept.

You do not have to leave it at that. Whatever you end up granting, FlaviBot
checks the permission it needs before every action, and when one is missing it
names it instead of failing quietly. Running `/ban` in a server where the bot
cannot ban answers privately with the missing permission, rather than doing
nothing.

The trade-off is that you find missing permissions one feature at a time. If
you would rather not, granting Administrator is what the invite link does by
default.

#### What happens right after it joins

FlaviBot posts a short welcome panel in your server. It picks:

1. your **system channel** (the one Discord uses for join messages), if it can
   post there;
2. otherwise the first text channel whose name contains `general`, `chat`,
   `discussion`, `global`, `cmd`, `admin` or `staff` that it can post in.

"Can post in" means the bot has **View Channel**, **Send Messages** and **Embed
Links** there. If none of your channels match, nothing is posted, and that is
the only symptom: the bot is in the server and working, it just had nowhere to
say hello.

The panel carries a language picker, a *Get Started with Music* button, and
links to the dashboard, the support server and the premium page. Picking a
language there sets the bot's language for the whole server, the same as
running `/language` later.

#### Next

- [What FlaviBot can see](https://flavibot.xyz/docs/getting-started/02-what-flavibot-sees) before
  you decide which channels to open to it.
- [Finding your dashboard](https://flavibot.xyz/docs/getting-started/03-the-dashboard) to configure
  anything at all.

### What FlaviBot can see

Source: https://flavibot.xyz/docs/getting-started/02-what-flavibot-sees
Summary: Which Discord events reach FlaviBot and which never do, what server statistics it stores, and what happens when a permission it needs is missing.

Two separate things decide what FlaviBot knows about your server: the
**permissions** you granted it, and the **event types** it subscribes to on
Discord. Permissions are per channel and you control them. The event list is
the same for every FlaviBot install, including custom bots.

#### What reaches the bot

| It receives | Used by |
| --- | --- |
| Servers, channels, roles | everything |
| Messages and their content | levels, autoresponder, counting, sticky messages, prefix commands |
| Reactions | starboard, reaction roles |
| Members joining, leaving, being updated | welcome, goodbye, persistent roles, invite tracker |
| Voice state changes | music, temp voice channels, voice statistics |
| Invites being created and deleted | invite tracker |
| Bans and audit log entries | moderation, server logs |
| Scheduled events | server logs |
| Discord AutoMod rules and hits | the Discord Automod page |

Message content is the one worth knowing about: FlaviBot reads messages in the
channels it can see, which is what makes XP, autoresponders and counting
channels possible at all.

#### What never reaches it

- **Presence.** FlaviBot does not receive who is online, idle or offline, nor
  what anyone is playing or listening to.
- **Typing indicators.**
- **Anything in a channel it cannot view.** If the bot has no View Channel on
  a channel, that channel does not exist as far as the bot is concerned: no
  messages, no XP, no autoresponder, no logs. Removing View Channel is the
  clean way to keep a private channel private.

#### What gets stored

Server statistics record message counts per channel, voice time per channel,
and music listening history. Any member can see and change that for
themselves with `/privacy`: the command replies privately with the current
state and two buttons, one to turn tracking off, one to delete the data
already collected. The setting is per member and applies to **every** server
using FlaviBot, not just the one where the command was run.

#### When a permission is missing

FlaviBot checks before it acts rather than after it fails. When the bot itself
lacks a permission, the reply lists the missing permission by name; when you
are the one who lacks it, the reply names the permission you need and the
command you tried. Both are private when the command was run as a slash command. A prefix command's refusal is posted publicly in the channel, unless the server changed that under *When a command is refused* on the Configuration page.

Automations built with the [autoresponder](https://flavibot.xyz/docs/modules/autoresponder/01-overview)
go one step further: a rule that keeps hitting the same permission error is
disabled automatically and the dashboard says why.

### Finding your dashboard

Source: https://flavibot.xyz/docs/getting-started/03-the-dashboard
Summary: How to open the FlaviBot dashboard, who is allowed in, why settings are stored per bot, why nothing saves by itself, and what to do if a server will not load.

Almost every FlaviBot setting lives on the web dashboard, not behind a command.

#### How do I open the dashboard?

Go to **flavibot.xyz/dashboard** and log in with Discord. You land on
**Select a Server**, a grid of the servers you can configure, with a search
box. Each card has either *Configure Dashboard* (FlaviBot is in that server)
or *Add FlaviBot* (it is not yet).

From inside Discord, `/dashboard` replies with the direct link to that
server's dashboard, which saves you the picking step.

*Configure Dashboard* drops you on **Configuration**, the page that holds the
server-wide basics: language, prefix, timezone, who may use commands where,
and dashboard access.

Screenshot: The top of the Configuration page, with the FlaviBot Channel, Language and Prefix cards

The first three cards of the Configuration page.

#### Who is allowed into a server's dashboard?

A server appears in your list when you have the **Manage Server** permission
on it (the server owner and anyone with Administrator qualify), or when
someone with Manage Server has explicitly granted you access.

That grant is on **Configuration → Dashboard Access**: up to 50 roles and up
to 50 users who can open this server's dashboard without holding Manage
Server. Only someone with real Manage Server can edit that list, so granted
access cannot be used to widen itself.

Ticket staff get a narrower door: they see the ticket pages rather than the
whole dashboard. That is covered with the ticketing module.

Every connection and every save is recorded on **FlaviBot Management → Access
Logs**: who connected, what they changed, and whether the attempt was allowed
or denied.

#### One dashboard, several bots

FlaviBot exists as several bot accounts, and a server can hold more than one
of them. **Settings are stored per bot.** The bot selector at the top of the
dashboard decides which one you are configuring, and the Configuration page
states it plainly: *You're configuring FlaviBot*.

This matters in practice. A prefix, a language or a welcome message saved while *FlaviBot* is selected does not apply to *FlaviBot 2* in the same server. Levels is the exception: there is one levels configuration per server, and saving it hands XP processing to whichever bot is selected rather than creating a second config. If a setting seems not to take effect, check the selector first.
The same rule is what makes a [custom bot](https://flavibot.xyz/docs/getting-started/07-custom-bot)
configurable: you switch the selector to it and configure it like any other.

#### Nothing saves by itself

Changing a field on a dashboard page does not apply it. A bar appears at the
bottom telling you there are unsaved changes, with **Save Changes** and
**Reset**. Leaving the page with the bar open loses the edits.

A handful of controls act immediately instead, and they say so: starting or
stopping a custom bot, and the module activation dialog described in
[turning things on and off](https://flavibot.xyz/docs/getting-started/06-modules-and-commands).

#### Reading the sidebar

The left menu is grouped by theme: FlaviBot Management, Miscellaneous,
Statistics, Music, Moderation, Server Management, Tickets, Engagement,
Automation, Utilities, Notifications.

Each module entry carries a small status dot, so you can tell at a glance
what is on, what is off, and what is broken. See
[turning things on and off](https://flavibot.xyz/docs/getting-started/06-modules-and-commands) for
what each colour means.

#### If the server will not load

The dashboard distinguishes the cases rather than showing one generic error:

- **The bot is not connected to this server.** Add it, or switch the bot
  selector to a bot that is actually in the server.
- **No permission.** You lost Manage Server, or the dashboard access list
  changed.
- **Server not found.** The bot is not in it at all.

### Commands and /help

Source: https://flavibot.xyz/docs/getting-started/04-commands-and-help
Summary: The two ways to run a FlaviBot command, what /help shows, the commands worth knowing on day one, and the five reasons a command can refuse.

#### Two ways to run the same command

**Slash commands.** Type `/` in any channel and pick FlaviBot. Discord shows
the command names, their descriptions and their options, in the reader's own
Discord language when a translation exists. This is the surface most members
use.

**Prefix commands.** The same commands also answer to a text prefix. If your
server's prefix is `f!`, then `f!play` runs `/play`. Prefix matching ignores
case, so `F!Play` works too. See
[prefix, mentions and language](https://flavibot.xyz/docs/getting-started/05-prefix-and-language)
for how to see or change your server's prefix.

A few commands are prefix-only and never appear in Discord's `/` list, such as
`profile`.

#### /help

`/help` with no argument lists **every** command, grouped by category, with the
total in the title. The categories are **Bot**, **Administration**, **Music**,
**Util**, **Moderation** and **Economy**. Three buttons sit under it: the full
command list on the website, an invite link, and the premium page.

`/help <command>` details one command instead: its description, its usage
string, its aliases, the permissions **FlaviBot** needs for it, and the
permissions **you** need. `/help ban` is the fastest way to answer "why can I
not use this".

`/help` also answers to `h`, `aide` and `commands` as prefix aliases. It needs
the **Embed Links** permission in the channel where you run it, since the menu
is an embed.

The whole catalogue is also browsable at **flavibot.xyz/commands**.

#### Commands worth knowing on day one

| Command | What it does |
| --- | --- |
| `/help` | the command list, or the details of one command |
| `/dashboard` | the direct dashboard link for this server |
| `/invite` | every FlaviBot in one message, with the button to add this one |
| `/support` | the support server invite |
| `/ping` | the bot's latency, useful to tell "slow" from "offline" |
| `/prefix view` | the prefix currently in use here |
| `/language` | change the bot's language for this server |
| `/settings` | view and manage bot settings |
| `/info`, `/avatar` | quick lookups — `/info server`, `/info user`, `/info role`, and more |
| `/privacy` | your own tracking settings, and data deletion |
| `/premium` | premium status, and activating it on this server |

#### When a command refuses

FlaviBot never refuses silently. There are five different reasons, and each
one says which:

1. **FlaviBot is missing a permission.** The reply names the permission.
2. **You are missing a permission.** The reply names it, and the command.
   `/prefix`, `/language` and `/settings` all require **Manage Server**.
3. **The command is disabled on this server.** The reply points to the
   Configuration page where it was turned off.
4. **You, your role, or this channel is restricted.** Set up on the
   Configuration page, see
   [turning things on and off](https://flavibot.xyz/docs/getting-started/06-modules-and-commands).
5. **The command needs premium.** The reply links to the plan that unlocks it.

Slash refusals are private for the first four reasons, visible only to the person who ran the command. The premium refusal is posted publicly in the channel. Prefix refusals are public by default, and you can change that: the
Configuration page offers *reply*, *stay silent*, *reply then delete the bot's
message*, and *reply then delete everything* (which needs Manage Messages to
delete the member's message).

### Prefix, mentions and language

Source: https://flavibot.xyz/docs/getting-started/05-prefix-and-language
Summary: Set FlaviBot's text prefix or turn prefix commands off, use the bot mention as a fallback, and choose the server's language, timezone and command restrictions.

#### The prefix

A prefix is the character or characters that turn a normal message into a
command. It is per server **and** per bot: each bot in your server carries its
own prefix, and each bot ships with its own default.

To see the one in use, run `/prefix view`. The reply states the current prefix
and reminds you of the usage.

To change it:

- `/prefix set <prefix>` in Discord (alias `setprefix`), or
- **Configuration → Prefix** on the dashboard.

A prefix is at most **10 characters**. Matching is case-insensitive, so a
prefix of `f!` also answers to `F!`. The change takes effect immediately, and
the bot confirms with the new form of the help command.

Remember the bot selector on the dashboard: setting a prefix while *FlaviBot*
is selected does not change *FlaviBot 2*.

#### How do I turn prefix commands off?

**Configuration → Prefix → Enable prefix commands** switches the text prefix
off for the whole server. With it off, only slash commands work, which is the
usual choice for servers that want a clean chat.

Mentioning the bot still works when prefix commands are off. That is
deliberate: it is the way back in if a prefix ever locks you out.

#### The bot mention

Mentioning FlaviBot with nothing else, just `@FlaviBot`, makes it reply with a
short card: your server's name, the sentence *my prefix is* followed by the
**current prefix**, the `/play` and `/help` commands as clickable links, and
three buttons (*Vote for me*, *All commands*, *Invite me*).

That card is the quickest way to answer "what is the prefix here" from inside
Discord, and it works even when nobody remembers the prefix.

Mentioning the bot **followed by a command** runs that command:
`@FlaviBot help` behaves exactly like `/help`. This is the fallback path that
keeps working when prefix commands are disabled.

#### The server language

FlaviBot answers in 25 languages, including English, French, Spanish, German,
Italian, Brazilian Portuguese, Polish, Turkish, Russian, Japanese, Korean and
Chinese. This setting is what the bot's own messages in your server are
written in. Command descriptions inside Discord's `/` menu follow each
reader's Discord language instead, which Discord decides, not you.

Three ways to set it:

- the language picker on the welcome panel posted when the bot joins,
- `/language` in Discord (aliases `lang`, `langs`, `langue`),
- **Configuration → Language** on the dashboard.

`/language` needs the **Manage Server** permission, like `/prefix`.

#### Timezone

**Configuration → Timezone** sets the server's timezone. Time-based features
read it, so scheduled things happen at the hour you meant rather than at UTC.

#### Restricting where commands work

Also on the Configuration page, three global presets decide who can run
commands and where:

| Preset | Effect |
| --- | --- |
| Channel restriction | commands accepted everywhere (default), only in the channels you list, or everywhere except them |
| Role restriction | any role (default), only the roles you list, or every role except them |
| User restriction | any user (default), only the users you list, or everyone except them |

The channel list holds 20 entries on a free server, more with premium.

These are the **global** presets. A per-command or per-category rule overrides
them, which is the subject of the
[next page](https://flavibot.xyz/docs/getting-started/06-modules-and-commands).

### Turning things on and off

Source: https://flavibot.xyz/docs/getting-started/06-modules-and-commands
Summary: Enable or disable a whole FlaviBot module or a single command, read the sidebar status dots, and see which access rule wins when several apply.

FlaviBot ships everything at once, and almost nothing is active until you turn
it on. Two levels of control exist: **modules** (levels, starboard, tickets,
welcome, ...) and **individual commands**.

#### Reading the status dots

Every module in the sidebar carries a dot:

| Dot | Meaning |
| --- | --- |
| Green | the module is on and healthy |
| Grey, with a dimmed label | the module exists but is off |
| Amber, with a number | that many of its configurations are in error |
| Red, with a *Disabled* badge | the FlaviBot team has disabled this module globally |

A red dot is not something you did. Hovering it shows the reason the team gave
when they switched the module off, for example an incident on a third-party
service. It comes back on its own.

The amber count is the useful one day to day: it means a config you saved
cannot run, usually a missing permission or a deleted channel. The module's own
page shows which one.

Pages that are not modules (Configuration, Access Logs, Cases) have no dot.
Jobs shows a pulsing dot with a count instead, while a batch job of yours is
running.

#### Turning a module on

Modules come in three shapes.

**One switch, one dialog.** Levels, Birthday, Economy, Boost, Server Tag,
Invite Tracker, Server Logs and Persistent Roles turn on from the sidebar:
clicking one while it is off opens a short confirmation, and confirming
enables it and takes you to its page to finish configuring. That confirmation
applies immediately, with no save bar step.

**On by being configured.** Welcome and Goodbye, and DJ Mode, have no bare
enable switch: they come alive once you fill them in and save.

**Many configurations.** Starboard, Ticketing, Counting, Sticky Messages,
Temp Voice and the notification feeds (YouTube, Twitch, Kick, Reddit) hold
several entries, each with its own switch: several starboards, several ticket
panels, several watched channels. The module reads as on as soon as one of its
entries is set up and active.

Tools such as Custom Commands, Embed Messages and Forms work the same way:
there is no global switch, they light up once you create your first entry.

#### Turning a module off

Single-switch module pages (Levels, Birthday, Economy, Boost, Server Tag, Invite Tracker, Server Logs, Persistent Roles, Welcome, DJ Mode) have their own **Enabled** switch: turn it off, then press **Save Changes** in the bar at the bottom. Multi-configuration modules have no module-level switch — you turn off each entry with its own switch, and that applies immediately. Configuration is kept, so turning it
back on later restores what you had.

#### How do I disable a single command?

**Configuration → Commands Settings** lists every command with a search box
and a switch each. A disabled command still appears in Discord's `/` list (that
list is global to the bot), but running it answers, privately, that the command
has been disabled by the server administrators, with a link to this page.

Next to the switch, *Configure access* opens the per-command rules:

| Mode | Effect |
| --- | --- |
| No restriction | anyone, anywhere (default) |
| Whitelist | only the channels, roles or users you list |
| Blacklist | everyone except the ones you list |

Channels, roles and users are three independent lists, up to 50 entries each.
The same dialog exists per **category** (Bot, Administration, Moderation,
Music, Utility, Economy) when you want to gate a whole family at once.

#### Which rule wins

The order matters and it is deliberate:

1. If a rule exists for **this command**, it decides, and the server-wide
   presets are ignored.
2. Otherwise, if a rule exists for its **category**, that decides.
3. Otherwise the server-wide presets from
   [the previous page](https://flavibot.xyz/docs/getting-started/05-prefix-and-language) apply.

Saving a per-command rule with every mode set to *No restriction* is
therefore meaningful: it says "this command opts out of the global preset".
*Reset to default* removes the override and puts the command back under the
global rules.

### Setting up a custom bot

Source: https://flavibot.xyz/docs/getting-started/07-custom-bot
Summary: Run FlaviBot's features under your own Discord application: what you need, creating and connecting the bot, and setting its name, avatar and status.

A **custom bot** is your own Discord application running FlaviBot. Members see
your name, your avatar, your banner and your status in the member list; the
features, the commands and the dashboard are FlaviBot's.

It is not a second copy of your settings: the bot joins your server like any
other bot, and you configure it on the same dashboard by switching the bot
selector to it.

#### What you need

- **Platinum premium.** The subscription must be on your account, and Platinum
  must be active on the server (`/premium activate`). Platinum includes one
  custom bot.
- **A Discord application** of your own, created on Discord's developer
  portal, with all three privileged intents enabled.

The Custom Bot page is in the sidebar under Miscellaneous. Without Platinum it
shows a locked preview.

#### Creating the application

This part happens on Discord's side, at
[discord.com/developers/applications](https://discord.com/developers/applications).

**1. Create a new application.** Press **New Application**, give it a name and
accept Discord's terms. The name is not final, you can change it later.

![The Create an application dialog, with the terms checkbox and the Create button](https://cdn-assets.flavibot.xyz/docs/getting-started/custom-bot/01-059d091f.png)

**2. Add a bot to it.** Open the **Bot** tab of your application.

![The Bot tab of a Discord application](https://cdn-assets.flavibot.xyz/docs/getting-started/custom-bot/02-48a9fa40.png)

**3. Enable the three intents.** Scroll to **Privileged Gateway Intents** and
turn on **Presence Intent**, **Server Members Intent** and **Message Content
Intent**. FlaviBot refuses a token whose application is missing any of them,
and tells you which ones.

![The Privileged Gateway Intents section with all three switches on](https://cdn-assets.flavibot.xyz/docs/getting-started/custom-bot/03-7b8b0cc2.png)

**4. Copy the token.** Press **Reset Token** to reveal it, confirm, then copy
the value.

![The Reset Token button on the Bot tab](https://cdn-assets.flavibot.xyz/docs/getting-started/custom-bot/04-8dad60f0.png)

![The token revealed, with the copy button](https://cdn-assets.flavibot.xyz/docs/getting-started/custom-bot/05-3e2656da.png)

The token is the password to your bot. Anyone holding it controls the bot
entirely. Never paste it anywhere except the dashboard field below, and reset
it in the portal if you think it leaked.

#### Connecting it

1. Dashboard → **Custom Bot**, then press **Add Custom Bot**: the token dialog opens straight away. (The enable switch only exists once a custom bot has been created; flipping it on afterwards starts that bot.)
2. Paste the token and press **Save Token**. FlaviBot checks it against
   Discord, reads the application's intents, and stores it encrypted. A bad
   token answers *Invalid token* and nothing is saved.
3. The bot starts. A progress line shows it connecting to Discord, then the
   badge next to the title turns **Online**.
4. A line under the title says whether the bot is actually in your server. If
   it is not, use the **Add your bot** link next to it. That invite asks for
   Administrator, and it targets this server directly.

Two refusals worth recognising:

- *Cannot change token while bot is running. Stop the bot first.* Turn the
  switch off, change the token, turn it back on.
- *This bot is already running. Stop it first.* The same application is
  already connected somewhere.

If the badge stays offline and a warning mentions the gateway being
unavailable, the bot cannot connect right now. That is on FlaviBot's side, not
on yours.

#### Making it yours

On the same page:

| Setting | Notes |
| --- | --- |
| Bot username | 2 to 32 characters |
| Avatar | image file, up to 8 MB |
| Banner | image file, up to 8 MB, 600x200 recommended |
| Status | Online, Idle or Do Not Disturb |
| Activity | Playing, Streaming, Listening, Watching or Competing |
| Activity text | up to 100 characters |
| Embed colour | the colour of the bot's embeds |

Name, avatar and banner are pushed to Discord through the running bot, so the
bot has to be **online** for those three to save.

The activity text accepts variables, replaced live:

| Variable | Value |
| --- | --- |
| `{guild.name}` | the server name |
| `{guild.id}` | the server id |
| `{guild.member_count}` | the member count |
| `{bot.name}` | the bot's name |
| `{music.title}` | the playing track's title |
| `{music.author}` | the playing track's artist |
| `{music.duration}` | the playing track's length |

Discord only renders the *Streaming* activity as streaming when it carries a
valid Twitch or YouTube link, so that activity type asks for a stream URL and
the dashboard rejects anything else.

#### Everything else

Prefix, language, welcome messages, levels, tickets: all of it is configured
exactly as for an official bot, from the normal dashboard pages, with the
**bot selector switched to your custom bot**. Settings are stored per bot, so
your custom bot starts from defaults rather than inheriting what you set on
FlaviBot.

The **Server Bot Profile** page (a Gold feature that changes an official
FlaviBot's nickname and avatar for one server) does not apply here, and says
so: a custom bot already has its own identity, set on the Custom Bot page.

#### Stopping, restarting, deleting

- The switch turns the bot off. Doing that while it is online asks for
  confirmation first, then the bot goes offline and stops answering.
- **Restart** disconnects and reconnects it, which is the usual answer to a
  bot that has gone strange.
- **Delete** removes the custom bot configuration and stops the bot. The
  Discord application stays yours, on the developer portal.

Starting, stopping and deleting are open to the bot's owner **and** to anyone
who can open that server's dashboard, not only to the person whose
subscription pays for it. Worth knowing before handing dashboard access out.

A custom bot belongs to the server it was created for. To run it in a
different server, delete it and set it up again from that server's dashboard.

### What an autoresponder is

Source: https://flavibot.xyz/docs/modules/autoresponder/01-overview
Summary: The mental model behind every FlaviBot autoresponder: a trigger, filters, actions and variables, the path an event takes, and what a rule is not.

An **autoresponder** is one FlaviBot rule that says: *when this happens, do
that.*

Everything you build with it (a reaction-role menu, an anti-raid gate, a
ticket panel, a strike system) is the same four pieces in a different order:

| Piece | Question it answers | Example |
| --- | --- | --- |
| **Trigger** | *When?* | someone posts a message |
| **Filters** | *Should we really react?* | only in #general, not from staff |
| **Actions** | *Do what?* | delete it, warn the author |
| **Variables** | *With which values?* | the author's name, the message text |

A rule runs top to bottom, one step at a time. If a step fails, the rest is
skipped by default, so a workflow never half-runs without telling you.

#### The path an event takes

1. Discord tells the bot something happened.
2. The bot looks up every enabled rule of your server listening for that event.
3. Each rule checks its **trigger config** (does the message actually contain
   the word you asked for?).
4. Then its **filters** (right channel? right roles? not a bot?).
5. Then its **gates**: the cooldown, and a per-rule rate cap.
6. Only then are the **actions** run, in order, with your **variables**
   filled in.

Every fire is recorded. The rule's page shows its recent runs, which step
failed and why; you never have to guess whether something ran.

#### What it is not

It is not a scripting language. There are no loops and no arbitrary code: a
rule is a list of steps with optional conditions. That is a deliberate
limit, it keeps rules readable by whoever inherits your server, and it makes
every rule safe to run on someone else's server, which is what makes the
[preset marketplace](https://flavibot.xyz/docs/modules/autoresponder/10-presets) possible.

When you need something genuinely reusable, you do not write a bigger rule:
you split it and let one rule [emit an event](https://flavibot.xyz/docs/modules/autoresponder/09-custom-events)
the others listen to.

### Triggers

Source: https://flavibot.xyz/docs/modules/autoresponder/02-triggers
Summary: Every event a FlaviBot autoresponder rule can listen to, from messages, reactions and members to voice, channels and roles, and what each one gives you.

A rule has exactly **one** trigger. It decides both *when* the rule runs and
*which variables* you get to work with.

#### Message triggers

| Trigger | Fires when |
| --- | --- |
| `message_create` | someone posts a message |
| `message_edit` | a message is edited |
| `message_delete` | a message is deleted |

`message_create` and `message_edit` accept a **match type**: `any_message`,
`contains`, `exact`, `starts_with`, `ends_with`, or `regex`. With `contains`
you can also demand a **whole word**, so `bad` stops matching inside
`badminton`. `message_delete` has no match type: the message is already gone,
so every delete in scope fires the rule and you filter with the channel and
role settings instead.

Regex patterns run on a linear-time engine (RE2). That is why some exotic
patterns are refused at save time: a pattern that could hang the bot on a
crafted message is rejected rather than shipped.

Discord's edit and delete events do not carry the message as it was *before*.
FlaviBot keeps its own short-lived copy for servers that have a rule needing
it, which is what makes `{message.old_content}` on an edit, and the whole
message on a delete, work at all. Two consequences: the copy starts being
kept when you save the rule, so a message posted before that has nothing to
recover, and it is dropped after 48 hours.

On a delete, the deleted message's author is `{message.author.mention}`, not
`{user.mention}`. Discord never says *who* pressed delete, so the rule has no
actor and `{user.*}` would be a lie.

#### Reaction triggers

| Trigger | Fires when |
| --- | --- |
| `reaction_add` | someone reacts to a message |
| `reaction_remove` | someone removes their reaction |

Both can be limited to specific emojis, and `reaction_add` exposes
`{reaction.count}`, the total on that message, which is what a
"pin it at 5 stars" or "mute at 10 flags" rule counts on.

#### Member triggers

`member_join`, `member_leave`, `boost_start`, `boost_end`,
`member_role_add`, `member_role_remove`, `member_nickname_change`,
`member_timeout`.

`member_join` can additionally require a **minimum account age**, which is
what turns it into an anti-scam gate: fire only for accounts younger than N
days, then kick or ban.

#### Interaction triggers

| Trigger | Fires when |
| --- | --- |
| `button_click` | a member clicks a button you placed |
| `select_menu` | a member picks an option in a dropdown |

These are how [panels](https://flavibot.xyz/docs/modules/autoresponder/07-groups-and-panels) work. A
button trigger can also **open a form** (a modal) on click: the rule then runs
when the form is submitted, and every answer becomes a variable.

A select menu exposes what was picked as `{select.value}`, and can be
restricted to specific option values.

#### Voice, channel and role triggers

`voice_join`, `voice_leave`, `voice_move`, `channel_create`,
`channel_update`, `channel_delete`, `thread_create`, `role_create`,
`role_update`, `role_delete`, `invite_create`, `invite_delete`.

#### Feature triggers

These come from FlaviBot itself rather than from Discord:

- `level_up`, a member gains a level (optionally only past level N)
- `music_track_start`, a track starts playing
- `ticket_open`, `ticket_close`
- `economy_balance_threshold`, `economy_item_purchase`
- `automod_action`, Discord AutoMod acted on someone

#### Triggers with no event at all

- `scheduled`, runs on a repeating interval, with no Discord event behind it
- `webhook`, runs when an outside service calls a secret URL
- `custom_event`, runs when **another rule of your server** emits a named
  event. See [custom events](https://flavibot.xyz/docs/modules/autoresponder/09-custom-events).

### Filters and gates

Source: https://flavibot.xyz/docs/modules/autoresponder/03-filters
Summary: The six checks that can stop a FlaviBot autoresponder between its trigger and its actions, plus its anti-loop protection and auto-disable.

The trigger matched. That does not mean the rule runs. Six things can still
stop it, in this order.

#### 1. Channel scope

Run **everywhere**, only **in** a list of channels or categories, or
**everywhere except** them. Categories cover every channel inside.

#### 2. Role filters

- **Required roles**: the member must have at least one of them
- **Ignored roles**: having any one of them stops the rule

Ignored roles win over required ones. This is how you exempt staff from a
word filter without writing a second rule.

#### 3. Member filters

The same idea for specific members. Note that a rule with specific member ids
cannot be shared as a preset: those ids mean nothing on someone else's
server, so sharing is refused rather than silently broken.

#### 4. Bots

By default a rule ignores messages and reactions from bots. Turning that off
lets a rule react to another bot's output, which is useful for bridges and
logs.

#### 5. Cooldown

A cooldown means *at most once per N seconds*, and you choose the scope:

| Scope | One fire per… |
| --- | --- |
| `user` | member |
| `channel` | channel |
| `guild` | whole server |

A cooldown is not a rate limit: it does not queue anything. Events that
arrive during the window are dropped, and the rule's page counts them as
cooldown skips so you can see it happening.

#### 6. Rate cap

Every rule is capped at 30 fires per minute, always. It exists so a
misconfigured rule cannot flood your server or Discord's API. Hitting it is
recorded as a skip too.

#### Anti-loop protection

A rule that creates a channel would normally trigger your `channel_create`
rules, including itself. FlaviBot marks the events its own actions cause and
skips them, so a rule cannot feed itself by accident.

The same applies to messages: a message sent by a workflow does not
re-trigger message rules.

#### Auto-disable

If a rule fails with a permission error 10 times **in a row**, it is disabled
automatically and the dashboard tells you why. Any single success resets the
counter, so an intermittent failure, one member the bot cannot touch, never
disables a rule that mostly works.

### Actions

Source: https://flavibot.xyz/docs/modules/autoresponder/04-actions
Summary: A tour of the action families a FlaviBot autoresponder can run, how steps hand values to each other, permission checks, and what happens when a step fails.

Actions are the *do that* half of a rule. A rule holds between 1 and 20 of
them, run in order.

This page is the tour. For the exact settings, permissions and returned
values of any single action, see
[every action in detail](https://flavibot.xyz/docs/modules/autoresponder/11-action-reference), which is
generated from the bot itself.

#### The families

**Messaging**, send a message, reply privately (only the person who clicked
sees it), send a DM, edit or delete a message, pin, add reactions, open a
thread, show the typing indicator.

**Roles**, add, remove, toggle. And **swap roles**, which replaces a whole
set in one atomic update: that is what makes a "pick one colour" menu
impossible to break by clicking twice quickly.

**Channels**, create (with full permission overwrites), edit, delete, lock,
unlock, purge messages.

**Moderation**, warn, timeout, kick, ban, soft-ban, unban, untimeout. These
create real FlaviBot infractions, visible in your case history exactly like a
manual moderation action, marked as coming from an autoresponder.

**Data**, set, read, increment, delete a stored value, and push/remove list
items. See [variables](https://flavibot.xyz/docs/modules/autoresponder/05-variables).

**Flow**, wait, wait-and-cancel, cancel, emit an event.

**Levels, economy, music**, give XP, set a level, add coins, give an item,
play a track, control playback, text-to-speech.

**Utility**, read a value into a variable, pick a random one, call an HTTP
endpoint.

#### Steps hand values to each other

Every action returns something. A later step reads it with
`{steps.<step number>.<field>}`:

```
1. Create channel        → {steps.1.channel_id}, {steps.1.channel_name}, …
2. Send message in {steps.1.channel_id}
```

Creating a role or a channel gives you **every** property back (id, name,
colour, position, permissions), not just the id.

#### Permissions are checked before the call

Before an action runs, FlaviBot verifies the bot actually has the permission
it needs, in the right channel. Missing permission means a clean, explicit
error on the rule's page instead of a silent failure.

That check **fails closed**: if the permission state cannot be read at all,
the action does not run.

#### When a step fails

Each step chooses what happens on failure:

- **abort** (default), stop the workflow here
- **continue**: carry on with the next step

A step skipped by a [condition](https://flavibot.xyz/docs/modules/autoresponder/06-conditions-and-flow) is
not a failure, and does not mark the run as failed.

### Variables

Source: https://flavibot.xyz/docs/modules/autoresponder/05-variables
Summary: Where the values in a FlaviBot autoresponder come from: always-available and trigger variables, earlier steps, transformers, and your own stored values.

Anywhere you can type text, you can type `{something}` and FlaviBot replaces
it when the rule runs.

```
Welcome {user.mention} to {server.name}!
```

Variable names follow a tree. Everything about the member sits under `user.`
or `member.`, everything about the message under `message.`, and so on, so
once you know one, you can guess the rest.

#### Always available

`{user}`, `{user.mention}`, `{user.id}`, `{channel.mention}`, `{channel.id}`,
`{server.id}`.

Triggers with no member behind them (a scheduled rule, a channel event) do
not offer the `user.` family, because there is nobody to name.

#### From the trigger

Each trigger adds its own. A few examples:

| Trigger | Adds |
| --- | --- |
| `message_create` | `{message.content}`, `{message.id}`, `{mentions.count}` |
| `message_edit` | `{message.content}` (after), `{message.old_content}` (before) |
| `message_delete` | `{message.content}`, `{message.author.mention}` |
| `reaction_add` | `{reaction.emoji}`, `{reaction.name}`, `{reaction.count}` |
| `member_join` | `{member.mention}`, `{member.account_age_days}`, `{member.join_type}` |
| `member_leave` | `{member.mention}`, `{member.time_in_server}`, `{member.roles}` |
| `boost_end` | `{member.mention}`, `{boost.duration}` |
| `ticket_close` | `{ticket.duration}`, `{ticket.closer.mention}`, `{ticket.claimed_by.mention}` |
| `level_up` | `{level.new}`, `{level.old}`, `{user.display_name}`, `{user.avatar_url}` |
| `music_track_start` | `{track.name}`, `{track.author}`, `{track.duration}` |
| `automod_action` | `{message.content}`, `{automod.action}`, `{message.url}` |
| `select_menu` | `{select.value}`, `{select.values}`, `{select.count}` |
| `custom_event` | `{event.name}` plus every key of the payload |

The editor lists the ones your trigger actually provides, so you never have
to memorise them. If a variable is offered, it resolves: a name in that list
that the bot could not fill would be a bug, not a blank.

A rule that belongs to a [group](https://flavibot.xyz/docs/modules/autoresponder/07-groups-and-panels) also
gets `{panel.message_id}` / `{panel.channel_id}` (the panel its own button sits
on) and `{group.panel_message_id}` / `{group.panel_channel_id}` (the group's
primary panel), which is how a rule edits the message its group published.

#### From earlier steps

`{steps.2.value}` is the value the second step returned. This is how a
counter reaches the message that announces it.

#### Reshaping a value

A variable gives you what it has, which is not always what you want to read.
A delay arrives as `300`. A member typed their name in capitals. A message is
four paragraphs long and you only want the first line in a log.

Add a **transformer** after the variable name, separated by a colon:

```
Hello {user.name:trim:uppercase}
Banned in {steps.3.deferred_seconds:duration}
They said: {message.content:truncate(80):escape}
```

Transformers apply left to right, so the chain reads in the order it happens.
Start typing a colon inside a `{token}` in any text field and the editor lists
them, with an example of what each one does.

##### Text

| Write | What it does | Example |
|---|---|---|
| `:uppercase` | Puts the whole value in capitals. | ada -> ADA |
| `:lowercase` | Puts the whole value in lower case. | Ada -> ada |
| `:title` | Capitalises the first letter of every word. | ada lovelace -> Ada Lovelace |
| `:trim` | Removes the spaces around the value. | '  ada  ' -> 'ada' |
| `:truncate(40)` | Cuts the value to a maximum length, on a word when it can. | truncate(10) on a long sentence -> 'the quick…' |
| `:default(nobody)` | Writes your text instead when the value came back empty. | default(nobody) on an empty nickname -> nobody |

##### Numbers

| Write | What it does | Example |
|---|---|---|
| `:number` | Adds thousands separators so a big number stays readable. | 1000000 -> 1,000,000 |
| `:round` | Rounds a decimal to the nearest whole number. | 12.7 -> 13 |

##### Dates and durations

| Write | What it does | Example |
|---|---|---|
| `:duration` | Turns a number of seconds into words. | 300 -> 5 minutes |
| `:from_now` | Turns a number of seconds into a live Discord countdown that keeps ticking after the message is posted. | 300 -> in 5 minutes |
| `:relative` | Shows a date as how long ago or how far ahead it is, in the reader's own language. | a timestamp -> 3 days ago |
| `:datetime` | Shows a date with its time, in each reader's own timezone. | a timestamp -> 1 January 2026 00:00 |
| `:date` | Shows a date without its time, in each reader's own timezone. | a timestamp -> 1 January 2026 |
| `:time` | Shows the time of day, in each reader's own timezone. | a timestamp -> 00:00 |
| `:plus(300)` | Moves a moment forward. Combine with a countdown to write a deadline: the trigger's own time, plus the delay, read as a countdown. | {trigger.timestamp:plus(300):relative} -> in 5 minutes |
| `:minus(3600)` | Moves a moment backward. | {trigger.timestamp:minus(3600):time} -> one hour before |

##### Lists

A stored list arrives as its raw form — `["ada", "bob"]` — which is almost never what you want in a message.

| Write | What it does | Example |
|---|---|---|
| `:join` | Writes the entries out separated by whatever you choose. Leave it empty for a comma and a space. | `["ada", "bob"]` -> ada, bob |
| `:nth(0)` | Takes a single entry. Counts from 0, and a negative number counts back from the end, so `nth(-1)` is the last one. | `["ada", "bob"]` -> ada |
| `:count` | How many entries there are. | `["ada", "bob"]` -> 2 |

These also work on a value that is not a stored list: a line-separated or comma-separated text is read as one entry per line or per comma, and a plain value counts as a single entry.

An empty list, a missing variable or a position that does not exist all give you nothing back rather than an error, so pair them with `:default` when you need a fallback:

```
The next in line is {vars.queue:nth(0):default(nobody yet)}.
```

Posting a whole list into a message can get long — Discord cuts a message off at 2000 characters, and an entry someone typed can contain a mention. `{vars.queue:join:truncate(1900):escape}` keeps both in check.

##### Picking one at random

Read the list with a **Read data** step, then use the alias it publishes:

| Write | What you get |
|---|---|
| `{vars.roster.random}` | One entry, picked at random |
| `{vars.roster.random_index}` | Which position it came from, counting from 0 |
| `{vars.roster.first}` | The first entry |
| `{vars.roster.last}` | The last entry |

The draw happens **once**, when the Read data step runs. Every later step and every later condition in that run sees the same entry, so you can check it in a condition and then name it in the message without the two disagreeing.

These four are also the only form that works in a field expecting an id — giving the drawn member a role, for instance. A field like that takes a plain `{vars.roster.random}` and refuses anything carrying a `:` transformer.

##### Safety

| Write | What it does | Example |
|---|---|---|
| `:escape` | Cancels Discord formatting and mass pings inside text a member wrote. Worth adding whenever you repeat someone's message. | **bold** @everyone -> \*\*bold\*\* (no ping) |

##### Deadlines that stay true

A card that says "you have 5 minutes" is wrong a minute later. Discord can
render a moment that every reader sees in their own language and timezone, and
that keeps counting down on its own:

```
You will be banned {trigger.timestamp:plus(300):relative}
```

`{trigger.timestamp}` is when the rule fired, `plus(300)` moves it five
minutes forward, and `relative` renders it as a live countdown. The same
chain with `:datetime` writes an absolute date instead.

##### A transformer you did not write is left alone

Exactly like an unknown variable: `{user.name:shout}` stays as typed, and the
editor underlines it, rather than posting the value with the formatting
silently dropped.

#### Your own stored values

The **data store** keeps values between runs: they survive restarts, and
they are per server.

- **Scope**: `guild` (one value for the server), `member` (one per member),
  `channel`, or `group` (one per workflow group, shared by its rules and
  invisible to the rest of the server)
- **Type**: text, number, boolean, list or JSON
- **Expiry**: optional; a strike that fades after 30 days is just a counter
  with an expiry

Read one into a variable and use it: `{vars.<name>.value}`, plus
`{vars.<name>.exists}` and, for lists, `{vars.<name>.length}`.

Counters are **atomic**: two members clicking at the exact same moment get 4
and 5, never 4 and 4. A missing counter starts at the default you set
(usually 0) instead of failing.

#### An unknown variable is left alone

If you type `{user.birthday}` and nothing provides it, the text stays exactly
as you typed it. It is never silently replaced by an empty string, so a
typo is visible instead of quietly producing a broken message.

### Conditions and timing

Source: https://flavibot.xyz/docs/modules/autoresponder/06-conditions-and-flow
Summary: Make a FlaviBot autoresponder step run only when its conditions hold, and make a rule wait before carrying on, including what survives the wait.

Any step of a FlaviBot autoresponder can carry conditions that decide whether
it runs, and the flow actions can pause a rule, for a few seconds or for up to
30 days. This page covers both.

#### Conditions on a step

Any step can carry up to 5 conditions. Choose whether **all** of them or
**any** of them must hold, and what happens when they do not: skip just this
step, or stop the workflow.

Available checks:

| Check | Use it for |
| --- | --- |
| has role / missing role | staff-only branches |
| in channel / not in channel | narrow a rule further |
| variable comparison | `=`, `≠`, contains, exists, and `>`, `≥`, `<`, `≤` |
| random chance | run a step only N% of the time |

The numeric comparisons are what make counters useful:

```
2. Add 1 to the strike counter        → {steps.2.value}
3. Warn the member
4. Time them out      only if {steps.2.value} ≥ 3
5. Reset the counter  only if {steps.2.value} ≥ 3
```

Both halves of a comparison accept variables, not just the left one. So you can
check a value against another value instead of against a constant:

```
2. Read the member's warning count    → {vars.strikes}
3. Read the server's warning limit    → {vars.limit}
4. Time them out      only if {vars.strikes} ≥ {vars.limit}
```

A token on the right that does not resolve is left as written rather than
treated as empty, so a typo shows up as a comparison that never matches instead
of one that quietly matches everything. "Contains" with an empty right side
matches nothing, for the same reason.

There is no if/else block. You get the same result with two steps carrying
opposite conditions (`≥ 3` and `< 3`), which stays readable in a flat list.

A condition that does not hold is a **normal outcome**, not an error: the run
is still a success.

#### Waiting

Three flow actions:

- **Wait**: pause up to 60 seconds inside the run
- **Wait, cancellable**: park everything below it for up to 30 days under a
  key you choose
- **Cancel**: cancel whatever is parked under that key

The cancellable wait is durable: the schedule lives in the database, not in
memory, so a restart in the middle changes nothing.

The key is what makes it safe. Use `honeypot:{user.id}` and each member's
timer is separate: cancelling only ever affects your own.

That pattern is the whole "second chance" idea:

```
1. Delete the message
2. Post a card with an "it was a mistake" button
3. Wait 5 minutes, cancellable, key honeypot:{user.id}
4. Ban            ← only reached if nobody cancelled
```

A second rule on that button cancels the same key. Click in time and step 4
never happens.

##### What survives the wait

A cancellable wait ends the run and starts a fresh one when the timer fires, so
what the second half can see is decided at the moment you park it. Carried
over: your trigger variables (`{user.mention}`, `{server.name}`, …), anything
you stored in a `{vars.…}` variable, the user, channel and message the rule
fired on, and the `{steps.N.field}` results of the earlier steps that your
parked steps actually read. So this works:

```
1. Send message        → {steps.1.message_id}
2. Wait 5 minutes, cancellable
3. Edit message {steps.1.message_id}   ← still resolves
```

Only what the steps below the wait mention is kept, so nothing you do not use
is stored. If those results add up to more than 16 KB the step refuses to park
with `deferred_state_too_large` instead of quietly dropping half of them; the
realistic way to hit that is a large HTTP response, and the fix is to pull out
the one field you need first:

```
1. Call HTTP endpoint into {vars.api}
2. Set variable  id = {vars.api.data.id}
3. Wait 1 hour, cancellable
4. Send message using {vars.id}
```

Note:

A step that reads `{steps.N.field}` from before the wait only resolves because
the value was carried. If you delete the step it points at, the token has
nothing to resolve to and the text is sent exactly as you typed it.

Two things are deliberately **not** carried. The interaction token, because
Discord expires it after 15 minutes, so a reply to the click that started the
rule cannot work an hour later. And the member's role list, because a snapshot
taken an hour ago could make a moderation step act on roles they no longer
have.

Steps *after* the wait hand values to each other normally: they all run in the
same second pass.

### Groups and panels

Source: https://flavibot.xyz/docs/modules/autoresponder/07-groups-and-panels
Summary: Group several FlaviBot autoresponder rules with the panels that host their buttons, edit them in one place, and share data inside the group.

A rule has one trigger. A panel has several buttons. In FlaviBot, a **group**
is what bridges the two: a set of rules plus the messages that host their buttons,
edited in a single place. A group can hold one panel, several, or none at
all — and a rule can sit in a group without any button pointing at it.

```
Group "Support"
├── panel "Open a ticket" (#support)
│   ├── button "Billing"   → rule: Ticket: billing
│   ├── button "Bug"       → rule: Ticket: bug
│   └── button "Other"     → rule: Ticket: other
├── panel "FAQ" (#faq)
│   └── dropdown           → rule: FAQ answers
├── rule: Auto-close idle tickets      (no button — runs on a schedule)
└── rule: Ping staff on a new ticket   (no button — runs on a message)
```

#### How a click finds its rule

Every component carries an id that points straight at a rule. The bot reads
that id and runs that rule; it never consults the group or the panel. Two
consequences worth knowing:

- Deleting a group deletes its rules and its panels with it — a group is the
  whole thing, and removing it must leave nothing running behind your back.
- A rule can live outside any group and still be wired to a button you place
  yourself.

#### Building one

Create a group, add rules to it, then add a **panel**: give it a name, pick
the channel it goes in, and compose its message in the builder. When you edit
a button you pick the rule it runs from the group's rules; the raw id is shown
next to each rule too, for a button you build elsewhere.

Every panel is published on its own. Publishing posts the message in the
panel's channel. Publish again after an edit and the same message is updated
in place; if it was deleted, a fresh one is posted. Until you republish, the
group page marks the panel **to republish** so a saved change is never
silently different from what members see.

The group page lists rules with the panel they belong to. Filter on **No
panel** to see the rules nothing points at — the ones that run on their own
triggers, or that you have not wired to a button yet.

#### Panels that talk back

A button rule can edit **the message it was clicked on**. That is how a
counter refreshes itself: read the stored value, rewrite the card with the
new number.

A rule can also send a message whose buttons point at the group's other
rules, the "second chance" honeypot works exactly that way.

#### Forms

A button can open a **form** before the rule runs. Up to 5 fields, short or
paragraph, required or not. Each answer becomes `{modal.<field>}`.

A form has to belong to the trigger rather than to an action, and that is
not an arbitrary restriction: Discord demands an answer to a click within
three seconds, long before a workflow could decide to open one.

#### The panel as a live scoreboard

A rule inside a group can address a panel without knowing its message id:

- `{panel.message_id}`, `{panel.channel_id}`, `{panel.name}` — the panel the
  rule's own button sits on. Blank for a rule no panel points at.
- `{group.panel_message_id}`, `{group.panel_channel_id}` — the group's
  **primary** panel (marked with a star on the group page; the first one you
  create, unless you pick another). Handy for a rule that runs without a
  button but still wants to refresh the group's main message.

That is what turns a panel into something that updates itself: the rule that
increments a counter can, in the same run, rewrite the panel and put the new
number in a button label.

The curated **Honeypot** preset does exactly that. Each ban increments a stored
counter and rewrites the panel, so the button reads "Caught so far: 12" without
anyone clicking anything.

These variables only appear for a rule that belongs to a group, and they are
only looked up for a rule that actually mentions one, so an ordinary role
button in a group costs nothing extra.

#### Data shared inside a group

A group is not only its messages: it is also a namespace. A data variable
stored with the **This group only** scope is readable by every rule of the
group and by nothing else on the server.

That matters for two reasons. A panel's own bookkeeping stops leaking into the
server-wide names, so two rules called `pick` in two different panels are two
different values. And the same preset can be installed twice: the copies do not
overwrite each other, because a variable name is fixed by the preset author and
could never be made unique per install.

The scope only appears on a rule that belongs to a group, since outside one
there is no group to store against. Deleting a group deletes its variables with
it.

### Batching

Source: https://flavibot.xyz/docs/modules/autoresponder/08-batching
Summary: Make a FlaviBot autoresponder react to a burst of events instead of each one: the three batching settings, what the rule receives, and an anti-raid gate.

Some things only make sense in bulk. Thirty messages in a minute is a raid;
one message is a Tuesday.

Turn batching on and FlaviBot stops running the rule once per event. Instead it
collects the events and runs the rule **once**, with the whole burst summarised.

#### The three settings

| Setting | Meaning |
| --- | --- |
| **Quiet window** | flush once no new event has arrived for this long |
| **Max wait** | never wait longer than this after the first event |
| **Only fire at** | a threshold, below it, nothing happens at all |

Set the quiet window and the max wait to the **same value** and you get a
fixed window: exactly "N events within X seconds".

**Only fire at** is a real threshold, not just an early flush. A window that
closes below it fires nothing, which is what lets a rate gate exist without
a defensive condition on every step.

You also choose the **scope**: one batch for the whole server, one per
channel, or one per member.

#### What the rule receives

The workflow runs on the **last** event of the batch, plus:

`{batch.count}`, `{batch.users}`, `{batch.user_ids}`, `{batch.first_at}`,
`{batch.last_at}`, `{batch.overflow}`.

#### The anti-raid gate, end to end

```
Trigger   any message
Batching  window 60s · max wait 60s · only fire at 30 · per channel
1. Lock the channel
2. Post "locked: {batch.count} messages in 60 seconds"
```

Below thirty messages a minute, nothing ever happens.

#### Two limits worth knowing

Batching is not offered on button and select triggers: Discord's reply
window would be long dead by the time the batch flushed, so early clickers
would get nothing.

The schedule is durable, a restart mid-window loses nothing, but the
collected events live in cache. If that cache is unavailable the rule falls
back to firing per event. It degrades to noisy, never to silent.

### Custom events

Source: https://flavibot.xyz/docs/modules/autoresponder/09-custom-events
Summary: Let one FlaviBot autoresponder rule emit a custom event that other rules listen for, instead of copying the same steps into each rule.

Three rules all need to welcome a member. You could paste the same four steps
into each one, and then fix the wording in three places forever.

Instead, in FlaviBot one rule **emits** an event and the others **listen** for
it.

```
Verify button  →  add role  →  emit "member_verified"
                                      ↓
                         rule: post the welcome
                         rule: log it
                         rule: give the starter coins
```

Adding a fourth reaction later means adding a rule. The button never changes.

#### Emitting

The **Emit event** action takes a name (lowercase letters, digits,
underscores) and an optional payload: up to 10 key/value pairs, and the
values can themselves contain variables.

#### Listening

A rule with the **Custom event** trigger names the event it waits for. It
receives `{event.name}` plus every payload key as `{event.<key>}`.

Listeners are ordinary rules: they have their own filters, cooldowns and
history, exactly like a rule triggered by Discord.

#### Scope and safety

An event never leaves the server that emitted it. Rules on another server
cannot hear it, and there is no way to address one.

Two guards keep chains sane:

- **Depth**: a chain stops after 3 hops, loudly. Two rules emitting each
  other's events terminate instead of ringing forever. The depth survives a
  cancellable wait, so inserting a timer cannot reset the counter.
- **Rate**: a server may emit 20 events a minute. Beyond that the emit
  fails with a clear error rather than degrading everything else.

### Presets

Source: https://flavibot.xyz/docs/modules/autoresponder/10-presets
Summary: Install a ready-made FlaviBot automation from the preset marketplace, share one of your own rules or groups, and review a preset's versions and updates.

A **preset** is a rule, or a whole group with its panel, packaged so it can
be installed on any server.

#### How do I install a preset?

Pick one in the marketplace, choose the target server, and fill in the blanks:
roles, channels, emojis, durations. The preview shows what it will do, with
your picks appearing in place as you fill them.

Everything is validated before anything is created. If a preset needs three
rules and you have room for two, nothing is installed at all: you are never
left with half a setup.

A preset that installs a group takes you straight to the group editor, where
you publish the panel and adjust anything you like. Installed rules are
yours: editing them never affects the original.

#### How do I share one of my rules or groups?

Any rule you own can be shared from its editor, and any group from the group
page. FlaviBot rewrites your server's specifics into blanks: roles, channels,
categories, emojis. What it cannot rewrite, it refuses:

- **Specific members**: an id means nothing on another server, so a rule
  targeting one is refused rather than shipped broken
- **A panel button wired outside its group**: its target would not travel
- **Scheduled and webhook rules**: a schedule and a secret URL are not
  shareable objects

Your `@everyone` role becomes a variable that resolves on the installing
server, so a "make this channel private" preset stays private.

Submissions are reviewed before they appear publicly. Presets that can kick,
ban or time out members must be curated before they can be published. That
rule is enforced by the database itself, not just by the review page.

#### What travels with a preset

The trigger and its settings, the filters, every action and its conditions,
the batching window, and for a group: every rule plus the panel, with the
buttons re-wired to the freshly created rules on arrival.

#### Versions and updates

A preset can be improved after it has been shared. Every published revision
gets a number, and the exact content of each one is kept, so nothing an
author changes later rewrites what you compared against.

On the preset page, **Version history** lists the revisions with the author's
note, and lets you compare any two of them.

If a preset you installed has been updated, the autoresponder page shows it
above your rules. **Review changes** opens the same comparison, phrased in
what it means for your server, before you decide:

- a step that was added, removed, reconfigured or moved
- a setting you will now be asked for, or one that is gone
- a trigger, a filter, a cooldown or a batching window that changed
- a badge when the new version can kick, ban or time out and the one you
  installed could not

Taking the update **replaces** that rule's configuration with the new
version. Your answers are reused, and so are the rule's name and whether you
had paused it, but any edit you made to the rule itself is overwritten. That
is why the comparison comes first: it is the whole decision.

When a group's preset gained rules, they are added to your group. Rules it
dropped are left alone, because a rule you may have grown attached to is not
FlaviBot's to delete.

### Every action, in detail

Source: https://flavibot.xyz/docs/modules/autoresponder/11-action-reference
Summary: Every FlaviBot autoresponder action in detail: what each one does, the bot permission it needs, its settings, and the values it hands to later steps.

One entry per FlaviBot autoresponder action, generated from the bot itself, so
what you read here is what the bot runs. [Actions](https://flavibot.xyz/docs/modules/autoresponder/04-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.<id>.<field>} pointing back across the wait still resolves. Only what the tail references is carried, and a snapshot over 16 KB is refused with deferred_state_too_large rather than truncated, so park a big HTTP response in a variable instead. The interaction token is deliberately not carried (Discord expires it after 15 minutes) and neither is the actor's role list, because a stale role snapshot could flip a moderation decision the wrong way. It fails loudly rather than silently doing nothing: no following step gives nothing_to_defer, a host with no scheduler bridge gives scheduler_unavailable, and a scheduler that refuses the task or cannot be reached gives defer_failed, in which case the following steps abort by default instead of running immediately.

**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.<key>}; the emitting user, channel and message ids plus the actor's name, display name and avatar url are forwarded automatically, so a listener can still name the member. Listener rules run through the normal autoresponder pipeline, so their own filters, cooldowns and rate caps still apply and nothing is bypassed. Two guards stop runaway loops: emits chain at most 3 levels deep and refuse past that with emit_depth_exceeded, and the worker caps a server at 20 emits per minute, surfacing as emit_rate_limited.

**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. A Components V2 payload has its content and embeds stripped because Discord refuses them alongside that flag. 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_<status> error code, and the step's 15 second budget is shorter than the 60 second executor default, so it gives up earlier than most actions.

**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_<status> error code, and the step's 15 second budget is shorter than the 60 second executor default, so it gives up earlier than most actions.

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

#### Projects, events and teams

##### Cancel an event

`events.cancel_event`

Calls an event off, exactly as the dashboard's cancel does: the panel is repainted as cancelled so nobody else signs up, the reminders are taken down so members are not told about something that is not happening, Discord's own copy is removed from the server header, the entry is deleted from the Google Calendar of everyone who had it, and the participant role is taken back off everyone who was wearing it. The record stays - a cancelled event keeps its history and its answers, which is why this is a cancel and not a delete; there is deliberately no action that destroys an event permanently, because a rule that can erase a server's calendar history on a regex match is not a feature anyone asked for. The event is named by its id, which an earlier step hands you, or by the message the rule fired on when that message is the RSVP panel - so reacting on a panel cancels its own event with no configuration. Cancelling an event that is already cancelled succeeds and reports it rather than failing, so a rule that fires twice is harmless. On one occurrence of a repeating event it cancels that occurrence and excludes it from the repeat so the next roll does not bring it back; with the scope set to series it calls off the whole thing. Note that this notifies everybody who answered, so a rule that can be triggered by an ordinary member is a rule that lets them call off the server's events - scope the trigger accordingly.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `scope`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `all_day` | true \| false | The text true or false: whether the event fills whole days rather than a clock time. |
| `already_cancelled` | true \| false | The text true or false: true when the event was already cancelled before this step. Branch on it so a rule that fires twice does not announce the same cancellation twice. |
| `cancelled` | true | Always the text true: the step only succeeds when the event is cancelled. |
| `capacity` | number, empty when unlimited | How many members can be going, empty when the event has no limit. |
| `channel_id` | id, empty when no panel | Channel the RSVP panel sits in. Empty for an off entry, the one kind that posts no panel. |
| `discord_event_id` | id, empty when not mirrored | Discord's own scheduled event for this, empty when it was not mirrored into the server header. |
| `discord_event_url` | link, empty when not mirrored | Link to Discord's own copy in the server header, ready to put in a message. |
| `ends_at` | date and time, empty when open-ended | When the event ends, as an ISO date and time. Empty when it has no end. |
| `event_id` | id | The event's raw id, which is what a later Update an event or Cancel an event step wants as its target. |
| `going_count` | number | How many members are going after the step. Always zero right after an event is created, since nobody can have answered yet. |
| `kind` | event \| meeting \| session \| stream \| milestone \| deadline \| off | What sort of entry it is. An off entry is a holiday or downtime: nobody signs up and no panel is posted. |
| `location` | text, empty when none | Where it happens, as free text, empty when none was given. |
| `maybe_count` | number | How many members answered maybe after the step. |
| `message_id` | id, empty when no panel | The RSVP panel message itself, for a follow-up that pins it, quotes it or reacts to it. |
| `panel_url` | link, empty when no panel | Jump link to the RSVP panel, which is the one place members can answer. |
| `series` | true \| false | The text true or false: whether this is a repeating event's template rather than a single event. |
| `series_affected` | number, empty when not a series | How many occurrences a series-wide change reached, empty when the step touched one event. |
| `starts_at` | date and time | When the event starts, as an ISO date and time. |
| `status` | scheduled \| live \| ended \| cancelled | Where the event is in its life after the step. |
| `team_id` | id, empty when none | The team invited to the event, empty when none. |
| `timezone` | for example Europe/Paris | The zone the event's times are read in, frozen onto it when it was created. |
| `title` | text | The event's title after the step. |
| `visibility` | public \| private | Whether the event is on the server's public calendar page and public feed. |
| `voice_channel_id` | id, empty when none | The voice channel it happens in, empty when it is not a voice event. |
| `waitlist_count` | number | How many members are queued for a seat after the step. |

##### Create an event

`events.create_event`

Puts a new event on the server's calendar, exactly as if somebody had created it from the dashboard or with /event create: it posts the RSVP panel members answer on, arms the reminders, mirrors the event into Discord's own Events list in the server header, invites the team when one is named, adds it to the Google Calendar of every member who subscribed, and refreshes the channel boards. Every property is available up front - description, end time or length, timezone, voice channel, location, capacity, image, required role, hosts, kind, all day, colour, team, visibility, the sign-up window, the participant role, the reminder offsets, the start announcement and named seat pools. The start can be a fixed date and time, or a delay counted from the moment the rule fires, which is usually what an automation means: a rule with a fixed date works once and is refused for ever after, because an event has to start in the future. The panel goes in the channel you name, otherwise in the server's configured events channel, otherwise in the channel the rule fired in. The event is created by whoever triggered the rule, so the panel reads hosted by them unless you name hosts; on a scheduled or webhook run, which carries no member, the bot is recorded instead. When another bot on this server is the one that handles events, the event is created by that bot rather than refused, so its panel buttons work; the server picks that bot in the dashboard and a rule never overrides it. It fails with a named refusal when the module is switched off, when the bot cannot post in the panel channel, and when the server has reached the active-event limit of its plan. Recurring events are deliberately not created here - a rule would make a whole repeating schedule on every fire, which nothing in this family can undo; build the series once in the dashboard and let rules edit and cancel it.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `title`, `start`, `ends_at`, `duration_minutes`, `channel_id`, `description`, `timezone`, `voice_channel_id`, `location`, `capacity`, `image_url`, `required_role_id`, `host_ids`, `kind`, `all_day`, `color`, `discord_sync`, `team_id`, `visibility`, `signups_open_at`, `signups_close_at`, `participant_role_id`, `reminder_offsets`, `announce_at_start`, `announce_role_id`, `rsvp_slots`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `all_day` | true \| false | The text true or false: whether the event fills whole days rather than a clock time. |
| `capacity` | number, empty when unlimited | How many members can be going, empty when the event has no limit. |
| `channel_id` | id, empty when no panel | Channel the RSVP panel sits in. Empty for an off entry, the one kind that posts no panel. |
| `discord_event_id` | id, empty when not mirrored | Discord's own scheduled event for this, empty when it was not mirrored into the server header. |
| `discord_event_url` | link, empty when not mirrored | Link to Discord's own copy in the server header, ready to put in a message. |
| `ends_at` | date and time, empty when open-ended | When the event ends, as an ISO date and time. Empty when it has no end. |
| `event_id` | id | The event's raw id, which is what a later Update an event or Cancel an event step wants as its target. |
| `going_count` | number | How many members are going after the step. Always zero right after an event is created, since nobody can have answered yet. |
| `kind` | event \| meeting \| session \| stream \| milestone \| deadline \| off | What sort of entry it is. An off entry is a holiday or downtime: nobody signs up and no panel is posted. |
| `location` | text, empty when none | Where it happens, as free text, empty when none was given. |
| `maybe_count` | number | How many members answered maybe after the step. |
| `message_id` | id, empty when no panel | The RSVP panel message itself, for a follow-up that pins it, quotes it or reacts to it. |
| `panel_url` | link, empty when no panel | Jump link to the RSVP panel, which is the one place members can answer. |
| `series` | true \| false | The text true or false: whether this is a repeating event's template rather than a single event. |
| `series_affected` | number, empty when not a series | How many occurrences a series-wide change reached, empty when the step touched one event. |
| `starts_at` | date and time | When the event starts, as an ISO date and time. |
| `status` | scheduled \| live \| ended \| cancelled | Where the event is in its life after the step. |
| `team_id` | id, empty when none | The team invited to the event, empty when none. |
| `timezone` | for example Europe/Paris | The zone the event's times are read in, frozen onto it when it was created. |
| `title` | text | The event's title after the step. |
| `visibility` | public \| private | Whether the event is on the server's public calendar page and public feed. |
| `voice_channel_id` | id, empty when none | The voice channel it happens in, empty when it is not a voice event. |
| `waitlist_count` | number | How many members are queued for a seat after the step. |

##### Answer for a member

`events.rsvp`

Sets a member's answer on an event, exactly as the dashboard's manage attendees panel does: going, maybe, can't make it, take their answer off entirely, or seat somebody off the waitlist. Everything an answer means happens - the RSVP panel is repainted, whoever a freed seat lets in is told, the participant role is given or taken back, their personal reminders are armed or cancelled, the server's calendar boards are refreshed and their Google Calendar entry is written or withdrawn. Capacity is respected the same way it is for a member pressing the button themselves: marking somebody as going to a full event, or to a full named seat pool, puts them on the waitlist instead and the step reports waitlist rather than going, so read that before announcing that they are in. Promote is the one answer that seats a member ahead of capacity, which is the organiser's deliberate call; it takes a named member or whoever has been waiting longest, and reports who actually got the seat. Taking an answer off is not the same as marking them as unable to come: a declined member is still on the roster under can't make it, while removing leaves no answer at all. Removing somebody who never answered is not an error. The event is named by its id, which an earlier step hands you, or by the message the rule fired on when that message is the RSVP panel - so a reaction on a panel signs the member up with no configuration at all. It refuses an event that is over or cancelled, a seat pool the event does not have, and a member who is not on this server; the sign-up window is deliberately not checked, because an organiser adding somebody by hand is their own decision, exactly as it is on the dashboard.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `member`, `answer`, `slot`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `all_day` | true \| false | The text true or false: whether the event fills whole days rather than a clock time. |
| `capacity` | number, empty when unlimited | How many members can be going, empty when the event has no limit. |
| `channel_id` | id, empty when no panel | Channel the RSVP panel sits in. Empty for an off entry, the one kind that posts no panel. |
| `discord_event_id` | id, empty when not mirrored | Discord's own scheduled event for this, empty when it was not mirrored into the server header. |
| `discord_event_url` | link, empty when not mirrored | Link to Discord's own copy in the server header, ready to put in a message. |
| `ends_at` | date and time, empty when open-ended | When the event ends, as an ISO date and time. Empty when it has no end. |
| `event_id` | id | The event's raw id, which is what a later Update an event or Cancel an event step wants as its target. |
| `going_count` | number | How many members are going after the step. Always zero right after an event is created, since nobody can have answered yet. |
| `kind` | event \| meeting \| session \| stream \| milestone \| deadline \| off | What sort of entry it is. An off entry is a holiday or downtime: nobody signs up and no panel is posted. |
| `location` | text, empty when none | Where it happens, as free text, empty when none was given. |
| `maybe_count` | number | How many members answered maybe after the step. |
| `message_id` | id, empty when no panel | The RSVP panel message itself, for a follow-up that pins it, quotes it or reacts to it. |
| `panel_url` | link, empty when no panel | Jump link to the RSVP panel, which is the one place members can answer. |
| `promoted_user_id` | id, empty when nobody moved | Whoever a freed seat let in when this step freed one - the member to congratulate after a removal. Empty when the waitlist did not move. |
| `rsvp_status` | going \| waitlist \| maybe \| declined \| removed - branch on this | What the MEMBER ended up as, which is a different thing from the event's own status beside it. Marking somebody as going to a full event reports waitlist rather than going, so read this before announcing that they are in. Removed means their answer was taken off entirely. |
| `series` | true \| false | The text true or false: whether this is a repeating event's template rather than a single event. |
| `series_affected` | number, empty when not a series | How many occurrences a series-wide change reached, empty when the step touched one event. |
| `slot` | seat pool key, empty when none | The named seat pool the member's answer carries, such as tank, empty when the event has no pools or the answer is not going. |
| `starts_at` | date and time | When the event starts, as an ISO date and time. |
| `status` | scheduled \| live \| ended \| cancelled | Where the event is in its life after the step. |
| `team_id` | id, empty when none | The team invited to the event, empty when none. |
| `timezone` | for example Europe/Paris | The zone the event's times are read in, frozen onto it when it was created. |
| `title` | text | The event's title after the step. |
| `user_id` | id | The member the answer is about. On a promote of whoever is next in line the rule named nobody, so this is where the member who actually got the seat appears. |
| `user_mention` | mention | A clickable mention of that member, ready to put in a message. |
| `visibility` | public \| private | Whether the event is on the server's public calendar page and public feed. |
| `voice_channel_id` | id, empty when none | The voice channel it happens in, empty when it is not a voice event. |
| `waitlist_count` | number | How many members are queued for a seat after the step. |

##### Update an event

`events.update_event`

Changes any property of an event already on the calendar: title, description, start, end or length, timezone, voice channel, location, capacity, image, required role, hosts, kind, all day, colour, team, visibility, the sign-up window, the participant role, the reminder offsets, the start announcement and the named seat pools. Only the fields you fill in are touched, everything else keeps its current value, and clearing a value is its own choice distinct from leaving it alone - emptying the location, the capacity, the team, the participant role or the reminder list unsets it. Whatever changes runs the full trail, which is the point of editing here rather than writing the row: the RSVP panel is repainted, the reminders are re-armed when the time moved, Discord's own copy in the server header is patched, every subscriber's Google Calendar entry is updated, raising the capacity seats the waitlist and tells the members who got in, and changing the participant role re-badges everyone already going. The event is named by its id, which an earlier Create an event step hands you, or by the message the rule fired on when that message is the RSVP panel - so reacting on a panel names its own event with no configuration. Either way the lookup is scoped to this server, so an id from another server simply answers not found. An occurrence of a repeating event can be edited on its own, which detaches it from the series, or through its template with the scope set to series, which moves every occurrence. Two things are deliberately not editable here: the channel the panel sits in, because moving a panel means deleting one message and posting another rather than editing a column, and the repeat rule itself, which is built in the dashboard. Cancelling is its own action. Note that a rule edits with organiser authority rather than as the member who triggered it, so anyone who can trigger it can change the event the rule names. It refuses an event that has already ended or been cancelled, and refuses lowering the capacity below the number already going, naming that number.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `scope`, `description`, `timezone`, `voice_channel_id`, `location`, `capacity`, `image_url`, `required_role_id`, `host_ids`, `kind`, `all_day`, `color`, `discord_sync`, `team_id`, `visibility`, `signups_open_at`, `signups_close_at`, `participant_role_id`, `reminder_offsets`, `announce_at_start`, `announce_role_id`, `rsvp_slots`, `title`, `start`, `ends_at`, `duration_minutes`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `all_day` | true \| false | The text true or false: whether the event fills whole days rather than a clock time. |
| `capacity` | number, empty when unlimited | How many members can be going, empty when the event has no limit. |
| `channel_id` | id, empty when no panel | Channel the RSVP panel sits in. Empty for an off entry, the one kind that posts no panel. |
| `discord_event_id` | id, empty when not mirrored | Discord's own scheduled event for this, empty when it was not mirrored into the server header. |
| `discord_event_url` | link, empty when not mirrored | Link to Discord's own copy in the server header, ready to put in a message. |
| `ends_at` | date and time, empty when open-ended | When the event ends, as an ISO date and time. Empty when it has no end. |
| `event_id` | id | The event's raw id, which is what a later Update an event or Cancel an event step wants as its target. |
| `going_count` | number | How many members are going after the step. Always zero right after an event is created, since nobody can have answered yet. |
| `kind` | event \| meeting \| session \| stream \| milestone \| deadline \| off | What sort of entry it is. An off entry is a holiday or downtime: nobody signs up and no panel is posted. |
| `location` | text, empty when none | Where it happens, as free text, empty when none was given. |
| `maybe_count` | number | How many members answered maybe after the step. |
| `message_id` | id, empty when no panel | The RSVP panel message itself, for a follow-up that pins it, quotes it or reacts to it. |
| `panel_url` | link, empty when no panel | Jump link to the RSVP panel, which is the one place members can answer. |
| `series` | true \| false | The text true or false: whether this is a repeating event's template rather than a single event. |
| `series_affected` | number, empty when not a series | How many occurrences a series-wide change reached, empty when the step touched one event. |
| `starts_at` | date and time | When the event starts, as an ISO date and time. |
| `status` | scheduled \| live \| ended \| cancelled | Where the event is in its life after the step. |
| `team_id` | id, empty when none | The team invited to the event, empty when none. |
| `timezone` | for example Europe/Paris | The zone the event's times are read in, frozen onto it when it was created. |
| `title` | text | The event's title after the step. |
| `visibility` | public \| private | Whether the event is on the server's public calendar page and public feed. |
| `voice_channel_id` | id, empty when none | The voice channel it happens in, empty when it is not a voice event. |
| `waitlist_count` | number | How many members are queued for a seat after the step. |

##### Archive an issue

`tasks.archive_issue`

Puts an issue away: out of every default list, still at its own address, still answering to its reference such as WEB-12, and restorable in one step. It is not a state change, so the issue keeps the column it was in and restoring puts it back exactly where it was rather than somewhere a rule chose. Archiving something already archived is not an error, it simply reports that nothing changed, which is what makes the step safe on a stale list or a retried run. Nobody is notified and the card is left where it is, matching what the dashboard's own archive button does. There is no delete action on purpose: deleting an issue takes its comments, its activity trail and its logged time with it, and that is not something a rule should be able to do on a text match. Closing an issue is a different thing again and belongs on Update an issue, by moving it to a completed column. The issue is named by its reference or by its raw id from an earlier step, and both are looked up inside this server only. The reference does not have to be the whole value: a rule triggered by a message can point this at the message itself, and the first reference written in it is the one that is used - so a message reading WEB-12 is fixed names WEB-12. Only the first is ever taken, so a message naming two issues acts on the one written first; use Find an issue and a step condition when a rule has to refuse an ambiguous message instead.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `restrict_to_project_id`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `archived` | true \| false | The text true or false: whether the issue is put away after the step. Always true after Archive and false after Restore, so it is the value to echo in a message rather than something to branch on. |
| `archived_at` | date, empty when not archived | When the issue was put away, kept from the first time it was archived rather than reset by a repeat. Empty once it is restored. |
| `assignee_id` | id, empty when nobody has it | The member the issue is assigned to after the step, empty when it is unassigned. |
| `card_error` | empty on success | Why the card is missing when it is: no_channel, missing_permissions or post_failed. Empty when the card is there. |
| `carded` | true \| false | The text true or false: whether the issue has a card in a channel. False is normal for an issue that lives only in the dashboard. |
| `changed` | true \| false - branch on this | The text true or false: true only when this step is the one that moved it. False means it was already in that state, which is not an error and is worth announcing differently. |
| `channel_id` | id, empty when uncarded | Channel the issue's card sits in, empty when it has no card. |
| `due_date` | YYYY-MM-DD, empty when none | The issue's due date, empty when it has none. |
| `identifier` | for example WEB-12 | The issue's human reference, the one people type and the one to quote in a message. |
| `issue_id` | id | The issue's raw id, which is what a later Update an issue or Assign an issue step wants as its target. |
| `issue_url` | link, empty when unknown | Dashboard link to the issue, empty when the deployment does not know its own public address. |
| `message_id` | id, empty when uncarded | The card message itself, for a follow-up that edits or reacts to it. Empty when the issue has no card. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `priority` | 0 to 4 | The issue's priority after the step, as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the issue belongs to. An issue never moves between projects. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the issue is in after the step. |
| `state_name` | text | Name of the column the issue is in after the step, ready to put in a message. |
| `team_id` | id, empty when none | The team the issue is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the card, empty when the issue has no card or no thread. |
| `title` | text | The issue's title after the step. |

##### Assign an issue

`tasks.assign_issue`

Puts an issue on a member, takes it off whoever has it, or hands it to the person who triggered the rule - the two-field shape of the most common tracker automation, reacting to a card to claim the work. It is the assignee half of Update an issue and behaves identically: the new assignee gets the standard notification, the activity trail records the change of hands, and the card is repainted. No member is subscribed to the issue by a rule: from the dashboard, a command or a card button the person who acted starts following the issue, but a rule acting in a member's name is not that member choosing to hear about it for ever, and the tracker has no unsubscribe button anywhere. Unassigning is a deliberate choice you pick rather than an empty field, so a rule can never drop somebody's work by accident because a trigger happened to carry no member. The issue is named by its identifier, such as WEB-12, or by its raw id from an earlier step, and both are looked up inside this server only. The reference does not have to be the whole value: a rule triggered by a message can point this at the message itself, and the first reference written in it is the one that is used - so a message reading WEB-12 is fixed names WEB-12. Only the first is ever taken, so a message naming two issues acts on the one written first; use Find an issue and a step condition when a rule has to refuse an ambiguous message instead. The step reports who had it before as well as who has it now, so a follow-up message can name both.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `assignee`, `restrict_to_project_id`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `assignee_id` | id, empty when nobody has it | The member the issue is assigned to after the step, empty when it is unassigned. |
| `card_error` | empty on success | Why the card is missing when it is: no_channel, missing_permissions or post_failed. Empty when the card is there. |
| `carded` | true \| false | The text true or false: whether the issue has a card in a channel. False is normal for an issue that lives only in the dashboard. |
| `channel_id` | id, empty when uncarded | Channel the issue's card sits in, empty when it has no card. |
| `due_date` | YYYY-MM-DD, empty when none | The issue's due date, empty when it has none. |
| `identifier` | for example WEB-12 | The issue's human reference, the one people type and the one to quote in a message. |
| `issue_id` | id | The issue's raw id, which is what a later Update an issue or Assign an issue step wants as its target. |
| `issue_url` | link, empty when unknown | Dashboard link to the issue, empty when the deployment does not know its own public address. |
| `message_id` | id, empty when uncarded | The card message itself, for a follow-up that edits or reacts to it. Empty when the issue has no card. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `previous_assignee_id` | id, empty when it was unassigned | Who was carrying the issue before this step, empty when nobody was. Pair it with assignee_id to announce a handover. |
| `priority` | 0 to 4 | The issue's priority after the step, as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the issue belongs to. An issue never moves between projects. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the issue is in after the step. |
| `state_name` | text | Name of the column the issue is in after the step, ready to put in a message. |
| `team_id` | id, empty when none | The team the issue is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the card, empty when the issue has no card or no thread. |
| `title` | text | The issue's title after the step. |

##### Comment on an issue

`tasks.comment_issue`

Adds a comment to an issue and runs everything a comment means: the text is mirrored into the issue's discussion thread in Discord, the followers get the usual notification and the card's comment count is repainted. No member is subscribed to the issue by a rule: from the dashboard, a command or a card button the person who acted starts following the issue, but a rule acting in a member's name is not that member choosing to hear about it for ever, and the tracker has no unsubscribe button anywhere. The comment is recorded as the member who triggered the rule by default, or as the bot if you prefer - there is deliberately no way to sign it with somebody else's name. On a trigger that carries no member, such as a schedule, it is the bot that speaks. The issue is named either by its reference as people type it, such as WEB-12, which tolerates a missing dash and the wrong case, or by its raw id from an earlier step, and both are looked up inside this server only. The reference does not have to be the whole value: a rule triggered by a message can point this at the message itself, and the first reference written in it is the one that is used - so a message reading WEB-12 is fixed names WEB-12. Only the first is ever taken, so a message naming two issues acts on the one written first; use Find an issue and a step condition when a rule has to refuse an ambiguous message instead. Mentions inside the text never ping: the mirror into the thread is posted with pings switched off, so an at-everyone in the comment renders as plain words. Note that a rule comments with tracker-manager authority rather than as the member who triggered it, so anyone who can trigger it can comment on any issue the rule names - set the restrict to project field whenever the reference comes from something a member typed.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `restrict_to_project_id`, `body`, `author`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `assignee_id` | id, empty when nobody has it | The member the issue is assigned to after the step, empty when it is unassigned. |
| `card_error` | empty on success | Why the card is missing when it is: no_channel, missing_permissions or post_failed. Empty when the card is there. |
| `carded` | true \| false | The text true or false: whether the issue has a card in a channel. False is normal for an issue that lives only in the dashboard. |
| `channel_id` | id, empty when uncarded | Channel the issue's card sits in, empty when it has no card. |
| `comment_count` | number | How many comments the issue has after this one, the same number the card's footer shows. |
| `comment_id` | id | The comment that was just written, for a follow-up step that wants to point at it. |
| `due_date` | YYYY-MM-DD, empty when none | The issue's due date, empty when it has none. |
| `identifier` | for example WEB-12 | The issue's human reference, the one people type and the one to quote in a message. |
| `issue_id` | id | The issue's raw id, which is what a later Update an issue or Assign an issue step wants as its target. |
| `issue_url` | link, empty when unknown | Dashboard link to the issue, empty when the deployment does not know its own public address. |
| `message_id` | id, empty when uncarded | The card message itself, for a follow-up that edits or reacts to it. Empty when the issue has no card. |
| `mirror_message_id` | id, empty when no thread | The message this step posted in the issue's discussion thread, for a follow-up that reacts to or replies to the comment. Empty when the issue has no thread. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `priority` | 0 to 4 | The issue's priority after the step, as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the issue belongs to. An issue never moves between projects. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the issue is in after the step. |
| `state_name` | text | Name of the column the issue is in after the step, ready to put in a message. |
| `team_id` | id, empty when none | The team the issue is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the card, empty when the issue has no card or no thread. |
| `title` | text | The issue's title after the step. |

##### Create an issue

`tasks.create_issue`

Opens a new issue in one of the server's task projects, exactly as if somebody had created it from the dashboard or with /task create: it gets its identifier, its activity trail, its card in the tasks channel with the discussion thread under it, and the assignee's notification. What a rule deliberately does not do is sign anybody up for the issue's notifications - a create from the dashboard or a command subscribes the member who opened it and the assignee, because those are people who chose to act. Every property is available up front - title, description, starting state, priority, assignee, due date, estimate, expected minutes, team, cycle, parent issue and labels - and anything you leave out takes the project's default. The issue is opened by the bot unless you say otherwise, and that default is a permission decision rather than a cosmetic one: whoever an issue is opened by can edit it afterwards - retitle it, move it, reassign it, close it - from the card buttons and from /task, for as long as the issue exists. Set opened by to the member who triggered the rule when you want them credited, and know that you are also handing them those rights on an issue in a project they may have nothing else to do with. Naming them as the assignee grants the same rights, which is why that field is a choice you make too. On a scheduled or webhook run, which carries no member, the bot is recorded whatever you picked. The card is posted in the server's configured tasks channel, or in the channel the rule was triggered from when there is none, and an issue whose card could not be posted is still a real issue that lives in the dashboard - the step reports that as carded false rather than failing. A rule acts with tracker-manager authority, so anyone who can trigger it can, through it, open an issue in the project you name. It fails with a named refusal when another bot owns the tracker on this server, when the module is switched off, or when the project has reached the issue limit of the server's plan.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `project_id`, `title`, `description`, `state_id`, `priority`, `assignee`, `author`, `due_date`, `estimate`, `team_id`, `time_estimate_minutes`, `cycle_id`, `parent_issue_id`, `label_ids`, `channel_id`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `assignee_id` | id, empty when nobody has it | The member the issue is assigned to after the step, empty when it is unassigned. |
| `card_error` | empty on success | Why the card is missing when it is: no_channel, missing_permissions or post_failed. Empty when the card is there. |
| `carded` | true \| false | The text true or false: whether the issue has a card in a channel. False is normal for an issue that lives only in the dashboard. |
| `channel_id` | id, empty when uncarded | Channel the issue's card sits in, empty when it has no card. |
| `due_date` | YYYY-MM-DD, empty when none | The issue's due date, empty when it has none. |
| `identifier` | for example WEB-12 | The issue's human reference, the one people type and the one to quote in a message. |
| `issue_id` | id | The issue's raw id, which is what a later Update an issue or Assign an issue step wants as its target. |
| `issue_url` | link, empty when unknown | Dashboard link to the issue, empty when the deployment does not know its own public address. |
| `message_id` | id, empty when uncarded | The card message itself, for a follow-up that edits or reacts to it. Empty when the issue has no card. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `priority` | 0 to 4 | The issue's priority after the step, as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the issue belongs to. An issue never moves between projects. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the issue is in after the step. |
| `state_name` | text | Name of the column the issue is in after the step, ready to put in a message. |
| `team_id` | id, empty when none | The team the issue is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the card, empty when the issue has no card or no thread. |
| `title` | text | The issue's title after the step. |

##### Link two issues

`tasks.link_issues`

Relates two issues to each other: blocks, is blocked by, relates to, duplicates or is duplicated by. The relation is stored on both issues at once - linking WEB-12 as blocking WEB-19 puts a blocked by WEB-12 entry on WEB-19 in the same breath - so whoever opens the other issue is told why it is stuck without having to find the first one. That is what makes create the bug, then link it as blocking the epic one rule: Create an issue hands over the new issue's id, and this step takes an id on either end. Each end is named by its reference such as WEB-12, or by its raw id from an earlier step, and both are looked up inside this server only, so an id from another server simply answers not found. Two issues can carry at most one relation between them, in either direction, so linking a pair that is already related is not an error: the step reports that nothing changed and says what they already are to each other, which is what makes it safe to run on every matching message. Changing an existing relation means unlinking first and linking again, both of which are steps. It refuses an issue related to itself, and refuses either end that already carries its twenty-five relations, naming the limit. Nothing is posted to Discord and nobody is notified: a relation is a fact about the tracker, it appears in both issues' drawers and in both activity trails, and the cards are left exactly as they are.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `issue`, `related`, `type`, `restrict_to_project_id`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `identifier` | for example WEB-12 | The first issue's human reference, the one people type and the one to quote in a message. |
| `inverse_type` | blocks \| blocked_by \| relates \| duplicates \| duplicated_by, empty when nothing changed | What the OTHER issue now says about this one - the mirror the tracker stored on it. Blocks on one end is blocked by on the other, which is the half to quote in a message addressed to the other issue's channel. |
| `issue_id` | id | The first issue's raw id, which is what a later Update an issue or Comment on an issue step wants as its target. |
| `related_identifier` | for example WEB-19 | The other issue's human reference, ready to put in a message. |
| `related_issue_id` | id | The other issue's raw id, for a later step that wants to act on that end of the pair. |
| `type` | blocks \| blocked_by \| relates \| duplicates \| duplicated_by, empty when nothing changed | The relation as the FIRST issue carries it. After a link that changed nothing it is what the two already were to each other; after an unlink that removed nothing it is empty. |
| `linked` | true \| false - branch on this | The text true or false: true only when this step is the one that related them. False means they were already related, which is not an error and is worth announcing differently. |

##### Log time on an issue

`tasks.log_time`

Writes down a stretch of work against an issue: how many minutes, optionally which day and a short note. Each step adds a row rather than overwriting a number, so the issue's total is the sum of every stretch anybody logged and the record of who did what on which day survives - which is why there is no way to simply set the total. The person who triggered the rule is credited by default, or the bot if you prefer, and on a trigger that carries no member it is the bot. Logging from the dashboard or a command subscribes whoever is credited, on the reasoning that somebody who has put an hour into an issue wants to hear what happens to it next; a rule deliberately does not, because the member it credits never chose to follow anything and the tracker has no unsubscribe button anywhere. Nobody is sent a notification: the card is repainted with the new total and that is how the channel finds out, because a tracker that direct-messages everyone each time somebody writes down twenty minutes is a tracker people mute. The day, when you give one, is a calendar date such as 2026-10-01 and never a full timestamp. The issue is named by its reference, such as WEB-12, or by its raw id from an earlier step, and both are looked up inside this server only. The reference does not have to be the whole value: a rule triggered by a message can point this at the message itself, and the first reference written in it is the one that is used - so a message reading WEB-12 is fixed names WEB-12. Only the first is ever taken, so a message naming two issues acts on the one written first; use Find an issue and a step condition when a rule has to refuse an ambiguous message instead.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `restrict_to_project_id`, `minutes`, `spent_on`, `note`, `author`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `assignee_id` | id, empty when nobody has it | The member the issue is assigned to after the step, empty when it is unassigned. |
| `card_error` | empty on success | Why the card is missing when it is: no_channel, missing_permissions or post_failed. Empty when the card is there. |
| `carded` | true \| false | The text true or false: whether the issue has a card in a channel. False is normal for an issue that lives only in the dashboard. |
| `channel_id` | id, empty when uncarded | Channel the issue's card sits in, empty when it has no card. |
| `due_date` | YYYY-MM-DD, empty when none | The issue's due date, empty when it has none. |
| `entry_id` | id | The time entry that was just written, one row per stretch of work. |
| `identifier` | for example WEB-12 | The issue's human reference, the one people type and the one to quote in a message. |
| `issue_id` | id | The issue's raw id, which is what a later Update an issue or Assign an issue step wants as its target. |
| `issue_url` | link, empty when unknown | Dashboard link to the issue, empty when the deployment does not know its own public address. |
| `logged_minutes` | number | How many minutes this step logged, echoing what was configured. |
| `message_id` | id, empty when uncarded | The card message itself, for a follow-up that edits or reacts to it. Empty when the issue has no card. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `priority` | 0 to 4 | The issue's priority after the step, as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the issue belongs to. An issue never moves between projects. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the issue is in after the step. |
| `state_name` | text | Name of the column the issue is in after the step, ready to put in a message. |
| `team_id` | id, empty when none | The team the issue is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the card, empty when the issue has no card or no thread. |
| `time_spent_minutes` | number | The issue's total logged minutes after this step: the sum of every stretch anybody has logged against it. |
| `title` | text | The issue's title after the step. |

##### Restore an issue

`tasks.restore_issue`

Brings an archived issue back into the lists it left. Because archiving is not a state change, the issue returns to exactly the column it was in rather than somewhere a rule chose, and every reference to it, such as WEB-12, kept working the whole time it was away. Restoring something that was not archived is not an error, it simply reports that nothing changed, which is what makes the step safe to put behind a button people may press twice. Nobody is notified and the card is left where it is. The issue is named by its reference or by its raw id from an earlier step, and both are looked up inside this server only. The reference does not have to be the whole value: a rule triggered by a message can point this at the message itself, and the first reference written in it is the one that is used - so a message reading WEB-12 is fixed names WEB-12. Only the first is ever taken, so a message naming two issues acts on the one written first; use Find an issue and a step condition when a rule has to refuse an ambiguous message instead.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `restrict_to_project_id`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `archived` | true \| false | The text true or false: whether the issue is put away after the step. Always true after Archive and false after Restore, so it is the value to echo in a message rather than something to branch on. |
| `archived_at` | date, empty when not archived | When the issue was put away, kept from the first time it was archived rather than reset by a repeat. Empty once it is restored. |
| `assignee_id` | id, empty when nobody has it | The member the issue is assigned to after the step, empty when it is unassigned. |
| `card_error` | empty on success | Why the card is missing when it is: no_channel, missing_permissions or post_failed. Empty when the card is there. |
| `carded` | true \| false | The text true or false: whether the issue has a card in a channel. False is normal for an issue that lives only in the dashboard. |
| `changed` | true \| false - branch on this | The text true or false: true only when this step is the one that moved it. False means it was already in that state, which is not an error and is worth announcing differently. |
| `channel_id` | id, empty when uncarded | Channel the issue's card sits in, empty when it has no card. |
| `due_date` | YYYY-MM-DD, empty when none | The issue's due date, empty when it has none. |
| `identifier` | for example WEB-12 | The issue's human reference, the one people type and the one to quote in a message. |
| `issue_id` | id | The issue's raw id, which is what a later Update an issue or Assign an issue step wants as its target. |
| `issue_url` | link, empty when unknown | Dashboard link to the issue, empty when the deployment does not know its own public address. |
| `message_id` | id, empty when uncarded | The card message itself, for a follow-up that edits or reacts to it. Empty when the issue has no card. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `priority` | 0 to 4 | The issue's priority after the step, as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the issue belongs to. An issue never moves between projects. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the issue is in after the step. |
| `state_name` | text | Name of the column the issue is in after the step, ready to put in a message. |
| `team_id` | id, empty when none | The team the issue is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the card, empty when the issue has no card or no thread. |
| `title` | text | The issue's title after the step. |

##### Unlink two issues

`tasks.unlink_issues`

Takes the relation between two issues off, both halves at once: unlinking WEB-12 from WEB-19 also removes the entry WEB-19 was carrying, so neither issue is left claiming to be blocked by something the other has forgotten. Each end is named by its reference such as WEB-12, or by its raw id from an earlier step, and both are looked up inside this server only. You can leave the kind of relation blank, which removes whatever relates the two - that is what the cross on a relation chip does in the dashboard, and two issues carry at most one relation between them so there is nothing to disambiguate. Setting it removes that relation and its mirror and nothing else. Unlinking two issues that are not related is not an error: the step reports that nothing changed, which is what makes it safe to run on every close or every state change without checking first. Nothing is posted to Discord and nobody is notified; both activity trails record the removal, and the cards are left exactly as they are. The issues themselves are untouched - this removes the link between them and never the issues, which is why there is no delete action anywhere in this family.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `issue`, `related`, `type`, `restrict_to_project_id`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `identifier` | for example WEB-12 | The first issue's human reference, the one people type and the one to quote in a message. |
| `inverse_type` | blocks \| blocked_by \| relates \| duplicates \| duplicated_by, empty when nothing changed | What the OTHER issue now says about this one - the mirror the tracker stored on it. Blocks on one end is blocked by on the other, which is the half to quote in a message addressed to the other issue's channel. |
| `issue_id` | id | The first issue's raw id, which is what a later Update an issue or Comment on an issue step wants as its target. |
| `related_identifier` | for example WEB-19 | The other issue's human reference, ready to put in a message. |
| `related_issue_id` | id | The other issue's raw id, for a later step that wants to act on that end of the pair. |
| `type` | blocks \| blocked_by \| relates \| duplicates \| duplicated_by, empty when nothing changed | The relation as the FIRST issue carries it. After a link that changed nothing it is what the two already were to each other; after an unlink that removed nothing it is empty. |
| `unlinked` | true \| false - branch on this | The text true or false: true only when this step is the one that removed the relation. False means they were not related to begin with, which is not an error. |

##### Update an issue

`tasks.update_issue`

Changes any property of an existing issue: title, description, state, position in its column, priority, assignee, due date, estimate, expected minutes, team, cycle, parent issue and labels. Only the fields you fill in are touched and everything you leave out keeps its current value, so one step can move an issue and re-label it at the same time. Clearing a value is its own choice, distinct from leaving it alone: emptying the assignee, the team, the cycle, the parent, the due date, the estimate or the expected minutes unsets it, and sending an empty label list removes every label. The issue is named either by its identifier as people type it, such as WEB-12, which tolerates a missing dash and the wrong case, or by its raw id, which is what an earlier Create an issue step hands you. Both are looked up inside this server only, so an id belonging to another server simply answers not found and there is no way to reach across. The reference does not have to be the whole value: a rule triggered by a message can point this at the message itself, and the first reference written in it is the one that is used - so a message reading WEB-12 is fixed names WEB-12. Only the first is ever taken, so a message naming two issues acts on the one written first; use Find an issue and a step condition when a rule has to refuse an ambiguous message instead. Whatever changes runs the full trail: the activity log records each field that moved, a new assignee is notified, a state change notifies the followers, and the card in the channel is repainted. No member is subscribed to the issue by a rule: from the dashboard, a command or a card button the person who acted starts following the issue, but a rule acting in a member's name is not that member choosing to hear about it for ever, and the tracker has no unsubscribe button anywhere. Note that a rule edits with tracker-manager authority rather than as the member who triggered it, so anyone who can trigger it can change any issue the rule names - set the restrict to project field whenever the identifier comes from something a member typed. Closing an issue is done here by moving it to a completed state, and a project that requires an estimate before work starts refuses that move here exactly as it does everywhere else.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `target`, `restrict_to_project_id`, `title`, `description`, `state_id`, `position`, `priority`, `assignee`, `due_date`, `estimate`, `team_id`, `time_estimate_minutes`, `cycle_id`, `parent_issue_id`, `label_ids`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `assignee_id` | id, empty when nobody has it | The member the issue is assigned to after the step, empty when it is unassigned. |
| `card_error` | empty on success | Why the card is missing when it is: no_channel, missing_permissions or post_failed. Empty when the card is there. |
| `carded` | true \| false | The text true or false: whether the issue has a card in a channel. False is normal for an issue that lives only in the dashboard. |
| `channel_id` | id, empty when uncarded | Channel the issue's card sits in, empty when it has no card. |
| `due_date` | YYYY-MM-DD, empty when none | The issue's due date, empty when it has none. |
| `identifier` | for example WEB-12 | The issue's human reference, the one people type and the one to quote in a message. |
| `issue_id` | id | The issue's raw id, which is what a later Update an issue or Assign an issue step wants as its target. |
| `issue_url` | link, empty when unknown | Dashboard link to the issue, empty when the deployment does not know its own public address. |
| `message_id` | id, empty when uncarded | The card message itself, for a follow-up that edits or reacts to it. Empty when the issue has no card. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `previous_assignee_id` | id, empty when it was unassigned | Who was carrying the issue before this step, empty when nobody was. Pair it with assignee_id to announce a handover. |
| `priority` | 0 to 4 | The issue's priority after the step, as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the issue belongs to. An issue never moves between projects. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the issue is in after the step. |
| `state_name` | text | Name of the column the issue is in after the step, ready to put in a message. |
| `team_id` | id, empty when none | The team the issue is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the card, empty when the issue has no card or no thread. |
| `title` | text | The issue's title after the step. |

##### Add to a team

`teams.add_member`

Puts one member on a shared team, with an optional free-text role such as Lead or Caster. Teams are shared by the whole of FlaviBot Management, so somebody added here is on the team the events calendar invites and the team the task board files issues under at the same time, and Time Finder will read their availability for it. The team is named either by its id, which is what an earlier Create a team step hands over, or simply by its name: a name is unique per server so it always means one team, and the case does not have to match. Adding somebody who is already on the team is not an error; the step reports that nothing changed and leaves them where they are, which is what makes it safe on a reaction somebody clicks twice. Leaving the role empty leaves whatever role they already have alone rather than taking it off them - the role somebody typed on the roster by hand survives a rule that re-adds them - and clearing the role is a separate choice you make on purpose. The step reports the role the roster carries after it, which on an add that left it alone is the one that was already there. The member has to be on this server, checked before the roster is written, because a team roster is what Time Finder answers for and an outsider on it would leak their busy time.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `team`, `member`, `role`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `added` | true \| false - branch on this | The text true or false: true only when this step is the one that put them on the team. False means they were already on it, which is not an error and is worth announcing differently. |
| `member_count` | number | How many members are on the team after the step. |
| `role` | text, empty when none | The free-text role they hold on the team, such as Lead or Caster. Empty when the step gave them none. |
| `team_color` | colour like #a1b2c3, empty when none | The team's colour, empty when it has none. |
| `team_description` | text, empty when none | The team's description, empty when it has none. |
| `team_id` | id | The team's raw id. It is what the team field of Create an issue and Create an event takes, and what a later Add to a team step wants as its target, so one rule can make a team and file things under it. |
| `team_name` | text | The team's name, ready to put in a message. |
| `user_id` | id | The member the step acted on. |

##### Create a team

`teams.create_team`

Creates a shared team on this server: a named group of members with an optional description, colour and starting roster. Teams are shared by the whole of FlaviBot Management, so a team made here is the same team the events calendar invites and the same team the task board files issues under, and the team id this step publishes can be dropped straight into the team field of Create an issue or Create an event to file something under it in the same rule. Everyone on the starting roster has to be a member of this server, which is checked before the team is written, so a roster naming somebody who is not here leaves no half-made team behind. A name is unique per server, compared without regard to case, so a rule that would make a second Moderators team says so rather than quietly making a duplicate. There is no action for renaming or deleting a team on purpose: both change the team everywhere at once, for the calendar and the board together, and that belongs on the dashboard in front of a person rather than behind a text match.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `name`, `description`, `color`, `member_ids`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `member_count` | number | How many members are on the team after the step. |
| `team_color` | colour like #a1b2c3, empty when none | The team's colour, empty when it has none. |
| `team_description` | text, empty when none | The team's description, empty when it has none. |
| `team_id` | id | The team's raw id. It is what the team field of Create an issue and Create an event takes, and what a later Add to a team step wants as its target, so one rule can make a team and file things under it. |
| `team_name` | text | The team's name, ready to put in a message. |

##### Remove from a team

`teams.remove_member`

Takes one member off a shared team. Teams are shared by the whole of FlaviBot Management, so somebody removed here comes off the team the events calendar invites and the team the task board files issues under at once, and Time Finder stops answering for them on it. The team is named either by its id, which an earlier step can hand over, or by its name, which is unique per server and does not have to match case. Removing somebody who is not on the team is not an error: the step reports that nothing changed, which is what makes it safe to run on every reaction removal without checking first. Nothing else about the team changes and nobody is told, so this is reversible by the opposite step. The team itself is never deleted by this action, even when the last member comes off: an empty team is a normal state, and deleting one is a dashboard decision because it reaches across the calendar and the board together.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `team`, `member`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `member_count` | number | How many members are on the team after the step. |
| `removed` | true \| false - branch on this | The text true or false: true only when this step is the one that took them off. False means they were not on the team to begin with, which is not an error. |
| `team_color` | colour like #a1b2c3, empty when none | The team's colour, empty when it has none. |
| `team_description` | text, empty when none | The team's description, empty when it has none. |
| `team_id` | id | The team's raw id. It is what the team field of Create an issue and Create an event takes, and what a later Add to a team step wants as its target, so one rule can make a team and file things under it. |
| `team_name` | text | The team's name, ready to put in a message. |
| `user_id` | id | The member the step acted on. |

##### Update a team

`teams.update_team`

Changes how a shared team is presented: its description and its colour. Only the fields you fill in are touched and everything else keeps its current value, and clearing a value is its own choice distinct from leaving it alone - emptying the description or the colour unsets it. Teams are shared by the whole of FlaviBot Management, so the change shows up on the events calendar and on the task board at once. The team is named either by its id, which an earlier Create a team step hands over, or by its name, which is unique per server and does not have to match case. A step that changes neither field is refused when you save it rather than running as a no-op. Renaming a team and deleting one are deliberately not possible from a rule: a name is how every other surface refers to a team - this action's own name target, the event invite line, the board filter - so renaming on a text match silently breaks every rule that named the old one, and deleting takes the roster with it. A description and a colour reach none of that, which is the whole reason they are here. Both stay on the dashboard, in front of a person.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `team`, `description`, `color`

Hands to later steps:

| Field | Shape | What it is |
| --- | --- | --- |
| `member_count` | number | How many members are on the team after the step. |
| `team_color` | colour like #a1b2c3, empty when none | The team's colour after the step, empty when it has none. |
| `team_description` | text, empty when none | The team's description after the step, empty when it has none. |
| `team_id` | id | The team's raw id. It is what the team field of Create an issue and Create an event takes, and what a later Add to a team step wants as its target, so one rule can make a team and file things under it. |
| `team_name` | text | The team's name, which this step never changes - renaming a team is deliberately not possible from a rule. |

#### 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. A fourth scope, group, keeps the value inside the workflow group the rule belongs to: it is invisible to every other rule of the server, it takes no target because the group is always the rule's own, and it is what lets the same panel be installed twice without the two copies overwriting each other. Outside a group it fails with missing_scope_target.

**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.<alias>} for the value plus {vars.<alias>.exists}, {vars.<alias>.type} and, for lists, {vars.<alias>.length} together with {vars.<alias>.first}, {vars.<alias>.last} and {vars.<alias>.random} (drawn once when the step runs, so it stays the same everywhere later in that run) alongside its {vars.<alias>.random_index}. A missing variable is NOT an error: exists comes back false and the optional default is served as the value, which is exactly what lets a condition branch on "first time seen". Scope targeting works like the other data actions, defaulting to the trigger member or trigger channel and failing with missing_scope_target when neither is available. Expired variables read as missing, and non-text values (numbers, booleans, lists, JSON) are rendered back to text before being exposed. A fourth scope, group, keeps the value inside the workflow group the rule belongs to: it is invisible to every other rule of the server, it takes no target because the group is always the rule's own, and it is what lets the same panel be installed twice without the two copies overwriting each other. Outside a group it fails with missing_scope_target.

**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. |
| `first` | first entry of a list | The first entry of a list, as text. Empty when the variable does not exist, is empty, or is not a list. |
| `last` | last entry of a list | The last entry of a list, as text. Empty when the variable does not exist, is empty, or is not a list. |
| `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. |
| `random` | one entry of a list, drawn once | One entry of a list, picked at random. Drawn once when the step runs, so it stays the same in every later step and condition of that run. Empty when the variable does not exist, is empty, or is not a list. |
| `random_index` | position of the drawn entry | The position of the entry that random picked, counting from 0, as text. Empty when nothing was drawn. |
| `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. A fourth scope, group, keeps the value inside the workflow group the rule belongs to: it is invisible to every other rule of the server, it takes no target because the group is always the rule's own, and it is what lets the same panel be installed twice without the two copies overwriting each other. Outside a group it fails with missing_scope_target.

**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). A fourth scope, group, keeps the value inside the workflow group the rule belongs to: it is invisible to every other rule of the server, it takes no target because the group is always the rule's own, and it is what lets the same panel be installed twice without the two copies overwriting each other. Outside a group it fails with missing_scope_target.

**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). A fourth scope, group, keeps the value inside the workflow group the rule belongs to: it is invisible to every other rule of the server, it takes no target because the group is always the rule's own, and it is what lets the same panel be installed twice without the two copies overwriting each other. Outside a group it fails with missing_scope_target.

**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. A fourth scope, group, keeps the value inside the workflow group the rule belongs to: it is invisible to every other rule of the server, it takes no target because the group is always the rule's own, and it is what lets the same panel be installed twice without the two copies overwriting each other. Outside a group it fails with missing_scope_target.

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

##### Find an event

`events.find_event`

Searches this server's calendar and publishes the match under the short alias you choose, so later steps can target it with the alias's id and later conditions can branch on what it found. This is what lets a rule that carries no event reach one at all: a weekly schedule has no message and no id to work from, so without this it cannot name the session it is supposed to move. Filter by free text over the title, description and location, by kind, team, channel, organiser and visibility. By default it looks at what is still to come, which includes an event happening right now, and answers with the one starting soonest - set it to past instead and it answers with the most recent event that has ended, which is what a recap rule wants. Cancelled events are left out unless you ask for them. Finding nothing is NOT a failure: found comes back false, match count is zero and every other field is empty, which is exactly what lets a condition branch on "only if there is one" instead of the step stopping the rule. That also means the failure setting on this step is about real faults, such as the calendar being switched off, and never about an empty result. When several events match, the soonest is published and ambiguous comes back true, so a rule can refuse to act rather than silently picking one - which is the whole reason searching lives here and not inside the update and cancel actions. Repeating events are read as their individual occurrences, never as the invisible template behind them.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `variable_name`, `search`, `kinds`, `team_id`, `channel_id`, `creator_id`, `visibility`, `when`, `include_cancelled`

Publishes under the name you choose, read as `{vars.<name>.<field>}` (or the bare `{vars.<name>}` for `value`):

| Field | Shape | What it is |
| --- | --- | --- |
| `all_day` | true \| false | The text true or false: whether the match fills whole days rather than a clock time. |
| `ambiguous` | true \| false - branch on this | The text true or false: true when more than one event matched. Refuse to act on it rather than letting the rule pick one for you. |
| `capacity` | number, empty when unlimited | How many members can be going, empty when the match has no limit or nothing matched. |
| `channel_id` | id, empty when no panel | Channel the match's RSVP panel sits in, empty when it has none. |
| `creator_id` | id | The member who created the match, or the bot when a rule created it. |
| `creator_mention` | mention, empty when nothing matched | A clickable mention of whoever created the match, ready to put in a message. |
| `description` | text, empty when none | The match's description, empty when it has none or nothing matched. |
| `discord_event_id` | id, empty when not mirrored | Discord's own scheduled event for the match, empty when it was not mirrored into the server header. |
| `discord_event_url` | link, empty when not mirrored | Link to Discord's own copy in the server header, ready to put in a message. |
| `ends_at` | date and time, empty when open-ended | When the match ends, as an ISO date and time. Empty when it has no end or nothing matched. |
| `found` | true \| false - branch on this | Whether anything matched, as a true or false value for step conditions. Finding nothing is not a failure, so this is how a rule decides whether to act. |
| `going_count` | number | How many members are going to the match. |
| `host_ids` | ids separated by commas, empty when none | Everyone billed as hosting the match, as a comma-separated list of ids. Empty when only the creator hosts it. |
| `id` | id | The match's raw id, which is what a later Update an event, Cancel an event or Answer for a member step wants as its target. |
| `kind` | event \| meeting \| session \| stream \| milestone \| deadline \| off | What sort of entry the match is. An off entry is a holiday or downtime: nobody signs up and no panel is posted. |
| `location` | text, empty when none | Where the match happens, as free text, empty when none was given. |
| `match_count` | number | How many events matched the filter in total, as text. Zero when nothing did. |
| `maybe_count` | number | How many members answered maybe on the match. |
| `message_id` | id, empty when no panel | The match's RSVP panel message itself, for a follow-up that quotes or reacts to it. |
| `occurrence` | true \| false | The text true or false: whether the match is one date of a repeating event rather than a one-off. Editing it touches that date alone unless the step asks for the whole series. |
| `panel_url` | link, empty when no panel | Jump link to the match's RSVP panel, which is the one place members can answer. |
| `participant_role_id` | id, empty when none | The role given to everyone going to the match, empty when it grants none. |
| `required_role_id` | id, empty when open to everyone | The role a member must have to answer on the match, empty when anyone who can see the panel may. |
| `starts_at` | date and time | When the match starts, as an ISO date and time. |
| `status` | scheduled \| live \| ended \| cancelled | Where the match is in its life. With the default upcoming search this is scheduled or live, live meaning it is happening right now. |
| `team_id` | id, empty when none | The team invited to the match, empty when none. |
| `timezone` | for example Europe/Paris | The zone the match's times are read in, frozen onto it when it was created. |
| `title` | text | The match's title, also readable as the bare {vars.<name>}. |
| `value` | the title, empty when nothing matched | The match's title, also readable as the bare {vars.<name>}. Empty when nothing matched. |
| `visibility` | public \| private | Whether the match is on the server's public calendar page and public feed. |
| `voice_channel_id` | id, empty when none | The voice channel the match happens in, empty when it is not a voice event. |
| `waitlist_count` | number | How many members are queued for a seat on the match. |

##### 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.<name>} resolves to the value field.

**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.<name>}: the track title when playing, else "playing" or "not playing". |
| `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.<alias>.<json path>} as well as {steps.N.body.<json path>}. It is GET only with no custom headers and no request body, it accepts only http and https addresses, it follows redirects, and all traffic leaves through the bot's shared egress proxy, so a host with no proxy configured fails with proxy_unavailable rather than calling out directly. The body is parsed as JSON only when the server sends an application/json content type (anything else, or unparseable JSON, arrives as body.text, cut off at 256K characters), the call gives up after about 12 seconds inside a 13 second step limit, a 404 or 500 still counts as a success so check {vars.<alias>.ok} before trusting the data, and the flattened values stop at 5 levels deep, 100 keys and 2000 characters per value. Two alias entries are reserved and written over the response body: {vars.<alias>.status} holds the HTTP status code and {vars.<alias>.ok} holds true or false, so a body with its own top-level status or ok field is readable as {steps.N.body.status} instead, which also works inside a run only if condition. {vars.<alias>.value}, and so the bare {vars.<alias>}, is the body's own top-level value field when it has one and falls back to the HTTP status code when it does not.

**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.<alias>}. |

##### Find an issue

`tasks.find_issue`

Searches this server's issues and publishes the newest match under the short alias you choose, so later steps can target it with the alias's id and later conditions can branch on what it found. Filter by free text over the title and description, by project, column, team, cycle, labels, priorities and assignee, where the assignee can be whoever triggered the rule or nobody at all. Closed and archived issues are left out unless you ask for them. Finding nothing is NOT a failure: found comes back false, match count is zero and every other field is empty, which is exactly what lets a condition branch on "only if there is one" instead of the step stopping the rule. That also means the failure setting on this step is about real faults, such as the tracker being switched off, and never about an empty result. When several issues match, the newest is published and ambiguous comes back true, so a rule can refuse to act rather than silently picking one - which is the whole reason searching lives here and not inside the update actions. Feeding the alias's id into a later step when nothing was found fails that step with a clear message rather than touching the wrong issue, but gating on found is still the right way to write the rule.

**Bot permission needed:** none  
**Gives up after:** 15s  
**Settings:** `variable_name`, `search`, `project_id`, `state_id`, `team_id`, `cycle_id`, `label_ids`, `priorities`, `assignee`, `include_closed`, `include_archived`

Publishes under the name you choose, read as `{vars.<name>.<field>}` (or the bare `{vars.<name>}` for `value`):

| Field | Shape | What it is |
| --- | --- | --- |
| `ambiguous` | true \| false - branch on this | The text true or false: true when more than one issue matched. Refuse to act on it rather than letting the rule pick one for you. |
| `archived` | true \| false | The text true or false telling you whether the match is archived. Only ever true when the step was asked to include archived issues. |
| `assignee_id` | id, empty when nobody has it | The member the match is assigned to, empty when it is unassigned or nothing matched. |
| `assignee_mention` | mention, empty when nobody has it | A clickable mention of whoever the match is assigned to, ready to put in a message. Empty when it is unassigned or nothing matched. |
| `channel_id` | id, empty when uncarded | Channel the match's card sits in, empty when it has no card or nothing matched. |
| `closed` | true \| false | The text true or false telling you whether the match sits in a completed or cancelled column. |
| `comment_count` | number | How many comments the match carries. |
| `creator_id` | id | The member who opened the match, or the bot when a rule opened it. |
| `cycle_id` | id, empty when none | The cycle the match is in, empty when it is in none. |
| `description` | text, empty when none | The match's description, empty when it has none or nothing matched. |
| `due_date` | YYYY-MM-DD, empty when none | The match's due date, empty when it has none or nothing matched. |
| `estimate` | number, empty when unestimated | The match's estimate in points, empty when nobody has sized it. |
| `found` | true \| false - branch on this | The text true or false: whether anything matched. Finding nothing is not a failure, so this is how a rule decides whether to carry on. |
| `id` | id | The match's raw id, which is what an Update, Assign, Comment or Archive step wants as its target. Empty when nothing matched, which makes any such step fail with a clear message rather than touching the wrong issue. |
| `identifier` | for example WEB-12 | The match's human reference, the one people type and the one to quote in a message. |
| `label_names` | comma-separated, empty when none | The match's labels as a comma-separated list of names, empty when it has none. |
| `match_count` | number | How many issues matched the filter in total, as text. Zero when nothing did. |
| `message_id` | id, empty when uncarded | The match's card message, for a follow-up that edits or reacts to it. Empty when it has no card. |
| `number` | number | The number half of the identifier: the 12 of WEB-12. |
| `parent_issue_id` | id, empty when none | The issue this one hangs off, empty when it is top level. |
| `priority` | 0 to 4 | The match's priority as a number: 0 none, 1 low, 2 medium, 3 high, 4 urgent. |
| `project_id` | id | Id of the project the match belongs to. |
| `project_key` | for example WEB | The project's key, the letters half of the identifier. |
| `state_id` | id | Id of the column the match is in, which is what an Update step wants to move it elsewhere. |
| `state_kind` | backlog \| unstarted \| started \| completed \| cancelled | What kind of column the match is in. Stable across a rename, so this is the one to compare in a condition rather than the column's name. |
| `state_name` | text | Name of the column the match is in, ready to put in a message. |
| `team_id` | id, empty when none | The team the match is filed under, empty when none. Never a second assignee. |
| `thread_id` | id, empty when none | The discussion thread hanging off the match's card, empty when it has no card or no thread. |
| `time_estimate_minutes` | number, empty when none | How many minutes somebody expected the match to take, empty when nobody guessed. |
| `time_spent_minutes` | number | Total minutes logged against the match so far. |
| `title` | text | The match's title. |
| `url` | link, empty when unknown | Dashboard link to the match, empty when the deployment does not know its own public address. |
| `value` | the identifier, empty when nothing matched | The match's reference such as WEB-12, also readable as the bare {vars.<name>}. Empty when nothing matched. |

##### Get channel variables

`variable.get_channel`

Resolves a channel of this server and publishes {vars.<name>.*} fields for it: mention, id, name, numeric channel type and parent category id. The channel id is required and can come from a placeholder such as an earlier step's output, so you can look up a channel the workflow just created. It reads the gateway cache first with a REST fallback, changes nothing and needs no permission. The step fails with resolve_not_found when the id does not resolve, so guard it with on_error continue if a missing channel should not stop the workflow.

**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.<name>}. |

##### Get role variables

`variable.get_role`

Resolves a role of this server and publishes {vars.<name>.*} fields for it: mention, id, name, position and whether it is managed by an integration. The role id is required and may come from a placeholder, for instance the id of a role an earlier step created. The role is looked up in the server's role list, cache first with a REST fallback, and the step fails with resolve_not_found when no role carries that id. Role colour is deliberately not exposed because the gateway cache does not keep it; nothing is modified and no permission is required.

**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.<name>}. |

##### Get user variables

`variable.get_user`

Looks a person up and publishes a whole set of {vars.<name>.*} fields about them: mention, id, username, display name, nickname, avatar URL, bot flag, account creation, account age in days and server join date. It resolves the trigger user by default, or any specific user id when the target is switched, and that id may come from a placeholder. It tries the server member cache first and falls back to a plain user lookup, so it still works for someone who is not in the server, in which case member_found is false and both nick and joined_at come back empty. It needs no permission and changes nothing, but the step fails with resolve_not_found when the id is neither a member nor a real Discord account.

**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.<name>}. |

##### Pick at random

`variable.random`

Draws one of up to 25 listed choices at random and publishes it as {vars.<name>}, for a different greeting each time or for a label that later steps branch on in their run conditions. Each choice may itself contain placeholders, which are resolved before the draw, so a choice can be built from the trigger or from earlier steps. The draw uses a cryptographic random source with rejection sampling so every choice is equally likely, and it also reports which slot was drawn so a condition can key on the position instead of comparing long text. Nothing is remembered between runs: every execution draws again, and no permission or Discord call is involved.

**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.<name>}. |

##### 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.<name>} and {vars.<name>.value}. The source can itself contain placeholders such as trigger variables or earlier step outputs, since they are resolved before this step runs. Transforms run in order and the result is cut off at 2000 characters; a regex extract that matches nothing yields an empty string, which is exactly what a following default transform is meant to catch. It touches nothing on Discord and needs no permission, but an unsupported regex pattern fails the step with invalid_regex.

**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.<name>}. |

### The Music module

Source: https://flavibot.xyz/docs/modules/music/01-overview
Summary: The FlaviBot Music module: the four ways to control playback, slash commands and prefix aliases, permissions, and what is free, vote-gated or Premium.

FlaviBot plays audio in a voice channel of your server. One server can have one
music session per bot at a time: the bot sits in a single voice channel, holds a
queue of tracks, and plays them in order.

#### Four ways to control it

You never have to pick one. They all act on the same player.

| Surface | Best for |
| --- | --- |
| **Slash commands** (`/play`, `/skip`, `/queue`, ...) | everyday use, anywhere in the server |
| **The buttons** on the now playing message | pause, skip, stop without typing |
| **The controller channel** | a dedicated channel where typing a song name plays it |
| **The web player** | a full interface with search, drag-and-drop queue and filters |

Server-wide settings live on the dashboard, under **Music** in the sidebar:
**Music settings** for everything about announcements, playback defaults and
the controller, and **DJ Mode** for who is allowed to do what.

#### Slash commands and their short forms

Every music command in these pages is a real slash command, under the exact name
written here. `/nowplaying`, `/clearqueue` and `/disconnect` are typed in full in
Discord's `/` list.

The short forms are **prefix aliases**. They work only with your server's text
prefix (`f!np`, `f!cq`, `f!dc`), never as a slash command, and Discord will not
autocomplete them. See [commands and
help](https://flavibot.xyz/docs/getting-started/04-commands-and-help) for how prefix commands work.

| Slash command | Also answers to, with the prefix |
| --- | --- |
| `/play` | `p` |
| `/play-file` | `pf`, but the prefix form cannot read an attached file |
| `/join` | `j`, `connect` |
| `/disconnect` | `dc`, `destroy`, `leave` |
| `/resume` | `unpause` |
| `/previous` | `prev` |
| `/nowplaying` | `np` |
| `/queue` | `q` |
| `/clearqueue` | `cq` |
| `/remove` | `rm` |
| `/volume` | `vol` |
| `/loop` | `repeat` |
| `/jump` | `skipto` |
| `/autoplay` | `autop` |
| `/24-7` | `247`, `24/7` |
| `/controller` | `setup` |
| `/music-panel` | `musicpanel` |

Every other music command is typed the same way in both places.

#### Getting started

1. Join a voice channel.
2. Run `/play` with a song name or a link.

That is the whole setup. FlaviBot joins your channel, queues the track and
announces it. Everything else on these pages is optional.

#### What you need permission-wise

FlaviBot needs **View Channel**, **Connect** and **Speak** in the voice channel
you are in. If any of the three is missing, `/play` says so and names the
missing permissions instead of silently failing.

Configuration commands (`/controller`, `/setvc`, `/announcechannel`, `/24-7`,
`/defaultplaylist`) require the **Manage Server** permission. Playback commands
do not, unless you turn on [DJ mode](https://flavibot.xyz/docs/modules/music/11-dj-mode).

#### Free, vote-gated and Premium

Three different levels show up throughout this module, so it is worth learning
them once.

- **Free**: works everywhere, always.
- **Vote or Premium**: the command asks you to vote for FlaviBot on top.gg. A
  vote is free and counts for 12 hours. Holding a Premium subscription, or
  being on a server that has Premium, skips the ask entirely.
- **Server Premium**: the whole server needs a FlaviBot Premium subscription
  applied to it. Voting does not help here.

Where a feature sits is stated on its own page. A few limits also change with
the server's plan:

| Limit | Free | Premium |
| --- | --- | --- |
| Tracks in the queue | 200 | 100,000 |
| Tracks taken from an imported playlist | 500 | 10,000 |
| DJ permission entries | 2 | unlimited |
| Saved playlists (per account) | 3 | unlimited |

#### Where to go next

- [Playing music](https://flavibot.xyz/docs/modules/music/02-playing-music), what you can put in
  `/play` and what happens next
- [Playback controls](https://flavibot.xyz/docs/modules/music/03-playback-controls), pause, skip,
  volume, loop
- [The queue](https://flavibot.xyz/docs/modules/music/04-the-queue), reorder, remove, clean up
- [DJ mode](https://flavibot.xyz/docs/modules/music/11-dj-mode), if you need to lock the music down

### Playing music

Source: https://flavibot.xyz/docs/modules/music/02-playing-music
Summary: What you can hand to FlaviBot's /play, the platforms it supports (FlaviBot does not support YouTube), uploaded files, TTS, radios, and how to stop playback.

FlaviBot supports Spotify, Apple Music, Deezer, Tidal and Amazon Music, plus
direct audio file URLs, radio stations and the files you upload yourself with
`/play-file`; YouTube is not supported. This page covers what you can hand to
`/play`, where the audio behind it comes from, and how to make it stop.

#### `/play`

Join a voice channel, then run `/play` with a **song name** or a **link**.

```
/play query: never gonna give you up
/play query: https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC
```

While you type, FlaviBot suggests matching tracks. Picking a suggestion is the
same as typing the title yourself, it just saves you a bad match.

If nothing is playing, the bot joins your channel and starts the track. If
something is already playing, the track goes to the end of the queue.

**`insert-first`** puts your track at the top of the queue instead, so it plays
next rather than last.

```
/play query: bohemian rhapsody insert-first: True
```

`/play` also accepts a **playlist, album or artist link**. Every track behind it
is queued in one go. Free servers take up to 500 tracks from a single link,
Premium servers up to 10,000.

As a prefix command, `/play` also answers to `p`.

#### Where the audio comes from

FlaviBot supports Spotify, Apple Music, Deezer, Tidal and Amazon Music, plus
direct audio file URLs and radio streams. Searches default to Spotify.

#### Does FlaviBot support YouTube?

No. YouTube is not supported: paste a link from one of the platforms above, or
search by title.

#### `/play-file`

Drag an audio file into the command and FlaviBot plays it.

```
/play-file file: <drop your file>
```

The track is named after the file (the extension is stripped and underscores
become spaces). `insert-first` works here too.

This one is slash only in practice: the file has to be attached to the command
itself, so the `pf` prefix form cannot read it and answers that it could not
read the attached file.

#### `/tts`

Makes the bot speak a sentence out loud in the voice channel.

```
/tts query: dinner is ready everyone
```

Requires a vote or Premium. By default the spoken message waits its turn in the
queue. The **TTS interrupts playback** setting on the dashboard changes that:
the current track pauses, the message is spoken, then the track resumes exactly
where it stopped.

#### `/radios`

FlaviBot ships a catalogue of radio stations.

| Subcommand | Does |
| --- | --- |
| `/radios list` | browse the catalogue |
| `/radios search` | find a station by name and play it |
| `/radios request` | submit a station for review, so it joins the list |

A submitted station needs a name, an official website and a genre. A stream URL,
tags and a note are optional. Submissions are approved manually, so they do not
appear instantly.

Radio Garden links pasted into `/play` work too, and so does a link to a station
page on the FlaviBot website.

#### `/start`

Opens an interactive panel with a genre dropdown holding fifteen genres (Pop Hits, Hip Hop, Rock, Electronic, Chill Vibes, Latin, K-Pop, Jazz, Classical, R&B, Indie, Metal, Country, Lo-Fi, Workout). Pick one and it starts playing, no query to type.

The panel also lets you search and reach the queue from the same message. Only
one `/start` panel exists per server: running the command again deletes the old
one and posts a fresh one.

#### `/join`

Brings the bot into your voice channel without queueing anything. Useful when
you want it connected and waiting. As a prefix command it also answers to `j`
and `connect`.

If the bot is already busy in a different voice channel, `/join` refuses and
names that channel rather than abandoning the people listening there.

#### Stopping

| Command | Effect |
| --- | --- |
| `/stop` | stops playback and clears the queue, the bot stays connected |
| `/disconnect` | leaves the voice channel entirely |

![The Discord command bar showing /stop with its description, stop the current music and clear the queue](https://cdn-assets.flavibot.xyz/docs/modules/music/stop-command.jpg)

As a prefix command, `disconnect` also answers to `dc`, `destroy` and `leave`.
Those three are prefix short forms only: in Discord's slash list the command
exists under its full name and nothing else.

`/stop` is a full reset of the session: it empties the queue, turns
[autoplay](https://flavibot.xyz/docs/modules/music/08-autoplay) off and clears any loop mode. The
bot stays in the channel, so the idle timeout below is what eventually removes
it.

If nothing is playing and nobody is left in the channel, FlaviBot disconnects on
its own after **10 minutes** of inactivity. Premium servers can shorten that
delay on the dashboard, and [24/7 mode](https://flavibot.xyz/docs/modules/music/13-24-7-mode)
removes it entirely.

#### When the bot is already in another channel

If FlaviBot is playing for people in a different voice channel, it refuses to
abandon them and tells you which channel it is in. If that channel is empty, it
moves to yours instead.

#### Common refusals

| The bot says | What to do |
| --- | --- |
| You must join a voice channel | join one first, then run the command again |
| I am missing permissions in your channel | give it View Channel, Connect and Speak there |
| No result | try the artist name plus the title, or paste a link |
| This server needs Premium / please vote | see [the overview](https://flavibot.xyz/docs/modules/music/01-overview) |
| The queue is full | free servers stop at 200 tracks |

### Playback controls

Source: https://flavibot.xyz/docs/modules/music/03-playback-controls
Summary: Pause, skip, go back, jump, seek, change the volume and loop in FlaviBot, how vote skip decides, and what to do about audio problems.

Every command here acts on the current player. You have to be in the same voice
channel as the bot.

#### Pause and resume

| Command | Effect |
| --- | --- |
| `/pause` | pauses the current track |
| `/resume` | resumes it, and answers to `unpause` as a prefix command |

The reply carries a single button that flips between **Pause** and **Resume**,
so you rarely need to type the second command. The button reads the player's
real state when clicked, so it stays correct even if somebody else paused
meanwhile.

#### Skip

`/skip` moves to the next track.

What actually happens depends on how many people are listening:

- **2 listeners or fewer**: the track is skipped immediately.
- **3 or more**: a vote starts, unless **Vote Skip** is turned off on the
  dashboard.

##### How the vote works

FlaviBot posts a public message with a **Vote** and a **Cancel** button and
counts the votes it needs:

| Listeners | Votes needed |
| --- | --- |
| 3 to 4 | 2 |
| 5 or more | 60% of them, rounded up |

The person who ran `/skip` counts as the first vote. Running `/skip` again adds
your vote, or tells you that you already voted. Clicking **Vote** a second time
takes your vote back.

The vote expires after **2 minutes** with no result. **Cancel** ends it early,
and only the person who started the vote, or someone with the **Manage
Messages** permission, can press it.

Only people in the voice channel can vote.

#### Previous

`/previous` plays the track that came before the current one. As a prefix
command it also answers to `prev`.

#### Jump

`/jump position: 5` skips straight to the fifth track in the queue, dropping
everything before it. Requires a vote or Premium. As a prefix command it also
answers to `skipto`.

#### Seek, fast forward, rewind

| Command | Does | Notes |
| --- | --- | --- |
| `/seek time: 1:30` | jumps to a timestamp | format is `H:M:S` |
| `/fastforward time: 30` | jumps forward | seconds, 10 to 3600, default 10 |
| `/rewind time: 30` | jumps backward | seconds, 10 to 3600, default 10 |

All three require a vote or Premium. None of them work on a live stream or a
radio station: there is no position to move to, and the bot says so.

Seeking past the end of the track is refused, and the reply tells you the
furthest point you can seek to.

#### Volume

`/volume` on its own prints the current level as a bar. `/volume volume: 60`
sets it. The accepted range is **1 to 200**. As a prefix command it also answers
to `vol`.

The reply carries three buttons: **-10%**, **+10%** and **100%**. They step in
tens and disable themselves at the edges (they refuse to go below 10% or above
200%). Requires a vote or Premium, and that check runs again for whoever clicks
the buttons, not just for whoever ran the command.

Premium servers can set a **default volume** on the dashboard, applied every
time the bot starts playing.

#### Loop

`/loop` on its own shows the current mode. `/loop mode: track` sets it. As a
prefix command it also answers to `repeat`.

| Mode | Effect |
| --- | --- |
| `track` | the current track repeats forever |
| `queue` | the whole queue repeats, tracks go back to the end after playing |
| `disable` | no looping |

The reply carries a dropdown to switch modes without retyping. Requires a vote
or Premium.

The dashboard has a **Default Loop Queue** switch that turns queue looping on
automatically whenever music starts.

#### Fixing audio problems

`/fix` opens a panel with three repairs, in increasing severity:

1. **Change Voice Region**, switches the voice channel's region. Needs
   **Manage Channels** on that channel, and the panel warns you upfront if the
   bot does not have it.
2. **Reconnect Bot**, rejoins the voice channel.
3. **Recreate Player**, tears the player down and rebuilds it.

The same three buttons live behind the **Dashboard** button on the
[controller panel](https://flavibot.xyz/docs/modules/music/06-now-playing-and-controller), next to a
**Debug** button that shows the same technical snapshot as `/debug music`.

#### Changing how it sounds

Speed, pitch and the effect presets are on their own page, see [audio
filters](https://flavibot.xyz/docs/modules/music/05-filters).

#### Seeing what happened

`/music-logs` lists the recent music actions on the server, who did them and
when. `/nowplaying` shows the current track as an image with a progress bar; as
a prefix command it also answers to `np`.

### The queue

Source: https://flavibot.xyz/docs/modules/music/04-the-queue
Summary: View FlaviBot's music queue, remove or reorder tracks, clean it up, see who may change it, and save it as a playlist. A free server's queue holds 200 tracks.

The queue is the list of tracks FlaviBot has waiting to play. It holds **200 tracks** on a
free server and **100,000** with Premium. When a queue is full, extra tracks are
dropped and the bot tells you how many did not fit.

#### Viewing it

`/queue` opens an interactive panel: the current track at the top, then the
queue five tracks per page, with the total count and the total listening time.
As a prefix command it also answers to `q`.

Each track has a **more** button. Opening it reveals five actions for that
track:

| Button | Effect |
| --- | --- |
| **Remove** | drops it from the queue |
| **+1** | moves it one slot earlier |
| **Top** | moves it to the front |
| **-1** | moves it one slot later |
| **Play** | moves it to the front and skips to it right away |

#### Removing one track

`/remove` removes a single track. The `query` option autocompletes from the
actual queue, so you pick the track from a list rather than counting positions.
As a prefix command it also answers to `rm`.

#### Reordering

`/move from: 4 to: 1` moves the fourth track to the first slot. Both numbers are
queue positions. The queue needs at least **3 tracks** for this to work.

`/shuffle` randomises the whole queue. Also needs at least 3 tracks. Requires a
vote or Premium.

The dashboard has an **Auto Shuffle** switch (Premium): adding more than two tracks at once re-shuffles the whole queue, not just the tracks you added. It does not apply when you use `insert-first`.

#### Cleaning up

| Command | Removes |
| --- | --- |
| `/clearqueue` | everything waiting, the current track keeps playing |
| `/removedupes` | duplicates, keeping the first copy of each track |
| `/leavecleanup` | tracks added by people who have left the voice channel |

`/clearqueue` answers to `cq` as a prefix command. The other two have no short
form.

`/removedupes` requires a vote or Premium and needs at least 3 tracks in the
queue.

#### Who can change the queue?

With [DJ mode](https://flavibot.xyz/docs/modules/music/11-dj-mode) off, anyone in the voice channel
can. With it on, these are the permissions that matter:

| Action | Permission |
| --- | --- |
| `/queue` | View Queue |
| `/remove`, `/clearqueue` | Remove Songs |
| `/move`, `/shuffle` | Move Songs |
| `/play`, `/play-file`, `/tts` | Add Songs (and Connect Bot) |

#### How do I save the queue as a playlist?

`/playlist add-queue playlist_name: <name>` snapshots everything currently
queued into one of your saved playlists, so you can replay the same set later.
See [playlists](https://flavibot.xyz/docs/modules/music/10-playlists).

### Audio filters

Source: https://flavibot.xyz/docs/modules/music/05-filters
Summary: The audio filters FlaviBot can put on playback, from bassboost and nightcore toggles to adjustable dials, how to clear them, and using them from the web player.

FlaviBot's audio filters change how the current track sounds. They apply to the player, not to
one track, so they stay on until you clear them.

Everything on this page requires a **vote or Premium**, and the **Filter
Effects** DJ permission when [DJ mode](https://flavibot.xyz/docs/modules/music/11-dj-mode) is on.

#### The toggles

Each of these flips on and off. Run the same subcommand again to undo it.

| Command | Effect |
| --- | --- |
| `/filter bassboost` | lifts the low frequencies |
| `/filter nightcore` | speeds the track up and raises its pitch |
| `/filter demon` | slows it down and drops the pitch hard |
| `/filter 8d` | rotates the sound around your head |
| `/filter karaoke` | tries to pull the vocals out |
| `/filter vibrato` | adds a pulsing wobble to the pitch |
| `/filter vaporwave` | listed, but currently applies no audio effect |

![The Discord command bar with the /filter nightcore subcommand picked, described as toggling the nightcore audio filter on or off](https://cdn-assets.flavibot.xyz/docs/modules/music/filter-nightcore.jpg)

Filters stack. Nightcore plus bassboost is a valid, loud combination.

#### The dials

| Command | Range | Effect |
| --- | --- | --- |
| `/filter speed speed: 1.5` | 0.1 to 2 | playback tempo |
| `/filter pitch pitch: 0.8` | 0.1 to 2 | pitch, without touching tempo |

`1` is normal for both. `/filter speed` is the only filter that works while
nothing is playing, so you can set the tempo before the next track starts.

#### How do I clear every filter?

![The Discord command bar with the /filter clear subcommand picked, described as clearing all audio filters](https://cdn-assets.flavibot.xyz/docs/modules/music/filter-clear.jpg)

`/filter clear` removes every filter at once and puts speed and pitch back to
normal. Faster than untoggling them one by one when a track sounds wrong and
you are not sure what is on.

#### Filters and restarts

The filters you have on are saved with the player. If the bot restarts mid
session and recovers your player, the same filters come back on with it.

#### From the web player

The [web player](https://flavibot.xyz/docs/modules/music/09-web-player) exposes the same filters as
switches. It applies the same vote or Premium requirement and the same DJ
permission, so nothing you cannot do with a command becomes possible there.

### The now playing message and the controller

Source: https://flavibot.xyz/docs/modules/music/06-now-playing-and-controller
Summary: The buttons FlaviBot posts on every now playing message, how to turn them off, and the controller channel where typing a song name plays it.

#### The now playing message

When a track starts, FlaviBot posts a message with the title, who added it, the
voice channel, the artwork and a line reading
`Queue Size: 12 · Volume: 100% · Loop: Off`.

Under it sit the controls:

| Button | Does |
| --- | --- |
| **Pause** / **Resume** | flips playback |
| **Skip** | next track, or starts a [vote](https://flavibot.xyz/docs/modules/music/03-playback-controls) |
| **Stop** | stops and clears the queue |
| **AutoPlay** | toggles [autoplay](https://flavibot.xyz/docs/modules/music/08-autoplay), lit up when on |
| **Dashboard** | opens a private panel with volume, shuffle, previous and the audio repairs |
| **Like** (heart) | saves the track to your personal Liked Songs playlist |
| **Love this** / **Not for me** | teaches the recommendation engine what you like |

The **Now playing** title is a link to the [web
player](https://flavibot.xyz/docs/modules/music/09-web-player).

##### Turning the buttons off

On the dashboard, under **Music settings**, the **Playback Controls** card has:

- **Buttons on Now Playing Messages**, off means the message is text only
- **Progress Bar Image**, off removes the generated progress bar from both the
  now playing message and the controller panel

#### The controller channel

The controller is a dedicated text channel where **typing a song name plays
it**. No slash command, no prefix. It holds one permanent panel message that
updates itself as the music changes.

##### Setting it up

```
/controller create
/controller create channel: #music
/controller remove
```

As a prefix command, `/controller` also answers to `setup`. It needs **Manage
Server** from you, and **Embed Links** plus **Manage Channels** for the bot.

With no `channel` option, FlaviBot creates a channel called `flavi-controller`
in the same category as the channel you ran the command in, with a 5 second
slowmode. Pass a channel to use an existing one instead.

`/controller remove` deletes the panel message and forgets the channel. The
channel itself stays.

You can do the same from the dashboard: **Music settings** has a **Controller
Channel** card with an optional target channel and a **Create** button.

##### Using it

Type a song name or paste a link. FlaviBot deletes your message, queues the
track and refreshes the panel.

Two things worth knowing:

- **One line per track.** A multi-line message queues every line, so you can
  paste a batch of links at once.
- **Lines starting with `> ` are ignored.** That is how you leave a comment in
  the channel without the bot trying to play it.

When something goes wrong (no result, missing permission, queue full), the bot
posts a short reply in the channel and deletes it automatically a moment later,
so the channel stays clean.

Music slash commands run in the controller channel answer privately, for the same reason. Other commands reply normally.

##### The panel

The panel shows the current track, the next track, a preview of what is queued
and the same status line as the now playing message. Its default buttons are:

| Row | Buttons |
| --- | --- |
| 1 | Volume down, Previous, Pause, Skip, Volume up |
| 2 | Shuffle, AutoPlay, Stop, Dashboard |
| 3 | Like, Not for me, Block, What's next? |

**Block** is Manage Server only. It adds the current track to the server's song
blocklist and skips it, so it never plays here again and stops being
recommended. If the blocklist was off, blocking switches it to blacklist mode.
If the server runs the blocklist in whitelist mode, the button refuses and
points you at the dashboard instead, because blocking would mean the opposite
there.

**What's next?** shows the tracks the autoplay engine is currently lining up, on servers where the new recommendation engine is enabled. Elsewhere the button reports that the preview is unavailable.

When nothing is playing, the panel shows a waiting card and a **Connect Bot**
button.

##### Customising it

The **Controller Buttons** card on the dashboard lets you pick the button style
(icon and text, text only, or icon only) and rearrange the button rows in a live
preview.

Premium servers also get a **Controller Customization** card: a custom image and
a custom footer text for the waiting card.

### Announcements and the announcement channel

Source: https://flavibot.xyz/docs/modules/music/07-announcements
Summary: Where FlaviBot posts music announcements, how to send them all to one channel, which message types get announced, and where they go when nothing is set.

By default, FlaviBot announces each track in **the channel where the music was
started from**. If someone runs `/play` in #general, the announcements land in
#general.

#### How do I send every announcement to one channel?

`/announcechannel set channel: #music` pins every music announcement to one
channel, whatever channel the commands come from.

![The Discord command bar on /announcechannel set, with the channel option highlighted and waiting for a text channel](https://cdn-assets.flavibot.xyz/docs/modules/music/announcechannel-set.jpg)

| Command | Effect |
| --- | --- |
| `/announcechannel set channel: #music` | always announce in that channel |
| `/announcechannel reset` | back to the default behaviour |
| `/announcechannel move channel: #other` | move the current session only |

All three need **Manage Server**. `set` and `reset` require a vote or Premium,
because they change a saved setting. `move` is free: it only redirects the
session that is playing right now and forgets it afterwards.

The same setting lives on the dashboard, under **Music settings**, as
**Announcement channel**. Leaving it empty is the same as `reset`.

#### Choosing what gets announced

The **Announcements** card on the dashboard has one switch per message type.

| Setting | What it controls |
| --- | --- |
| **Now Playing Announcements** | post a message when a track starts |
| **Delete Announce Song** | delete that message once the track ends |
| **Announce Queue End** | say something when the queue runs out |
| **Stage Announce** | show the current track in the voice channel status |
| **Silent Announce Message** | post without pinging anyone's notifications |
| **TTS Track Announcements** | also announce tracks played through `/tts` |

**Delete Announce Song** is the one to turn on if a busy queue is burying your
chat: each announcement disappears when its track finishes, so at most one is on
screen.

**Silent Announce Message** is the gentler option: the messages stay, they just
stop lighting up the channel for everyone.

If you would rather have no announcements in a normal channel at all, set up a
[controller channel](https://flavibot.xyz/docs/modules/music/06-now-playing-and-controller): the
panel there replaces them.

#### Where announcements go when nothing is set

The bot remembers the channel that started the session. `/stop` followed by
`/play` in a different channel is therefore a quick way to move the
announcements without touching any setting.

### Autoplay

Source: https://flavibot.xyz/docs/modules/music/08-autoplay
Summary: Keep FlaviBot playing when the queue runs out: turning autoplay on, its three modes, personal and server defaults, and teaching or blocking what it picks.

Autoplay keeps the bot playing after the last queued track. Instead of going
quiet, FlaviBot picks a track that fits what has been playing and queues it.

#### How do I turn autoplay on?

`/autoplay` opens a small panel with a green **Enable Autoplay** button, a mode
dropdown, and the current state. The panel needs a live player: with the bot not
connected it answers that there is no player. As a prefix command it also
answers to `autop`.

The **AutoPlay** button on the [now playing
message](https://flavibot.xyz/docs/modules/music/06-now-playing-and-controller) and on the
controller panel is the same toggle. It lights up when autoplay is on.

Toggling it on while the bot is connected with an empty queue starts a
recommendation right away, so you do not need a `/play` first.

Autoplay requires the **Toggle Autoplay** DJ permission when [DJ
mode](https://flavibot.xyz/docs/modules/music/11-dj-mode) is on.

#### The three modes

| Mode | Picks | Availability |
| --- | --- | --- |
| **Mix** | a balance of similar tracks and new discoveries | free |
| **Similar** | stays close to the current vibe | Premium server |
| **Discover** | pushes towards tracks you do not know yet | Premium server |

Set yours with the panel dropdown, or directly:

```
/autoplay mode: discover
```

Unlike the toggle, `/autoplay mode:` works without an active player.

#### Personal choice versus server default

The mode is resolved in this order:

1. **Your own** setting, if you have picked one. It follows you across servers.
2. The **server default**, set on the dashboard.
3. **Mix**, if neither is set.

The panel shows which of the three is in play. When you have a personal
override, a **Reset** button appears to drop it and fall back to the server
default.

Server owners set the default on the dashboard, under **Music settings**, in the
**Autoplay Mode** card.

#### Starting autoplay automatically

The **Default Auto Play** switch in the **Playback Controls** card (Premium)
turns autoplay on for every new session, so nobody has to remember to press the
button.

#### Teaching it what you like

Two buttons under the now playing message feed the recommendations: **Love
this** and **Not for me**. Picking **Not for me** asks you briefly why (the
artist, the genre, or just this song) so the engine knows how wide to apply it.

The engine learns most from tracks that were deliberately picked or searched
for. Tracks queued in bulk from a playlist, or filler queued by 24/7 mode, count
for much less, so a single long playlist will not drown out what your members
actually listen to.

The **What's next?** button shows the candidates the engine is currently lining
up, with a short reason for each.

#### Blocking what it suggests

The **Block** button on the controller panel (Manage Server only) removes a
track from the server and stops it being recommended again. The server song
blocklist on the dashboard does the same for whole artists, and autoplay
respects it.

#### Generating a playlist instead

If you want the recommendations saved rather than played once, `/playlist
autoplay` builds a playlist from a seed track. See
[playlists](https://flavibot.xyz/docs/modules/music/10-playlists).

### The web player

Source: https://flavibot.xyz/docs/modules/music/09-web-player
Summary: Control FlaviBot's music from a browser: opening the web player, how it finds your voice session, what you can do there, and turning it off for a server.

The web player is a full interface for the same music session your server is
listening to. Search, queue, reorder, filters, lyrics and playback controls, all
in a browser, updating live.

It lives at **flavibot.xyz/music-player**.

#### How do I open the web player?

- Run `/music-panel`, which replies with the link and a button. As a prefix
  command it also answers to `musicpanel`.
- Or click the **Now playing** title on an announcement or on the controller
  panel.
- Or go to the address directly.

You need to be signed in with Discord.

#### Finding your session

The player does not ask you which server you are on. It looks at **the voice
channel you are currently in** and opens that session. If you are in a voice
channel where no FlaviBot is connected yet, it offers to connect one.

Three things have to be true for it to work:

1. You are signed in and a member of the server.
2. You are in a voice channel.
3. You are in **the same voice channel as the bot**.

If any is missing, the page says which one. If nobody has started the music yet,
a **Connect Bot** button does it.

#### What you can do

| Area | Contents |
| --- | --- |
| **Queue** | the live queue, reorder by dragging, remove, jump to a track |
| **Playlists** | your saved playlists and your Spotify playlists |
| **Radios** | the same station catalogue as `/radios` |
| **Statistics** | what has been played on the server |
| **Sessions** | past listening sessions, replayable into the queue |

The player bar handles play and pause, skip, previous, seek by dragging the
progress bar, volume, shuffle and loop. A side panel carries the
[filters](https://flavibot.xyz/docs/modules/music/05-filters) and another shows **synced lyrics**
when they exist.

The **Tools** menu next to the queue holds the bulk actions: remove duplicates,
remove tracks of absent users, text to speech, and uploading an audio file to
play. Uploads must be audio and stay under 25 MB.

#### Permissions still apply

The web player is not a way around the rules:

- [DJ mode](https://flavibot.xyz/docs/modules/music/11-dj-mode) is enforced. Buttons you are not
  allowed to use are greyed out rather than failing on click.
- The vote or Premium requirements are the same as for the equivalent commands.
  Volume, loop, filters, seek and text to speech ask for the same thing there as
  they do in Discord.

#### How do I turn the web player off for a server?

The **Web Player** switch under **Playback Controls** on the **Music settings**
dashboard page controls it. It is on by default. Turning it off means members of
that server get "Web player is disabled" instead of the interface.

### Playlists

Source: https://flavibot.xyz/docs/modules/music/10-playlists
Summary: Personal and server playlists in FlaviBot: create, fill, play, reorder, share and delete them, who can change a server playlist, and playlists built for you.

A playlist belongs either to **you** or to a **server**.

A personal playlist follows you: create it on one server and you can play it on
any other server where FlaviBot is present. A [server
playlist](#server-playlists) belongs to the server itself — everyone there sees
it, and the roles you choose can add or remove tracks.

Everything lives under a single command, `/playlist`, with subcommands. Every
`playlist_name` option autocompletes, marking whose playlist each one is:
nothing for your own, 🔗 for one shared with you, and 🏠 for one this server
owns.

#### How do I create a playlist?

```
/playlist create playlist_name: road trip
```

Names go up to 80 characters and have to be unique among your own playlists.

Free accounts can hold **3 playlists**. Premium removes the limit. The automatic
**Liked Songs** playlist counts towards that 3, so a free account has room for
two of its own.

The reply carries a link to the playlist page on the website, which is where
richer editing lives.

#### Adding songs

```
/playlist add-song playlist_name: road trip query: africa toto
```

`query` takes a song name, a track link, or an entire external playlist link,
in which case every track from it is added at once.

To save what you are listening to right now:

```
/playlist add-queue playlist_name: road trip
```

The **heart button** on the now playing message adds the current track to your
**Liked Songs** playlist, which FlaviBot creates for you the first time you use
it. Pressing it again on the same track removes it.

#### Playing one

```
/playlist play playlist_name: road trip
```

Same rules as `/play`: you must be in a voice channel. If the playlist is longer
than what the queue can hold, the extra tracks are dropped and the bot tells
you.

#### Looking at them

| Command | Shows |
| --- | --- |
| `/playlist list` | your playlists, a preview of Liked Songs, playlists shared with you, and this server's own |
| `/playlist view playlist_name: road trip` | the tracks inside one playlist |

`/playlist list` also prints how many of your allowance you have used. Next to
each server playlist it shows what **you** may do with it, worked out from your
roles.

#### Reordering and removing

`/playlist shuffle playlist_name: road trip` permanently randomises the stored
order.

`/playlist remove-song` gives you the link to the playlist page: removing
individual tracks is done there, where you can see the whole list.

#### How do I delete a playlist?

```
/playlist delete playlist_name: road trip
```

Liked Songs cannot be deleted, since FlaviBot uses it as your like target.

#### How do I share a playlist?

```
/playlist visibility playlist_name: road trip visibility: public
```

**Public** means anyone with the link can play the playlist. It does not put the
playlist in other people's autocomplete, and it does not let them edit it.

Letting someone actually add, remove or reorder tracks is a separate, per-person
permission you grant on the playlist's page on the website. Playlists shared
with you that way show up in your `/playlist list` with the rights you were
given.

For a playlist a whole server should be able to edit, use a [server
playlist](#server-playlists) instead — sharing it with everyone one person at a
time is not the same thing.

#### Server playlists

A server playlist belongs to the **server**, not to whoever created it. Everyone
on that server sees it in `/playlist` and can play it, and it only ever appears
there — not in another server, not even for the person who made it.

Creating one needs the **Manage Server** permission:

```
/playlist create playlist_name: friday night owner: This server
```

The optional `members_can` option sets what **@everyone** may do from the start:
view and play only, **add songs** (the default), add and remove, or also
reorder. Adding is the default because it is undoable — someone can always ask
for a track to be taken back out — while removing is destructive and anonymous.

##### Who can change it

Permissions are **per role** and they add up, exactly like Discord's own role
permissions: a member gets everything their roles give them, put together. There
is no "deny" — a role either grants something or says nothing about it.

```
/playlist perms playlist_name: friday night
```

opens a panel where you pick a role and tick what it can do:

| Permission | Lets the role |
| --- | --- |
| Add | add tracks, and save the current queue into the playlist |
| Remove | take tracks out |
| Reorder | shuffle and move tracks around |
| Manage | rename it, change its visibility, delete it, and edit these permissions |

**@everyone** is always listed and cannot be removed — locking a playlist down
means unticking everything for it, which leaves the playlist visible and
playable but read-only. **Manage** is the one permission @everyone cannot be
given, for the same reason: it would let any member delete the playlist.
Granting it to a specific role is a real delegation — that role can delete it.

Roles are the only thing that grants anything on a server playlist. The
per-person sharing that personal playlists use does not apply here, so there is
never a second, invisible list of people with rights.

Anyone with **Manage Server** can always do everything, whatever the roles say.
The person who created the playlist gets no special rights: the server owns it,
so a moderator who leaves or is demoted takes nothing with them.

The same permissions are editable on the dashboard, under **Music → Server
Playlists**.

##### Moving a playlist in or out

```
/playlist transfer playlist_name: road trip to: This server
```

hands one of your playlists to the server, and `to: Me` takes one back. Both
directions need **Manage Server**, and both check that the receiving side has
room and no playlist with that name already.

Moving a playlist clears every permission on it, in both directions, and a
playlist that was public becomes private again on the way in — the server never
asked to publish it. Taking one back makes you its owner, and if it was the
server's 24/7 default that setting is cleared too.

How many server playlists a server can hold depends on its premium tier, and it
is counted on the **server**, not on the person creating them.

#### Playlists FlaviBot builds for you

| Command | Builds |
| --- | --- |
| `/playlist autoplay query: <seed track>` | a playlist around one track you name |
| `/playlist vibe` | a blend of the tastes of everyone in your voice channel |
| `/playlist server-discover` | this server's automatically refreshed discovery playlist |
| `/playlist top50` | FlaviBot's global top 50 |

`/playlist autoplay` accepts an optional `name` and a `count` between 5 and 50
(20 by default). The result is saved as a normal playlist of yours, so it counts
towards your allowance.

`/playlist vibe` needs **at least 2 humans** in your voice channel, blends up to
10 of them, and saves a 25 track playlist. With nobody else in the channel it
points you at `/playlist autoplay` instead.

`server-discover` and `top50` play their playlist directly rather than saving a
copy for you. They are regenerated on a schedule, so they change over time.

#### Spotify playlists

`/playlist spotify` lists your public Spotify playlists with a **Play** button
each. It needs your Spotify account linked first, see
[Spotify](https://flavibot.xyz/docs/modules/music/14-spotify).

#### The web player

The **Playlists** tab of the [web player](https://flavibot.xyz/docs/modules/music/09-web-player)
does all of this with a mouse, including adding a track to a playlist straight
from the queue.

### DJ mode

Source: https://flavibot.xyz/docs/modules/music/11-dj-mode
Summary: FlaviBot DJ mode decides who may play, skip, stop or change the volume: turning it on, how allow and deny entries combine, the actions, and a worked example.

With DJ mode **off**, anyone in the voice channel can use the music commands.
That is the default and it suits most servers.

With DJ mode **on**, every music action is blocked unless a rule allows it. You
then hand out actions to roles and to individual members.

#### How do I turn DJ mode on?

Go to the dashboard, **Music**, then **DJ Mode**, and flip the switch at the top
of the page. That page is also where every permission is configured.

`/djmode` in Discord shows the current state: on or off, what `@everyone` is
allowed to do, how many roles and members have their own entry, and a button
that opens the dashboard page. It is read only, so anyone can run it, and it
changes nothing.

> **Careful.** Turning DJ mode on with nothing allowed blocks **every** music
> command for **everyone**. The dashboard and `/djmode` both warn you when the
> server is in that state. Allow at least the basics on the `@everyone` base
> level.

#### How a decision is made

Permissions are a list of entries, each one attached to a member, a role, or the
`@everyone` base level. Each entry sets every action to **Allow**, **Deny** or
**Inherit**.

For a given action, FlaviBot walks the entries that apply to the member, in
priority order:

1. The member's **own entry**, if they have one.
2. Their **roles**, in the order you arranged them.
3. The **`@everyone`** base level.

The first **Deny** it meets refuses immediately. The first **Allow** it meets
grants the action. **Inherit** means "no opinion", so it keeps looking. If it
reaches the end with no explicit Allow, the action is **denied**.

That last rule is the one to remember: DJ mode is deny by default. Silence is
not permission.

Roles are dragged into order on the dashboard, and the order is what sets
priority. A **Deny** on a high-priority role beats an **Allow** further down.

#### All Permissions

Every entry has an **All Permissions** switch. Allowing it grants every music
action, including ones added later. Denying it blocks everything for that entry,
whatever the individual actions say.

#### The actions

| Permission | Covers |
| --- | --- |
| Connect Bot | `/join`, and starting playback |
| Add Songs | `/play`, `/play-file`, `/tts` |
| Remove Songs | `/remove`, `/clearqueue` |
| Disconnect | `/disconnect` |
| Pause/Resume | `/pause`, `/resume` |
| Stop | `/stop` |
| Skip | `/skip`, `/previous`, `/jump` |
| Volume | `/volume` |
| View Queue | `/queue` |
| Move Songs | `/move`, `/shuffle` |
| Toggle Loop | `/loop` |
| Toggle Autoplay | `/autoplay` |
| Seek | `/seek`, `/fastforward`, `/rewind` |
| Filter Effects | `/filter` |
| Fix Audio | `/fix` |

One special case: **Connect Bot** is treated as allowed when the bot is already
connected to a voice channel, so someone who may add songs but not summon the
bot can still queue tracks into a running session.

#### A worked example

A common setup: everyone may queue and vote, only the DJ role may control
playback.

1. On the `@everyone` base level, allow **Connect Bot**, **Add Songs** and
   **View Queue**. Leave the rest on Inherit.
2. Add your **DJ** role and allow **All Permissions** on it.

Members get music, the DJ role gets control, and nobody has to be an
administrator.

To silence a specific troublemaker without touching roles, add them as a
**member** entry and deny what they abused. Member entries are always checked
first, so they win over any role they hold.

#### How many entries you get

Free servers get **2** permission entries, and the `@everyone` base level counts
as one of them. Premium removes the limit.

#### Where DJ mode applies

Everywhere: slash commands, prefix commands, the buttons on the now playing
message, the controller panel and the [web
player](https://flavibot.xyz/docs/modules/music/09-web-player). The web player greys out what you
are not allowed to do instead of failing after the click.

Music actions run by an [autoresponder](https://flavibot.xyz/docs/modules/autoresponder/01-overview)
are the one exception. They were configured by someone with Manage Server, and
they have no member in a voice channel, so they are not subject to DJ mode.

### Restricting the bot to certain voice channels

Source: https://flavibot.xyz/docs/modules/music/12-voice-channel-restrictions
Summary: Keep FlaviBot's music in the voice channels you choose with /setvc or the dashboard, including categories, what members see and when the list is checked.

By default FlaviBot follows whoever asks it to play, into any voice channel it
can join. If you want the music confined to one or two channels, you can list
the channels it is allowed to move into.

#### With commands

```
/setvc set channel: Music
/setvc view
/setvc reset
```

All three need **Manage Server**.

**`set` is a toggle.** Running it on a channel that is not in the list adds it.
Running it again on the same channel removes it. There is no separate remove
subcommand.

**`view`** lists what is currently allowed, separating channels from categories.

**`reset`** clears the whole list at once and takes no channel. Running
`/setvc reset` puts the server back to "the bot can join anywhere".

#### With the dashboard

**Music settings** has a **Restricted Voice Channels** card. Pick voice channels
or categories from the selector. An empty list means no restriction, and the
card says so.

#### Categories count too

Selecting a **category** allows every voice channel inside it, including ones
created later. That is the low-maintenance option for servers with a "Music"
category.

#### What members see

With a list configured, someone in a channel that is not on it gets a refusal
naming the allowed channels, so they know where to go.

#### When it is checked

The list is enforced both when the bot is asked to **join** a channel and when
it is asked to **move** to another one while already connected. Either way the
refusal names the allowed channels.

It does not replace Discord's own permissions. If you want to be certain the bot
can never enter a channel under any circumstance, also remove its **Connect**
permission on that channel in Discord's channel settings.

#### Related settings

- [24/7 mode](https://flavibot.xyz/docs/modules/music/13-24-7-mode) pins the bot to one channel
  permanently, which is a stronger version of the same idea.
- **Bot Deaf**, in the **Playback Controls** card, makes the bot deafen itself
  when it joins, which hides the headset icon and makes it obvious it is not
  listening.

### 24/7 mode

Source: https://flavibot.xyz/docs/modules/music/13-24-7-mode
Summary: Keep FlaviBot connected to one voice channel with 24/7 mode: the Premium requirement, the commands, reconnects, and what it plays when the queue is empty.

Normally FlaviBot leaves a voice channel once the music stops and the channel
empties. 24/7 mode pins it to one channel instead: it stays connected, and it
reconnects on its own after a restart or a disconnect.

#### Does 24/7 mode need Premium?

Yes. 24/7 mode requires **FlaviBot Premium on the server**. Voting does not
unlock it.

#### How do you turn on 24/7 mode?

```
/24-7 enable
/24-7 disable
/24-7 connect
```

As prefix commands, `/24-7` also answers to `247` and `24/7`. All three
subcommands need **Manage Server**.

`enable` uses **the voice channel you are currently in**, so join it first. The
bot connects immediately and stays.

`connect` sends the bot back to the configured channel without changing any
setting. Use it if it is not where it should be.

`disable` turns the mode off and disconnects the bot.

#### From the dashboard

**Music settings** has a **24/7 Mode** card: a switch and a voice channel
picker. Saving connects the bot right away. The card warns you if you enable the
mode without choosing a channel, since 24/7 stays inactive until one is set.

#### What happens when FlaviBot is disconnected in 24/7 mode?

FlaviBot re-checks its 24/7 servers **every 30 minutes** and reconnects any bot
that is no longer in a voice channel. Once a day it goes further and re-applies
the whole setting to the bots that are connected, so one that drifted into the
wrong channel is put back.

If it cannot get in, the dashboard card shows why, in plain terms:

- the configured voice channel no longer exists
- the bot cannot view, join or speak in that channel

##### What can switch 24/7 off, and what only warns

Exactly one failure switches the mode off on its own: **the configured voice
channel no longer exists**. That one is terminal, since the setting points at a
channel that cannot come back, and it has to keep happening for **7 days with no
successful connection in between**. The card then says 24/7 was turned off
automatically. Nothing else is cleared, so getting back is two steps: point the
card at a voice channel that exists, and switch the mode on again.

**Missing permissions never switch it off.** If the bot cannot view, join or
speak in the channel, the card warns you and keeps warning you, for as long as it
takes. That is deliberate: a permission is one click away from being fixed, and
cutting the feature off would be the wrong answer.

Everything that is not about your configuration counts for nothing either way. A
FlaviBot restart, a moment where the bot cannot be reached, or a server whose
Premium has lapsed neither builds the 7 day streak nor clears it. Only a real
configuration failure builds it, and only a real connection clears it.

Any successful connection resets the streak and removes the warning, so a bad
afternoon costs you nothing. So does fixing it by hand: turning the mode back on
clears the streak and the "turned off automatically" notice, whether you do it
from the dashboard, with `/24-7 enable`, or from the web player, and simply
**picking a different voice channel** restarts the 7 day window from zero.

If the server's Premium subscription lapses, 24/7 stops holding the connection
and the normal idle timeout applies again.

#### What does FlaviBot play when the queue is empty?

An empty 24/7 channel is a silent one, unless you give the bot something to
play. There are three options, all Premium.

##### Default songs

The **Default Songs** card on the dashboard takes up to **10** entries, each a
song name or a link. They are queued in order whenever the bot connects in 24/7
mode with nothing to play.

##### Default playlist

Pick one of your own saved playlists and the bot starts it whenever it connects
idle.

```
/defaultplaylist state: True playlist-name: lounge
/defaultplaylist
```

`/defaultplaylist` with no options shows the current setting. `state` turns the
feature on or off, `playlist-name` chooses the playlist. The command requires
**Manage Server** and the server needs Premium. The same setting lives in the
**Default Playlist** card on the dashboard.

The playlist has to be one of yours, see
[playlists](https://flavibot.xyz/docs/modules/music/10-playlists).

##### Autoplay

Turning on [autoplay](https://flavibot.xyz/docs/modules/music/08-autoplay) makes the bot pick its own
tracks when the queue empties, which pairs naturally with 24/7 mode.

#### How long does FlaviBot stay in a voice channel without 24/7 mode?

Without 24/7, FlaviBot disconnects after **10 minutes** of inactivity, meaning
nothing playing, or nobody left in the channel.

Premium servers can shorten that on the dashboard with the **Inactive Disconnect
Timeout** slider, down to 1 minute. Ten minutes is both the default and the
maximum.

The bot posts a short notice when it disconnects this way, so nobody wonders
where it went.

#### Related

- [Restricting the bot to certain voice channels](https://flavibot.xyz/docs/modules/music/12-voice-channel-restrictions)
- [The announcement channel](https://flavibot.xyz/docs/modules/music/07-announcements), worth
  setting so a permanently connected bot does not scatter messages around

### Spotify

Source: https://flavibot.xyz/docs/modules/music/14-spotify
Summary: Play Spotify tracks, albums and playlists with FlaviBot, link your Spotify account to reach your own public playlists, and copy one into a FlaviBot playlist.

Spotify is FlaviBot's default source. You do not need to link anything to use
it.

#### How do I play a Spotify link?

Paste it into `/play`:

```
/play query: https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC
```

![A Spotify track link pasted into the query option of /play, with FlaviBot offering the resolved track title and duration above the command bar](https://cdn-assets.flavibot.xyz/docs/modules/music/spotify-link-in-play.jpg)

Track, album and playlist links all work. An album or playlist link queues
everything in it, up to 500 tracks on a free server and 10,000 on a Premium one.

To get a link in the Spotify app, open the three dot menu on a track, album or
playlist and choose **Share**, then **Copy link**.

You do not have to use links at all: a plain search like
`/play query: fleetwood mac dreams` already searches Spotify.

#### How do I link my Spotify account?

Linking lets FlaviBot list **your own public Spotify playlists** so you can play
them with a button instead of hunting for links.

```
/spotify set-id id: yourusername
```

The command accepts your Spotify **username**, a link to your profile
(`https://open.spotify.com/user/...`), or a `spotify:user:...` URI, so whichever
one you happen to have copied will work.

Where to find your username:

- **Mobile app**: Home, your picture at the top left, Settings and privacy,
  Account, Username.
- **Web player**: your picture at the top right, Account, Edit profile, the
  Username field.

The three steps in the Spotify web player, in order:

![The Spotify profile menu opened from the picture at the top right, with Account as the first entry](https://cdn-assets.flavibot.xyz/docs/modules/music/spotify-profile-menu-account.png)

![The Spotify account page, where Edit profile is the first entry of the Account card](https://cdn-assets.flavibot.xyz/docs/modules/music/spotify-account-edit-profile.jpg)

![The Spotify Edit profile page, with the Username field holding the long string of letters and digits to give to FlaviBot](https://cdn-assets.flavibot.xyz/docs/modules/music/spotify-edit-profile-username.jpg)

FlaviBot confirms with the display name of the account it found. If that is not
you, you copied the wrong id, so run the command again with the right one.

`/spotify remove-id` unlinks the account.

Note that this is a one way lookup of a public profile. FlaviBot does not sign
into your Spotify account, cannot see your private playlists, and cannot change
anything there.

#### Playing your playlists

```
/playlist spotify
```

This lists your public Spotify playlists, five per page, each with a **Play**
button that queues it. Arrows move between pages.

Three things can go wrong, and the reply says which:

| Message | Cause |
| --- | --- |
| No Spotify account linked | run `/spotify set-id` first |
| User not found | the id is wrong, or Spotify is having trouble |
| No public playlist | your playlists exist but are all private |

That last one is the common one. Spotify playlists are private by default. In
the Spotify app, open the playlist, use the three dot menu, and make it public.
FlaviBot can only see what Spotify shows to the public.

The [web player](https://flavibot.xyz/docs/modules/music/09-web-player) shows the same list in its
**Playlists** tab.

#### How do I copy a Spotify playlist into a FlaviBot playlist?

If you would rather own the tracks in a [FlaviBot
playlist](https://flavibot.xyz/docs/modules/music/10-playlists), hand the Spotify link to
`add-song`:

```
/playlist add-song playlist_name: chill query: https://open.spotify.com/playlist/...
```

Every track from the Spotify playlist is added to yours in one go, and from then
on it is yours to reorder and share.

### How moderation works

Source: https://flavibot.xyz/docs/modules/moderation/01-overview
Summary: How FlaviBot moderation works: the case and action it records for every sanction, the commands, who is allowed to moderate, and the guard rails in place.

Moderation in FlaviBot is built around one idea: **nothing happens without a
record**. Every sanction you take leaves a numbered **case** that stays in your
server's history, whether it was typed by a moderator, handed out by a Discord
AutoMod rule, or applied automatically by the bot.

That record is what makes the rest possible: a member's past is one command
away, a mistake can be revoked, and repeated offences can trigger a heavier
sanction on their own.

#### The two things that get recorded

| Record | What it is | Gets a case number |
| --- | --- | --- |
| **Case** | a note or a warn written against a member | yes |
| **Action** | what was actually done on Discord: timeout, kick, ban, unban | only through its case |

A timeout, a kick and a ban each record **both**: the action itself, and a warn
attached to it. So a ban is not only a ban, it is also a strike on the member's
record, and it counts toward
[auto-escalation](https://flavibot.xyz/docs/modules/moderation/03-settings) exactly like a manual
warn does.

Undoing something (`/unmute`, `/unban`) is recorded too, but as a reversal
rather than a judgment: it has no case number and does not count as a strike.

#### The commands

| Command | What it does | You need | The bot needs |
| --- | --- | --- | --- |
| `/note` | private staff note, never seen by the member | Manage Messages | nothing beyond Embed Links |
| `/warn` | formal warning | Timeout Members | nothing beyond Embed Links |
| `/mute` | Discord timeout for a duration | Timeout Members | Timeout Members |
| `/unmute` | lifts the timeout | Timeout Members | Timeout Members |
| `/kick` | removes the member | Kick Members | Kick Members |
| `/ban` | permanent ban | Ban Members | Ban Members |
| `/tempban` | ban that lifts itself after a duration | Ban Members | Ban Members |
| `/softban` | ban then instant unban, to purge messages | Ban Members | Ban Members |
| `/unban` | lifts a ban, by user ID | Ban Members | Ban Members |
| `/case` | view, edit or revoke a case | Timeout Members | nothing beyond Embed Links |
| `/modhistory` | a member's full record | Timeout Members | nothing beyond Embed Links |

`/mute` and `/unmute` also answer to `timeout` and `untimeout` as prefix
commands. Those two short forms exist only with the prefix, not in Discord's `/`
list.

Each one is covered in [The sanctions](https://flavibot.xyz/docs/modules/moderation/02-sanctions).

##### What the bot needs, exactly

Every command in the table replies with an embed, so the bot needs **Embed
Links** in the channel you run it from. That is the *whole* requirement for
`/note`, `/warn`, `/case` and `/modhistory`: a note and a warn are records in
FlaviBot, they change nothing on Discord, so no moderation permission is
involved and there is nothing for the bot to be denied.

The commands that really do something on Discord are checked before anything
runs:

- `/mute` and `/unmute` need the bot to have **Timeout Members** on the server
- `/kick` needs **Kick Members**
- `/ban`, `/tempban`, `/softban` and `/unban` need **Ban Members**

That check is server-wide, not per channel, and it fails closed: if the bot's
permissions cannot be read yet, the command is refused rather than attempted.
When the bot has the permission but Discord still refuses the action, usually
because the target sits above the bot in the role list, the reply says Discord
rejected it and gives the status.

#### Who is allowed to moderate

The **You need** column in the table above is what decides it. A member without
that permission is refused before anything runs. Administrators count as having
every permission, though the guard rails below still apply to them.

The Moderation page also offers a **Moderator roles** field. Be aware that
adding a role there does not, on its own, let its members run the commands: the
Discord permission is still checked first. Give your staff the real Discord
permission.

#### The guard rails

Even with the right permission, a manual sanction is refused when:

- the target is **you**
- the target is **the bot**
- the target is **the server owner**
- the target's highest role is **equal to or above yours**
- **the bot itself** lacks the Discord permission for that action, which only
  ever applies to `/mute`, `/unmute`, `/kick`, `/ban`, `/tempban`, `/softban`
  and `/unban`

The first four apply to a note and a warn too: they record something about a
member, so FlaviBot still refuses to let you write one against yourself, the
bot, the owner, or someone ranked above you.

There is one more case worth recognising: right after the bot restarts, its view
of your server can still be loading. Rather than guess, it refuses the command
and tells you to try again in a few seconds. That is deliberate, a moderation
check that cannot be verified is never waved through.

#### Where things live in the dashboard

| Page | What you do there |
| --- | --- |
| **Moderation** | [mod-log channel, DM behaviour, appeal link, warn decay, auto-escalation](https://flavibot.xyz/docs/modules/moderation/03-settings) |
| **Cases & history** | [search cases, read one, fix a reason, revoke](https://flavibot.xyz/docs/modules/moderation/04-cases) |
| **Discord AutoMod** | [Discord's own filters, and the warns they hand out](https://flavibot.xyz/docs/modules/moderation/05-automod) |
| **Server Logs** | [the written record of everything else happening on the server](https://flavibot.xyz/docs/modules/moderation/06-server-logs) |

Automations can moderate too. An autoresponder rule can warn, timeout, kick or
ban, and those land in the same history as a manual sanction. See the
[action reference](https://flavibot.xyz/docs/modules/autoresponder/11-action-reference).

### The sanctions

Source: https://flavibot.xyz/docs/modules/moderation/02-sanctions
Summary: Every FlaviBot sanction command, from note, warn and mute to kick, ban, tempban and softban, plus unban, the duration syntax and prefix use.

Every command below leaves a record, posts to the
[mod-log](https://flavibot.xyz/docs/modules/moderation/03-settings) and, where relevant, tells the
member what happened. The reply in the channel is short on purpose: who was
sanctioned, what was done, and the case number when there is one.

A **reason** is accepted on every command and is capped at 1500 characters. It
is optional everywhere except `/note` (where it *is* the note) and
[`/case revoke`](https://flavibot.xyz/docs/modules/moderation/04-cases).

#### Durations

`/mute` and `/tempban` take a duration written as **one number and one unit**:

| Unit | Means | Example |
| --- | --- | --- |
| `s` | seconds | `30s` |
| `m` | minutes | `10m` |
| `h` | hours | `1h` |
| `d` | days | `7d` |
| `w` | weeks | `2w` |

Combinations like `1h30m` are not accepted. Round to the nearest single unit
(`90m` works).

#### Note

`/note user <reason>` writes a private staff observation (the `reason` option is the note text).

- the member is **never** told, and nothing changes on Discord
- the reply is visible only to you
- it does **not** count as a strike toward auto-escalation
- it appears in the mod-log only if you turned on "Show notes in mod-log"

Use it for context you want the next moderator to have: "warned verbally", "says
they share the account with a sibling".

#### Warn

`/warn user [reason]` records a formal warning. The member is sent a DM by
default, the case lands in the mod-log, and the warn counts as an active strike
until it is revoked or it decays.

Warns are what drive [auto-escalation](https://flavibot.xyz/docs/modules/moderation/03-settings): a
member who reaches the threshold you configured is sanctioned automatically.

#### Mute (Discord timeout)

`/mute user <duration> [reason]` applies Discord's native timeout, so the member
can no longer talk, react or join voice. It is Discord's own mechanism, not a
muted role, so nothing to set up and nothing to clean up.

Discord caps timeouts at **28 days**. Ask for more and the command refuses and
points you to `/tempban`.

A mute records a warn as well as the timeout, so it counts as a strike.

`/unmute user [reason]` lifts it. That is a reversal: no new case number, and
the original mute is marked as reverted in the history.

#### Kick

`/kick user [reason]` removes the member from the server. They can come back
with a new invite.

The DM goes out **before** the kick, because once someone is out of the server
the bot can no longer open a conversation with them.

#### Ban

`/ban user [reason] [delete_messages_days]` bans permanently.

`delete_messages_days` accepts **0 to 7** and defaults to 0. It is Discord's own
message purge: set it to 1 to wipe the last day of the banned member's messages.

Here too the DM is sent before the ban. If the target is not on your server (a
ban by ID of someone who never joined), the DM is skipped, the ban still
happens.

#### Tempban

`/tempban user <duration> [reason] [delete_messages_days]` is a ban that lifts
itself. FlaviBot schedules the unban when it applies the ban, so it survives a
bot restart: the countdown is stored, not held in memory.

Unlike `/mute`, there is no 28 day ceiling. This is the command for a suspension
measured in weeks or months.

If you unban the member by hand before the timer runs out, the pending automatic
unban is cancelled, so the member is not "unbanned twice". And if the ban is
applied on Discord but FlaviBot fails to record it, the ban is lifted back
rather than left in place forever with no timer attached.

#### Softban

`/softban user [reason] [delete_messages_days]` bans and immediately unbans. The
point is not the ban, it is the message purge that comes with it: it is the fast
way to clear a spam or raid flood from a member you want to let back in.

`delete_messages_days` defaults to **7** here (the maximum), where `/ban`
defaults to 0.

If the ban goes through but the unban step fails, the command tells you so
explicitly and asks you to run `/unban`. The member is left banned until you do,
which is rare but worth knowing.

#### Unban

`/unban user_id [reason]` takes a **user ID**, not a mention, because the person
is not on the server to be picked from a list.

It lifts the ban, marks the original ban or tempban as reverted, and cancels a
tempban's pending automatic unban if one was scheduled.

#### Calling them without a slash

If you use FlaviBot with a message prefix rather than slash commands, `mute`
also answers to `timeout`, and `unmute` to `untimeout`.

### Mod-log, DMs and auto-escalation

Source: https://flavibot.xyz/docs/modules/moderation/03-settings
Summary: Where moderation events are posted, what the sanctioned member is told, and how warns turn into automatic sanctions.

Everything on this page lives on the dashboard's **Moderation** page. Saving it
requires the Manage Server permission, since these settings decide who gets
banned automatically.

#### The mod-log channel

The mod-log is your staff's paper trail: one message per moderation event, in
one channel, in order.

Pick it under **Mod-log channel**. The bot needs **Send Messages** and **Embed
Links** there. Text, announcement, voice and stage channels all work, and so do
forum and media channels:

- pick a **forum** and FlaviBot opens a new post for every event
- pick a forum, then a **specific post**, and everything goes into that post

Each entry carries the action and its case number, the member (with their ID),
the moderator, the reason, the duration when there is one, and how long ago it
happened. Colours run from grey for a note through amber, orange and red as the
sanction gets heavier, so a glance down the channel surfaces the worst events.

Posting to the mod-log is best effort. If the channel was deleted or the bot
lost access, the case is still recorded, you just lose that one message.

**Show notes in mod-log** is off by default: private staff notes stay out of the
channel. Turn it on if your staff prefers everything in one stream.

#### What the member is told

Four switches, all **on** by default:

| Switch | Fires on |
| --- | --- |
| DM target on warn | `/warn` |
| DM target on mute | `/mute` |
| DM target on kick | `/kick` |
| DM target on ban | `/ban`, `/tempban`, `/softban` |

The DM says what was done, in which server, for how long when there is a
duration, and the reason you typed.

Three cases never produce a DM, whatever these switches say:

- **notes**, which are internal by design
- warns handed out by a **Discord AutoMod rule**, otherwise a spam filter would
  DM someone five times in a row
- any target who is **not on your server**, for example a ban by ID of someone
  who never joined

For kicks and bans the DM is sent **before** the action, because afterwards
there is no shared server left through which to reach them.

#### Letting members appeal

Set an **appeal link** (a full web address, up to 512 characters) and every
moderation DM gains an **Appeal** button pointing at it.

Bind a **ban appeal form** in the **Appeal form** field just below and `/ban`
and `/tempban` DMs point their button at that form instead. `/softban` DMs,
like warn, mute and kick DMs, keep using the plain link, so it is worth setting
both. See [responses and ban
appeals](https://flavibot.xyz/docs/modules/forms/03-responses-and-appeals).

#### Warn decay

By default a warn stays active forever. Set **Warn decay** to a number of days
(1 to 365) and a warn stops counting after that long.

Decay never deletes anything. An expired warn is still in the member's history,
tagged as expired, it simply no longer counts toward auto-escalation.

One thing to know: the expiry is stamped on the warn when it is issued. Changing
the setting later applies to new warns, not to the ones already on record.

#### Auto-escalation

Auto-escalation is the rule "three strikes and you are out", written down once
so no moderator has to remember it.

Each rule reads **N active warns, then do X**:

| Field | Accepted values |
| --- | --- |
| active warns | 1 to 50, no duplicates between rules |
| action | mute, kick or ban |
| duration (mute only) | at least 60 seconds, at most 28 days |

After every strike, FlaviBot counts the member's active warns and applies the
**highest** rule whose threshold has been reached. With rules at 3, 5 and 10, a
member landing on their sixth warn gets the 5 rule, not the 3 rule.

What counts as a strike:

- a `/warn`
- a `/mute`, `/kick`, `/ban`, `/tempban` or `/softban`, because each of those
  records a warn as well
- a warn assigned by a [Discord AutoMod rule](https://flavibot.xyz/docs/modules/moderation/05-automod)

What does not count: notes, revoked cases, decayed warns, and the sanctions
auto-escalation applies itself (otherwise a rule would feed itself forever).

Two safeguards are always on. The server owner is never auto-escalated, and the
same warn can never trigger the same sanction twice, so two moderators warning
at the same instant cannot produce a double ban.

An automatic sanction is recorded exactly like a manual one: it gets a case
number, appears in the mod-log and in the member's history, is attributed to the
bot, and carries the reason "Auto-escalation: reached N active warns".

### Cases and history

Source: https://flavibot.xyz/docs/modules/moderation/04-cases
Summary: Look up a FlaviBot moderation case, read a member's record in Discord or on the dashboard, correct a reason, and revoke a sanction without erasing it.

A **case** is one entry in the moderation record FlaviBot keeps for your server. Case numbers start
at 1 and count up per server, shared between notes and warns, so "case #47" is
unambiguous to your whole staff.

Reversals (`/unmute`, `/unban`) do not take a number. They attach to the
sanction they undo, which is then shown as reverted.

#### Reading one case

`/case view <case_number>` shows everything known about it:

- its type (note or warn) and the member it targets
- the moderator, the reason, and when it was created
- when it expires, if warn decay is on
- when it was revoked, by whom and why, if it was
- the **linked actions**, for example the ban that this warn belongs to
- the **edit history**, with the five most recent changes listed

#### Reading a member's record

`/modhistory user [type_filter] [include_revoked]` prints the member's history,
newest first, up to 25 entries.

By default it hides revoked entries and shows everything else. `type_filter`
narrows it to notes only or warns only. `include_revoked` brings the cancelled
ones back into view.

Each line shows the case number, what it was, the sanction it led to when there
was one, the moderator and how long ago it happened. Entries that no longer
count are tagged `(revoked)` or `(expired)`, and reversals appear as their own
lines so you can see when someone was let back in.

#### The dashboard

The **Cases & history** page is the same data with a search box.

- the search field accepts a **case number** or a **user ID**, and works out
  which one you typed
- filter by type, or tick **include revoked**
- pick a member with the user selector to see only their cases
- click any row to open it, with its linked actions and its edit history

Browsing cases only requires access to the server's dashboard. Changing one, the
reason or a revoke, requires the Manage Server permission, so a member with
read-only dashboard access cannot quietly clean up their own record.

#### How do I correct a case's reason?

`/case edit-reason <case_number> <new_reason>`, or the reason field on the
dashboard, replaces the text (1 to 1500 characters).

The old reason is not lost. Every edit is appended to the case's audit trail
with who made it and when, and `/case view` shows that trail.

#### How do I revoke a case?

`/case revoke <case_number> <reason>` cancels a sanction's effect while keeping
the paper trail. On the dashboard it is the Revoke button in the case, and the
reason must be at least 4 characters (up to 500).

After a revoke:

- the case **stays** in the history, marked as revoked, with who revoked it and
  why
- it no longer counts as an active warn, so it stops feeding
  [auto-escalation](https://flavibot.xyz/docs/modules/moderation/03-settings)
- nothing is deleted, and revoking twice is harmless

Revoking is bookkeeping, not an undo of the Discord action. Revoking the case
attached to a ban does not unban anyone. Run `/unban` for that.

### Discord AutoMod

Source: https://flavibot.xyz/docs/modules/moderation/05-automod
Summary: Manage Discord's own AutoMod rules from the FlaviBot dashboard: what a rule watches, what Discord does, exemptions, templates, and warns handed out on a hit.

AutoMod is Discord's own filter. It inspects a message **before** it is posted
and can block it outright, which is something no bot can do on its own.

What it cannot do is remember. Discord will block the same slur from the same
member a hundred times and never escalate. That is the gap the **Discord
AutoMod** dashboard page closes: you build the rules there, and you decide how
many FlaviBot warns each one hands out when it fires.

#### Before you start

The rules live on Discord, not in FlaviBot, so the bot needs to be **in your
server** with the **Manage Server** permission to read or write them. If the bot
selected in the top bar is not a member of the server, the page says so and
offers to invite it rather than showing an error.

Deleting a rule from the page deletes it on Discord too. Deleting it from
Discord's own settings also clears its FlaviBot configuration, so the two never
drift apart.

#### Building a rule

Give it a name, choose what it watches, choose what Discord does, and optionally
exempt roles or channels.

##### What it watches

| Trigger | What it catches | What you configure |
| --- | --- | --- |
| **Keyword** | messages containing your words | keywords, regex patterns, allow list |
| **Keyword preset** | Discord's own curated lists | profanity, sexual content, slurs, plus an allow list |
| **Spam** | Discord's spam detector | nothing, it has no settings |
| **Mention spam** | mass pings in one message | the maximum number of mentions, 1 to 50 |
| **Member profile** | names and bios, not messages | keywords, regex patterns, allow list |

Keywords and regex patterns are entered comma separated. Regex is limited to 10
patterns and uses Discord's engine, which supports no backreferences and no
lookbehinds. The allow list holds phrases that override a match, which is how
you keep "assassin" from tripping a filter on a shorter word inside it.

The trigger type cannot be changed once a rule exists. To switch, create a new
rule and delete the old one.

##### What Discord does

Up to **three** actions per rule:

| Action | Effect |
| --- | --- |
| **Block message** | the message is never posted, with an optional rejection notice of up to 150 characters |
| **Send alert** | a copy goes to a channel you pick |
| **Timeout member** | Discord times them out, up to 28 days |
| **Block member interaction** | the member is blocked from interacting with the server |

##### Exemptions

**Exempt roles** (up to 20) and **exempt channels** (up to 50) are never
inspected by the rule. This is how staff channels and moderator roles stay out
of a keyword filter.

#### The FlaviBot reaction

At the bottom of the rule form:

| Field | What it does |
| --- | --- |
| **Assign warns when this rule fires** | 0 to 50 FlaviBot warns per fire, 0 means the rule stays purely Discord side |
| **Warn reason template** | the reason written on those warns, defaulting to `Automod:` followed by the rule name |
| **Notify mod-log** | posts an automod summary to your mod-log, on by default |

Those warns are **real** warns. They get case numbers, appear in
`/modhistory`, and count toward
[auto-escalation](https://flavibot.xyz/docs/modules/moderation/03-settings). That is what turns a
flat filter into a strike system: set the rule to assign 1 warn, set an
escalation rule at 3 warns, and the third offence times the member out without
anyone watching.

Two behaviours are worth knowing:

- **The member is not DM'd** for an automod warn. A spam filter firing on a wall
  of messages would otherwise become its own spam.
- **Repeat fires are collapsed.** If the same rule fires against the same member
  again within about 5 seconds, it counts once. Someone pasting ten forbidden
  lines gets the configured warns once, not ten times.

The mod-log summary, when enabled, names the rule, what Discord did, and how
many warns were assigned. It sits alongside the individual mod-log entry each
warn writes on its own.

#### Templates

The page's **Browse templates** button installs a ready-made rule from the
community marketplace, and the share icon on any of your rules submits it as a
template for others. Handy for the rules everyone needs and nobody enjoys
writing from scratch.

### Server logs

Source: https://flavibot.xyz/docs/modules/moderation/06-server-logs
Summary: FlaviBot server logs: a written record of what happens on your server, with log categories, the audit log badge, per-category channels, filters and bursts.

Server logs answer the question every staff team eventually asks: *who deleted
that channel, and when?* FlaviBot watches your server and writes each event down
as a compact message in a log channel.

This is separate from the [mod-log](https://flavibot.xyz/docs/modules/moderation/03-settings). The
mod-log records what **your moderators** did through FlaviBot. Server logs record
what happens on the **server itself**, including changes made directly in
Discord by people who never touch the bot.

Everything below is on the dashboard's **Server Logs** page.

#### How do I turn server logs on?

Flip **Enable server logs**, then pick a **default log channel**. That channel
is the fallback for every category you do not route elsewhere.

The bot needs three permissions there: **Send Messages**, **Embed Links** and
**Manage Webhooks**. The last one is not optional: logs are delivered through a
webhook FlaviBot creates in the channel, which is what lets a burst of events
post quickly without hitting Discord's rate limits. The messages still appear
under your bot's name and avatar.

Forum and media channels work, but you must also pick the **specific post**
inside them, otherwise there is nowhere to deliver.

#### Choosing what to log

**Log all events** is on by default, and means exactly that: every event in
every category below.

Untick any single event and the switch flips to a custom list, starting from
everything and minus what you removed. Untick everything and the page warns you
that nothing will be logged, since that is almost never what someone meant to
do.

#### What each category records

| Category | Events |
| --- | --- |
| **Messages** | message deleted, edited, bulk deleted, pinned, unpinned |
| **Members** | joined, left, nickname changed, roles changed, avatar changed, timed out, timeout lifted, started boosting, stopped boosting |
| **Moderation** | banned, unbanned, kicked, pruned, AutoMod action taken, AutoMod rule created, updated or deleted |
| **Roles** | role created, updated, deleted |
| **Channels** | channel created, updated, deleted, permission overwrite created, updated or deleted, thread created, updated, deleted |
| **Voice** | joined a voice channel, left, moved between channels |
| **Server** | server settings updated, emoji created, updated or deleted, sticker created, updated or deleted, soundboard sound created, updated or deleted, onboarding updated |
| **Invites** | invite created, invite deleted |
| **Integrations** | webhook created, updated or deleted, integration created, updated or deleted, command permissions updated |
| **Events** | scheduled event created, updated, deleted, stage created, updated, deleted |

A member join line also carries the account's creation date, the server's member
count, and the invite code plus the inviter when FlaviBot can attribute the
join.

#### The audit log badge

Some events are marked **Audit log** in the dashboard. Discord does not announce
those over the normal event stream, they exist only as audit log entries, so the
bot needs the **View Audit Log** permission to see them at all. Kicks, prunes,
permission overwrite changes, pins, emoji, sticker, soundboard, webhook,
integration and onboarding changes are all in this group.

The audit log is also where the *who* comes from. For an event like a ban or a
role change, Discord's raw notification says what changed but not who did it, so
FlaviBot pairs the two and adds the responsible member to the line.

#### Routing categories to different channels

By default everything lands in the default channel. You can give any category
its own channel, which is how most servers separate a noisy message log from a
quiet moderation log.

The number of **distinct** channels you may use depends on your plan:

| Plan | Distinct log channels |
| --- | --- |
| Free | 1 |
| Silver | 5 |
| Gold | 10 |
| Platinum | 15 |

The page shows your current count next to the limit, and saving is rejected if
you go over it rather than silently dropping a route.

#### Filters

Under **Advanced filters**:

- **Exclude channels**: activity in them is never logged
- **Exclude roles**: events caused by a member holding any of them are skipped
- **Exclude members**: same, for specific people
- **Log bot actions**: off by default, since bots generate most of the noise
- **Silent messages**: on by default, so log posts never ping anyone
- **New account age**: set a number of days and any member joining with a
  younger account is flagged on their join line, which is your early warning
  during a raid

#### Bursts

A purge or a raid can produce dozens of events per second. FlaviBot groups what
arrives within the same short window into a single log message rather than
posting each one separately, so the channel stays readable and delivery keeps
up.

### Levels and economy

Source: https://flavibot.xyz/docs/modules/leveling/01-overview
Summary: FlaviBot's two reward systems: XP that turns into levels, and a per-server coin economy members spend in your shop. Where each lives and what members can run.

This module covers two systems that are built to work together but are
configured, enabled and used separately.

**Levels** turn activity into progress. Members earn XP for talking and for
sitting in voice channels, XP becomes levels, and levels can hand out roles,
post an announcement, fill a rank card and rank everyone on a leaderboard.

**Economy** is a per-server currency. Members hold a balance, earn coins from
the sources you switch on, and spend them in a shop you write yourself.

Neither is on by default, and neither needs the other. You can run levels with
no economy, an economy with no levels, or wire them together (paying coins on
every level-up is one of the earning rules).

#### Where each one lives

| System | Dashboard page | Commands |
| --- | --- | --- |
| Levels | **Levels** | `/rank`, `/leaderboard`, `/xp` |
| Economy | **Economy** | `/economy`, `/casino` |

The Levels page has one master switch at the top (**Enable Level System**).
The Economy page has its own (**Enable the economy**). Until the switch is on,
the bot ignores the feature entirely and its commands refuse to run.

#### The economy is a limited rollout

The economy is still being rolled out server by server. On a server that has
not been enrolled, `/economy` replies with a short beta notice pointing at the
support server instead of running, and the Economy page is hidden from the
dashboard menu. Levels have no such gate and are available everywhere.

#### What members can run

| Command | Who can run it | What it does |
| --- | --- | --- |
| `/rank [user]` | everyone | Posts the rank card for you or someone else |
| `/leaderboard` (alias `lb`) | everyone | Links to the web leaderboard for this server |
| `/xp add\|remove\|set\|reset` | Manage Server | Edits a member's XP directly |
| `/economy ...` | everyone (the `admin` group needs Manage Server) | Balance, earning, shop, items, trading |
| `/casino ...` | everyone, when the casino is enabled | Wager games against the server currency |

`/rank` needs the bot to have **Embed Links** and **Attach Files** in the
channel it answers in, because the card is an image. `/leaderboard` needs
**Embed Links**.

#### One bot owns the numbers

If you run FlaviBot alongside a custom bot, only one of them processes XP for
your server. That bot is set every time the **Levels** page is saved: saving
from a given bot's dashboard makes that bot the one that grants XP, posts
level-up announcements and gives out reward roles. The other bot simply stops
counting.

The economy works differently: it is stored **per bot**. Each bot has its own
currency settings, its own shop and its own balances, so switching the
dashboard from one bot to another shows a completely separate economy rather
than the same one.

#### Where to go next

- [How XP is earned](https://flavibot.xyz/docs/modules/leveling/02-earning-xp) covers messages,
  voice, the cooldown, multipliers and channel restrictions.
- [Levels and role rewards](https://flavibot.xyz/docs/modules/leveling/03-levels-and-role-rewards)
  explains the level curve and how roles are handed out.
- [Level-up announcements](https://flavibot.xyz/docs/modules/leveling/04-level-up-announcements)
  is the message members actually see.
- [The rank card and the leaderboard](https://flavibot.xyz/docs/modules/leveling/05-rank-card-and-leaderboard)
  covers `/rank`, the card editor and the public ranking.
- [Currency and balances](https://flavibot.xyz/docs/modules/leveling/06-economy-currency-and-balances),
  [Earning coins](https://flavibot.xyz/docs/modules/leveling/07-economy-earning) and
  [The shop, items and trading](https://flavibot.xyz/docs/modules/leveling/08-economy-shop-and-trading)
  cover the economy.

### How XP is earned

Source: https://flavibot.xyz/docs/modules/leveling/02-earning-xp
Summary: How FlaviBot grants XP for messages and time in voice: the cooldown, voice ticks, the XP rate, per-role multipliers, and restricting where XP is earned.

In FlaviBot's level system, XP is the raw number behind every level. There are exactly two sources:
**messages** and **time in voice**. Nothing else grants XP on its own, apart
from a staff member using `/xp`.

#### Message XP

Every message a member sends rolls a random amount of XP: **0 to 40** at the
default XP rate, rounded to a whole number. The roll really can come out at 0,
so a single message is never a guaranteed gain.

Then a **50 second cooldown** starts for that member on that server. Messages
sent during those 50 seconds still count towards the message counter shown on
the rank card, but they earn no XP. The cooldown is per member and per server,
not per channel, so spreading messages across channels does not beat it.

Bots never earn XP. Neither do webhooks, and neither do direct messages.

#### Voice XP

Voice XP is granted in **ticks of 2 minutes**. Each whole 2 minutes spent in a
voice channel rolls the same 0 to 40 XP, and the rolls are added together. A
continuous 10 minute call pays 5 rolls.

The bot records voice time in segments rather than in one block, and starts a
new segment whenever the member switches channel or toggles mute, deafen,
stream or video. Each of those transitions **restarts the 2 minute clock**: a
member who toggles their microphone every 90 seconds never completes a tick,
and earns nothing however long they stay. Everything between two transitions is
counted continuously, so the internal 5 minute bookkeeping costs nobody
anything.

Time spent **deafened** is skipped entirely. A member who cannot hear the
channel is not participating, so the bot does not pay for it. Being muted, on
the other hand, still earns XP — listening counts. A **server** mute or deafen
applied by a moderator is not taken into account at all.

Voice XP has its own switch on the **Levels** page (**Enable Voice XP**), so
you can turn it off without touching message XP.

##### Voice XP depends on Server Stats

Note:

Voice time is recorded by one bot per server, named on both the **Levels** and
**Server Stats** pages. When those two name different bots — or the named bot
is offline — no voice time is recorded, however the rest is configured.

Voice time is recorded once per server, by one bot. **Enable Voice XP** on the
Levels page is enough on its own — you no longer have to turn on the Server
Stats voice module as well. What still matters is *which* bot: the Levels page
and the Server Stats page each name one, and voice time is only recorded when
the bot you are configuring is the one named. If they disagree, or the named
bot has left the server, nothing is recorded and no voice XP is granted. The
dashboard warns you in both cases.

A member who has opted out of statistics collection (`/privacy`) earns no XP at
all — message or voice. That opt-out is also the marker a data erasure leaves
behind, so anything that kept writing rows for those members would undo the
erasure minutes later.

##### When voice XP is not granted

Time in voice turns into XP only when every one of these is true. They are
listed in the order the bot checks them, so work down the list: the first one
that fails is your answer.

| What to check | Why it matters |
| --- | --- |
| Voice tracking is **on** in Server Stats, **or** voice XP is on under Levels | Either one is enough to record voice time. Turning voice XP on no longer requires enabling statistics. |
| The bot recording voice is **in your server and online** | Voice time is recorded by exactly one bot per server. If it has left or is offline, nothing is recorded for anyone. |
| The member has not opted out of statistics | `/privacy` stops all XP for that member, message and voice alike. |
| The member is not a bot | Bots never earn XP, in voice or anywhere else. |
| The member is not **deafened** | Deafened time is skipped entirely. Muted time still counts. |
| The **Levels** module is enabled | The whole feature, message XP included. |
| **Enable Voice XP** is on | The voice-only switch on the Levels page. |
| The Levels page points at the **same bot** as Server Stats | These are two separate settings on two separate pages. When they name different bots, voice XP stops while both pages still look correct. |
| The voice channel passes **Channel Restriction** | The allow-list and deny-list apply to voice channels too, matched on the channel the member is sitting in. A whitelist made only of text channels earns nobody any voice XP — add your voice channels, or the category holding them. |
| The member stayed still long enough | A tick is 2 uninterrupted minutes. Switching channel or toggling mute, deafen, stream or camera restarts that clock. |

If all of those hold and a member still shows no gain, the rolls themselves can
land on 0 — the same 0 to 40 range as messages. Over an hour that evens out.

#### How fast that actually is

At the default 1x rate a member averages around 20 XP per message that clears the cooldown, and around 600 XP per hour of uninterrupted voice time (30 ticks of roughly 20 XP). Levels get more
expensive as they go up, which is covered in
[Levels and role rewards](https://flavibot.xyz/docs/modules/leveling/03-levels-and-role-rewards).

#### XP rate

The **XP Rate** slider multiplies every roll, from **0.25x to 3x** in steps of
0.25. It applies to message XP and voice XP alike.

This is a premium setting: the card is locked on the free tier, where your
server stays at 1x. Silver, Gold and Platinum can change it.

#### XP multipliers per role

**XP Multipliers per Role** gives a boosted rate to specific roles: boosters,
veterans, staff, whatever you want to reward.

When a member has several matching roles, the **highest** multiplier wins.
They are never added together. A member with no matching role is on 1x.

The multiplier stacks on top of the server XP rate, and the resulting total is
**capped at 3x**. That cap is deliberate: it is the top of the XP rate slider,
so no combination of settings and purchases can push a member past what you
could already configure directly. Entering a value above 3 is accepted by the
form but behaves the same as 3.

How many role multipliers you can add depends on your tier:

| Tier | Role multipliers |
| --- | --- |
| Free | 0 |
| Silver | 3 |
| Gold | 10 |
| Platinum | 25 |

Members can also buy a temporary XP boost from the economy shop. It multiplies
with the role multiplier and lands under the same 3x ceiling. See
[The shop, items and trading](https://flavibot.xyz/docs/modules/leveling/08-economy-shop-and-trading).

#### Restricting where XP is earned

**Channel Restriction** decides where XP can be earned at all. It applies to
message XP and voice XP together.

| Mode | Effect |
| --- | --- |
| No restriction | Every channel grants XP. This is the default. |
| Whitelist | Only the selected channels grant XP. |
| Blacklist | Every channel except the selected ones grants XP. |

The picker accepts categories, text channels, voice channels, stage channels,
threads, forums and media channels. Listing a container covers what is inside
it, one level deep: a category covers each of its channels, and a text or forum
channel covers its threads and posts. A category does not reach the threads
inside its text channels — list those text channels if you want their threads
covered.

Voice XP is matched on the **voice channel** the member is sitting in, using
this same list. A whitelist made only of text channels therefore earns nobody
any XP for time spent in voice — add your voice channels, or the category that
holds them. The dashboard warns you when that is the case.

A message in an excluded channel is skipped completely: no XP, and no increment
to the message counter on the rank card either. The same goes for voice: an
excluded voice channel adds no XP and no voice minutes.

#### Text XP and voice XP: three ways to combine them

**Leveling Mode**, on the **Levels** page, decides how the two sources relate.

| Mode | What happens |
| --- | --- |
| **Separate** | Text and voice are two independent tracks, each with its own level and its own level-up announcement. This is the default. |
| **Combined** | Voice XP is added to the text pool. One level, one announcement. |
| **Sum** | The two tracks stay separate, but the level announced (and shown on the rank card and the leaderboard) is text level plus voice level. |

Switching to **Combined** does not migrate anything. XP already banked on the
voice track stays there and does not count towards the unified level; only
future voice XP feeds it. Switching back to **Separate** restores the old
picture.

The mode also changes who gets reward roles and which announcement is used, so
read [Levels and role rewards](https://flavibot.xyz/docs/modules/leveling/03-levels-and-role-rewards)
and [Level-up announcements](https://flavibot.xyz/docs/modules/leveling/04-level-up-announcements)
before you change it on a busy server.

### Levels and role rewards

Source: https://flavibot.xyz/docs/modules/leveling/03-levels-and-role-rewards
Summary: The FlaviBot level curve and maximum level, role rewards handed out as members climb, editing XP by hand, and resetting a member or the whole server.

This page covers how FlaviBot turns XP into levels, the optional level cap,
role rewards, editing XP by hand and starting over.

#### How XP becomes a level

Each level costs more than the one before it. The XP needed to go from a level
to the next one is:

```
5 x level x level + 50 x level + 100
```

So level 0 to 1 costs 100 XP, level 1 to 2 costs 155, level 2 to 3 costs 220,
and it keeps growing. What a member sees on their rank card is the progress
inside their current level, not their lifetime total.

Cumulative totals, if you want a sense of the scale:

| Level | Total XP from zero |
| --- | --- |
| 1 | 100 |
| 2 | 255 |
| 3 | 475 |
| 5 | 1,150 |
| 10 | 4,675 |
| 20 | 23,850 |
| 50 | 268,375 |
| 100 | 1,899,250 |

At the default 1x rate, level 10 is roughly 230 qualifying messages, or about ten hours in voice. Halving or doubling the
[XP rate](https://flavibot.xyz/docs/modules/leveling/02-earning-xp) moves those numbers with it.

The curve is the same for the text track and the voice track, so in **Separate**
mode a voice level 10 costs exactly as much as a text level 10.

#### Maximum level

**Max Level**, under Additional Settings, stops progression at a level you
choose. `0` means no limit, and that is the default.

Once a member is at the cap they keep earning XP inside that level until the
bar is full, then stop. They never level up again, so no further announcement
is posted and no higher reward role is granted. The cap applies per track, so
in Separate mode text and voice each stop at the same number independently.

#### Role rewards

**Role Rewards** map a level to a role. When a member reaches that level, the
bot gives them the role.

Two rules decide what actually happens:

- Only **one** role is granted per level-up: the highest tier the member has
  now reached. If someone jumps from level 4 to level 12 in one go and you have
  rewards at 5, 10 and 15, they get the level 10 role, not all three.
- **Remove Old Reward Roles** (Additional Settings, off by default) strips
  every strictly lower reward role when a new one is granted. Leave it off if
  reward roles are meant to stack visibly, turn it on if they are meant to be
  a single rank.

For the grant to work, the bot's own highest role must sit **above** the reward
role in your role list, and the role must not be a managed one (a bot role or a
booster role). If the bot cannot give a role, the rest of the level-up still
happens: the announcement is posted and the member keeps their level.

The dashboard also refuses reward roles you would not be able to assign
yourself, so you cannot use the level system to hand out a role above your own.

##### Rewards and leveling mode

In **Separate** mode, reward roles are granted by the **text** track only. A
voice level-up never grants a reward role, otherwise every voice level would
re-grant whatever role sits at that number. In **Combined** and **Sum** mode
there is one level being announced, so every level-up can grant.

##### How many rewards you can set

| Tier | Role rewards |
| --- | --- |
| Free | 3 |
| Silver | 10 |
| Gold | 25 |
| Platinum | 50 |

Saving more than your tier allows is rejected and the dashboard shows an
upgrade prompt rather than silently dropping rows.

#### Editing XP by hand

`/xp` lets anyone with **Manage Server** correct a member's progress. It is
slash only.

| Subcommand | What it does |
| --- | --- |
| `/xp add user amount [kind]` | Grants XP, levelling the member up if it is enough |
| `/xp remove user amount [kind]` | Takes XP away |
| `/xp set user level xp [kind]` | Sets an exact level and an exact XP-within-that-level |
| `/xp reset user [kind]` | Puts the member back to level 0 with 0 XP |

`kind` picks the track: `message` (the default) or `voice`. Amounts go up to
1,000,000,000 and levels up to 1,000.

Three things to know before you use it:

- `/xp` never posts a level-up announcement and never grants reward roles, even
  when the member crosses a reward threshold. Give the role by hand if you want
  it.
- It refuses to run if leveling is disabled, and it refuses to run on a bot
  account.
- If another bot owns leveling for your server, `/xp` tells you which one to
  use instead of writing to the wrong place.

#### Starting over

Sometimes you want a clean slate — a new season, a fresh start after a spam
wave, or a member asking to be taken off the leaderboard. The **Danger zone** at
the bottom of the Levels page does that.

It is limited to members with the **Manage Server** permission. Dashboard access
granted through the Access settings is not enough, even though it is enough to
change every other setting on the page.

##### Resetting one member

Pick the member, confirm, done. Their level, XP, voice level, voice XP, message
count and voice minutes all go back to zero.

##### Resetting the whole server

Every member goes back to level 0. Because a large server can have hundreds of
thousands of members, this runs as a background job — you can follow it, and
cancel it, from the **Jobs** page. While it runs, nobody earns XP on the server:
messages and voice time are ignored until the reset finishes, so nothing lands
half-way through and survives the wipe.

To make an accidental click impossible, the confirmation asks you to type your
server's name.

A cancelled reset is not undone. It stops where it is, leaving the server
partly reset, and the Jobs page offers to resume it.

##### What a reset does not do

**Reward roles stay.** Nothing tracks which level roles a member is currently
wearing, so a reset cannot reliably take them back — and even if it did,
persistent roles would hand them out again on the member's next rejoin. Remove
them yourself in Discord if you want a true restart, ideally before you run the
reset.

##### The XP history

Both resets offer an extra option: **also delete the XP history**.

Leave it off — the default — and the levels are still reset, but the record of
how they were earned stays in your Stats page. Turn it on and those charts start
over too.

The history is also the only copy we could rebuild your server's levels from if
something ever went wrong on our side, so deleting it is the one part of a reset
that cannot be undone. Turn it on when erasing the record is the point;
otherwise the default is the safer choice, and it changes nothing a member sees.

### Level-up announcements

Source: https://flavibot.xyz/docs/modules/leveling/04-level-up-announcements
Summary: Where FlaviBot posts the level-up message, what you can write in it, using an embed instead, the separate voice message, and reacting with an automation.

The announcement is the only part of FlaviBot's level system members see without
asking for it, so it is worth getting right. It is configured on the **Levels**
page under Level Up.

#### Where it is posted

**Announcement Location** has three settings.

| Setting | Where the message goes |
| --- | --- |
| **Configured channel** | Always the channel you pick below. This is the default. |
| **Where earned** | The channel the member was talking in, or the voice channel they levelled up in. |
| **Off** | Nowhere. |

With **Configured channel**, if you leave the channel empty the bot falls back
to the channel the XP was earned in, which is the same behaviour as **Where
earned**.

Turning announcements **Off** only silences the message. Reward roles are still
granted, XP still accrues, and the rank card and leaderboard still update.

##### Forums and threads

If the configured channel is a forum or a media channel, the bot opens a new
post per level-up, titled after the member and the level they reached.

You can also point at a specific post inside that forum instead. In that case
the message is added to the existing post and no new one is created. The post
you pick must belong to the channel you picked above it.

#### Writing the message

**Level Up Message** is a plain text template, up to 1500 characters. These
placeholders are filled in:

| Placeholder | Becomes |
| --- | --- |
| `{user}` | A mention of the member |
| `{user.name}` | Their username |
| `{user.display_name}` | Their display name |
| `{user.id}` | Their user id |
| `{user.tag}` | Their username. Level-up announcements never carry a discriminator, so this renders exactly like `{user.name}` |
| `{level}` | The level they just reached |
| `{level_type}` | `voice` or `text` in Separate mode, empty in Combined and Sum |
| `{server.name}` | The server name |
| `{server.member_count}` | The member count |

Anything the bot cannot fill in is left in the message exactly as you typed it,
so a typo shows up as itself rather than as a blank.

A worked example:

```
Nice one {user}, you just hit level **{level}** in {server.name}!
```

Only the member being congratulated is pinged. Role and everyone mentions in
the template are not delivered as pings.

If the message is empty and you have not linked an embed template, nothing is
posted at all. That is the default state of a fresh server, so writing the
message is part of switching the feature on.

#### Using an embed instead of plain text

You can link a saved template from the **Embed Messages** page to the level-up
announcement. When one is linked, the announcement is sent as that template
with the same placeholders filled in, which gets you embeds, images, buttons
and components instead of a line of text.

If the linked template has been deleted, the bot falls back to the plain text
message rather than dropping the announcement.

#### The voice message

In **Separate** mode a voice level-up is a different event from a text
level-up, so it gets its own handling. **Voice Level Up Message** offers two
choices:

- **Same message for both**: voice level-ups reuse the message above. Add
  `{level_type}` somewhere so members can tell which track levelled.
- **Separate voice message**: voice level-ups use their own text, written in
  its own box, up to 2500 characters. Leaving it empty uses the built-in
  default, `🎙️ {user} just reached voice level {level}!`

One ordering rule is worth knowing: if you have both a linked embed template
and an explicit custom voice message, the voice message wins for voice
level-ups and the embed is used for text level-ups. Without a custom voice
message, the embed is used for both.

In **Combined** and **Sum** mode there is a single pool, so there is only one
message and `{level_type}` renders empty.

#### Reacting to a level-up with an automation

`level_up` is also an autoresponder trigger, so you can do more than post a
message: give a temporary role, send a DM, run several steps in order. Build it
on the **Autoresponder** page and see
[triggers](https://flavibot.xyz/docs/modules/autoresponder/02-triggers) for how to set it up.

One limitation: the trigger only fires for **message** level-ups. A level-up
caused by voice time does not fire `level_up` automations, in any leveling
mode. Neither does `/xp`.

### The rank card and the leaderboard

Source: https://flavibot.xyz/docs/modules/leveling/05-rank-card-and-leaderboard
Summary: What FlaviBot's /rank card shows, how far you can redesign it with templates and per-role cards, and how the leaderboard and its visibility work.

#### /rank

`/rank` posts a rank card as an image. Run it on its own for your own card, or
pass a member to see theirs.

The card shows the member's avatar and name, their level, their XP inside that
level, the XP the level needs, a progress bar, their position in the ranking,
and how many messages they have sent on the server.

In **Separate** mode with voice XP on, the card carries a second track as well:
voice level, voice XP and progress, voice rank, and minutes spent in voice. In
**Sum** mode the level shown is text level plus voice level, matching what the
announcement and the leaderboard say.

`/rank` refuses to run when leveling is disabled, and it says so when the
member has never earned anything on either track.

For the card to arrive, the bot needs **Embed Links** and **Attach Files** in
the channel.

#### Redesigning the card

The **Levels** page has a visual card editor under Rank Card Customization.
Start from the built-in design or create your own template, then set it active.

A template is a stack of layers you place on a canvas. The layer types are:

| Layer | What it is for |
| --- | --- |
| Background | Solid colour, gradient or image |
| Text | Text with variables, so it can say the level, the rank, the name |
| Avatar | The member's avatar |
| Image | An overlay: a logo, a badge, artwork |
| Progress bar | An XP bar, or a custom one |
| Shape | Rectangle, circle, line |
| Decoration | Grids, glows, patterns |
| Group | Several layers moved and edited together |

Twenty-four ready-made designs ship with the bot, from **Glass** and **Neon** to **Luxe**, **Ocean**, **Retro** and **Monochrome**, and you can start from any of them instead of a blank canvas. Templates can be duplicated, saved to your
personal library so you can reuse them on another server, and deleted.

##### Two active cards in Separate mode

Servers in **Separate** mode with voice XP on have two active card slots: the
**Text + Voice** card, which is what `/rank` serves, and the **single-bar**
card, which is used if you switch the mode back to Combined or Sum. The editor
shows a tab for each and marks the one currently in use.

##### A different card per role

**Role-Based Rank Cards** assigns a different template to members holding a
given role. The list is ordered, and the **first** matching role from the top
wins, so put your most exclusive role first. Members with none of the listed
roles get the server default.

| Tier | Role-based cards |
| --- | --- |
| Free | 1 |
| Silver | 5 |
| Gold | 10 |
| Platinum | 25 |

#### The leaderboard

`/leaderboard` (or `lb`) does not post a ranking in chat. It replies with a
button that opens the ranking for your server on the FlaviBot website.

The web ranking pages 100 members at a time and shows, next to each member,
their level, their progress and their message count. It also carries a summary
for the whole server: total XP, average level, total messages, how many members
are ranked, and a seven-day activity chart. Members who have left the server
keep their place and are flagged as gone rather than disappearing.

In **Separate** mode the page has a switch between the **text** and **voice**
rankings, since they are two different competitions. In **Sum** mode the
ranking is by the combined level.

Opening the ranking requires signing in with Discord.

##### Making it public, or not

**Online Leaderboard** on the **Levels** page controls whether the web ranking
answers for your server at all. It is on by default. Turn it off and the page
tells visitors the server has hidden its leaderboard. Disabling the whole level
system hides it too.

##### Deeper statistics

The ranking is a scoreboard, not an analysis. For XP and level trends over
time, the **Server Stats** page has an XP tab, reachable from the **View level
stats** button at the top of the Levels page.

### Currency and balances

Source: https://flavibot.xyz/docs/modules/leveling/06-economy-currency-and-balances
Summary: Name your FlaviBot economy's currency, understand wallets and the account age gate, read the record of every movement, and correct a member's balance.

The economy gives every member of your server a balance in a currency you
define, and a paper trail for every coin that moves. It is configured on the
**Economy** page, under the **Settings** tab.

Turning on **Enable the economy** is the master switch. While it is off, every
economy command refuses to run.

Two things to know before you start:

- The economy is a limited rollout. On a server that has not been enrolled,
  `/economy` answers with a beta notice instead of running.
- The economy is stored **per bot**. If you also run a custom bot, it has its
  own currency, its own shop and its own balances, entirely separate from
  FlaviBot's.

#### Naming the currency

Under **Currency** you set:

- **Currency name**, the plural form. The default is `Coins`.
- **Singular name**, optional, used when the amount is exactly 1, so you get
  `1 Coin` instead of `1 Coins`.
- **Currency emoji**, shown next to every amount in chat. The default is 🪙 and
  your own server emojis work.
- **Currency image URL**, optional, used in embeds and on the web.

Every amount the bot prints follows the same shape: emoji, amount, name. With
the defaults, `🪙 1,250 Coins`.

#### Wallets

A wallet is created the first time a member touches the economy. Under
**Balances**:

- **Starting balance** is granted once, at that moment. It defaults to 0.
- **Maximum balance** is a hard ceiling. Leave it empty for no cap, which is
  the default.

The cap only ever blocks **credits**. A payment out of a wallet always lands,
so a member sitting at the cap can still spend, and a refund is never blocked
by it. When a credit would push a member past the cap, the operation is
refused rather than silently trimmed.

A balance can never go below zero. Anything that would overdraw a member is
rejected instead.

Members check their own with `/economy balance`, or someone else's with
`/economy balance user`.

#### The minimum account age gate

**Minimum account age (days)**, in the Transfers section, is an anti-alt
measure. Discord accounts younger than that earn nothing from the passive sources (message activity, voice activity, XP gain and level up), cannot send or receive a payment and cannot rob. `/economy daily`, `/economy work`, `/economy crime` and `/economy collect` are not gated by it. Setting it to 0, the default, disables the gate.

It is worth setting on a server that has been raided: a throwaway account made
this morning cannot farm coins to pass to a main account.

#### Every movement is recorded

Each coin movement writes one line to a ledger, tagged with a reason: a daily
claim, a shop purchase, a transfer, a fine, an admin adjustment. Nothing moves
coins outside of it, and lines are never edited: a correction is a new,
opposite line.

Members see their own lines with `/economy history`. Staff with **Manage
Server** can pass a member to `/economy history user` to inspect someone else's.

The **Activity** tab of the Economy page is the full picture: how much currency
exists, what is minting and what is burning it per day, a breakdown by reason,
top earners and top spenders, and the raw ledger with filters by member and by
reason.

How far back the dashboard can look depends on your tier:

| Tier | Ledger and analytics history |
| --- | --- |
| Free | 30 days |
| Silver | 90 days |
| Gold | 365 days |
| Platinum | Unlimited |

#### Correcting a balance

From Discord, with **Manage Server**:

| Command | What it does |
| --- | --- |
| `/economy admin give user amount [reason]` | Adds coins |
| `/economy admin take user amount [reason]` | Removes coins, stopping at 0 |
| `/economy admin set user amount [reason]` | Sets the balance to an exact number |
| `/economy admin reset user` | Sets the balance to 0 |
| `/economy admin reset-all` | Wipes every balance, inventory and history on the server |

`reset-all` is irreversible and asks for a confirmation first. The optional
reason on the other three is an audit note, up to 200 characters, stored with
the ledger line.

From the dashboard, the **Activity** tab has a member lookup that shows a
member's balance, rank, lifetime earned and spent, inventory and recent
history, with the same give, take and set actions. There a **reason is
mandatory**, and it is stored alongside your own user id.

#### Public pages

Under **Visibility and integrations** you can expose two read-only web pages
for your server, both off by default:

- a **public leaderboard** of the richest members;
- a **public shop page** listing your items.

Each has a **Copy link** button next to it. Visitors need to be signed in with
Discord to open either one.

The same section holds **Refund ticket cost on close**: if a ticket panel
charges coins to open a ticket, the coins are returned when the ticket is
closed.

### Earning coins

Source: https://flavibot.xyz/docs/modules/leveling/07-economy-earning
Summary: The eight earning rules that pay coins in a FlaviBot economy, what each one pays for, how many rules you can have, and paying coins from an automation.

A new economy mints **nothing**. There is no default income: until you add an
earning rule, every member sits at their starting balance forever, and
`/economy daily` answers that it is not configured.

That is deliberate. Every coin that exists on your server exists because you
switched on a source for it, which is what makes the currency worth anything.

Rules live on the **Economy** page, **Earning** tab. Each rule can be switched
off without deleting it, and edited at any time.

#### The eight kinds

| Kind | Pays for | Members trigger it with |
| --- | --- | --- |
| **Message activity** | Chatting | nothing, it is passive |
| **Voice activity** | Time in voice | nothing, it is passive |
| **Daily reward** | Showing up | `/economy daily` |
| **Work** | An honest shift | `/economy work` |
| **Crime** | A risky job | `/economy crime` |
| **Level up** | Reaching a level | nothing, it is automatic |
| **XP gain** | Earning XP at all | nothing, it is passive |
| **Role income** | Holding a role | `/economy collect`, or automatic |

You can only have one rule of each kind, except **Role income**, which can have
one rule per role.

#### Message activity

Pays a random amount between a **minimum** and a **maximum** for a message,
then holds a **cooldown** before that member can earn again. The cooldown
defaults to 60 seconds and cannot go below 30.

You can limit it to a list of channels, or exclude a list of channels, and set
a **daily cap** on how much one member can earn from chatting per day. Days run
on UTC.

This is independent of message XP: it works whether or not the level system is
on, and it uses its own cooldown, not the 50 second XP one.

#### Voice activity

Pays a fixed amount per **tick**, where a tick is a number of minutes you
choose, from 1 to 60, defaulting to 5. A 22 minute call at a 5 minute tick pays
four ticks, and the same call at a 20 minute tick pays one — every tick length
counts the call continuously. It also takes a daily cap.

A tick has to be completed in one go: switching channel or toggling mute,
deafen, stream or camera starts a new one from zero, exactly as it does for
voice XP.

Members who are **deafened or muted** earn nothing: if you cannot take part,
you do not get paid. Note that this is stricter than voice XP, which only skips
deafened members.

Voice time is recorded whenever either the **Server Stats** voice module or
**Enable Voice XP** on the Levels page is on — either is enough. A member who
has opted out of tracking with `/privacy` earns nothing here.

#### Daily reward

`/economy daily` claims a **base amount**, plus a streak bonus that grows with
each consecutive day claimed. The bonus defaults to 10% per day, capped at 300%
of the base, and **grace days** (1 by default) is how many missed days are
forgiven before the streak resets to zero.

Two reset behaviours are available:

- **Once per day**, the default: claimable again at midnight, in a timezone you
  pick. The timezone defaults to UTC.
- **Cooldown after claim**: claimable again a fixed number of hours after the
  last claim, 24 by default.

The command tells the member their new streak and when they can claim next.

#### Work and crime

`/economy work` pays a random amount between a minimum and a maximum, on a
cooldown that defaults to one hour.

`/economy crime` is the same shape with a gamble on top: a **fail chance**,
60% by default, and on a failure the member is fined a random percentage of
their balance, 20% to 40% by default, instead of being paid.

Both take **custom replies**: lines of flavour text, up to 300 characters each,
picked at random. Write `{{amount}}` where the payout should appear. Crime
takes two lists, one for successes and one for failures.

| Tier | Custom replies per rule |
| --- | --- |
| Free | 3 |
| Silver | 10 |
| Gold | 25 |
| Platinum | 50 |

#### Level up and XP gain

**Level up** pays whenever a member gains a level. Either a flat amount per
level, or an exact amount for specific levels (up to 100 of them), so you can
make level 10 and level 50 feel like milestones. It pays on voice level-ups as
well as text ones.

**XP gain** pays proportionally to XP earned, expressed as coins per 100 XP. It
is the smoothest faucet of the three, because it tracks activity directly
rather than milestones.

Both only pay for **organic** XP, earned by talking or by being in voice. XP
handed out by `/xp` or by an automation never mints coins.

Both obviously need the level system to be on.

#### Role income

Pays members holding a role a fixed amount, **daily** or **weekly**. Two payout
styles:

- **Claim with collect**: the member runs `/economy collect` to take it. Income
  from several roles stacks and is collected in one go.
- **Automatic**: the bot pays everyone with the role on schedule, without them
  asking.

Either way a member is only paid once per period, even if a payout runs twice.

#### When passive earnings actually appear

Message, voice and XP-gain earnings are batched and written to balances roughly
once a minute. A member who checks their balance the second after sending a
message may not see it yet. Everything triggered by a command (daily, work,
crime, collect) is immediate.

#### How many rules you can have

| Tier | Earning rules |
| --- | --- |
| Free | 2 |
| Silver | 5 |
| Gold | 10 |
| Platinum | 20 |

#### Two more sources, off by default

The **Settings** tab also has two optional faucets that move coins between
members rather than minting them:

- **Rob** lets a member try to steal from another. The odds are not fixed: the
  richer the robber is compared to their target, the likelier they fail,
  between 20% and 80%. A success takes at most a percentage of the target's
  balance, 10% by default; a failure fines the robber a percentage of their
  own, 20% to 40% by default. Either way the cooldown is consumed, four hours
  by default. Members who joined the server less than 24 hours ago can neither
  rob nor be robbed, and so can accounts below the minimum age gate.
- **Casino** (`/casino`) offers blackjack, roulette, slots, coinflip and higher
  or lower, with a minimum and maximum bet and a limit on how many games a
  member can start inside a time window (4 games per 5 minutes by default). You
  can disable individual games and keep the rest.

Both are off until you switch them on. FlaviBot can also pause the casino
across all servers at any time; while it is paused, casino commands answer with
a temporarily-disabled message and the rest of the economy carries on.

#### Paying coins from an automation

Autoresponder actions can add, remove or set a member's balance, so an
automation can pay a reward for anything you can trigger on. There is also an
`economy_balance_threshold` trigger, which fires when a member's balance
crosses an amount you set, in the direction you set, once per crossing rather
than on every transaction. See
[actions](https://flavibot.xyz/docs/modules/autoresponder/04-actions) and
[triggers](https://flavibot.xyz/docs/modules/autoresponder/02-triggers).

### The shop, items and trading

Source: https://flavibot.xyz/docs/modules/leveling/08-economy-shop-and-trading
Summary: Build a FlaviBot economy shop with categories, items and sales, set purchase requirements, and let members send coins, trade and use the marketplace.

A currency with nothing to buy is a scoreboard. The shop is what turns a
balance into a decision, so it is worth building before you switch FlaviBot's
earning rules on.

The shop lives on the **Economy** page, **Shop** tab: categories, items and
sales.

#### Categories

Categories group items into browsable sections. Each has a name, an optional
description, an emoji and a position. They are optional: items without one
still show up in the shop.

| Tier | Categories |
| --- | --- |
| Free | 2 |
| Silver | 5 |
| Gold | 10 |
| Platinum | 25 |

#### Items

Every item has a name, a price, and optionally a description, an emoji, an
image and a category. Beyond that:

| Setting | What it does |
| --- | --- |
| **Stock** | Total copies that will ever be sold. Empty means unlimited. |
| **Max per member** | How many copies one member may own. Empty means unlimited. |
| **Available from / until** | A window during which the item can be bought. |
| **Enabled** | Hides the item without deleting it. |
| **Tradeable** | Whether it can change hands through a trade or the market. |
| **Usable** | Whether `/economy item use` does anything with it. |
| **Consumable** | Whether a use burns one copy. |
| **Sell-back** | The percentage refunded when a member sells it back. 0 disables sell-back. |

| Tier | Shop items |
| --- | --- |
| Free | 5 |
| Silver | 15 |
| Gold | 30 |
| Platinum | 100 |

Deleting an item also destroys every copy of it in members' inventories, plus any market listing for it. To pull an item from the shop while letting owners keep what they bought, switch **Enabled** off instead of deleting.

#### What an item actually does

Each item is one of four kinds.

**Role** grants a role. Leave the duration empty for a permanent role, or set
one for a temporary role that is removed automatically when the timer runs out,
even if the bot restarts in between. The duration can run from one minute to a
year.

**XP boost** multiplies the buyer's XP for a while. The multiplier goes from
1.25x to 3x in steps of 0.25, and the duration from five minutes to thirty
days. It stacks with any [role XP multiplier](https://flavibot.xyz/docs/modules/leveling/02-earning-xp)
the member has, and the combined result is capped at 3x, so a boost can never
push someone past what the server could already configure.

**Workflow** runs a list of actions, up to ten of them, through the same
automation pipeline as the autoresponder. You choose when they run:

- **Purchase**: on buying the item.
- **Use**: every time the member runs `/economy item use`.
- **On consumption**: only when a copy is actually burned, which means the item
  has to be both Usable and Consumable, otherwise it never fires.

**Cosmetic** does nothing on purpose: a collectible, a badge, something to show
off in an inventory or to require as the price of entry for something else.

#### Purchase requirements

An item can demand any combination of:

- a **required role**;
- a **minimum balance**, checked against what the member is holding before paying, on top of the price, so you can reserve an item for members who have already accumulated a certain amount;
- a **required item**, so items can chain into a progression;
- a **minimum level**, read from the level system.

All of the requirements you set must be met. A member who fails any of them is
told the requirements are not met rather than being charged.

#### Sales

A sale is a time-boxed discount over the whole shop, one category, or one item,
from 1% to 99%, with a start (optional, immediate if left empty) and an end
(required).

Sales never stack. If several apply to the same item, the largest discount wins
and that is the price charged.

| Tier | Active sales |
| --- | --- |
| Free | 1 |
| Silver | 3 |
| Gold | 5 |
| Platinum | 10 |

#### Buying and using

| Command | What it does |
| --- | --- |
| `/economy shop view` | Browses the shop, page by page |
| `/economy shop buy item [quantity]` | Buys up to 100 copies at once |
| `/economy inventory [user]` | Lists what you, or someone else, own |
| `/economy item info item` | Shows an item's details |
| `/economy item use item` | Uses an owned, usable item |
| `/economy item sell item [quantity]` | Sells copies back to the shop |

The item options autocomplete, so members can type a name instead of hunting
for an id.

A purchase is one atomic operation: the availability window, the stock, the
per-member limit and the balance are all re-checked at the moment of payment,
and either everything happens or nothing does. If the item's effect then fails
in a way that leaves the member paying for nothing, such as a role the bot
cannot grant or an XP boost that could not be activated, the item is taken back
and the coins are refunded.

Sell-back deserves one note. The refund is the sell-back percentage of the
**lower** of the item's current price and the average price the member actually
paid for it. That prevents buying at a discount during a sale and selling back
at full price for a profit.

#### Sending coins to another member

`/economy pay user amount` transfers coins. The bot asks for a confirmation
before anything moves.

Under **Transfers** on the Settings tab:

- **Allow transfers** can turn the whole thing off.
- **Transfer tax (%)** is burned on every transfer: the sender pays the full
  amount, the recipient receives the amount minus the tax, and the difference
  leaves circulation. It is your main lever against a currency that only ever
  grows.
- **Daily transfer cap** limits how much one member can send per UTC day. Empty
  means no cap.

You cannot pay yourself or a bot, and both sides must clear the
[minimum account age gate](https://flavibot.xyz/docs/modules/leveling/06-economy-currency-and-balances).
A transfer that would push the recipient past the maximum balance is refused
rather than partially delivered.

#### Trading

`/economy trade offer user` opens a trade panel in the channel. Both members
stage what they are putting in, coins and items, and both must lock before
anything moves. Execution is a single operation that re-checks both sides'
balances and inventories, so nothing can be traded twice or traded away in the
meantime.

A trade expires after 10 minutes, and `/economy trade cancel` drops your
pending one. Items marked as not tradeable cannot be staged.

#### The marketplace

The market is member-to-member selling, without both people needing to be
online at once.

| Command | What it does |
| --- | --- |
| `/economy market view` | Browses current listings |
| `/economy market sell item quantity price` | Lists items at a price per unit |
| `/economy market buy listing` | Buys a listing |
| `/economy market cancel listing` | Cancels one of your listings |

Listing costs a fee, taken when the listing is created: a percentage of the
total asking price, 2% by default, never less than one coin. It is **not**
refunded if you cancel, which is what stops the market being spammed with
listings nobody intends to sell.

The items are held in escrow while the listing is up, so they cannot be traded
or used at the same time. Listings run for 7 days, and the items come back if
one expires or is cancelled. Only tradeable items can be listed.

| Tier | Active listings per member |
| --- | --- |
| Free | 2 |
| Silver | 5 |
| Gold | 10 |
| Platinum | 25 |

#### Reacting to a purchase

`economy_item_purchase` is an autoresponder trigger: it fires after a purchase
commits, and can be limited to specific items. Use it to announce a purchase,
open a ticket, or give something the item itself cannot.

Autoresponder actions can also give and take items directly, so an automation
can hand out a collectible without a shop transaction. See
[triggers](https://flavibot.xyz/docs/modules/autoresponder/02-triggers) and
[actions](https://flavibot.xyz/docs/modules/autoresponder/04-actions).

### What the Engagement module covers

Source: https://flavibot.xyz/docs/modules/engagement/01-overview
Summary: The six FlaviBot Engagement features, where each lives on the dashboard, and what they share: channels, embeds, variables, permissions and auto-disable.

Engagement is the set of features that react to your members instead of
policing them. Someone joins and gets greeted. A funny message gets pinned by
popular vote. A member asks for a change and the server votes on it. A prize
gets handed out. A birthday gets celebrated. A creator goes live and the
server hears about it.

| Feature | What it does | Page |
| --- | --- | --- |
| Welcome and goodbye | Greets joiners, says goodbye to leavers, hands out a join role | [Welcome and goodbye](https://flavibot.xyz/docs/modules/engagement/02-welcome-and-goodbye) |
| Starboard | Reposts messages that get enough star reactions | [Starboard](https://flavibot.xyz/docs/modules/engagement/03-starboard) |
| Suggestions | Collects member suggestions with voting and a status workflow | [Suggestions](https://flavibot.xyz/docs/modules/engagement/04-suggestions) |
| Giveaways | Runs prize draws with entry rules and automatic winner picking | [Giveaways](https://flavibot.xyz/docs/modules/engagement/05-giveaways) |
| Birthdays | Announces a member's birthday, optionally with a temporary role | [Birthdays](https://flavibot.xyz/docs/modules/engagement/06-birthdays) |
| Feed announcements | Posts new YouTube videos, Twitch and Kick streams, Reddit posts | [Feed announcements](https://flavibot.xyz/docs/modules/engagement/07-feed-announcements) |

#### Where they live in the dashboard

Open your server on the dashboard and look at the left sidebar.

- **Engagement** holds *Starboard*, *Giveaways*, *Suggestion* and *Birthday*.
- **Server Management** holds *Welcome & Goodbye*.
- **Notifications** holds *YouTube*, *Twitch*, *Kick* and *Reddit*.

#### What every one of them has in common

Learning these five things once saves you reading them six times.

##### Choosing where a message goes

Every feature that posts a message asks for a channel, and the picker accepts
more than a plain text channel: text channels, the built-in text chat of a
voice or stage channel, forum channels and media channels all work.

Pick a **forum or media channel** and the feature opens a **new thread per
event** instead of posting into the channel, because a forum has no channel
body to post into. A welcome opens one thread per joiner, a starboard opens
one thread per starred message, a YouTube subscription opens one thread per
video. Several features let you name those threads with a template.

You can also point a feature at **one specific thread or forum post** inside
the parent channel. Do that and everything lands in that single thread, with
no new thread per event.

##### Plain text or a built embed

Most message fields offer two modes: **Text (Legacy)**, a plain message you
type in the box, or **Template**, an embed you built on the *Embed Messages*
page and picked here by name. The template mode gives you colours, images,
fields and buttons; the text mode is one box and is faster to set up.

##### Variables

Both modes support variables written in single braces, like `{user}` or
`{server.name}`. Each feature exposes its own set, listed on that feature's
page.

A variable that does not exist in the current context is **left in the message
exactly as you typed it**, braces and all. It is never replaced by a blank.
So if a goodbye message prints `{member.joined_at}` literally, that variable
simply is not available there.

##### The bot needs the right permissions

Nothing posts if the bot cannot post. In every target channel the bot needs
**View Channel**, **Send Messages** and **Embed Links**, plus **Create Public
Threads** when the target is a forum. Features that hand out a role also need
**Manage Roles**, and the role they hand out must sit **below** the bot's own
highest role in the server settings list.

##### Auto-disable when something is permanently broken

Welcome and goodbye, starboard panels, birthdays and feed subscriptions track their own health: if one of them fails for a permanent reason (the channel was deleted, the bot lost a permission) **10 times in a row**, FlaviBot switches it off and stamps the reason, and the dashboard shows a red banner on that feature's page telling you what broke and when, with a button to switch it back on once you have fixed it. Suggestions and giveaways have no auto-disable: they simply keep failing until you fix the configuration.

Any event where nothing failed permanently resets the counter, so an occasional failure never disables a feature that mostly works. Note that a single event can run several parts at once (a join runs the role, the message, the greet and the DM), and one part failing permanently still counts against the feature even when the others worked.

#### When a feature does not do quite what you want

These six features cover the common cases with a small number of settings.
When you need a rule that is specific to your server, the
[autoresponder](https://flavibot.xyz/docs/modules/autoresponder/01-overview) does the same kind of
work with a trigger, filters and a list of actions you choose yourself.

### Welcome and goodbye messages

Source: https://flavibot.xyz/docs/modules/engagement/02-welcome-and-goodbye
Summary: Greet new members with FlaviBot, hand them roles, send a join DM, say goodbye when they leave, and design the card image that goes with the message.

The first thing a new member sees decides whether they read your rules or
close the tab. This feature covers everything that happens the moment someone
joins, and the one thing that happens when they leave.

Everything here lives on one dashboard page: **Server Management > Welcome &
Goodbye**. Each block below is a card on that page with its own on/off switch,
and they are independent: you can run the join role without any message, or a
goodbye message without a welcome.

#### What fires when someone joins

Up to four things, all at once and independently of each other.

| Block | What it does |
| --- | --- |
| **Auto Role** | Gives the new member up to 3 roles |
| **Welcome Message** | Posts a public message, optionally with a card image |
| **Greet** | Posts a bare mention that pings the member, then deletes it |
| **Join DM** | Sends the member a private message |

And when someone leaves, the **Goodbye Message** block posts a public message,
optionally with its own card image.

#### Auto Role

Pick up to **3 roles**. Every human who joins gets all of them. Bots are
skipped.

Two rules from Discord apply and are enforced silently: a role that sits at or
above the bot's own highest role cannot be given, and neither can a role
Discord manages itself (a booster role, a role owned by another bot, a
Twitch subscriber role). Roles in that situation are skipped and the rest are
still applied.

The dashboard also refuses to save a role you could not grant yourself. That
stops a moderator with dashboard access from quietly making the autorole hand
out administrator.

#### Welcome Message and Goodbye Message

Both work the same way. Choose the channel, choose whether to attach the card
image, then write the message.

Screenshot: The Enable Welcome Messages card, with its switch, the image switch, the channel picker and the message box

The goodbye card below it holds the same four controls.

**The message**, in Text mode, is a plain message of up to 1500 characters
with variables. If you leave it empty, the defaults are:

- welcome: `Welcome to {user} on {server.name}!`
- goodbye: `{user.name} left the server!`

In Template mode you pick an embed you built on the *Embed Messages* page
instead, and the variables are filled in there too.

**The image** is a generated card showing the member's avatar, name and your
server. It is a separate switch from the message, so you can have a message
with no card, or a card with a one-word message. See
[the greet card](#the-greet-card) below for how to design it.

**The channel** can be a text channel, a voice or stage channel's text chat, a
forum or a media channel. Point it at a forum and each join opens its own
thread, named `Welcome <username>` (or `Goodbye <username>`). Point it at one
specific thread inside the forum instead and everything goes into that thread.

#### Greet

A separate, deliberately noisy notification: the bot posts a message that is
nothing but a mention of the new member, which pings them, and **deletes it
5 seconds later**.

The point is the ping, not the message. It pulls the member's attention to a
channel (usually the one where the real welcome message lives) without leaving
a trail of pings behind.

There is no message to write here, only a channel to choose. If you choose a
forum channel, the greet opens a thread and deletes the ping inside it; the
thread itself stays.

#### Join DM

A private message to the member, up to 2000 characters, using the same
variables as the public message.

Many Discord users have direct messages from servers turned off, and the bot
cannot override that. When a member's DMs are closed the message is simply
skipped: it is not an error, it does not retry, and it never counts against
the feature's health.

#### Variables

Type these anywhere in a welcome, goodbye or join DM message.

| Variable | Becomes |
| --- | --- |
| `{user}` | A mention that pings the member |
| `{user.name}` | Their username |
| `{user.id}` | Their numeric Discord id |
| `{user.created_at}` | When their Discord account was created, as a relative timestamp |
| `{user.avatar}` | A direct link to their avatar image |
| `{server.name}` | Your server's name |
| `{server.member_count}` | The member count, with thousands separators |
| `{member.joined_at}` | When they joined, as a relative timestamp |

These also exist, but **only on join, and only when the Invite Tracker is
turned on** and FlaviBot could attribute the join to an inviter:

| Variable | Becomes |
| --- | --- |
| `{inviter}` | A mention of the member who invited them |
| `{inviter.name}` | The inviter's username |
| `{inviter.invites}` | The inviter's invite count after this join |

Three notes worth knowing before you use them:

- `{member.joined_at}` and `{user.avatar}` are filled in on **join** only. In
  a goodbye message they stay in the text as literal `{member.joined_at}`.
  `{user.avatar}` also needs the member to have set an avatar of their own; a
  member on the Discord default avatar leaves the variable unreplaced.
- The invite variables never appear on a goodbye either, and on a join they
  only resolve when the invite was matched. Write the message so it still
  reads correctly without them.
- `{user.name}` is the one to reach for when you want a plain name. FlaviBot
  also accepts `{user.tag}` and `{user.display_name}`, but since Discord
  retired the `#1234` discriminator they render as `Name#0` and as the
  username respectively.

#### The greet card

The card is the image attached to a welcome or goodbye message. Turn the image
switch on and you get a built-in design showing the member's avatar, a
heading, their name, your server and the member count. No setup needed.

Screenshots:

    numbered: false
    items:
      - src: https://cdn-assets.flavibot.xyz/docs/engagement/welcome-goodbye/welcome-card-default.png
        alt: The built-in welcome card, with the member's avatar above the word Welcome, their name and the member count
        caption: The built-in welcome card.
      - src: https://cdn-assets.flavibot.xyz/docs/engagement/welcome-goodbye/goodbye-card-default.png
        alt: The built-in goodbye card, identical to the welcome one except for its heading
        caption: The goodbye card is the same design with a different heading.

The member above has no avatar of their own, so Discord's default one is
drawn; a member with an avatar gets theirs. The small circle at the bottom is
your server icon.

To design your own, scroll to **Welcome Card Customization** on the same
dashboard page. You get a grid holding the **Default Preset** plus any
templates you have made, and the one marked *Active* is the one that gets
used. **Use Default** puts you back on the built-in design.

**Create Template** opens a visual editor on a blank 950 by 300 canvas, where
a card is a stack of layers: a background, decorations, shapes, the avatar,
and text. You drag things around, restyle them, and save.

Inside a text layer you can use these placeholders, written with double
braces:

| Placeholder | Becomes |
| --- | --- |
| `{{greeting}}` | `Welcome` on a welcome card, `Goodbye` on a goodbye card |
| `{{user.username}}` | The member's name |
| `{{guild.name}}` | Your server's name |
| `{{memberInfo}}` | `Member #1234` on welcome, `1234 members remaining` on goodbye |

Each row in the grid also lets you **duplicate** a template, **save it to your
profile** so you can reuse the design on another server, or delete it.

How many templates you can keep, and how many layers each can hold, depends on
your server's premium tier:

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Card templates | 3 | 10 | 20 | 50 |
| Layers per template | 15 | 25 | 35 | 50 |

One limitation to know: the template picker currently drives the **welcome**
card. Turning on the goodbye image gives you the built-in goodbye design.

#### When the welcome system switches itself off

The join role, the welcome message, the greet and the goodbye message all
share one health counter. If they fail for a permanent reason 10 times in a
row **and** that streak has lasted at least 48 hours, the whole Welcome &
Goodbye feature is switched off and the page shows a red banner with the
reason.

The usual causes are a deleted channel, a bot that lost **Send Messages** or
**Embed Links** in the target channel, or an autorole that was moved above the
bot's own role. Fix the cause, then press **Reactivate** on the banner. Saving
the page also clears the error state.

A join DM that could not be delivered is never counted here, because a closed
DM is the member's choice and not a misconfiguration.

### Starboard

Source: https://flavibot.xyz/docs/modules/engagement/03-starboard
Summary: Let members pin the best messages themselves: FlaviBot reposts a message to your starboard channel once it gets enough reactions. Setup, filters, forums.

A starboard is a hall of fame your members curate. Anyone reacts to a message
with a chosen emoji; once enough people have done it, FlaviBot reposts that
message in a channel of your choice with a link back to the original.

It is the community version of pinned messages: nobody needs **Manage
Messages**, and nothing gets highlighted unless several people agree.

#### How do I set up a starboard?

Go to **Engagement > Starboard** in the dashboard. Press **Add**, give the
panel a name (the name is only for you, it never appears in Discord), then
open it with **Edit**.

Inside, you need three things:

1. The **Starboard Channel**, where highlighted messages get posted.
2. The **Reaction emoji** members react with. A standard emoji or one of your
   server's custom emojis. Leave it unset and it is the star, `⭐`.
3. The **Reaction threshold**, the number of reactions a message needs. The
   minimum is 2, so a message can never be highlighted by one person alone.

Then flip the panel's switch on.

Screenshot: The Starboard Panels card, listing two panels with their channel, emoji, threshold and health

Two panels, each with its own emoji and its own threshold. The counter next to
**Add** is how much of your allowance you have used.

#### How many starboard panels can I have?

One panel is one emoji with one threshold posting to one channel. A second
panel lets you run, for example, a `⭐` board at 5 stars for general highlights
and a `💡` board at 3 for ideas.

| Free | Silver | Gold | Platinum |
| --- | --- | --- | --- |
| 1 | 3 | 5 | 20 |

#### What actually gets posted

The starboard message shows the reaction count, then an embed with the
original author's name and avatar, the message text, its first image
attachment if it has one, and a footer with the source channel name and the
original message id.

Screenshot: A starboard post, showing a star and the count 2, then a yellow embed carrying the original author, their message, and a footer naming the source channel

The count on the first line is live. The channel name and the long number in
the footer are where the message came from, so anyone can find the original.

That post stays in sync with reality:

- More reactions come in, the count on the post is updated.
- Reactions are taken back and the total falls **below the threshold**, the
  post is deleted.
- The original message is deleted, the post is deleted.
- A moderator clears the reactions off the original, the post is deleted.

If you delete the starboard post by hand, it comes back the next time someone
adds a reaction to the source message. Removing reactions instead is the way
to make something leave the board for good.

#### Filters

Four settings decide which reactions count. They are all in the panel's
configuration.

**Allow self-stars** is off by default: the author of a message cannot star
their own message. When someone tries, the reaction is removed from the
message so it does not sit there looking like it counted.

**Allow bot messages** is off by default: messages posted by bots are not
eligible.

**Channel restriction** limits which channels messages can be starred from.
Set it to *whitelist* to allow only the channels you list, or *blacklist* to
allow everywhere except those. Up to 50 channels.

**Role restriction** limits who is allowed to star. *Whitelist* means the
member needs at least one of the listed roles; *blacklist* means having any of
them disqualifies them. Up to 25 roles. A reaction from someone who is not
allowed to star is removed from the message.

#### Posting into a forum

Point the panel at a forum or media channel and each starred message opens its
own thread instead of adding a message to a channel.

You can name those threads with **Forum thread name**, using these
placeholders:

| Placeholder | Becomes |
| --- | --- |
| `{user.username}` | The original author's name |
| `{user.id}` | The original author's id |
| `{message.id}` | The original message's id |
| `{message.preview}` | The first 60 characters of the message |

Leave it blank and threads are named `<username> starred a message`. Thread
names are cut to 100 characters, which is Discord's limit. You can also apply
up to 5 of the forum's tags to every thread.

Forum threads track the count internally but the visible post is not rewritten
as reactions come and go, unlike a normal channel post.

#### Permissions

In the starboard channel the bot needs **View Channel**, **Embed Links** and
**Send Messages**. For a forum target it needs **Embed Links** and **Create
Public Threads** instead. It also needs to be able to read the channels
members star from, and **Manage Messages** there if you want it to clean up
reactions it rejected.

#### When a panel switches itself off

Each panel has its own health. After 10 consecutive permanent failures, for
example the starboard channel was deleted or the bot lost **Embed Links**
there, that panel is disabled and the panel list shows why and when. Fix the
cause, then switch the panel back on.

### Suggestions

Source: https://flavibot.xyz/docs/modules/engagement/04-suggestions
Summary: Collect member ideas in one channel with FlaviBot: submit with a command, vote on each suggestion, then mark it approved, rejected or implemented.

Suggestions give ideas a single place to land and a visible outcome. A member
submits one with a command, it gets posted for the server to vote on, and a
moderator later marks it approved, rejected or implemented. The message
updates itself to show that decision.

#### How do I set up a suggestion panel?

Go to **Engagement > Suggestion** in the dashboard, press **Add**, name the
panel, then open it with **Edit** and pick the **channel** suggestions get
posted to. Turn the panel on.

The panel name matters here, unlike the starboard: members type it when they
submit, so give it a name they can guess, like `features` or `events`.

Screenshot: The Suggestion Panels card, listing one panel named features posting to the suggestions channel

The Emojis column shows the vote marks members will see on this panel.

How many panels you can run depends on your tier:

| Free | Silver | Gold | Platinum |
| --- | --- | --- | --- |
| 1 | 3 | 5 | 20 |

#### How do members submit a suggestion?

Members use:

```
/suggestion create panel:<panel name> suggestion:<their idea>
```

The panel field autocompletes, so they pick from your panels rather than
typing a name from memory. The confirmation they get back is private; only the
posted suggestion is public.

Example Discord message:

    color: "#947cea"
    author: Nova
    title: 💡 features
    timestamp: Today at 14:32

Add a channel for tournament announcements, so they stop getting lost in
general.

The title is the panel's name, the author line is the member who submitted,
and the body is their text, untouched. In reaction voting the bot then adds
👍 and 👎 under it.

Three settings on the panel control what gets accepted:

| Setting | Default | What it does |
| --- | --- | --- |
| Minimum length | 10 | Shorter submissions are refused |
| Maximum length | 2000 | Longer submissions are refused |
| Cooldown | 0 seconds | How long a member must wait between two submissions to this panel |

Both length bounds can be set anywhere from 1 to 2000 characters, and the
cooldown up to 86400 seconds (24 hours). A member who is still on cooldown is
told exactly when they can submit again.

#### Voting

Two styles, chosen with a switch on the panel.

**Reactions** is the default: FlaviBot adds 👍 and 👎 to the suggestion and
members react. Simple, and anyone can see who voted.

**Buttons** posts vote buttons instead and keeps the count itself. Clicking
the same button twice removes your vote; clicking the other one switches it.
One vote per member is enforced, and the counts on the message update
immediately.

Button voting also closes automatically: once a suggestion is no longer
pending, clicking a vote button tells the member voting is closed.

#### Deciding on a suggestion

A moderator with **Manage Messages** runs:

```
/suggestion status message_id:<id> status:<Pending|Approved|Rejected|Implemented> reason:<optional>
```

The `message_id` is the id of the bot's suggestion message in the channel:
right-click it and choose *Copy Message ID*.

The suggestion message is rewritten in place with the new status, its colour
and, if you gave one, your reason. Nothing new is posted, so the channel stays
readable.

Example Discord message:

    color: "#57f287"
    author: Nova
    title: 💡 features
    fields:
      - name: ✅ Status
        value: Approved
    timestamp: Today at 16:05

Add a channel for tournament announcements, so they stop getting lost in
general.

Each status has its own colour and mark: pending is blurple 🕒, approved green
✅, rejected red ❌, implemented yellow 🚀. With button voting, the buttons on
a suggestion that is no longer pending stop accepting votes.

There is also `/suggestion refresh message_id:<id>`, also **Manage Messages**,
which re-renders a suggestion message from what FlaviBot has stored without
changing its status. Use it if a message got edited or looks out of sync.

#### Threads

**Auto thread** opens a discussion thread on each suggestion automatically, so
debate happens in the thread instead of burying the next suggestion.

If you point the panel at a **forum or media channel** instead, each
suggestion opens its own forum thread, and auto thread does not apply. Name
those threads with the **Forum thread name** field:

| Placeholder | Becomes |
| --- | --- |
| `{panel}` | The panel's name |
| `{suggestion.title}` | The first line of the suggestion |
| `{user.username}` | The submitter's name |

Leave it blank and threads are named after the panel followed by the first
line of the suggestion. Thread names are cut to Discord's 100-character limit.

#### Permissions

In the suggestion channel the bot needs **View Channel**, **Send Messages**
and **Embed Links**. Reaction voting also needs **Add Reactions**. Auto thread
needs **Create Public Threads**, and a forum target needs the same.

### Giveaways

Source: https://flavibot.xyz/docs/modules/engagement/05-giveaways
Summary: Run prize draws with FlaviBot: who can run one, starting and managing a giveaway, entry requirements, premium options, limits by tier, and winner DMs.

A giveaway is a prize, a deadline and a list of people who entered. FlaviBot
posts the message, counts the entries, draws the winners the second the timer
runs out, and lets you reroll if a winner never shows up.

#### Who can run a giveaway?

Anyone with **Manage Server** or **Administrator**. You can also nominate
**manager roles** on the dashboard's Giveaways page, under *Settings*: members
holding one of those roles can create, edit, end, reroll and delete giveaways
without any Discord permission.

#### How do I start a giveaway?

The fastest way is one command:

```
/giveaway start duration:2d winners:3 prize:Nitro
```

`duration` accepts a combination of weeks, days, hours, minutes and seconds
written together: `2d`, `1h30m`, `1w`, `45m`. It also takes an optional
`channel`, `description`, `required_role` and `image`.

`/giveaway create` opens a form instead, if you prefer filling in fields to
remembering option names.

The dashboard's **Create** button gives you the full set of options, including
everything premium below.

#### The giveaway message

The posted message shows the prize as its title, when it ends as a live
countdown, the number of winners, the host and the running entry count.

How members enter depends on the **entry method** you chose:

- **Button**: a `🎉 Join` button on the message. This is the default.
- **Reaction**: members react to the message with 🎉.

Both kinds of message also carry a `⋯` button. Anyone can press it, but only a
giveaway manager gets action buttons in the panel that opens: **end**,
**pause**, **resume** and **reroll**, whichever apply to the giveaway's
current state.

When the timer runs out, the message is rewritten to show the winners, and a
separate announcement mentioning them is posted in the same channel. If nobody
entered, that is what the announcement says.

#### Managing a running giveaway

Except for `/giveaway list`, every subcommand in this table takes a `reference`, which is either the giveaway's id or the id of its message in Discord.

| Command | What it does |
| --- | --- |
| `/giveaway list` | Lists the giveaways currently running |
| `/giveaway edit` | Changes the prize, the winner count or the remaining time |
| `/giveaway pause` | Freezes a running giveaway and blocks new entries |
| `/giveaway resume` | Unfreezes it |
| `/giveaway end` | Ends it now and draws the winners |
| `/giveaway reroll` | Draws replacement winners, `winners` defaults to 1 |
| `/giveaway delete` | Cancels it without drawing anything |

A reroll never picks someone who already won that giveaway.

#### Who is allowed to enter

Set on each giveaway, in the create form:

- **Required roles**, with a mode: the member needs *any* of them, or *all* of
  them.
- **Blacklist roles** and **blacklist users**, who can never enter this one.
- **Bypass roles**, whose holders skip every requirement check.

Two of those also exist server-wide, under *Settings* on the Giveaways page:
a **blacklist roles** list that applies to every giveaway, and a **bypass
roles** list that applies to every giveaway. The per-giveaway lists add to
them rather than replacing them.

Order matters: bypass is checked first and wins, then the blacklists, then the
required roles.

#### Premium options

The following are available from **Silver** upward. On a free server the
controls are visible but locked, and the settings are ignored even if a value
somehow got saved.

**Advanced requirements** add gates that are not about roles: minimum account
age, minimum time in your server, minimum level, minimum message count, and
server boosters only.

**Bonus entries** make some members count more than once. Each entry is worth
1 by default; you add extra entries for holding a role (these stack if a
member holds several), for boosting the server, and per member they invited
who is still around. More entries means more chances, not a guaranteed win.

**Prize tiers and a claim window**. Tiers split one giveaway into ranked
prizes: the first winners drawn get the first tier's prize, the next ones the
second tier's, and so on, with the winner count derived from the tiers. A
claim window makes winners press a `🎁 Claim` button within a number of
minutes; a winner who does not claim in time forfeits and a replacement is
drawn automatically.

**Scheduled start and recurring giveaways**. A scheduled giveaway is created
now and posts itself later, with the countdown starting then. A recurring
giveaway starts a fresh one from a saved template on a repeating schedule.

**Templates** save a giveaway's whole configuration so you can apply it to a
new giveaway in one click, and they are what a recurring giveaway is built
from.

**A public entry page** publishes a web page for the giveaway so people can
enter from a browser as well as from Discord, with an optional captcha check.

**Analytics** break down entries, which requirements filtered people out and
where bonus entries came from, with a CSV export.

#### Limits by tier

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Giveaways running at once | 5 | 25 | 50 | Unlimited |
| Saved templates | 0 | 5 | 15 | Unlimited |
| Bonus-entry role rules per giveaway | 0 | 10 | 20 | 30 |

Scheduled giveaways do not count toward the running limit until they actually
start.

#### Direct messages to winners

**DM the winners** is a switch on the create form and it is **off** by
default. Turn it on and each winner also gets a direct message on top of the
public announcement. Members with DMs closed simply do not get one.

#### Permissions

In the giveaway channel the bot needs **View Channel**, **Send Messages** and
**Embed Links**. Reaction giveaways also need **Add Reactions**.

### Birthdays

Source: https://flavibot.xyz/docs/modules/engagement/06-birthdays
Summary: Announce members' birthdays with FlaviBot: the server settings, how each member opts in, when announcements go out, and the optional birthday role.

Birthdays are the one automation that costs nothing and gets remembered.
A member tells FlaviBot their date once, chooses which of their servers may
celebrate it, and every year on the day your server gets a message.

Two halves make it work, and both are required: **you** turn the feature on
for the server, and **each member** opts in for themselves.

#### The server side

Go to **Engagement > Birthday** in the dashboard and switch the feature on.

| Setting | What it does |
| --- | --- |
| **Announcement channel** | Where birthday messages are posted. Required |
| **Message** | Leave it as the default text, or pick an embed you built on the Embed Messages page |
| **Birthday role** | Optional. Given on the day and taken back afterwards |
| **Role duration (hours)** | How long that role stays. Default 24, anywhere from 1 to 168 |

If no embed template is chosen, the default message is a `🎂 Happy Birthday`
mention, and it includes the member's new age when they told FlaviBot their
birth year.

The message supports variables: `{user}` to ping them, `{user.name}` for their
name, `{server.name}` for the server, and `{birthday.user.age}` for the age. A
member who did not give a birth year leaves the age blank.

Like the welcome and goodbye channels, the announcement channel can be a text
channel, a voice or stage channel's text chat, a forum or a media channel, or
a single thread inside one of those.

#### The member side

Nothing is announced for a member who has not asked for it. Members set
themselves up with:

| Command | What it does |
| --- | --- |
| `/birthday set month:<1-12> day:<1-31> year:<optional>` | Records their birthday. The year is optional and only used to show an age |
| `/birthday announce` | Turns announcements on or off **for the server they run it in** |
| `/birthday info user:<optional>` | Shows someone's birthday |
| `/birthday upcoming` | Lists the next birthdays in the server |
| `/birthday remove` | Deletes their birthday from FlaviBot entirely |

The date is stored once per member, not once per server. The opt-in is per
server: a member can be celebrated in one community and stay private in
another. They can also manage the date and the per-server opt-ins from the
**Birthday** page in their own dashboard area.

That opt-in is what makes the feature safe to enable. Turning it on does not
expose anybody who has not asked to be exposed.

#### When announcements go out

FlaviBot checks for birthdays **once a day, at 08:00 UTC**.

Which calendar day counts is read from your server's **timezone**, set on the
dashboard's *Configuration* page, so a server set ahead of or behind UTC
celebrates on its own local date rather than on UTC's. The check itself still
happens at the same moment worldwide, so the message arrives at 08:00 UTC
whichever timezone you picked.

Each member is announced at most once per year per server, so a retry or a
restart never doubles a message.

#### The birthday role

If you set one, FlaviBot gives it on the day and schedules its removal after
the duration you chose. The role must sit **below** the bot's own highest role
and must not be a role Discord manages itself, otherwise Discord refuses to
assign it.

Make it a purely cosmetic role. It is handed out automatically to anyone whose
birthday it is, so it should not carry permissions.

#### Permissions

In the announcement channel the bot needs **View Channel**, **Send Messages**
and **Embed Links**. Add **Manage Roles** if you use a birthday role, and
**Create Public Threads** if the channel is a forum.

#### When birthdays switch themselves off

Like the other features here, 10 consecutive permanent failures disable the
birthday system and the dashboard page shows the reason. Since it runs once a
day, that is about ten days of a broken configuration, usually a deleted
announcement channel. Fix it and switch the feature back on.

### Feed announcements

Source: https://flavibot.xyz/docs/modules/engagement/07-feed-announcements
Summary: Post new YouTube videos, Twitch and Kick streams, and Reddit posts with FlaviBot: adding a subscription, how fast each source is, the message and filters.

Feed announcements pull your community back in when something new happens
outside Discord. A creator uploads, a streamer goes live, a subreddit gets a
post, and your server hears about it without anyone refreshing anything.

Four sources, each with its own dashboard page under **Notifications**:
*YouTube*, *Twitch*, *Kick* and *Reddit*. Each page holds a list of
subscriptions, and each subscription is one source channel posting into one
Discord channel.

#### How do I add a subscription?

The shape is the same on all four pages: identify the source, choose the
Discord channel, and optionally write the message.

| Source | What you paste |
| --- | --- |
| YouTube | A channel URL, an `@handle`, or a channel id starting with `UC`. The page can also search by name |
| Twitch | A username, or a `https://twitch.tv/<username>` URL |
| Kick | A username, or a `https://kick.com/<username>` URL |
| Reddit | A subreddit name, `r/name`, or a Reddit URL |

Handles and URLs are resolved for you when you save. If a name cannot be
resolved, the save is refused with an explanation rather than accepted into a
subscription that would never deliver anything.

You cannot subscribe to the same source twice on the same server; the second
attempt is refused.

#### How fast is each source?

They do not all work the same way, and the difference is visible.

| Source | How it is detected | Practical delay |
| --- | --- | --- |
| YouTube | YouTube notifies FlaviBot when a video is published | Close to immediate |
| Twitch | Twitch notifies FlaviBot when a stream starts | Close to immediate |
| Kick | Kick notifies FlaviBot when a stream starts | Close to immediate |
| Reddit | FlaviBot polls the subreddit's feed | Up to 10 minutes |

Twitch and Kick announce **going live** only. There is no separate message
when a stream ends.

YouTube ignores two categories of video so that adding a subscription does not
flood a channel: anything **published more than 24 hours ago**, and anything
published **before you created the subscription**. Reddit likewise ignores
posts older than the subscription.

#### The message

Leave the **Custom Message** empty and you get a sensible default:

- YouTube and Reddit: the title on one line, the link on the next.
- Twitch and Kick: the channel name is now live, the stream title, the link.

Write your own (up to 2000 characters) and it replaces the default. Or switch
to **Template** and pick an embed you built on the *Embed Messages* page, for
a full embed with colours and images.

Each source has its own variables.

##### YouTube

| Variable | Becomes |
| --- | --- |
| `{title}` | The video title |
| `{url}` | The video link |

There is also a `{channel}` variable, but it currently renders the YouTube
channel's internal id rather than its name. Write the creator's name in the
message yourself until that is fixed.

##### Reddit

| Variable | Becomes |
| --- | --- |
| `{subreddit}` | The subreddit name |
| `{reddit.title}` | The post title |
| `{reddit.url}` | The post link |
| `{reddit.author}` | The poster's name |
| `{reddit.flair}` | The post flair, if any |

##### Twitch and Kick

Identical sets, prefixed `twitch.` and `kick.` respectively.

| Variable | Becomes |
| --- | --- |
| `{twitch.channel}` | The streamer's display name |
| `{twitch.login}` | Their username |
| `{twitch.title}` | The stream title |
| `{twitch.game}` | The category being streamed |
| `{twitch.url}` | The channel link |
| `{twitch.viewer_count}` | Viewers at the moment the stream was detected |
| `{twitch.thumbnail}` | The stream thumbnail image link |
| `{twitch.avatar}` | The streamer's avatar link |

#### Content type filters

Reddit lets you choose between **text posts** and **link posts**, and unticking
one really does stop those posts from being announced.

YouTube shows the same style of checkboxes for videos, shorts and lives. Those
are saved with the subscription but are **not applied yet**: today every new
upload from a subscribed channel is announced, whichever boxes are ticked. If
a channel posts shorts you do not want in Discord, do not subscribe to it.

#### Posting into a forum

Like the other engagement features, a feed can target a forum or media
channel, and then each item opens its own thread. Name those threads with the
**Forum thread name** field:

| Source | Placeholders |
| --- | --- |
| YouTube | `{video.title}` |
| Twitch | `{twitch.channel}`, `{twitch.title}`, `{twitch.game}` |
| Kick | `{kick.channel}`, `{kick.title}`, `{kick.game}` |
| Reddit | `{post.title}` |

Leave it blank and you get a reasonable default: the video or post title for
YouTube and Reddit, and the streamer's name with the stream title for Twitch
and Kick. Thread names are cut to Discord's 100-character limit.

You can also point a subscription at a single thread inside a forum, in which
case everything lands there and no new thread is opened.

#### Permissions

In the announcement channel the bot needs **View Channel**, **Send Messages**
and **Embed Links**, plus **Create Public Threads** for a forum target.

#### When a subscription switches itself off

Each subscription has its own health. After 10 consecutive permanent failures,
typically the Discord channel was deleted or the bot lost permission to post
there, that subscription is disabled and the list shows the reason. Fix the
cause and switch it back on.

#### If Twitch or Kick refuse to save

Those two integrations depend on credentials configured for the FlaviBot
instance you are using. If they are not set up, adding a subscription is
refused with a message saying the integration is not configured, and there is
nothing to fix on your side.

### How events work

Source: https://flavibot.xyz/docs/modules/events/01-overview
Summary: How a FlaviBot event works: the objects involved, the path from creation to RSVP panel and reminders, sign-up slots, the participant role, commands and limits.

An **event** is something your server does at a given time: a movie night, a
raid, a staff meeting, a stream. You give it a title, a start, a timezone and a
place; FlaviBot posts an **RSVP panel** in a channel, members say whether they
are coming, and the bot reminds the ones who said yes. The same event can also
be mirrored into your server's own **Events** tab as a native Discord scheduled
event, can land in members' calendars, and can be published on a page anyone
can read without a Discord account.

Events are being rolled out gradually. If **Events** is not in your dashboard's
Management section, and `/event` answers that it is not available, the feature is
not enabled for your server yet.

#### The objects

| Object | What it is | Where it lives |
| --- | --- | --- |
| **Event** | One thing happening once, with its own panel, RSVPs and reminders | The calendar, and one channel |
| **Series** | A repeat rule that produces events on a schedule | The calendar, drawn as its occurrences |
| **Slot** | A named group of seats on one event, with its own capacity: "Tank 0/5" | The event's panel and roster |
| **Team** | A named group of members, used to invite and to find a time | The Teams tab |
| **Panel** | The message members click to sign up | One channel per event |
| **Discord event** | The native copy in your server's Events tab, if you sync it | Discord |
| **Board** | A message listing what is coming, kept up to date by the bot | One channel per board |

A series is never shown on its own. What you see and what members sign up for
are its **occurrences**, each one a normal event. See
[recurring events](https://flavibot.xyz/docs/modules/events/03-recurring-events).

#### The path an event takes

1. Someone creates it, from the dashboard, from `/event create`, or from an
   automation.
2. FlaviBot posts the RSVP panel in the channel you picked: title, start time
   shown in each reader's own timezone, host, place, and the buttons **Going**,
   **Maybe**, **Can't make it**, **Who's coming**. An event with
   [sign-up slots](#sign-up-slots) has one button per slot instead of the
   single **Going**, "Tank 0/5", "Healer 0/2", each with its own count. When
   the event has a [sign-up window](#when-sign-ups-open-and-close), the
   buttons outside it are disabled and say when they open, or that sign-ups
   are closed.
3. Members answer. A **Going** past the capacity goes on the **waitlist**, in
   order, and is promoted automatically (with a DM) when a seat frees up; on
   an event with slots, the count and the waitlist are per slot. If the event
   has a required role, only its holders can sign up. A member can also answer
   with `/event rsvp`, and a manager can sign someone up, remove them or
   promote them from the dashboard; see
   [managing attendees](https://flavibot.xyz/docs/modules/events/02-calendar-views#managing-attendees).
   An event with a [participant role](#the-participant-role) hands it out as
   members say **Going**, and takes it back when they change their mind.
4. Before the start, FlaviBot DMs the members who said **Going** and the ones
   on the waitlist. By default that is one day and one hour before; you choose
   the offsets in the settings, and any event can carry its own list instead.
   The DM shows the start as a Discord timestamp and, for a member who set a
   timezone on their profile, as a plain "20:00 your time" next to it. A
   member who would rather not be reminded switches it off on their profile
   page, and one who wants a nudge on their own schedule sets
   [their own reminders](#your-own-reminders). See
   [reminders and announcements](#reminders-and-announcements).
5. At the start the event flips to **live** and the panel changes colour.
   When **Announce when it starts** is on, for that event or for the server
   by default, the bot also posts "🎉 **Title** is starting now" with a link
   to the panel, pinging the members who said Going or one role you chose.
   At the end (or two hours after the start when no end was given) it flips
   to **ended**, and the buttons are disabled but stay readable, so a member
   who opens the channel a week later still sees who came.

Cancelling an event repaints the panel, stops the reminders, removes the native
Discord copy, and takes the entry out of the calendars it was written to.

Cancelling keeps the event, though: the panel stays in the channel painted as
cancelled, the attendee list stays readable and the calendar shows the event
struck through. **Deleting permanently** removes every trace instead — the
panel message, the RSVPs, the calendar entries and the event itself (for a
series, every occurrence and the series with them). It is offered as a
checkbox on the cancel confirmation, **Also delete it permanently**, and as a
**Delete permanently** button on an event that is already cancelled or over.
An event still open is cancelled first, so the reminders stop and the native
Discord copy goes exactly as above. There is no undo.

#### Kinds

Every event has a **kind**. It decides the colour of the event everywhere (the
panel, the calendar chips, the list) and the defaults of a few fields when you
create it. You can override the colour on any event.

| Kind | Colour | Defaults |
| --- | --- | --- |
| Event | Blurple | Synced to Discord |
| Meeting | Sky blue | Not synced to Discord |
| Session | Green | Not synced to Discord |
| Stream | Purple | Synced to Discord |
| Milestone | Amber | All-day, not synced |
| Deadline | Red | All-day, not synced |
| Off | Grey | All-day, not synced, **no panel** |

**Off** is the odd one out: it is a day off, a holiday, a "no raid this
week". Nobody signs up for it, so no panel is posted and no channel is needed;
it only exists on the calendar.

A default is only a default. Pick **Meeting** and the Discord sync switch
starts off; flip it on if you want that meeting in the server's Events tab.

#### Sign-up slots

Some events need more than a head count: a raid wants two healers and five
tanks, a podcast wants a host and three guests, a tournament wants eight
players and two casters. **Sign-up slots** split an event's seats into named
groups, each with its own capacity.

You add them in the event form, under **Sign-up slots**: one row per slot,
with an optional emoji, a **label** (up to 32 characters) and a **capacity**,
or none for an open slot. Up to ten slots fit on one event. Each slot also
has a **key**, generated from the label and editable: lower-case letters,
digits, `_` and `-`, 32 characters at most. The key is what `/event rsvp
slot:` takes, so keep it short: `tank`, `healer`, `dps`.

What changes once an event has slots:

- The panel shows **one button per slot**, "🛡️ Tank 2/5", in place of the
  single **Going** button. **Maybe**, **Can't make it** and **Who's coming**
  stay as they are. The embed lists each slot's count, "Tank 3/5", above
  the overall **Going** line.
- Pressing a slot button signs the member up **for that slot**. Pressing
  another one moves them; the seat they held is freed.
- Capacity and the **waitlist are per slot**: a full slot puts the member on
  that slot's waitlist, shown as "(waitlist 2)" next to it, and a seat that
  frees up in that slot goes to the first person waiting for it, with a DM.
  A slot with no capacity never waitlists anyone. The event's own
  **Capacity**, when you set one, still caps the whole roster.
- **Who's coming**, the dashboard roster and `/event info` group the names by
  slot; `/event list` sums the slots up after each event.

On a series, slots are part of the shared details: set them once and every
occurrence gets the same slots, each with its own sign-ups. Removing the
slots from an event that already has answers keeps the answers: everyone
stays **Going**, in one pool again.

#### When sign-ups open and close

Two optional moments on an event, **Sign-ups open** and **Sign-ups close**,
decide when its panel takes answers. Neither has anything to do with the
start: open sign-ups a week early for a raid that fills up in an hour, or
close them the day before so you can plan around a settled roster.

- Set **only the opening**, and the panel is a teaser until then.
- Set **only the closing**, and answers run from the moment the panel is
  posted until that moment.
- Set **neither**, which is the default, and the panel takes answers for as
  long as the event is open.

Outside the window the panel's buttons are **disabled** and say why:
"Sign-ups open in 3 days" before it, "Sign-ups closed" after it, as a
countdown each reader sees in their own time. `/event rsvp` answers the same.
Everything else carries on as usual — the panel and **Who's coming** stay
readable, reminders are still sent, the start announcement still fires, and
the members who already answered keep their answer.

Both moments are typed in the event's own timezone, like its start, and
either can be cleared later. A manager is not bound by the window: **Manage
attendees** on the dashboard can still add, remove or promote someone once
sign-ups have closed.

#### The participant role

An event can hand out a **role** to the people coming to it: `@Movie Night`,
so they can be pinged in one mention, let into a channel for the evening, or
simply counted at a glance.

Pick it in the event form, under **Participant role**. From then on:

- a member who says **Going** is given the role;
- a member who withdraws, switches to **Maybe** or **Can't make it**, or is
  removed by a manager, loses it;
- someone on the **waitlist** does not hold it, and gets it when a seat frees
  up and they are promoted;
- when the event **ends or is cancelled**, it is taken back from everyone.

FlaviBot needs **Manage Roles**, and the role must sit **below** its own
highest role — Discord refuses to hand out anything above it. When the grant
fails, the RSVP still counts and the member is simply left without the role:
an evening does not fall over because a role could not be moved.

Since the role is handed out to whoever presses a button, give it nothing you
would not give to everyone who presses that button. A channel for the event,
yes; moderator permissions, no.

#### Reminders and announcements

**Reminders** are direct messages sent before the start to the members who
said **Going** and to the ones on the waitlist. The server sets a default
list in the module's **Settings**, up to five offsets; the default is one day
and one hour before.

Any event can have its **own** list. In the event form, under **Reminders**,
switch **Use the server defaults** off and pick the offsets for this event,
from the same presets or as a number of minutes, up to ten of them and up to
four weeks before the start. An event with its own list keeps it when the
server default changes later; the others follow the new default.

Each reminder reads "**Title** starts in 2 hours", as a Discord timestamp
that every reader sees in their own local time. When the member has set a
**timezone** in the Calendar preferences of their profile page, the DM adds
that time in plain words next to it, "(20:00 your time)", so a member who
reads it from a phone set to another zone is not caught out. Team
invitations and waitlist promotions carry the same line.

A member who does not want reminders switches **Event reminders by DM** off
in the **Notifications** card of their profile page. That stops the
reminders from every server; invitations and waitlist promotions still
arrive, since those are answers to something the member did.

**Start announcements** are for the channel rather than the inbox. When
they are on, the bot posts "🎉 **Title** is starting now" at the start, with
a link to the panel, in the server's **Announcement channel** or, when none
is set, in the event's own panel channel. The announcement pings either the
**role** you chose on the event or, without one, the members who said
**Going**, up to forty of them and then "+N"; nobody else is mentioned.

The choice is made twice: **Announce when events start** in the module's
Settings is the default for the server, off to begin with, and each event's
**Announce when it starts** can follow that default, force it on or force it
off. The bot needs **Send Messages** in the channel it announces in; without
it, the announcement is skipped and the event runs as usual.

#### Your own reminders

The reminders above are the organiser's: one list, the same for everyone who
signed up. A member can also set **their own**, the way a calendar app lets
you nudge yourself. They are extra, not a replacement, and they arrive as a
DM like the others.

There are two levels of them:

- **A default**, on the profile page, under **Calendar > Default event
  reminders**: up to five offsets — "1 day" and "30 minutes", say — applied to
  every event the member says **Going** to, in every server. It starts empty,
  so nobody is signed up for more DMs than before.
- **A per-event override**, for the one event the default does not suit:
  "this raid, wake me two hours before".

From Discord, the confirmation that follows a **Going** carries a **Remind
me** button. It opens a list — 10 minutes, 30 minutes, 1 hour, 3 hours, 1 day,
2 days — where up to five can be picked at once, plus **My default** to go
back to the default and **None** to keep this one event quiet. From the
dashboard, the event's detail panel has the same **Remind me** block.

"Use my default" and "none for this event" are two different answers, and both
are remembered: silencing one raid does not touch the default, and changing
the default later does not wake that raid up.

The rest follows the organiser's reminders:

- only a member who is **Going** or on the **waitlist** gets one;
- **Event reminders by DM**, off in the profile's **Notifications** card,
  silences every reminder, the member's own included;
- offsets are minutes before the start, up to four weeks ahead;
- they stack. A member may hear about the same raid a day before from the
  organiser's reminder, thirty minutes before from their own, and again from
  their Google Calendar; each is switched off on its own.

#### Where you configure it

Everything is on the dashboard, under **Members > Events**. The page has four
tabs:

- **Calendar**: the events themselves, in day, week, month or agenda form.
  See [the calendar](https://flavibot.xyz/docs/modules/events/02-calendar-views).
- **Teams**: named groups of members. See
  [teams and finding a time](https://flavibot.xyz/docs/modules/events/05-teams-and-availability).
- **Availability**: when the members of a team are free, and a slot finder.
- **Settings**: the module switches below, the public calendar and the
  channel boards. See
  [the public calendar and boards](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards).

Members also get a **Calendar** section on their own profile page, for their
timezone, working hours, default event reminders, calendar subscription and
Google account (see
[Google Calendar and ICS](https://flavibot.xyz/docs/modules/events/06-google-calendar-and-ics)),
and an **Event reminders by DM** switch in its **Notifications** card.

#### Who can create and manage events

From the dashboard, **Manage Server**, like every other dashboard page.

In Discord, `/event create`, `/event edit` and `/event cancel` accept anyone
with **Manage Server**, **Manage Events** or **Administrator**, plus the
holders of the **manager role** you can name in the settings. The command is
registered with Manage Events as its default, so it is hidden from everyone
else unless you widen it in *Server Settings > Integrations*.

`/event list`, `/event info` and `/event rsvp` are for anyone the command is
visible to: `rsvp` answers for the person typing it, exactly like pressing a
button on the panel, and `info` reads one event. Members never need a
command, though: the panel's buttons are the whole member-side experience.

#### Commands

| Command | What it does |
| --- | --- |
| `/event create title:… starts_in:…` (or `date:…`) | Schedules an event and posts its panel. See [recurring events](https://flavibot.xyz/docs/modules/events/03-recurring-events) for the `repeat` options |
| `/event edit reference:…` | Changes the **title**, **date**, **timezone**, **duration**, **location**, **capacity** or **description** of one event; every option is optional, the panel is repainted. On an occurrence of a series it changes that occurrence alone, like **This occurrence** on the dashboard |
| `/event rsvp reference:… status:…` | Your own answer, `going`, `maybe` or `declined`, with an optional `slot:` key on an event that has slots |
| `/event info reference:…` | One event in full: when, where, host, capacity, the count per slot, and the link to its panel. For a native Discord event that FlaviBot did not create, use `/info event` instead |
| `/event list` | The upcoming events, each with its kind, a **↻** on occurrences of a series, the going count and the slot counts |
| `/event cancel reference:… [series:True]` | Cancels one event, or the whole series |

A **reference** is the event's ID, shown on the dashboard, or the ID of its
panel message.

#### Settings

| Setting | What it does |
| --- | --- |
| **Enable events** | Off: no panels are posted, no reminders are sent, and existing panels stop taking sign-ups |
| **Primary bot** | The one bot that runs events here. A Discord scheduled event belongs to the application that created it, so two bots cannot share the job |
| **Default channel** | Pre-selected in the create form and used by `/event create` when no channel is given |
| **Manager role** | Lets its holders run the commands without a Discord permission |
| **Reminders** | Minutes before the start, up to five, for every event that does not set its own list. Default: 1 day and 1 hour |
| **Announce when events start** | Whether an event that leaves the choice to the server posts a start announcement. Off by default |
| **Announcement channel** | Where those announcements go. Empty: each event's own panel channel |
| **Create Discord events** | The default of the per-event Discord sync switch |
| **Occurrences posted ahead** | How many future occurrences of a series have a panel at any time, 1 to 5 |
| **Week starts on** | The first column of the week and month views for this server |
| **Calendar subscription** | The server's ICS feed link, and a button to rotate it |
| **Public calendar** | Whether the server has a [public page](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards), listing the events marked public. Off by default |
| **New events are public** | Whether the create form starts on **Public**. Off: you publish event by event |
| **Public description** | The line under the server name on that page, up to 300 characters |
| **Boards** | The [channel boards](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards#channel-boards), added and edited here |

#### Limits

How many events can be **scheduled or live at the same time** depends on the
server's plan: **3** on a free server, **15** with Silver, **50** with Gold,
and no limit with Platinum. A series does not take a slot for its rule, but
each occurrence that has been posted does, so a series posting three
occurrences ahead uses three on its own. Ended and cancelled events free
their slot. The create form and `/event create` both name the limit you hit;
the full table is in [limits by tier](https://flavibot.xyz/docs/premium/06-limits-by-tier).

Titles are limited to 100 characters, descriptions to 2000, locations to 200,
and an event cannot run longer than 30 days. Up to ten named hosts and up to
ten sign-up slots fit on a panel, each slot labelled with 32 characters at
most. An event's own reminder list holds ten offsets; the server default
holds five, and so does a member's own list, default or per event. A server
can run five boards, one per channel, and its public description is limited
to 300 characters.

#### What FlaviBot needs

In the panel channel: **View Channel**, **Send Messages** and **Embed Links**.
Without them the event is created but the panel is not posted, and the form
tells you so. A start announcement needs **Send Messages** in the channel it
is posted to; without it the announcement is skipped.

To mirror an event into the server's Events tab, the bot also needs the
**Create Events** permission; see
[Discord scheduled events](https://flavibot.xyz/docs/modules/events/04-discord-scheduled-events).
An event with a [participant role](#the-participant-role) needs **Manage
Roles**, with that role below FlaviBot's own, and a
[board](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards#channel-boards)
needs **View Channel** and **Send Messages** in the channel it is posted in.
Reminders, waitlist promotions and team invitations are direct messages, so
a member with DMs closed simply does not get one.

Next: [the calendar](https://flavibot.xyz/docs/modules/events/02-calendar-views).

### The calendar

Source: https://flavibot.xyz/docs/modules/events/02-calendar-views
Summary: Day, week, month and agenda views, the sidebar, dragging events around, the form behind every event, and managing who is coming.

The **Calendar** tab of FlaviBot's Events page is where events are made and read. It
works the way a calendar app does: four views of the same events, a toolbar
that moves through time, a sidebar with a mini month and the filters, a click
on an empty slot to create something there, a click on an event to open it,
and events you can drag to another time.

#### The four views

| View | What it shows | Good for |
| --- | --- | --- |
| **Day** | One day as an hour grid, 00:00 to 24:00 | A busy day with overlapping things |
| **Week** | Seven days side by side, same grid | Planning the week, spotting gaps |
| **Month** | The classic grid, up to three chips per day and a **+N** for the rest | The overview |
| **Agenda** | A list grouped by day, the next 60 days, **Load more** at the bottom, and a **Show past** toggle | Reading, on a phone |

The day and week views share one grid: an **all-day row** on top, hour rows
below, one column per day. Events that overlap are laid side by side rather
than stacked, and a red **now line** crosses the grid at the current time in
the timezone you are viewing in. The grid opens scrolled to 08:00.

The month view stretches all-day and multi-day events across the days they
cover. The week starts on the day set in the module's **Settings**, or on the
day you chose in your own profile preferences, which win for you.

The calendar fills the height of the window: the day header and the hour
gutter stay put while the grid scrolls underneath them, the month and agenda
views scroll inside their own frame, and the page itself never scrolls.

#### Moving around

The toolbar is one row: **previous**, **Today** and **next**, a label naming
the range on screen, the four view buttons, and **Create**. Everything also
answers the keyboard, as long as you are not typing in a field:

| Key | Does |
| --- | --- |
| `T` | Today |
| `D`, `W`, `M`, `A` | Day, week, month, agenda |
| `←`, `→` | Previous, next |
| `N` | New event |

The view, the date, the timezone and the team filter are kept in the page
address, so a link you copy opens the same week, in the same zone, for the
person you send it to.

#### The sidebar

On a wide screen a column on the left of the calendar holds everything that
is not the grid itself. On a narrow one the same column sits behind a
**Filters** button in the toolbar and slides in from the side.

- The **mini month** browses on its own; days with events carry a dot, and
  clicking one jumps the current view to that day.
- **Filters**: the timezone you read in (below), the **Team** filter with a
  chip to clear it, and the **kind** chips, which toggle kinds on and off so
  a calendar full of meetings can be reduced to the streams in one click.
- The **Add this server's events to my Google Calendar** switch, when your
  Google account is connected; see
  [Google Calendar and ICS](https://flavibot.xyz/docs/modules/events/06-google-calendar-and-ics).
- **Next up**: the next five upcoming events, each with its kind, a **↻** on
  an occurrence of a series and a slot summary when it has sign-up slots.
  Click one to open it.

Past events are not in the sidebar: switch the **Agenda** view to **Show
past** to read them.

#### The timezone you read in

Every event has its own timezone, the one its organiser typed the time in. The
calendar, though, is displayed in **one** zone at a time, chosen in the
sidebar's filters:

- **Server zone**, the timezone set on your server's *Configuration* page.
  This is the default.
- **My zone**, the timezone from your profile preferences, or your browser's
  when you set none.
- Any other zone from the list, for when you are planning for members
  elsewhere.

Hovering an event shows its time in the zone on screen and, when they differ,
in the event's own zone too. Members reading the panel in Discord never see
any of this: Discord renders the start in each reader's own local time.

#### What a chip tells you

- The **colour** is the event's kind, or the colour override set on it.
- A **struck-through, muted** chip is cancelled; a plain muted one has ended;
  a chip with a pulsing dot is live now.
- A **dashed** chip marked **↻** is a future occurrence of a series that has
  no panel yet. It is real, it will be posted when its turn comes, but it
  cannot be edited on its own until then. See
  [recurring events](https://flavibot.xyz/docs/modules/events/03-recurring-events).

The calendar loads the window on screen plus a margin. When a range holds
more events than can be shown at once, a banner says the list was cut; narrow
the range or filter by team to see the rest.

#### Moving an event by dragging

Rescheduling does not need the form. In the **day** and **week** views, drag
a chip to another hour or another day to move it; the time snaps to a
quarter of an hour, a ghost of the chip follows the pointer, and `Escape`
drops it back where it was. Drag the **bottom edge** of a chip to change its
duration. In the **month** view, drag a chip to another day: it keeps its
time and changes its date. On a touch screen, press and hold the chip to
pick it up.

The new time is read in the zone you are viewing in and saved on the event
in its own timezone, so a raid dragged to 21:00 on a calendar shown in Paris
time starts at 21:00 Paris time, whatever zone the event was created in. The
calendar shows the move at once; if the save fails, the chip goes back and
an error says why.

Three kinds of chip cannot be dragged, and say so when you try: a **dashed**
occurrence that has no panel yet, a **cancelled** event, and an **ended**
one. Dropping an occurrence of a series asks whether to move **this
occurrence only** or **the whole series**, the same choice the form gives;
see [recurring events](https://flavibot.xyz/docs/modules/events/03-recurring-events).

#### Creating an event

Press **Create**, hit `N`, click an empty slot in the day or week grid (that
pre-fills the start and a 60-minute duration), or drag across a few hours to
pre-fill the exact range. Clicking a day in the month view does the same for
that day.

The form:

| Field | Notes |
| --- | --- |
| **Title** | Up to 100 characters. It is the title of the panel |
| **Kind** | Sets the colour and a few defaults; see [the overview](https://flavibot.xyz/docs/modules/events/01-overview) |
| **All-day** | Hides the time inputs and counts the duration in days |
| **Starts at** and **Timezone** | The time is read in that zone. It defaults to your server's zone, and daylight saving is handled |
| **Duration** | In minutes, with an **ends at** readout next to it. Empty means no declared end |
| **Repeat** | Does not repeat, daily, weekly, monthly, yearly, or a custom rule |
| **Post the RSVP panel in** | Where members sign up. Not needed for an **Off** entry |
| **Voice channel** | Where it happens, if it is a voice or stage event |
| **Location** | Free text, for anything that is not a voice channel |
| **Capacity** | Optional. Past it, sign-ups go to the waitlist |
| **Sign-up slots** | Optional. Named groups of seats, "Tank", "Healer", "DPS", each with an emoji, a label and its own capacity; up to ten. See [sign-up slots](https://flavibot.xyz/docs/modules/events/01-overview#sign-up-slots) |
| **Required role** | Optional. Only holders can sign up |
| **Sign-ups open** and **close** | Optional. The window the panel takes answers in; outside it the buttons are disabled and say why. See [when sign-ups open and close](https://flavibot.xyz/docs/modules/events/01-overview#when-sign-ups-open-and-close) |
| **Participant role** | Optional. Given to the members who say **Going**, taken back when they change their mind or the event ends. See [the participant role](https://flavibot.xyz/docs/modules/events/01-overview#the-participant-role) |
| **Hosts** | Up to ten members shown as hosts. Empty means whoever created it |
| **Team** | Invites a team; its members are mentioned in the panel and DMed |
| **Reminders** | **Use the server defaults**, or switch it off and pick this event's own offsets, up to ten. See [reminders and announcements](https://flavibot.xyz/docs/modules/events/01-overview#reminders-and-announcements) |
| **Announce when it starts** | Server default, on, or off. With an optional **role** to ping instead of the members who said Going |
| **Cover image** | A direct image link, shown in the panel |
| **Colour** | Overrides the kind colour, with a reset |
| **Visibility** | **Public** puts the event on the server's [public calendar](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards) and its public feed, **Private** keeps it off both. It starts on the server default and changes nothing else about the event |
| **Create a Discord event** | Mirrors it into the server's Events tab. Greyed out until the event has a voice channel or a location |

The panel preview at the bottom shows how the embed will read in Discord. The
sign-up buttons are not part of the preview, but they are always there.

#### Opening an event

Click any chip and the event opens in a panel: when (in your zone and in the
event's own), where, the capacity, the hosts, the team, the kind, the repeat
rule if it is part of a series, the RSVP tally with the names behind it,
grouped by slot when the event has slots, a link to the panel message in
Discord, and the native Discord event link or, if the sync failed, why. A
**Public** or **Private** badge sits next to the title, and the sign-up
window and the participant role are listed when the event has them.

The panel is also where you set **your own** reminders for this event: the
**Remind me** block offers the same presets the Discord button does, and
saving there applies to you alone, not to the other attendees. See
[your own reminders](https://flavibot.xyz/docs/modules/events/01-overview#your-own-reminders).

Four things you can do from there:

- **Add to calendar** offers a Google Calendar link, an Outlook link and a
  `.ics` download. None of them needs a connected account; see
  [Google Calendar and ICS](https://flavibot.xyz/docs/modules/events/06-google-calendar-and-ics).
- **Edit** reopens the form. On an occurrence of a series you choose between
  editing that occurrence and editing the whole series.
- **Duplicate** opens the create form filled in from this event: title,
  description, kind, colour, duration, timezone, channel, place, capacity,
  slots, required role, hosts, team, image, reminders and announcement
  settings. Only the date is left for you, pre-set to the next same weekday.
  Nothing is copied from the original's sign-ups; the copy gets a fresh
  panel of its own.
- **Cancel** asks for a confirmation, and on an occurrence asks the same
  question. Members keep their RSVP on a cancelled event; it simply does not
  run. Tick **Also delete it permanently** on that confirmation to erase the
  event instead of keeping it as cancelled; on an event already cancelled or
  over, the button reads **Delete permanently** (see
  [the overview](https://flavibot.xyz/docs/modules/events/01-overview)).

#### Managing attendees

The panel's buttons are the normal way in, but a manager sometimes has to
answer for someone: a member who asked in a DM, a no-show to take off the
list, a regular to pull off the waitlist. With **Manage Server**, the event's
detail panel has a **Manage attendees** section:

- **Add a member** as **Going** or **Maybe**, with a slot when the event has
  slots. The member must be in the server. A **Going** on a full event, or
  a full slot, lands on the waitlist exactly as if they had pressed the
  button, and the same DMs and calendar entries follow.
- **Remove** any attendee with the button on their row. The seat frees up
  and the first person waiting is promoted, as usual.
- **Promote** a member on the waitlist with the button on their row, or the
  first in line with the button above the list. A promotion by a manager
  goes through even when the event is full: the choice is yours, the
  capacity is only a default. The member is DMed like any promotion.
- **Export CSV** downloads the whole list, one row per answer, with the
  member's ID, their status, their slot and when they answered.

Adding and promoting are offered only while the event still takes answers
(scheduled or running). Once it is over or cancelled, those controls go away —
the bot would refuse the write — while **Remove** and **Export CSV** stay, so
a roster can still be tidied up or archived afterwards.

Every one of those repaints the panel in Discord at once.

Next: [recurring events](https://flavibot.xyz/docs/modules/events/03-recurring-events).

### Recurring events

Source: https://flavibot.xyz/docs/modules/events/03-recurring-events
Summary: A repeat rule, the occurrences it produces, how many get a panel ahead of time, and what editing one of them means.

A weekly raid, a monthly town hall, a stream every Tuesday and Thursday: you
create it once with a **repeat rule**, and FlaviBot produces the occurrences.

Two things to keep apart, because everything on this page follows from the
difference:

- The **series** is the rule and the shared details: title, description,
  place, capacity, sign-up slots, reminders, kind, team, and so on. It is
  never displayed on its own and never has a panel.
- An **occurrence** is one date produced by the rule. Each one is a **normal
  event**: its own panel, its own RSVPs, its own reminders, its own Discord
  scheduled event when the rule cannot be mirrored as one. Members sign up for
  the occurrence they want, not for the series.

#### Writing the rule

In the create form, **Repeat** offers *Daily*, *Weekly*, *Monthly*, *Yearly*
and *Custom*. The quick choices repeat every day, every week on the start's
weekday, every month on the start's date, every year on the start's date.
*Custom* opens the full editor:

| Repeats | Options |
| --- | --- |
| **Daily** | Every N days. Optionally only on some weekdays, for a "weekdays only" schedule |
| **Weekly** | Every N weeks, on one or more weekdays |
| **Monthly** | Every N months, either **on day N** (1 to 31, or *last day*) or **on the Nth weekday** (first to fourth, or last) |
| **Yearly** | Every N years, on a month and a day |

**Every N** goes up to 99. **Ends** is *Never*, *On a date* (at most two
years ahead) or *After N occurrences* (at most 365); pick both and whichever
comes first wins. A summary sentence under the editor reads the rule back to
you: "Every 2 weeks on Monday and Wednesday, until 12 Dec 2026".

Three details of how dates are produced:

- The **start you type is the first occurrence**, provided it fits the rule.
  A weekly-on-Monday series started on a Wednesday begins the following
  Monday.
- Occurrences keep the **local time** of the start in the event's timezone.
  A 20:00 raid stays at 20:00 through a daylight-saving change; the instant
  moves, the wall clock does not.
- A monthly rule on **day 31** skips the months that do not have one; use
  **last day** for "the end of every month".

The same rule is available from Discord, in a shorter form: `/event create
… repeat:weekly repeat_until:2026-12-31`. The choices there are `none`,
`daily`, `weekdays`, `weekly`, `biweekly`, `monthly` and `yearly`, always on
the start's weekday or date.

#### How many occurrences exist at a time

FlaviBot does not post a panel for every future date. The setting
**Occurrences posted ahead** (1 to 5, in the module's Settings tab) says how
many upcoming occurrences have a panel at any moment. The default is one.

- Those occurrences are real events, taking sign-ups and reminders.
- Every later date is still on the calendar, drawn **dashed with a ↻**, so the
  month view shows the whole season. It gets its panel when its turn comes.
- When an occurrence ends, the next one is posted about a minute later, so
  the channel always has the coming date ready to sign up for.

Raise the number when your members like to book the next few weeks at once;
keep it at one when a single "this week" panel is enough. Remember that each
posted occurrence counts toward the server's active-event cap.

When the rule runs out (its end date passed, or its count is reached) the
series **ends** on its own after its last occurrence, and no new dates appear.

#### Editing

Open any occurrence and press **Edit**. The form asks whether you are editing
**this occurrence** or **the whole series**.

**This occurrence** changes that date alone and marks it as **detached**: it
keeps its own title, time or place from now on, and later edits to the series
leave it alone. Use it for "the meeting is at 21:00 this week only".

**The whole series** changes the rule and the shared details:

- Change the title, description, place, capacity, sign-up slots, hosts,
  kind, colour, team, reminders, start announcement or Discord sync, and
  every posted occurrence that was not detached is updated in place: its
  panel is repainted, members keep their RSVPs.
- Change the **start time, timezone or repeat rule**, and the occurrences
  that had not started yet are **cancelled and posted again** on the new
  dates. That is the honest outcome of moving a series: a panel for
  "Tuesday 20:00" cannot become "Thursday 19:00" and keep everyone's answer
  meaningful. Occurrences that are already live finish as they were, and
  detached ones are untouched.

Where you open the series from decides which of those two you can change:

- From a **posted** occurrence, move the **start time or timezone** and the
  whole series shifts by the same amount. The repeat rule is shown there but
  not editable.
- From a **dashed** occurrence, one without a panel yet, change the **repeat
  rule**. Its date, time and timezone are locked, since that occurrence's own
  date is not the series' start.

A dashed occurrence cannot be edited by itself: edit the series instead, or
wait until it is posted.

Dragging an occurrence to another time in the calendar asks the same
question, **this occurrence** or **the whole series**, with the same
outcomes; a dashed occurrence cannot be dragged at all. From Discord,
`/event edit` on an occurrence always changes that occurrence alone, and
detaches it the same way. See
[moving an event by dragging](https://flavibot.xyz/docs/modules/events/02-calendar-views#moving-an-event-by-dragging).

#### Cancelling

Cancelling an occurrence asks the same question.

**This occurrence** cancels that date and records it as an **exception** in
the rule: the series carries on, and that date never comes back, even if you
later change the rule. Use it for a skipped week.

**The whole series** cancels every occurrence that had not started, lets a
live one finish, removes the series' native Discord event, and stops
producing dates. From Discord that is `/event cancel reference:<id>
series:True`; without `series` the command cancels the one occurrence.

#### What members see

The panel of an occurrence carries a **Repeats** line with the rule in plain
words, so someone reading "Every week on Tuesday" knows there will be a next
one. `/event list` marks occurrences with **↻** and shows their kind and, on
an event with sign-up slots, the count per slot; so does the **Next up** list
on the dashboard.

Reminders, the waitlist, the slots and the calendar entries all work per
occurrence. A member who said **Going** to this week's raid has not said
anything about next week's; that is by design, and it is why the panel is
posted fresh each time. To copy one occurrence as a stand-alone event, open
it and press **Duplicate**.

Next: [Discord scheduled events](https://flavibot.xyz/docs/modules/events/04-discord-scheduled-events).

### Discord scheduled events

Source: https://flavibot.xyz/docs/modules/events/04-discord-scheduled-events
Summary: Mirror an event into your server's Events tab, the Create Events permission it needs, and what to do when the sync fails.

Discord has its own events: the **Events** tab at the top of the channel
list, with a countdown, an **Interested** bell, and a banner in the voice
channel when it starts. FlaviBot can create one of those for each of your
events, so the server header and the RSVP panel say the same thing.

The panel is the real surface: sign-ups, the waitlist, reminders and the
roster all live there. The Discord event is a mirror of it. If the mirror
cannot be created, the event still works.

#### When one is created

Each event has a **Create a Discord event** switch in its form. Its default
comes from two places:

- The module setting **Create Discord events by default**, on by default.
- The **kind**: *Meeting*, *Session*, *Milestone*, *Deadline* and *Off*
  start with the switch off, because a staff meeting or a day off has no
  business in the public Events tab. *Event* and *Stream* start with it on.

Flip the switch on an existing event and the native copy is created or
removed accordingly.

Discord also needs a **place**. An event happening in a **voice or stage
channel** is created as a voice event (or a stage event, with the *Start
Stage* button, when the channel is a stage). An event with a **location** and
no channel is created as an external event, which Discord requires an end time
for; when you gave none, FlaviBot declares two hours. An event with neither a
channel nor a location cannot be mirrored, and the switch is greyed out with
that explanation until you add one.

#### The permission it needs

Note:

Since February 2026 an application must hold the **Create Events** permission
to create a scheduled event. **Manage Events** alone is no longer enough. A
server that invited FlaviBot before then may well be missing it. Either grant
**Create Events** to the bot's role in *Server Settings > Roles*, or
[re-invite the bot](https://flavibot.xyz/docs/getting-started/01-add-flavibot): the invite link
asks for Administrator, which includes it.

For a voice or stage event the bot also needs **View Channel** and
**Connect** on that channel. FlaviBot checks all of this before calling
Discord, so a missing permission is reported at once rather than as a refusal
from Discord.

#### What is mirrored

Title, description, start, end, and the place. Editing any of them on the
event pushes the change to the native copy; cancelling the event deletes it.

What is **not** mirrored: the cover image, the capacity, the sign-up slots,
the hosts, the RSVPs, and the reminders. Discord's **Interested** count is
Discord's own and has nothing to do with the panel, and Discord's own
notifications are not FlaviBot's reminders. Members who want a reminder from FlaviBot press **Going** on
the panel; members who want Discord's own notification press the bell. Both
are fine.

#### When the sync fails

The event's detail panel shows the native event as a link when it exists,
and the **reason** when it does not. The reason is stored on the event, so it
survives a page reload and can be read by another admin.

| Reason | What happened | Fix |
| --- | --- | --- |
| **Missing permissions** | The bot lacks Create Events, or View Channel / Connect on the voice channel | Re-invite the bot, or grant the permission to its role |
| **No place** | The event has neither a voice channel nor a location | Add one of the two |
| **Limit reached** | The server already has as many scheduled events as Discord allows | Let some finish, or cancel old ones in the Events tab |
| **Unsupported repeat rule** | The series' rule has no equivalent on Discord | Nothing to fix: each occurrence is mirrored on its own instead |
| **Refused** | Discord rejected the request for another reason | Try editing the event; if it persists, ask on the support server |

A failure never blocks the event. Fix the cause and edit the event (any save
retries the sync), and the reason is cleared once the native copy exists.

#### Recurring series

Discord can repeat a scheduled event on its own, but only for a small set of
rules. When your series uses one of them, FlaviBot creates **one** native
recurring event for the whole series, and the individual occurrences do not
get their own. Otherwise, each posted occurrence gets a native event of its
own, as a plain event would.

| Rule | Mirrored as one native recurring event |
| --- | --- |
| Every day | Yes |
| Every day on Monday to Friday, Tuesday to Saturday, Sunday to Thursday, Friday and Saturday, Saturday and Sunday, or Sunday and Monday | Yes |
| Every week or every two weeks, on one weekday | Yes |
| Every month on the Nth weekday (first Tuesday, last Friday) | Yes |
| Every year on a month and a day | Yes |
| Every 3+ weeks, several weekdays a week, every month on day N, custom intervals | No, one native event per occurrence |

Two things Discord's version cannot express:

- **An end.** A series that ends on a date or after N occurrences is mirrored
  as an open-ended native event; FlaviBot deletes it itself when the series
  ends.
- **A skipped date.** Cancelling one occurrence of a series takes down that
  occurrence's own native event when it has one, but a series mirrored as a
  single recurring event still shows that date in the Events tab. Post a
  message, or cancel the occurrence's own native copy by hand if you need
  the tab to match.

#### One bot per server

A Discord scheduled event belongs to the application that created it. No
other bot, not even another FlaviBot custom bot, can edit or delete it. That
is why the module has a **primary bot** setting: only that bot creates and
maintains the native copies. Switching the primary bot deletes every native
event the old bot made and recreates them under the new one, which takes a
moment and needs the old bot to still be in the server to remove its copies.

Next: [teams and finding a time](https://flavibot.xyz/docs/modules/events/05-teams-and-availability).

### Teams and finding a time

Source: https://flavibot.xyz/docs/modules/events/05-teams-and-availability
Summary: Group members into teams, invite a team to an event, see when its members are free, and let FlaviBot suggest a slot.

A **team** is a named list of members: your moderators, the raid group, the
podcast crew. Teams do three things on the Events page: they filter the
calendar, they get invited to an event as a group, and their members'
availability can be laid side by side to find a time that works.

#### Creating a team

Open the **Teams** tab and press **Create**. A team has a **name** (up to 64
characters, unique on the server), an optional **description**, a **colour**,
and its **members**, picked from the server with an optional free-text
**role** next to each one ("tank", "host", "backup"). The role is a label for
your own reading; it grants nothing.

A server can hold **25 teams** of up to **100 members** each. Editing a team
replaces its member list with what the picker shows, so removing someone is a
matter of taking them out of the picker and saving.

The team list shows each team's colour, its members' avatars and a **team
clock**: the current local time of every member, read from the timezone they
set in their profile. A member with no timezone shows an **unknown zone**
badge; they can fix that on their own profile page, under *Calendar*.

#### Inviting a team to an event

In the event form, **Team** attaches one team. Two things follow:

- The panel carries an **Invited team** line naming the team and mentioning
  its members, so they are pinged when the panel is posted.
- Each member receives a **direct message** with a link to the panel when the
  event is created (or when each occurrence of a series is posted). The
  message shows the start as a Discord timestamp and, for a member who set
  a timezone on their profile, in plain words next to it, "(20:00 your
  time)".

An invitation is not an RSVP. Invited members still press **Going** like
anyone else; nobody is signed up for something they did not answer.

In the calendar, the **Team** filter in the sidebar keeps only the events
attached to that team, and a
[channel board](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards#channel-boards)
can be narrowed to one team the same way, so a raid group gets its own
schedule in its own channel.

#### The Availability tab

Pick a team and a window: **this week**, **next week**, or a custom range of
up to a month. The tab draws one **lane per member** over a week grid, in the
timezone you are viewing in:

- **Shaded** hours are the member's working hours, from their preferences.
- **Busy** blocks come from three sources, each in its own colour: the
  member's **Google Calendar** (slate), **FlaviBot events** they said *Going*
  to, in any server (indigo), and **unavailability blocks** they entered by
  hand (amber).
- A member greyed out with **no data** has not set anything up, or has
  switched availability sharing off.

What you see is **free or busy, never why**. Google entries arrive without
titles, FlaviBot events from other servers are shown as busy time only, and
manual blocks are shown as blocks. Each member decides whether to share at
all, with the **Share my availability** switch on their profile page, and a
member who turns it off is shown as not shared rather than as free.

#### Find a time

Under the lanes, the **Find a time** panel turns those lanes into
suggestions:

1. Choose a **duration** (30, 60, 90 or 120 minutes).
2. Set the **earliest** and **latest** hour you are willing to start, in
   your own zone.
3. Choose **everyone** or **at least N members**.

FlaviBot ranks the free slots: the more members are free, the more of them
are inside their working hours, and the more of them are in daytime, the
higher a slot goes. Each result lists the **local time for every member**, so
a 19:00 in Paris that is 04:00 in Sydney is visible before you pick it.

**Schedule** on a result opens the create form pre-filled with that start,
that duration, the team, the kind *Meeting*, and Discord sync off. Change
whatever you want before saving.

#### What each member sets up

Availability is only as good as what members told FlaviBot. Everything below
is on a member's own profile page on the dashboard, under **Calendar**, and
applies to every server they are in:

| Preference | What it does |
| --- | --- |
| **Timezone** | Manual, from the browser, or from Google Calendar once connected. Drives the team clock, the local times in Find a time, and the "your time" line in the invitations and reminders FlaviBot sends by DM |
| **Week starts on** and **time format** | How their own calendar views read |
| **Working hours** | Per weekday, on or off, with a start and end. Default Monday to Friday, 09:00 to 18:00 |
| **Share my availability** | Whether teams can see their busy time at all. On by default; nothing is shared with anyone outside a team they belong to |
| **Unavailability blocks** | Holidays and appointments entered by hand, with a label, a start, an end, and an all-day switch. Up to 200 |

Connecting a Google account adds the member's Google busy time; see
[Google Calendar and ICS](https://flavibot.xyz/docs/modules/events/06-google-calendar-and-ics).

Next: [Google Calendar and ICS](https://flavibot.xyz/docs/modules/events/06-google-calendar-and-ics).

### Google Calendar and ICS

Source: https://flavibot.xyz/docs/modules/events/06-google-calendar-and-ics
Summary: Three ways to get FlaviBot events into a real calendar, and what connecting a Google account does and does not let FlaviBot see.

An event on the dashboard is not on anyone's calendar yet. There are three
ways to get it there, and only the last one asks for a Google account.

| Way | What it gives | Needs an account |
| --- | --- | --- |
| **Add to calendar** on one event | That event, in your calendar app, once | No |
| **Subscribing** to a feed | A calendar that updates itself | No |
| **Connecting Google** | Your RSVPs, or every event of a server, written into your Google Calendar, and your busy time shared with your teams | Yes |

#### Adding one event

Open any event on the dashboard and press **Add to calendar**. The menu
offers a **Google Calendar** link, an **Outlook** link, and a **Download
.ics** file that any calendar app opens: Apple Calendar, Thunderbird, the
phone's built-in one. The links open the calendar's own "new event" form
pre-filled with the title, time, place and description; nothing is sent
through FlaviBot.

Members reading the panel in Discord get a similar hook: after pressing
**Going**, the confirmation carries an **Add to Google Calendar** button for
those who have not connected an account yet.

#### Subscribing to a feed

A **feed** is a link your calendar app checks on its own, so new events,
changes and cancellations show up without anyone doing anything. Two of them
are private, and the link is what keeps them private:

- **Your feed**: every event you said **Going** to, across all your servers.
  It is on your profile page, under **Calendar > My calendar feed**.
- **The server feed**: every upcoming event of one server, including the
  series as repeating entries with their skipped dates. It is on the Events
  page, **Settings > Calendar subscription**, and needs Manage Server to read.

Both feeds cover the last 30 days and the next year, and leave cancelled
events out.

A server that publishes a
[public calendar](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards) has a
third one, offered on that page itself: the same kind of subscription, for
the events marked public, and with no secret in the link since the page is
open to everyone anyway.

To subscribe: in **Google Calendar**, *Other calendars > + > From URL*; in
**Apple Calendar**, *File > New Calendar Subscription*; in **Outlook**, *Add
calendar > Subscribe from web*. Paste the link and you are done. How often the
app refreshes is up to the app: Apple lets you choose, Outlook checks every
few hours, and Google Calendar can take up to a day to pick up a change.

The link is the only thing protecting the feed, so treat it like a password.
If it leaks, press **Rotate**: the old link stops working immediately and
you paste the new one into your calendar app.

#### Connecting a Google account

The **Google Calendar** card on your profile page offers two levels. Each
asks Google for exactly the access it needs, and the second includes the
first, so upgrading is one more consent screen, not a new connection.

##### Share my availability

Lets your teams see **when** you are busy, so that
[Find a time](https://flavibot.xyz/docs/modules/events/05-teams-and-availability) can work
around your real calendar. FlaviBot asks Google for three read-only things:

- your **free/busy** information, which tells it *that* you are busy between
  two times and nothing else;
- the **list of your calendars**, so you can tick which ones count;
- your **timezone setting**, offered as a source for your FlaviBot timezone.

Once connected, the card lists your calendars with a checkbox each; by default
only your primary calendar counts. Nothing is read until a team member opens
an availability view that includes you, and even then FlaviBot receives busy
intervals only: **it never sees the titles, guests or details of your Google
events**, and it never stores the intervals beyond a few minutes of caching.
Switch **Share my availability** off in your preferences and the connection
stays but nothing is looked up.

##### Add my RSVPs to Google Calendar

Lets FlaviBot write into your calendar. Press **Going** on any event and an
entry appears in your primary Google calendar, with the title, the time in
the event's timezone, the place, the description, and a "Via FlaviBot"
line. Your calendar's own reminder settings apply to it.

- Withdraw, press **Can't make it**, or have the event cancelled, and the entry is
  removed.
- Edit the event on the server side, and the entry is updated.
- Get promoted off the waitlist, and the entry is created at that moment.
- Be signed up as **Going** by a server manager from the dashboard, and the
  entry is created too; be removed by one, and it goes.
- A member who was **Maybe** gets no entry: a maybe is not a plan.

FlaviBot only ever touches entries it created itself. It does not read your
calendar at this level either; the access it holds is limited to creating,
editing and deleting events.

##### Add every event of a server to your calendar

RSVPs cover the events you chose; a server you follow closely may deserve
all of them. On that server's **Events** page, in the sidebar next to the
calendar (behind the toolbar's **Filters** button on a phone), switch on
**Add this server's events to my Google Calendar**: every
upcoming event of the server (the next 90 days) is written into your
calendar at once, and each new event follows as it is created. The switch is
per server, so a member of ten servers picks the ones they care about.

It is **on by default for the server you linked Google from**: connect from
a server's dashboard and that server's events start arriving without a
second step. Any other server stays off until you switch it on there.

The entries behave like RSVP entries: an edit updates them, a cancellation
removes them, and pressing **Can't make it** removes that one entry, since a
declined event does not belong on your calendar even with the sync on.
Withdrawing a **Going** answer only removes an entry your RSVP created; one
the server sync placed stays, because you asked for the whole server, and so
does a **Maybe**. Switching the sync off removes every entry it created and
leaves the ones from your own RSVPs in place.

The switch only appears once your Google link holds this level; with
availability sharing alone, the same line offers to grant it.

##### Reconnecting and disconnecting

If you remove FlaviBot from your Google account's connected apps, or the
access expires unused, the card shows **Reconnect needed** and the sync
pauses until you reconnect; nothing else breaks.

**Disconnect** revokes the access at Google and deletes the tokens FlaviBot
held. Entries already written into your calendar are **left in place**: they
are yours, and a disconnect should not empty your agenda.

#### What is stored, and where to read more

For a connected account FlaviBot keeps your Google email address (to show
"connected as"), the access tokens, encrypted, the ids and names of the
calendars you ticked, your Google timezone, and the list of servers where you
switched on the server-wide sync. It never stores the content
of your Google calendar. The
[privacy policy](https://flavibot.xyz/legal/privacy-policy) has the full statement, including our
adherence to Google's API Services User Data Policy.

Next: [the public calendar and boards](https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards).

### The public calendar and boards

Source: https://flavibot.xyz/docs/modules/events/07-public-calendar-and-boards
Summary: A public web page and feed for the events you choose to show, and a channel message that keeps the schedule in front of your members.

An event lives in two places by default: the dashboard, which only managers
open, and its RSVP panel, which only the people reading that channel see. Two
surfaces widen that.

- The **public calendar** is a web page at
  `flavibot.xyz/calendar/<server ID>` that anyone can open, with no Discord
  account and no login, plus a feed they can subscribe to. You choose, event
  by event, what goes on it.
- A **board** is a message in one of your channels listing what is coming,
  edited in place as things change, so the schedule is never a pinned message
  three weeks out of date.

The two are independent. A server can run a board and publish nothing, or
publish a page and never post a board.

#### Public or private

Every event carries a **visibility**, **Public** or **Private**, and it
decides exactly one thing: whether the event appears on the server's public
calendar page and in its public feed.

Private is the default, and a private event is not a hidden one. Whatever the
visibility says:

- the RSVP panel is posted in its channel, and sign-ups, the waitlist and the
  roster work as usual;
- reminders, team invitations and the start announcement are sent as usual;
- the native Discord scheduled event is created as usual;
- members' Google Calendar entries and ICS feeds are written as usual.

**Public** adds a row to a page anyone can read, and nothing else. Reading
the setting as a permission is the one mistake to avoid: marking an event
private does not stop FlaviBot from telling people about it.

You set it in the event form, under **Visibility**: a Public / Private
control that starts on whatever the server default says. The event's detail
panel carries a badge, so reading a calendar tells you at a glance what is
published. On a series, visibility is one of the shared details: set it on
the series and the dates it posts are published with it.

#### Turning the page on

The **Settings** tab of the Events page has a **Public calendar** block:

| Setting | What it does |
| --- | --- |
| **Public calendar** | Whether the page exists at all. Off by default |
| **New events are public** | The default of the **Visibility** control on the create form. Off: you publish event by event |
| **Public description** | One line under the server name on the page, up to 300 characters |
| **Page address** | `flavibot.xyz/calendar/<server ID>`, with a copy button |

The switch and the default are independent, which is what lets you prepare:
mark a few events public while the page is still off, then turn it on when
the list looks right.

Switch it off and the address answers **not found** — deliberately the same
answer as a server that never had a page, so nobody can tell one from the
other by poking at addresses. Nothing is deleted and no event's visibility
changes; switch it back on and the page returns as it was.

Note:

The titles, descriptions and locations of the events you publish are readable
by anyone holding the address, and the page may be picked up by search
engines. Keep the private voice link, the staff-only agenda and members' real
names out of an event you mark public.

#### What a visitor sees

A header with the server's icon, name and public description, then the events
themselves as a month grid or an agenda list, read-only: nothing to create,
nothing to drag.

- The page opens in the **visitor's own timezone**, read from their browser,
  and a selector switches to the server's zone or to any other one.
- Clicking an event opens its details: when it is, in the zone on screen and
  in the event's own, the place, the description, how many people are going,
  the capacity when there is one, and "Every Tuesday" when it belongs to a
  series.
- From there: **Add to Google Calendar**, **Download .ics** and, when the
  server has an invite FlaviBot knows about, **Join the server**.
- **Subscribe to this calendar** gives the feed link described below.

The page covers the next **60 days** and at most **200 events**, and it is
cached for a couple of minutes: an event created on the dashboard shows up
there shortly after, not instantly.

Three kinds of event are **never** listed:

- the ones marked **Private**;
- **cancelled** events, and events that are already over;
- the dates of a series that have **no panel yet** — the dashed occurrences
  of [recurring events](https://flavibot.xyz/docs/modules/events/03-recurring-events). The page
  lists real events, not projections. Raise **Occurrences posted ahead** if
  you want more of the season visible.

And for the events that are listed, the page carries the event and not the
server: no channel, no link to the panel message, no hosts, no invited team,
no required role, and none of the names of who answered. A visitor sees that
an event exists, when it is, and how full it is.

#### The public feed

**Subscribe to this calendar** is an ICS feed of the same events, for a
calendar app rather than a browser. It needs no account and carries no secret
token — it is as public as the page, and it stops answering the moment the
page is switched off.

That makes three feeds, with three different audiences:

| Feed | Holds | Who can read it |
| --- | --- | --- |
| **The public feed**, on the public page | The server's public events | Anyone at all |
| **The server feed**, Settings > Calendar subscription | Every upcoming event of the server, private ones included | Anyone holding the secret link. Rotate it if it leaks |
| **Your feed**, on your profile page | Every event you said **Going** to, in every server | Anyone holding the secret link |

Subscribing works the same way for all three; the steps for Google Calendar,
Apple Calendar and Outlook are in
[Google Calendar and ICS](https://flavibot.xyz/docs/modules/events/06-google-calendar-and-ics#subscribing-to-a-feed).
The public one has nothing to rotate, since nothing about it is secret.

#### Channel boards

A **board** is one message, in one channel, listing the server's events for a
period. FlaviBot keeps it correct on its own: nobody reposts it, nobody edits
it by hand, and it does not scroll away into last month the way a pinned
announcement does.

##### Creating one

**Settings** tab, **Boards**, then **Add**:

| Field | Notes |
| --- | --- |
| **Channel** | A text channel. One board per channel |
| **Shows** | **Today**, **This week**, **This month**, or **The next N events** |
| **How many** | 1 to 25. Only **The next N** reads it; the other three are bounded by their period |
| **Team** | Only the events attached to that team. Empty: every event |
| **Kinds** | Tick the kinds to keep. None ticked: every kind |
| **Title** | The heading of the message, up to 100 characters. Empty: a default one naming the period |

A preview next to the form shows the message as Discord will render it. Save,
and it is posted straight away.

##### What it reads like

One line per event: the kind's emoji and colour, the start as a Discord
timestamp so every reader sees it in their own local time, the title, the
going count, and the tally per slot on an event that has
[sign-up slots](https://flavibot.xyz/docs/modules/events/01-overview#sign-up-slots). Under the
list, an "updated a few minutes ago" line. A period with nothing in it says
so rather than leaving an empty message behind.

A board is a summary, not a second panel: members read it to know what is on
and still answer on the event's own panel.

A board lists the events that match its filters whether they are **public or
private**: visibility is about the web page, while a board is already inside
the server, behind the channel's own permissions. A board in a staff channel
shows the staff meetings. What keeps an event off a board is the channel you
put the board in, its team filter or its kinds — not its visibility.

##### How it stays up to date

- **On every change** to the server's events: created, edited, moved,
  cancelled, deleted, and on sign-ups too. A burst of answers is grouped, so
  a panel filling up nudges the board about every ten seconds rather than on
  every click.
- **Every fifteen minutes** in any case. That is what rolls a **Today** board
  over at midnight, and what brings a board back into line after an outage.
- **If someone deletes the message**, the next refresh posts a new one and
  forgets the old. Deleting the message is not how you remove a board.
- **Refresh now**, on the board's row, when you would rather not wait.

##### Editing and removing

**Edit** reopens the same form and re-renders the message with what you
changed. **Delete** removes the board and its message from the channel.

##### Limits and permissions

A server can run **five boards**, one per channel; the form refuses a channel
that already has one and tells you so. FlaviBot needs **View Channel** and
**Send Messages** in the board's channel — without them nothing is posted and
the board's row keeps reading as never updated, so grant the permission and
press **Refresh now**.

### How projects and tasks work

Source: https://flavibot.xyz/docs/modules/tasks/01-overview
Summary: Projects, issues, states, labels and cycles, what an identifier like WEB-12 means, and how the Discord half meets the dashboard.

**Projects & Tasks** is a to-do tracker that lives in your server. A
**project** is a piece of work your team is doing — the website, the modding
queue, the next event — and an **issue** is one thing to do inside it. Issues
sit in columns on a board, move to the right as the work advances, and can be
assigned to somebody, given a priority, a due date and a label or two.

If you have used Linear, Jira or GitHub issues, this is the same shape, made
smaller and moved into Discord. If you have not: picture a wall of sticky
notes. Each note is one thing to do. The wall is split into columns — *not
started*, *being worked on*, *done* — and a note moves from one column to the
next as somebody does the work. That is the whole idea; everything below is
detail.

Projects & Tasks is being rolled out gradually. If **Projects & Tasks** is not
in your dashboard's Management section, and `/task` answers that it is not
available, the module is not enabled for your server yet.

#### The objects

| Object | What it is | Where it lives |
| --- | --- | --- |
| **Project** | A named body of work with a short key, `WEB` | The project switcher, at the top of the page |
| **Sub-project** | A project nested under another one, `SHOP` under `WEB`, with its own key and columns | Indented under its parent in the switcher |
| **Issue** | One thing to do, identified as `WEB-12` | A column of the board, and optionally a card in a channel |
| **State** | One column of a project's board: *Todo*, *In Progress*, *Done* | The board, edited in Settings |
| **Label** | A tag shared by every project of the server: *bug*, *design* | On the issue, edited in Settings |
| **Cycle** | A dated slice of a project's work: *Sprint 4*, two weeks long | The Cycles view, behind its button in the page header |
| **Team** | A named group of members, the same one the calendar uses | On the issue, edited on the Events page |
| **Relation** | A link between two issues: *blocks*, *relates to*, *duplicates* | The issue, in its Relations block |
| **View** | A saved slice of the board: its filters, its grouping, its order | The pills above the board |
| **Template** | A pre-filled issue you start from instead of a blank one | **New from template**, on the filter bar; edited in Settings |
| **Comment** | Something somebody wrote on an issue | The issue's page, mirrored into the issue's thread |
| **Activity** | The automatic trail of what changed, and who changed it | The issue's page, under the comments |
| **Time entry** | One stretch of work somebody logged: 90 minutes, on Tuesday | The issue, in its time log |
| **GitHub link** | A pull request or a GitHub issue attached to this one | The issue, and a badge on its card |
| **Card** | The Discord message that shows one issue and carries its buttons | One channel, one message per issue |
| **Page** | A Markdown document beside the issues: a spec, meeting notes, a how-to | The Docs view of a project, or the server wiki |
| **Thread** | The discussion hanging off that card | Under the card |
| **Customer** | Somebody the team tracks requests for: a member, or another server | The Customers view, behind its button in the page header |
| **Request** | One thing a customer asked for, on the issue that would answer it | The issue, in its Requests block |

Projects, states, labels and cycles are the furniture: you set them up once and
rarely touch them again. Issues are the work, and [doc
pages](https://flavibot.xyz/docs/modules/tasks/09-doc-pages) are the writing around it.
[Customers](https://flavibot.xyz/docs/modules/tasks/10-customers) are who asked for the work, and
requests are what they asked for.

#### Identifiers

Every project has a **key**: two to eight characters, upper case, starting with
a letter — `WEB`, `MOD`, `ART2`. Every issue in the project gets a **number**,
handed out in order, and the two together are the issue's **identifier**:

```
WEB-12
 │   └─ the number, handed out in order inside that project
 └───── the project's key
```

That is what people say out loud and what they type. `/task view
reference:WEB-12` opens it, and typing `web-12` works just as well — the key is
read case-insensitively. A bare `12` is never an identifier: with two projects
open it would be ambiguous, so FlaviBot refuses it rather than guessing.

Numbers are handed out **per project** and are **never reused**. Delete
`WEB-12` and the next issue is still `WEB-13`, so an identifier quoted in an
old thread never comes back pointing at different work. Two people creating an
issue at the same second get two different numbers.

The issue's identifier follows its project's key, so **renaming a key rewrites
every identifier the project shows**. See
[projects, states and labels](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels#renaming-a-key).

#### The path an issue takes

1. Somebody creates it, from the **+** on a board column, from `C` on the
   keyboard, from **New from template** on the filter bar, or from `/task
   create` in Discord. It needs a title and nothing else: it lands in the
   project's first column with no assignee and no priority.
2. FlaviBot posts its **card** in the module's default channel, or in the
   channel the command ran in. The card shows the identifier and title, the
   state, the priority, the assignee, the labels, the due date and the cycle,
   and carries four buttons: change the state, take it (or drop it), comment,
   and open it on the dashboard.
3. When **Create a thread** is on, a thread named `WEB-12 Title` is opened
   under the card, and every comment written on the dashboard is mirrored into
   it. That is where the conversation goes, so the channel itself stays a list
   of cards rather than a chat.
4. People work on it. Each change — state, assignee, priority, title, due date,
   cycle, labels — is written to the issue's **activity trail** with who did it
   and when, repaints the card, and where it matters sends a DM.
5. Moving the issue into a **completed** or **cancelled** column closes it and
   records the moment. It drops out of the default filters, stops counting as
   open, and counts towards its cycle's progress. Moving it back out reopens
   it and clears that moment; moving it straight from one closed column to the
   other, Done to Cancelled, keeps it.

**Archiving** an issue puts it away without losing anything: it leaves the
board, the list and every view, keeps its identifier, its comments and its
activity, and comes back whole when you restore it. It is the right answer to
an issue that is neither done nor wanted — a duplicate, something dropped, a
board that has silted up.

**Deleting** an issue removes it, its comments, its activity and its card
(Discord removes the thread with the card it hangs from). Its sub-issues
survive; they simply stop having a parent. There is no undo — closing an issue,
and archiving it when it is still in the way, is the normal way to be done with
something. See [archiving an
issue](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#archiving-an-issue).

#### States and what they mean

A state has a **name** you choose and a **type** you pick from five. The name
is what everyone reads; the type is what FlaviBot understands.

| Type | Means | In the default workflow |
| --- | --- | --- |
| **Backlog** | Noted, not planned yet | Backlog |
| **Unstarted** | Planned, nobody has started | Todo |
| **Started** | Being worked on | In Progress, In Review |
| **Completed** | Done, the work happened | Done |
| **Cancelled** | Done with, the work did not happen | Cancelled |

Two states can share a type: *In Progress* and *In Review* are both **started**,
and a team that also wants a *QA* column simply adds a third. **Completed** and
**cancelled** both close an issue; the difference between them is whether the
work actually happened, which matters to a report and to nothing else.

Every new project is seeded with the six columns above, so it is usable the
second it exists. Renaming, recolouring, reordering, adding and deleting them
is in [projects, states and
labels](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels#the-workflow).

#### Priorities

Five levels, from none to urgent. The level is drawn as a small meter — a chip
on the card, a column on the list, a row on the issue's own page — and it is
the meter's *shape* that carries it, with the word beside it wherever there is
room for one. The list can be sorted and filtered by it.

| Priority | How it is drawn |
| --- | --- |
| **Urgent** | A solid tile with an exclamation |
| **High** | Three rungs lit |
| **Medium** | Two rungs lit |
| **Low** | One rung lit |
| **None** | Three dashes — the default, and it means nobody has answered |

Grouping the board by priority is the one place a level carries a colour of its
own: the dot beside a column or swimlane heading. See [views and
shortcuts](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#grouping).

#### Sizes, time, teams and GitHub

Four things an issue can carry beyond the properties above, all optional and
all off until somebody turns them on:

| | What it answers | Where |
| --- | --- | --- |
| **Estimate** | How big this is, in points on a scale the project picks | [Estimates, time and teams](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#estimates) |
| **Time** | How long it was expected to take, and how long it actually took | [Estimates, time and teams](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#time) |
| **Team** | Whose work this is, as a group rather than a person | [Estimates, time and teams](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#teams) |
| **GitHub** | Which pull request fixes it | [GitHub](https://flavibot.xyz/docs/modules/tasks/08-github) |

A server that ignores all four has exactly the tracker described above, which
is why none of them is on to begin with.

#### Where you configure it

Everything is on the dashboard, under **Management > Projects & Tasks**, on one
page with a project switcher in its header:

- **The issues**, drawn either as a **board** of columns you drag cards between
  or as a dense **list** for reading and sorting — whichever you pick in
  **Display**. See [the board and the
  issue](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues).
- **Cycles**: the dated slices of work and their progress, behind the
  **Cycles** button in the header. See [cycles](https://flavibot.xyz/docs/modules/tasks/04-cycles).
- **Docs**: the project's pages, or the server wiki when no project is
  chosen, behind the **Docs** button beside it. See [doc
  pages](https://flavibot.xyz/docs/modules/tasks/09-doc-pages).
- **Customers**: who asked for the work, and the requests behind each issue,
  behind the **Customers** button — on a server that keeps them. See
  [customers and requests](https://flavibot.xyz/docs/modules/tasks/10-customers).
- **Settings**: the switches below, the server's labels, the current project's
  columns, and its issue templates, behind the cog beside it.

Those buttons are toggles — pressing the lit one puts you back on the
issues. The issues themselves can be grouped by something other than the state,
sliced with filters and saved as a view, and the whole page answers the
keyboard — see [views and
shortcuts](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts).

#### Who can do what

What a member may do in the tracker is a **set of permissions**, the same set
whether they act from the dashboard, from a card's buttons or from `/task` —
a member is never refused by one and let through by another. There are seventeen,
and most come in two sizes: **own**, for an issue you created or are assigned
to, and **any**, for everybody's.

| Permission | What it lets you do |
| --- | --- |
| **View the tracker** | Open the tracker on the dashboard. Discord never needs it: whoever sees the card sees the issue |
| **Create issues** | File one, from `/task create` or the dashboard. Also write a [doc page](https://flavibot.xyz/docs/modules/tasks/09-doc-pages) |
| **Comment** | Post a comment and react to one. Editing or deleting your **own** comment is authorship, never a permission |
| **Claim free issues** | Take an issue nobody holds. Dropping yourself again is *editing your own* |
| **Edit own issues** / **Edit any issue** | Title, description, priority, labels, due date, estimate, team, cycle, parent, relations, archive — everything but the state and, beyond claiming, the assignee |
| **Move own issues** / **Move any issue** | Move an issue between **open** columns — backlog, not started, in progress — and back out of a closed one |
| **Close own issues** / **Close any issue** | Move an issue **into** a *completed* or *cancelled* column |
| **Delete issues** | Delete an issue, anybody's |
| **Log time** | Log time on an issue, run its timer, and edit or delete what **you** logged |
| **View customers** | See the customers on an issue and the requests behind it — who asked for this. Reading a customer's notes and tier is more than reading an issue, so it is not part of *View the tracker* |
| **Manage customers** | Create and edit customers, attach a request to an issue, move or drop one, and mark one important |
| **Moderate** | Edit or delete somebody **else's** comment or time entry |
| **Manage projects** | Projects, states, labels, cycles, issue templates, shared views, and archiving, deleting or moving a doc page |
| **Manage settings** | The module's settings, these permissions, and the GitHub connection |

These are the names the dashboard uses, and the names the bot quotes when it
refuses.

**Done is a permission of its own.** Moving a card between open columns and
moving it into a finished one are two different rights, and which one a move
needs is decided by the column's **type** — *completed* or *cancelled* — never
by its name. A column called *Done* of type *in progress* is a move; a column
called *Parked* of type *cancelled* is a close. Filing an issue straight into a
finished column counts as closing it too. That is what lets a server say "my
team files and works, I say when it is done" whatever the columns are called.

##### Presets

Four ready-made sets, each holding everything the one before it does:

| Preset | For | Adds |
| --- | --- | --- |
| **Reporter** | Somebody who files and keeps their own issues current | View the tracker, Create issues, Comment, Edit own issues, Move own issues, Log time, View customers. Cannot close anything, cannot touch anyone else's |
| **Contributor** | Somebody who works the board | Claim free issues, Edit any issue, Move any issue, Close own issues, Manage customers |
| **Manager** | Somebody who runs the board | Close any issue, Delete issues, Moderate, Manage projects |
| **Admin** | Somebody who runs the module | Manage settings — which is where these permissions are handed out |

A set that is not exactly one of the four is shown as **custom**; tick whatever
combination you like.

##### The floor, and how roles add up

The **floor** is what **every member of the server** can do with no role or
member row at all. Its default is exactly what the bot always allowed from
Discord: file, comment, claim a free issue, log your own time, and edit, move
or close your **own** issue. The dashboard is not part of it, and cannot be:
the floor can never carry **View the tracker** or **Manage settings**, so it
lets somebody act on the card and in `/task`, and nothing more. A server that
never opens the settings notices no difference.

On top of the floor, a **role or member row** grants a set of permissions to a
Discord **role** or to a single **member**. A member's permissions are the
**union** of the floor, every row naming them or one of their roles, and what
their standing already implies: **Manage Server** and **Administrator** are
always the full **Admin** set, and the server's dashboard allowlist is
**Manager**. A row only ever adds; nothing here takes a right away, and nothing
can take the module away from Manage Server — an admin who could lock themself
out of a page would have no way back in.

A member whose only standing is one of those rows — no Manage Server, not on
the dashboard allowlist — can log into the dashboard and gets **the tracker
and nothing else**: no other page of your server's dashboard opens for them.

It is all set under **Management > Projects & Tasks > Settings > Members &
roles**: the floor is one list of checkboxes, and under it the **Roles** list,
one row per role or member, each with a preset or a custom set. Saving the
roles applies at once, on the dashboard and on the next command.

The **own** rows are the ones worth reading twice. A tracker where only admins
can move a card is a tracker nobody updates, and one where anybody can reassign
anybody's work is a tracker nobody trusts — so the people who own a piece of
work, the person who asked for it and the person doing it, are the ones who can
change it by default. **Commenting is on the floor by default**: answering a
question is not a change. A server that unticks **Comment** from the floor
keeps it to the roles and members listed under it.

`/task` itself carries no Discord permission, because a tracker only works
when the person who spots the problem can write it down; what each subcommand
then does is decided by the permissions above. If that is not what you want,
the command can be restricted or switched off for your server in
**Configuration > Commands Settings**, like any other command — see [turning
things on and off](https://flavibot.xyz/docs/getting-started/06-modules-and-commands).

Editing or deleting a **comment** is the author's own right; **Moderate** lets
a member edit or delete anyone's, and an edited comment says that it was
edited. Reacting to one is open to anybody who can comment.

When the bot **refuses** — a button, a subcommand or a save on the dashboard —
the answer is private, names the permission that is missing by the name it has
in the table above, and the issue is untouched. Whoever manages the settings
can hand it out.

#### Notifications

FlaviBot sends direct messages, never channel pings. No task notification ever
mentions a role or `@everyone`.

| When | Who is DMed |
| --- | --- |
| An issue is assigned to somebody | The new assignee, unless **Notify the assignee** is off |
| An issue changes state | The people following it |
| A comment is posted | The people following it |
| Somebody is `@mentioned` in a comment or a description | The member mentioned |
| An issue somebody **asked for** is closed | The people who asked, once — see [customers and requests](https://flavibot.xyz/docs/modules/tasks/10-customers#when-the-issue-closes) |

You **follow** an issue by being involved with it: creating it, being assigned
it, or commenting on it. Following is per issue, so a project you set up in
January does not fill your inbox in June. **Notify the assignee** only gates the
assignment DM — the other two answer something the member themselves started.

To stop an issue's DMs, press **Mute this issue** under any DM it sent you, or
the bell beside the issue's title on the dashboard. A muted issue sends you
nothing — no state changes, no comments, no assignment — and it **stays muted**
when you comment on it or are assigned it again, so being involved later does
not quietly switch the DMs back on. **Unmute this issue**, or the bell again,
brings them back. A mute is yours alone: everyone else following the issue is
told exactly as before.

Putting an issue on a **team** notifies nobody either: a team is a label
saying whose work this is, not a mailing list, and nothing about it mentions a
role. A timer running on an issue is nobody else's business and is never
announced.

Nobody is ever told about their own action: assigning an issue to yourself,
moving one or commenting on one sends you nothing.

Naming somebody with an `@mention` in a description or a comment sends them
**one** DM — the issue's title and a link, not the text around their name — and
nothing more: it does not assign them anything and does not make them follow
the issue. A few rules keep it from turning into a way to message people:

- only members of the server are told, and only the first **five** people a
  comment or description names;
- a member who follows the issue already gets the comment itself, so a mention
  in a comment sends them nothing extra;
- editing a description only notifies the people the edit **added**;
- a muted issue stays muted, mentions included;
- a mention written inside code, or by an **automation**, notifies nobody.

A member with DMs closed simply does not get one; nothing else changes, and the
issue carries on.

#### Commands

| Command | What it does |
| --- | --- |
| `/task create project:… title:…` | Files an issue and posts its card |
| `/task list` | The open issues, filtered by project, state, assignee, or just yours |
| `/task mine` | Your own open issues: what is late, what is due today, then the rest by state |
| `/task view reference:WEB-12` | One issue in full |
| `/task assign reference:WEB-12 user:…` | Assigns it, or clears the assignee |
| `/task state reference:WEB-12 state:…` | Moves it to another column |
| `/task comment reference:WEB-12 body:…` | Adds a comment |
| `/task estimate reference:WEB-12 points:…` | Sizes it, or clears the estimate |
| `/task time reference:WEB-12 minutes:…` | Logs a stretch of work, or reads back what is logged |
| `/task team reference:WEB-12 team:…` | Puts it on a team, or takes it off one |
| `/task request reference:WEB-12 body:…` | Records that somebody asked for it, so they hear when it ships |

The full detail, the card's buttons and what each error means are in [the
Discord side](https://flavibot.xyz/docs/modules/tasks/05-discord-commands).

#### Settings

| Setting | What it does |
| --- | --- |
| **Enable projects and tasks** | Off: no cards are posted or updated, no DMs are sent, and `/task` answers that the module is off. Everything you configured is kept |
| **Primary bot** | The one bot that acts on tasks here. An issue has one card, one thread and one set of DMs; two bots would post everything twice. The first `/task create` claims it for the bot that answered, and this setting is how you change it |
| **Default channel** | Where issue cards are posted. Empty: the channel the command ran in, and nothing at all for an issue created on the dashboard |
| **Create a thread** | Opens a thread under each card, named after the issue, for its discussion |
| **Notify the assignee** | Whether being assigned an issue sends a DM |
| **Track customers and requests** | Adds the Customers view and a "who asked for this" list on every issue, and tells the people who asked when their issue closes. Off by default — see [customers and requests](https://flavibot.xyz/docs/modules/tasks/10-customers) |
| **Members & roles** | The **floor** every member gets, and the **Roles** list that gives a role or a member more — see [who can do what](#who-can-do-what). The old **manager role** setting became a **Manager** row on that role |

#### Limits

How many **active projects** a server may have, and how many **issues** each
project may hold, depend on the plan:

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Projects | 1 | 10 | 25 | Unlimited |
| Issues per project | 100 | 2,000 | 10,000 | Unlimited |
| Linked GitHub repositories | 1 | 5 | 15 | Unlimited |

Archiving a project frees its slot while keeping its issues readable and its
key taken, so a finished project never has to be deleted to start the next one.
A [sub-project](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels#sub-projects)
takes a slot like any other project.
The issue cap counts **closed issues too** — it is how much a project holds,
not how much is open — and it is per project so that splitting the same work
across two projects does not dodge it. The full table is in [limits by
tier](https://flavibot.xyz/docs/premium/06-limits-by-tier).

The rest are the same for everybody:

| Thing | Limit |
| --- | --- |
| Project key | 2 to 8 characters, `A-Z` and digits, starting with a letter |
| Project name / description | 80 / 500 characters |
| Issue title / description | 200 / 4,000 characters |
| Comment | 4,000 characters |
| Columns on one board | 12 |
| Labels on one issue | 8 |
| Labels on the server | 50 |
| State and label names | 32 characters |
| Cycle name | 60 characters |
| Estimate | 0 to 100 points, however the project's scale spells them |
| Time estimate, and one logged entry | 1 to 100,000 minutes |
| A time entry's note | 200 characters |
| Time entries on one issue | 100 |
| Doc pages on the server | 500 live ones, of up to 100,000 characters each — archiving frees a slot |
| [Customers](https://flavibot.xyz/docs/modules/tasks/10-customers#limits) on the server | 1,000 live ones — archiving frees a slot |
| Requests on one issue | 200 live ones, of up to 4,000 characters each |
| Running timers | One per person, per server |
| GitHub links on one issue | 25 |
| Relations on one issue | 25 |
| Saved views on the server | 30 |
| Issue templates on the server | 30 |
| View and template names | 60 characters |

#### What FlaviBot needs

In the channel where cards are posted: **View Channel**, **Send Messages** and
**Embed Links**. Without them the issue is still created — it simply has no
card, and the dashboard is the only place it shows. Add **Create Public
Threads** to open the discussion thread, and **Send Messages in Threads** to
mirror comments into it.

Nothing else is needed. Tasks never touch roles, never delete messages and
never mention anyone in a channel.

Next: [the board and the issue](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues).

### The board and the issue

Source: https://flavibot.xyz/docs/modules/tasks/02-board-and-issues
Summary: Dragging cards between columns, the filters, the list, everything the issue page holds, relations, Markdown, templates and archiving.

The **board** is where the work is. One column per state, cards in each column,
and a drag to move a card from one to the next. The **list** is the same issues
as a table, for reading rather than moving — the two are one page, and which of
them you get is a switch at the top of **Display**. Clicking a card on either
opens **the issue's own page**, a real address you can link, bookmark and
send.

#### The project switcher

Everything on the page is about **one project** at a time, chosen in the
switcher in the header. It lists the server's active projects by name and icon,
with the one you are reading marked in the accent colour, followed by **New
project**, **Edit project** for the one you are on, and a **Show archived**
toggle, which is always there whether or not any project is archived. **New
project** is also a button of its own, beside the switcher.

Switching project switches the whole page: the board, the list, the cycles and
the workflow shown in Settings all follow it. An **archived** project is still
readable — pick it under **Show archived** — but read-only: the columns draw,
the cards open, and nothing can be dragged or created.

A server with no project at all gets a short "create your first project" card
instead of a board. See [projects, states and
labels](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels).

#### The board

One column per state of the project, left to right in the order the workflow is
in. Each column has a coloured header — the state's own colour — with its name,
the number of issues in it, and a **+** that creates an issue directly in that
column. Where the issues are sized, the header also sums their **estimates**,
and where the state carries a **WIP limit** it shows the count against that
limit and warns when it is over.

Columns do not have to be states. In the **Display** popover at the end of the
filter bar, **Group by** re-buckets the same issues by assignee, priority,
label, cycle or project, and **Swimlane by** adds horizontal bands on top of
that, so "columns by state, lanes by assignee" is a board you can have — see
[views and shortcuts](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#grouping).
Everything below describes the board grouped by state, which is where it
starts.

A card is read top to bottom:

- the **identifier**, `WEB-12`;
- the **parent**, when the issue has one — `WEB-4 › rebuild the pricing page`,
  which is a link to it;
- the **status glyph** and the **title**, over two lines at most. The glyph is
  the shape of the state's type: a dashed circle for a backlog state, an empty
  one for unstarted, a circle filling like a pie for something in progress, a
  tick for completed and a cross for cancelled;
- a **property row**: its **priority**, its **project**, its **estimate**, its
  **labels**, a `#2891` badge for anything attached from
  [GitHub](https://flavibot.xyz/docs/modules/tasks/08-github), its **cycle**, its **team**, the
  **time** on it, the **due date** marked once it is in the past, and the
  number of **comments** and of **sub-issues** when it has any;
- the month it was **created**, at the foot.

The **assignee**'s avatar sits at the right of the identifier line. Everything
in the property row is silent when the issue does not carry it, so a bare issue
is still a short card. The **title** and the **created** month always draw. The
identifier and the assignee's avatar are properties like the rest — turn both
off in [Display](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#how-the-page-looks-to-you)
and the top line of the card goes with them.

Which of those properties a card shows is [yours to
choose](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#how-the-page-looks-to-you)
under **Display**, along with how tightly the cards are packed. It is a
per-person choice: turning the due date off on your own board changes nobody
else's.

Columns whose state type is **completed** or **cancelled** hold the issues that
are done. They are shown like any other column — a board without its Done
column is hard to read — but their contents are what **Show closed** hides
everywhere else.

#### Moving a card

Drag a card and drop it: into another column to change its state, or higher and
lower inside its own column to change its order. With a mouse the drag starts
as soon as the pointer travels a little; on a touch screen, press and hold —
which is why a tap still opens the issue and a swipe still scrolls the column.

While it runs, the card you picked up stays dimmed in place, a copy of it
follows the pointer, and a thin line shows the slot it would drop into.
**Escape** abandons the drag and nothing is sent. On a drop the board moves the
card straight away and saves in the background; a refused move rolls back on
its own, and an error says why.

Order within a column is yours to decide and is kept per column: it is the
board's "what next", not a sort. Moving a card to another column puts it
exactly where you dropped it there.

Dropping a card into a **completed** or **cancelled** column closes the issue:
the moment is recorded, it leaves the open counts, and it counts towards its
cycle's progress. Dragging it back out reopens it. Nothing is deleted either
way.

Every drop repaints the issue's card in Discord and is written to its activity
trail, so somebody reading the channel sees the same move you just made.

#### The filter bar

The board and the list share one filter bar, and what you set applies to both:

| Filter | Notes |
| --- | --- |
| **State** | One or several columns. A board reading across several projects does not offer it — the columns belong to each project — and drops it while it is wide |
| **Assignee** | A member, or **Unassigned** |
| **Label** | One or several; an issue matching any of them is kept |
| **Priority** | One or several levels |
| **Team** | One team, or **No team**. See [teams](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#teams) |
| **Estimate** | A smallest and a largest, either end on its own |
| **Has time logged** | Issues somebody has logged time on, or the ones nobody has |
| **Linked to GitHub** | Issues with a pull request or a GitHub issue attached, or the ones with none |
| **Shipped** | Issues **completed** between two instants, whatever cycle they sit in — the filter behind [patch notes](https://flavibot.xyz/docs/modules/tasks/04-cycles#patch-notes). Both ends are required, in order, and **7**, **14** and **30 days** are one click each. Cancelled issues do not count. Switching it on moves the scope to **All issues** so they show; **Show closed** is left as it was. The popover also copies the window's patch notes, **as Markdown** or **for Discord** |
| **Search** | Matches the title and the description of an issue, not its comments |
| **Show closed** | Off by default on the list. The board always draws its closed columns |
| **Show archived** | Off. The only way to see [archived issues](#archiving-an-issue) again |

The bar sits above both drawings and what you set there stays as you move
between them, so narrowing to *everything urgent and unassigned* on the board
and then switching to the list gives you the same issues as a table. The bar
also carries **Save view**, and **Display** at the end of it holds the
board-or-list switch, the grouping, the swimlanes, the ordering, the density
and what a card shows: a slice you keep rebuilding is a [saved
view](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#saved-views) away.

##### Active, Backlog, All issues

Above the board, beside the view pills, three segments cut the same issues the
way most people actually ask for them:

| Segment | Shows |
| --- | --- |
| **Active** | Everything in an **unstarted** or a **started** column — what is planned and what is under way |
| **Backlog** | Everything in a **backlog** column — noted, not planned |
| **All issues** | Everything that is not archived, closed columns included |

It is a filter rather than another kind of view, and it narrows whatever the
board is already showing: pick a [saved
view](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#saved-views) and then a
segment, and you get both. **Active** is where most teams live, because a
board that draws its backlog next to its work in progress is a board where the
work in progress is three columns from the left.

The segments also sit above the list, where they behave slightly differently
and it is worth knowing why. On the board they are exact — the board holds the
whole project — and they hide the columns they exclude, so **Active** has no
Done column to drop anything into. The list pages, so there a segment narrows
the rows already loaded rather than the project: a segment that empties the
page still has a **Load more** under it. Switching segments never costs a
request either way, which is what makes it feel like a tab.

#### The list

The same issues as a dense table of eight columns: identifier, title, state,
priority, assignee, labels, due date and last update. Click a header to sort by
it, click again to reverse it — every column but **labels** sorts. Unlike the
board, the list hides closed issues until **Show closed** asks for them.

It loads a page at a time, with a **Load more** at the foot and a count of how
many rows are loaded. Sorting applies to the rows already loaded, so on a
project with more than one page, sorting by due date orders what is on screen
rather than the whole project — load the rest first, or narrow the filters.

**Order by**, in **Display**, sets the list's order the same way it sets the
board's — and a click on a column header overrides it until you pick an order
there again. Whichever you touched last is the one you get.

Grouping applies here too: a grouped list is the same rows under headed bands
rather than in columns. Each band is headed by the same name, the same count
and the same estimate total the board draws over that column, and clicking the
header folds the band away — folded bands are remembered per project and per
grouping, in the browser you fold them in.

Use the board to decide what happens next, and the list to answer questions —
what is overdue, what nobody has picked up, what one person is carrying.

#### The issue page

Clicking any card, on either, opens that issue at [its own
address](#the-address-of-an-issue). It holds everything about that one issue, and
there is no form to submit: every control saves the moment you change it, so
leaving the page loses nothing.

**The crumb** at the top names the board and the project, and takes you back to
them. Your browser's own back button is the shorter way: it returns you to the
board exactly as you left it.

Beside the crumb, `3 of 21` and a pair of arrows walk the issues you were
looking at when you opened this one — the board's order, or the list's, filters
included. It is how you read a triage list end to end without going back to the
board between each one. An issue you opened from a Discord link belongs to no
such list, so it shows no counter.

The title and the description are edited in place, and they are no exception to
the rule above: they save like everything else does.

The **title** is the page's heading — no box around it, no pencil beside it.
Click into it and type. **Enter** stores it and drops the caret, **leaving the
field** stores it, and **Escape** puts the stored one back. Emptying it and
leaving brings the stored title back rather than saving a blank one, because an
issue with no title is an issue nobody can find again.

The **description** is the rendered text itself: click in the prose and the
caret lands near the line you clicked. It saves after a short pause in typing,
when you leave it, on **⌘Enter** / **Ctrl+Enter**, and on **Escape** — all four
commit, so there is no undo key here, only *Unsaved changes* turning into
*Saved* beside it. If somebody edited the same description elsewhere while you
were typing, the save stops and asks which version to keep rather than posting
over theirs. It takes up to 4,000 characters and is written in
[Markdown](#markdown-mentions-and-checkboxes).

**The properties**, down the side:

| Property | Notes |
| --- | --- |
| **State** | The project's columns. Choosing a closing one closes the issue, exactly like dragging it there |
| **Priority** | None, Low, Medium, High, Urgent |
| **Assignee** | One member of the server, or nobody |
| **Labels** | Up to eight, from the server's set |
| **Cycle** | One cycle of this project, or none. See [cycles](https://flavibot.xyz/docs/modules/tasks/04-cycles) |
| **Due date** | A calendar date, with no time of day: "due Friday" means the same thing to a reader in Sydney and one in Paris |
| **Estimate** | How big it is, in points spelled the way the project's [scale](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#choosing-a-scale) spells them. Hidden entirely on a project that has not picked one |
| **Team** | One [team](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#teams) of the server, or none. Not a second assignee: it says whose work this is, not who is doing it |
| **Time estimate** | How long somebody thinks it will take, in minutes. Separate from the log below, on purpose |

**The time log** is under the properties: the total logged so far, an entry
per stretch of work with who logged it and on which day, and **Start timer**
for counting it as you go rather than remembering afterwards. All of it is in
[estimates, time and teams](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#time).

**GitHub**, on a server that has [connected one](https://flavibot.xyz/docs/modules/tasks/08-github),
adds a banner naming the pull requests attached to the issue and what state
each one is in. Most of them attach themselves, from the identifier somebody
wrote in the pull request.

**Sub-issues** link two issues of the same project, and are nothing more than
that: a child names its parent at the top, with an **×** to detach it, a parent
lists its children underneath the properties, and one click on either gets you
to the other. Closing a parent does not close its children,
and deleting a parent does not delete them — they simply stop having one. Use
it for "this is part of that", not as a second board.

A parent's list of children carries a **progress bar** and a *completed of
total* count. A child counts towards it once it is **closed** — in a completed
or a cancelled column, the same meaning the word has everywhere else here. It
is the number that makes a parent worth having: *3 of 7* says more about
"release the new site" than the parent's own state ever will.

Children are made two ways. **Add sub-issue** on the parent types a title
inline and files the child straight away, in the project's first column. On an
issue that already exists, **convert to sub-issue** picks the parent instead —
which is what you want when something you filed in a hurry turns out to be part
of something bigger. The picker never offers the issue itself, its own parent
or one of its children: each of those would be a loop.

**The trail**, at the bottom, is the comments and the activity merged into one
chronological list, because they are one story: *assigned to Ana* — *"on it"* —
*moved to In Progress* only reads correctly interleaved. Activity entries keep
the words they were written with, so an entry naming a state or a label stays
readable long after that state or label has been deleted.

**The comment box** is under the trail. A comment is up to 4,000 characters,
written in [Markdown](#markdown-mentions-and-checkboxes), and mirrored into the
issue's Discord thread when it has one. Where a thread exists, a link to it
sits at the top of the page.

The person who wrote a comment can **edit** it, and an edited comment says so
next to its date — the point of the marker being that a thread where an answer
can change under you silently is a thread nobody trusts. Deleting one is the
author's right too, and a member with **Moderate** can delete anyone's.

Anyone reading the issue can **react** to a comment, from a fixed set of eight
— 👍 👎 😄 🎉 😕 ❤️ 🚀 👀. Clicking one adds yours and grows the count, and
clicking it again takes yours away. Eight rather than every emoji there is,
because a reaction is meant to be read at a glance — and because a thumbs-up on
"shipped it" saves the comment that says nothing but "ok", which is most of
what a tracker's comments are otherwise made of.

#### Relations

A **relation** is a line drawn between two issues that have something to do
with each other without one being part of the other. *This cannot start until
that is done*, *these two are the same bug*, *read this one alongside that
one*. Sub-issues answer "is part of"; relations answer everything else.

The Relations block sits on the issue's page, under the properties. **Add relation** picks the kind, then finds the other issue by
identifier or by title. Say you are reading `WEB-12` and you pick `WEB-19`:

| Kind | What it says | `WEB-19` then reads |
| --- | --- | --- |
| **Blocks** | `WEB-19` cannot get done until this one is | Blocked by `WEB-12` |
| **Blocked by** | This one cannot get done until `WEB-19` is | Blocks `WEB-12` |
| **Relates to** | Worth reading together; neither waits on the other | Relates to `WEB-12` |
| **Duplicates** | This one is the same work, already filed as `WEB-19` | Duplicated by `WEB-12` |

**A relation is always written on both issues**, which is the last column. Add
it on `WEB-12` and `WEB-19` carries its half from that second, without anybody
opening it; remove it from either side and it goes from both. There is no such
thing as a one-sided relation somebody forgot to mirror, which is the reason
to have the feature at all rather than a sentence in a description.

Relations can cross **projects**, as long as both issues are on the same
server: *the website cannot ship until the moderation queue is emptied* is a
real dependency and a sub-issue could not express it, since sub-issues stay
inside one project. An issue cannot relate to itself, the same pair cannot be
added twice, and an issue may carry **25** relations.

The block lists what blocks this issue first and the trivia last, and an issue
with something still in its way says so in a **banner above its description** —
that one fact is the reason the feature exists, and it is no use three scrolls
down.

**Nothing is enforced.** *Blocks* does not stop you moving, closing or working
on the blocked issue, and no DM is sent when a blocker closes. It is a note two
issues carry about each other, for the people reading them — a tracker that
refuses to let you finish something because a year-old issue still says it is
blocking is a tracker people work around.

#### Markdown, mentions and checkboxes

Issue **descriptions** and **comments** are written in one box that **shows
the formatting as you type it**: bold is bold, a heading is a heading, a
checkbox is a checkbox and a mention is a chip with the member's name. Eight
buttons sit over it — **bold**, *italic*, ~~strikethrough~~, `code`, a link, a
quote, a bullet list and a task list — and each one toggles: press it again on
the same text and the formatting comes off. Markdown shortcuts work as you
type them, so `**bold**` goes bold the moment you close it and `- ` starts a
list.

What is *stored* is still Markdown — the exact text Discord reads — so a
description written here, a description typed into the Discord modal and one
pushed from GitHub are the same kind of string. The **Source** button, next to
the toolbar, swaps the box for that raw text whenever you want to see it or
edit a character by hand; the choice is remembered in your browser. One thing
is deliberately missing from the formatted box: **tables cannot be built**
there, because Discord has none. A table already in a description is left
exactly as it is, and Source is where to edit it.

| You write | You get |
| --- | --- |
| `**bold**`, `*italic*`, `~~struck~~` | **bold**, *italic*, ~~struck~~ |
| `` `code` `` and fenced blocks | Code, monospaced and unwrapped |
| `# Heading` | A heading |
| `- item`, `1. item` | A list |
| `- [ ] thing` | A checkbox |
| `> quoted` | A quote |
| `[text](https://…)` | A link, opening in a new tab |
| A table's pipes and dashes | A table — readable everywhere, editable in **Source** |

**Enter** starts a new paragraph and **Shift+Enter** breaks the line without
one — and a single line break is a single line break everywhere this text goes,
here and in Discord. **Images are not rendered**, and neither is HTML: a
`<b>` typed into a description reads as the four characters you typed. What is
rendered is cleaned first, so a description can only ever carry text and
formatting — never anything that behaves.

**Checkboxes are clickable.** A task list in a **description** is a real
checklist: tick a box and the description is saved with the box ticked, for
everybody. It is the right size of tool for the five things an issue needs
doing in order, where five sub-issues would be five cards nobody wants on the
board.

**Mentions** turn an id into a name. Paste a mention out of Discord — the
`<@123456789>` you get by copying one — and it renders as a chip carrying that
member's display name, so the sentence reads *ask @Ana about the copy* rather
than a row of digits. It also sends them a single DM pointing at the issue. It
does not assign them the issue and does not make them follow it: assigning is
the assignee property, and following — and the limits on mention DMs — are
explained under [notifications](https://flavibot.xyz/docs/modules/tasks/01-overview#notifications).

A mention or a checkbox written **inside code** is left exactly as typed —
somebody showing how the syntax works is not mentioning anybody.

A comment mirrored into the issue's Discord thread is sent as you wrote it, and
Discord renders the parts of Markdown it knows — bold, italics, lists, code —
while the rest, checkboxes included, reads there as the plain text it is.

#### The address of an issue

Every issue has an address of its own:

```
/dashboard/123/tasks/WEB-12
           │     │    │
           │     │    └─ the issue's identifier
           │     └───── the tasks page
           └─────────── your server's id
```

That is the page a card opens, and it is the only place an issue is read: the
properties, the sub-issues, the relations, the trail and the comment box, with
room to read them in.

That is what the address is for. Pasted into a Discord message, a commit or a
document, it lands the reader on the issue itself rather than on a board they
then have to search. The link is also one keypress away: **copy link** in the
[command palette](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#the-command-palette).

It is still a dashboard page, so it needs **View the tracker**: somebody who
cannot open the tracker cannot read the issue there, and the card in Discord
is what they have. A member let in by a tracker role alone sees the tracker
and nothing else of your server's dashboard. An identifier that
names nothing — a deleted issue, a typo, a project whose key was renamed —
gets a plain "no such issue" page rather than an empty board.

#### Creating an issue

Three ways in, all ending at the same place:

- The **+** on a board column, which opens a one-line composer at the top of
  it: type a title, press enter, and the issue is filed in that column. A
  create that fails keeps what you typed.
- `C` on the keyboard, which is the same composer, opened at the top of the
  first column.
- `/task create` in Discord — see [the Discord
  side](https://flavibot.xyz/docs/modules/tasks/05-discord-commands).

Only the **title** is required. Everything else can be set now or later, on the
issue's page, from the card, or with `/task`.

A project that has reached its issue cap refuses the create and names the cap;
see the [limits](https://flavibot.xyz/docs/modules/tasks/01-overview#limits). Closed issues count
towards it, so a very old project reaches its ceiling by being long-lived
rather than busy — archive it and start the next one, or move up a tier.

##### Templates

A **template** is an issue filled in ahead of time. A server that files the
same shape of thing over and over — a bug report that always wants the same
three headings, a weekly stream checklist, an application to review — makes it
once and starts from it afterwards: **New from template**, on the filter bar,
offers the templates the project can use. A template that already carries a
title is a complete issue and is filed the moment you pick it; one that does
not opens the column composer with the rest of it filled in, leaving you to
type the title and change whatever else is different this time.

A template is made from an issue that already got it right: **Save as
template** on that issue captures what it holds — title, description, state,
priority, estimate and labels, plus an assignee, a due date or a cycle if it
had them — under a name of up to 60 characters. Templates are then edited in
**Settings**, where the editor owns the first six and shows the rest as chips
you can take off, and a server may keep **30**.

Each template is either tied to **one project** or offered to **all** of them.
A column belongs to one project's workflow, so making a template global drops
the column it had captured — the form says so as you switch — and an issue made
from it lands in whichever column is first, like any other new issue.

Starting from a template copies it; it does not attach the issue to anything.
Editing a template later changes what the next issue starts from and leaves the
issues already made exactly as they are.

#### Archiving an issue

**Archive** is how an issue leaves without being destroyed. The archived issue
drops off the board, out of the list, out of every filter and every saved view;
it keeps its identifier, its description, its comments, its activity trail and
its place in whatever relations and sub-issue links it had, and restoring it
puts all of that back.

Two ways to it: the button on a **list row**, which appears as you hover it,
and **Archive** in the [command
palette](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#the-command-palette). The
list is therefore where a board gets its spring clean — one row after another,
without leaving the page.

To find one again, turn on **Show archived** in the filter bar. Archived issues
come back into view drawn apart from the rest — dimmed, with an **Archived**
badge — and where every other row offers an archive button, they offer
**Restore**, because the reason to go looking in an archive is nearly always to
undo it.

Three things that sound alike and are not:

| | Means | Reversible |
| --- | --- | --- |
| **Closing** | The work is finished, or was cancelled. Counts in a cycle | Move it back out of the column |
| **Archiving** | Out of the way. Says nothing about whether it was done | **Restore** |
| **Deleting** | Gone, with its comments and its activity | No |

An issue does not have to be closed before it is archived, and archiving is not
a second way of closing: a duplicate you archive was never done. When you are
unsure which of the three you want, it is archiving — it is the only one of the
three you can take back.

What archiving does not do is make room: the project still holds the issue, so
it still counts towards the project's issue cap, exactly as a closed one does.

#### Keeping the two halves in step

Anything you do on the dashboard repaints the Discord card, and anything done
from Discord — a button, a `/task` command — shows on the dashboard when you
next open it. The card is only a view of the issue: deleting it by hand loses
nothing, and the next change to the issue posts a fresh card in its place. An
issue whose card could not be posted at all — no default channel, or a missing
permission — is a perfectly normal issue that happens to live only here.

Next: [projects, states and
labels](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels).

### Projects, states and labels

Source: https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels
Summary: Creating a project and its key, archiving one, editing the columns of its board and their WIP limits, and the labels the whole server shares.

The three things you set up once. A **project** holds the issues, its **states**
are the columns they move through, and **labels** are the tags they carry.
Editing any of them needs the **Manage projects** permission — held by
**Manage Server**, the **Manager** and **Admin** presets, or any role or
member row that ticks it (see [who can do
what](https://flavibot.xyz/docs/modules/tasks/01-overview#who-can-do-what)).

#### Projects

A project is a body of work with a name and a short key. One server, one
project is a perfectly good setup: use a second one when the work is genuinely
separate — different people, different rhythm, nothing in common but the server
— rather than as a category. A label or a state is the right tool for splitting
work that the same people do together.

**New project** at the bottom of the project switcher opens the form, and the
**Edit** action next to a project in the same switcher reopens it:

| Field | Notes |
| --- | --- |
| **Key** | 2 to 8 characters, `A-Z` and digits, starting with a letter. Upper-cased for you, and unique on the server. This is the `WEB` of `WEB-12` |
| **Name** | Up to 80 characters. What the switcher shows |
| **Description** | Up to 500 characters, for the people who join later |
| **Colour** | Identifies the project at a glance across the page |
| **Icon** | The small glyph in the switcher: one of the presets offered, or the name of any icon from the Iconify set |
| **Lead** | The member who answers for the project. A label, not a permission: it grants nothing |
| **Parent project** | Makes this a sub-project of another one. Empty — the default — keeps it at the top level. See [sub-projects](#sub-projects) |
| **Estimate scale** | How this project spells its points: none, linear, exponential, fibonacci or t-shirt. **None** is the default and hides the estimate control entirely |
| **Allow a zero estimate** | Whether **0** is one of the sizes the picker offers. On by default |
| **Require an estimate before an issue can be started** | Off by default. With it on, an issue cannot move into a **started** column until somebody has sized it |

Saving creates the project **and its six default columns** in one go, so it is
usable straight away.

The last three are the project's own answer to *how big is this*, and they are
explained — including what happens to the issues already estimated when you
change your mind — in [estimates, time and
teams](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#estimates).

##### Choosing a key

The key is read aloud and typed by hand — in a Discord message, in a commit, in
a meeting. Short and obvious beats descriptive: `WEB`, `MOD`, `ART`, `OPS`. It
is the only part of a project that other things quote, which is why it has
rules the name does not.

##### Renaming a key

You can change a key later, and the form lets you, but read this first: the
identifier of every issue in the project is its key plus its number, composed
fresh each time it is shown. Rename `WEB` to `SITE` and `WEB-12` becomes
`SITE-12` **everywhere at once** — on the board, on the cards, in `/task`.

Nothing breaks in the module. What breaks is everything outside it: a `WEB-12`
written in a Discord message last month, in a commit, in a document, no longer
resolves, and `/task view reference:WEB-12` answers that there is no such
issue. Rename a key while a project is young, or not at all.

##### Archiving and deleting

Both live at the foot of the project's own form, next to the fields they act
on rather than behind a menu somewhere else.

**Archiving** puts a project away: it drops out of the switcher unless you ask
for the archived ones, it stops counting against your plan's project limit, and
it **keeps its key**, so every identifier it ever handed out still resolves.
Un-archiving is one click, and nothing inside the project is changed either
way. This is how a finished project makes room for the next one.

**Deleting** removes the project and, with it, its issues, their comments, their
activity and their states. The cards already posted in Discord stay where they
are as ordinary messages nobody can press the buttons of. There is no undo, and
the key becomes free again — so a future project could take it and start its own
`WEB-1`. Archive unless you genuinely want the work gone.

Either one done to a **parent** project leaves its sub-projects where they
are: archiving a parent touches none of its children, and deleting one lifts
them to the top level, whole. See [what happens to the
children](#what-happens-to-the-children).

##### Sub-projects

A project can sit **under** another one. The website has its blog, its shop
and its docs; each is work of its own, with its own people and its own rhythm,
and each is still *the website*. A **sub-project** is that: a project like any
other — its own key, its own issues, its own columns, its own cycles — that
happens to have a parent.

The parent is an ordinary project too. It holds issues of its own, and it is
not a folder: nothing forces you to file everything under a child. Pick the
parent in the **Parent project** field of the form, when you create a project
or later in **Edit**, and clear it to lift the project back to the top level.

**Two levels, no more.** A parent cannot have a parent of its own, and a
project that already has children cannot be made somebody's child; the form
refuses both rather than letting a tree grow that the switcher could not draw.
Three levels of *the website / the shop / checkout* is what a label is for.

What a sub-project keeps, and what it does not:

- **Its key.** `SHOP-4` stays `SHOP-4`. Nothing about its identifiers mentions
  the parent, and moving a project under one — or out again — rewrites nothing.
- **Its own states and cycles.** The columns are per project, so a child's
  board is its own board, and its sprints are its own sprints. A parent's cycle
  does not reach into its children.
- **Its own slot.** A sub-project counts against the plan's [project
  limit](https://flavibot.xyz/docs/modules/tasks/01-overview#limits) exactly like a top-level one.
  Nesting is how the switcher reads, not a way round the cap.

The **project switcher** draws the tree: a parent, and its children indented
under it. Picking a child shows that child and nothing else, as before.

Picking the **parent** shows the parent's own issues — and its filter bar
gains **Include sub-projects**, which widens the board to the whole family.
Because states belong to each project, a board holding three projects cannot
be laid out by column, so with the switch on the board is **grouped by
project**: one lane per project, the parent first, each card carrying its own
project's state. A cycle belongs to one project too, so grouping by cycle
falls back the same way, bands by state or cycle are dropped, and the **state
filter** steps aside while the family is on. Every other grouping, the rest of
the filters and the list work as usual across the family; creating an issue
still files it into the project you are on.

The three built-in views — **My issues**, **Created by me**, **Subscribed** —
answer for **all projects**, parents and children alike, since *what is mine*
is not a question about one project. Lighting one puts an **All projects**
chip beside the switcher, lit with it: press the chip to narrow the view back
to the project you are on, and again to widen it. Each row carries its own
identifier, so a wide answer never leaves you wondering which project a card
came from. See [the three you already
have](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#the-three-you-already-have).

###### What happens to the children

- **Archiving a parent** archives the parent only. Its children stay active,
  stay in the switcher and keep working; the archived parent shows under
  **Show archived** with them still attached, and un-archiving it puts the
  tree back as it was.
- **Deleting a parent** removes the parent and its own issues, as deleting any
  project does — and **lifts its children to the top level**, untouched: their
  issues, states, cycles and keys are exactly as they were, they simply stop
  having a parent.

Neither ever deletes a child. A sub-project goes away only when somebody
deletes that sub-project.

#### The workflow

A project's **states** are the columns of its board, in the order you put them.
Settings lists them for the project you are on — each row with its name, its
colour, its type, its optional WIP limit and a pair of arrows that move it left
or right on the board.

Every new project starts with six:

| Column | Type | Colour |
| --- | --- | --- |
| **Backlog** | Backlog | Grey |
| **Todo** | Unstarted | Near-white |
| **In Progress** | Started | Yellow |
| **In Review** | Started | Green |
| **Done** | Completed | Indigo |
| **Cancelled** | Cancelled | Slate |

Rename them, recolour them, reorder them, add your own and delete the ones you
do not use. What a **type** means, and why two columns can share one, is in
[the overview](https://flavibot.xyz/docs/modules/tasks/01-overview#states-and-what-they-mean).

Things worth knowing:

- **The first column is the default.** An issue created without a state — from
  `/task create`, or from a template that captured none — lands in whatever is
  leftmost. Put the column you file into first.
- **A name is yours, a type is FlaviBot's.** Call a column *Shipped*, *Live* or
  *Merged*; give it the **completed** type and FlaviBot will close the issues
  that reach it, count them in a cycle's progress and hide them behind **Show
  closed**.
- **Twelve columns is the ceiling**, and it is generous: past a screenful, a
  board stops being readable.
- **Reordering is the arrows on a row**, not a drag: the cards are the thing
  you drag, and a second drag surface in a settings list earns nothing. It
  moves the column for everybody and touches none of the issues in it.
- **A column holding issues cannot be deleted.** The row says how many are in
  it and offers the way out — *move these 7 issues to …* — and only then
  unlocks the delete. An issue has to be in some column, and silently dropping
  seven of them into another one is not a decision a dialog should make on its
  own.
- **The last column cannot be deleted either.** A project with no column could
  not hold an issue at all.
- **A column can be named as a destination elsewhere.** A column chosen as
  where a merged pull request lands its issues simply stops being one when it
  is deleted — the [repository's setting](https://flavibot.xyz/docs/modules/tasks/08-github#linking-a-repository)
  falls back to moving nothing, and nothing else changes.
- **A colour is not decoration.** It is the column's header on the board and
  the stripe down the issue's card in Discord, which is how somebody scrolling
  a channel sees at a glance what is in progress and what is done.

##### WIP limits

A column can carry a **WIP limit** — *work in progress* — which is a number
saying how much belongs in it at one time. *In Progress, at most three.* The
column header then reads the count against the limit, and warns once it is
over.

It is worth setting on exactly one column: the one where work actually happens.
A team with eight things started and none finished is a team that will finish
none of them, and a limit is how that becomes visible to everybody rather than
being a feeling somebody has on a Friday. When the column is full, the next
thing to do is to finish something in it.

FlaviBot **warns and never refuses**: an issue can always be dragged into a
full column, and nothing is blocked or rolled back. Leave the limit empty on
the columns where a number would mean nothing — a backlog is supposed to be
long. How the header reads is in [views and
shortcuts](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#what-a-column-header-tells-you).

#### Labels

Labels are the tags on an issue: *bug*, *design*, *good first issue*, *waiting
on Discord*. Each has a name of up to 32 characters and a colour, and they are
edited in Settings.

Labels belong to the **server**, not to a project. `bug` means the same thing
everywhere, and a new project gets the whole set without you re-creating it.
Names are unique case-insensitively — `Bug` and `bug` are the same label — and a
server may define up to **50** of them, with up to **8** on any one issue.

Deleting a label removes it from every issue that carried it. The issues
themselves are untouched, and their activity trail keeps the words, so an entry
that reads "added label *design*" stays readable after *design* is gone.

Use a label for a property that cuts across projects and states — *who is
blocked*, *what kind of work it is* — and a state for where the work has got
to. A label named *Done* is a sign the board should have had a column instead.

Next: [cycles](https://flavibot.xyz/docs/modules/tasks/04-cycles).

### Cycles

Source: https://flavibot.xyz/docs/modules/tasks/04-cycles
Summary: Time-boxed slices of a project's work, what a cycle page counts, the burn-up, completing a cycle and rolling the rest over, velocity, and the patch notes a cycle writes for you.

A **cycle** is a dated slice of a project's work: "these issues, these two
weeks". It is the same idea a software team calls a sprint, and you do not have
to use it — a server that just keeps a board of things to do can ignore cycles
entirely. It becomes useful the day somebody asks "what are we actually doing
this fortnight", because that is a question a board of fifty issues cannot
answer and a cycle of eight can. It becomes useful a second time the day
somebody asks "what did we ship", because a completed cycle answers that with
its numbers frozen and its patch notes written.

Cycles belong to **one project**. Each has a **number**, handed out in order
when you create it, and an optional **name**: *Cycle 4*, or *Cycle 4 —
Launch week*.

#### Creating one

The **Cycles** view — the button in the page header, which you press again to
go back to the issues — groups the project's cycles into active, upcoming and
past, and **New cycle** opens the form:

| Field | Notes |
| --- | --- |
| **Starts at** and **ends at** | The window. The end must be after the start; a day and six months are both allowed |
| **Name** | Optional, up to 60 characters. The number is always there, so a name is only for the ones worth naming |

The number is FlaviBot's to hand out: cycle 1, then 2, then 3, per project. You
never type it, and two cycles of the same project can never carry the same one.

Nothing stops two cycles from overlapping, and nothing forces them to be
back-to-back — a gap between one cycle and the next is a perfectly normal way to
run a volunteer team.

#### Putting issues in a cycle

An issue's **Cycle** is one of its properties, set in the [issue
page](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#the-issue-page). An issue can
be in one cycle or in none; only the cycles of its own project are offered, and
**No cycle** takes it back out.

A cycle is a window over the work, not another place to put it: an issue in a
cycle sits on the board exactly where its state says it should, and what a
cycle holds is read in the Cycles view. If you do want to see the cycles side by
side for a moment, the board can be [grouped by
cycle](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#grouping), which turns them
into columns for as long as you are looking — it moves nothing.

#### The list

The Cycles view is a list and a page: every cycle of the project down the left,
the one you picked drawn as the page beside it. The list groups them into
**running**, **upcoming** and **finished** — the running one being whichever
open window today falls inside, first in the list — and each row carries its
dates, when it starts or ends, how many of its issues are **closed** and what
share of it that is, as a percentage and a small ring.

The **running cycle is picked for you** when you open the view, because "are we
going to finish" is the question it is usually opened with. Picking another one
replaces it, and a live update never moves your choice. The one you are reading
is in the address bar as `?cycle=`, so a cycle is something you can send
somebody a link to.

Closed means the issue is in a **completed** *or* a **cancelled** column. Both
count as resolved, so a cycle whose issues were all cancelled still reads 100%.
That is on purpose: the percentage answers "is anything still open in this
cycle", which is what you glance at a list for. The cycle's own page tells the
two apart.

A cycle is **open** until somebody [completes it](#completing-a-cycle). The
end date passing is not that: an open cycle whose window is over still counts
live, and sits under past with its numbers still moving, until you complete it.

#### The cycle page

Picking a cycle draws its page on the right. The [burn-up](#burn-up) leads it,
because the shape of a fortnight is the thing a percentage cannot draw, and the
numbers hang under the chart on its own grid rather than in front of it.

Between the two is the **composition bar**: the cycle's scope drawn as itself,
one band per state — completed, in progress, not started, backlog, and
cancelled hatched — in the same order and the same colours as the cells below
it. It is the same five numbers as a shape, for the glance that does not stop
to read them.

Those cells each answer a different question, so it is worth knowing which:

| Number | What it counts |
| --- | --- |
| **In the cycle** | Every issue the cycle holds, cancelled ones included, and their points. It is what the composition bar splits and what the percentage is taken over. Archived issues are out, as they are everywhere. The burn-up's **Scope** line is a different number: it drops an issue the day it is cancelled, so on a cycle with cancelled work the chart reads lower than this cell |
| **Completed** | Issues in the cycle in a **completed** column. Cancelled ones are not completed — this is the number the list's bar does not give you |
| **In progress** | Issues in the cycle in a **started** column right now: under way, not yet closed |
| **Not started** and **Backlog** | Issues in the cycle in an **unstarted** or a **backlog** column — the two kinds of not-yet |
| **Cancelled** | Issues in the cycle in a **cancelled** column. Closed, but not shipped |

Two **badges** sit beside those tiles when they apply:

| Badge | What it counts |
| --- | --- |
| **+N mid-cycle** | Issues that joined the cycle after its **first day**. An issue put in before the cycle began, or within 24 hours of its start, was **planned** — a planning session on the morning the cycle starts is still the plan. Everything after is scope added while the cycle ran |
| **N carried over** | Issues that a [rollover](#completing-a-cycle) brought in from the previous cycle. They are never counted as added mid-cycle, whatever day the rollover happened |

And one line stands apart from the tiles: **N completed in the window · N of
them outside this cycle** — issues of the project **completed between the
cycle's start and its end**, by the date they closed, whatever cycle they are
in now, or none, with how many of those are not in this cycle.

That last line is the difference between **in this cycle** and **completed in
the window**, and it is the difference that matters when you write up a
fortnight. *In this cycle* is what you planned and how it went: scope,
completed, in progress, added, carried over. *Completed in the window* is what
actually landed between those two dates — including the urgent fix nobody put
in the cycle, and excluding the issue that was in the cycle but closed the
week after. The [patch notes](#patch-notes) are built from the second one.

Under the header, three **breakdowns** — by assignee, by label and by state —
each row with how many issues and how many of them are done, and each of them
a click away from the board filtered to exactly those.

Where anything in the cycle carries an
[estimate](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#estimates), the page
shows **points** beside the issue counts — the same numbers the board's
[column
headers](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#what-a-column-header-tells-you)
sum — and the issues nobody sized count as zero.

#### Burn-up

An open cycle that has started gets a chart: a **burn-up**, one point per day
of the cycle so far, from its first day to today. It is the picture of a
fortnight that a percentage cannot draw.

| Line | What it is, at the end of each day |
| --- | --- |
| **Scope** | Issues in the cycle by then, minus the ones cancelled by then |
| **Started or done** | Issues that had **entered** a started column by then and were not yet closed, **stacked on** the completed line — so the band between the two is the work in flight |
| **Completed** | Issues completed by then |
| **Even finish** | The dashed straight line the completed line would follow if the whole scope were finished at an even pace by the last day |

The first three stop at **today** — the days that have not happened yet are not
days the team failed to finish anything — while the even finish carries on to
the end, which is the whole point of it. Days are counted in **UTC**, the way
every chart in the module is, and a cycle longer than 120 days draws its first
120.

The started line reads **when work began**, not where the issue sits today:
an issue moved back out of a started column stays on the line from the day it
first entered one. That is also why the line can sit above the header's **In
progress** tile, which only counts the issues in a started column right now.

Read it as a shape rather than a number. A completed line running above the
even finish is a cycle that is ahead; one running flat for four days and then
jumping is a team that finished everything on the last afternoon, which is
worth knowing before you plan the next one; a scope line that keeps climbing
is work being added to a cycle that had already started; a wide gap between
started and completed that never closes is too much in progress at once — see
[WIP limits](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#what-a-column-header-tells-you).

The chart is drawn in **points** when anything in the cycle is estimated, with
the unsized issues counting as **zero**; when **nothing** is estimated it
counts **issues** instead. In points every number the chart prints carries
`pts` after it — the end of each line and the day panel alike — because the
cells hung under the chart count **issues**, and the two would otherwise print
different numbers under the same word. The axis label says which unit it is
and, in points, that unsized issues count as zero — it does not say how many of
them there are. A team that does not size its work gets a useful chart either way; a team
that sizes half of it should know that the other half weighs nothing here
before trusting the shape.

Points are points whichever ladder the project spells them in: a t-shirt
**XL** and a fibonacci **8** are the same 8 to this chart, because the scale is
only how the number is written down. Changing a project's scale therefore
redraws nothing here. See [choosing a
scale](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#choosing-a-scale).

Scope is dated by the day an issue was **put into** the cycle, not the day it
was created: pull a month-old backlog issue in on day six and the chart shows
it as scope on day six. An issue taken out and put back counts from the day it
came back, and one that was already closed when it joined only counts as
completed from that day on. Saving an issue without touching its cycle changes
none of this.

On an **open** cycle the chart is live, and live means honest about today
rather than about history: which line an issue is on for every past day is
decided by what the issue is **now**, so reopening an issue yesterday takes it
off the completed line for the whole cycle. That is the reason a completed
cycle keeps the chart it had the moment it was completed.

While the cycle is running the plot shades the days that have not happened,
rules the last counted day, carries today's scope forward to the finish line as
a dotted guide and brackets the gap still between **completed** and that line —
the **N left** the cycle is actually being asked about. A cycle whose window
has closed drops all four: there is no today inside it any more, and the shape
it ended on is the whole of what there is to read.

Each line is named at its own end rather than in a legend, and hovering the
plot reads one day out of it: the date, and where each of the four lines stood
at the end of it.

An upcoming cycle has no chart: nothing has happened yet.

#### Completing a cycle

A cycle ends when you say so, not when its end date passes. **Complete
cycle**, on the cycle's page, is for anyone who can manage the project, and it
does three things at once:

- **Freezes the numbers.** The header, the breakdowns, the burn-up and the list
  of issues still open at that moment are written down as they stand, and a
  completed cycle reads from that record from then on. Its numbers **never move
  again**: reopen one of its issues next month, cancel another, archive a
  third, and the completed cycle still says what it said on the day. The
  page marks itself as frozen so you know you are reading a record and not a
  live count.
- **Ends the window.** Completing a cycle **early** moves its end date to
  today, so the days after are not drawn as days the cycle was still running
  and *completed in the window* stops at the moment you pressed the button.
  Completing one whose date has already passed leaves the dates alone.
- **Asks about what is left.** The dialog counts the issues still open and
  offers to **roll them over** into the next open cycle of the project —
  or to leave them where they are.

**Rolling over** moves every open issue of the cycle into the one you chose,
in one go, and marks each of them **carried over** there: the new cycle's page
counts them under *carried over*, not under *added mid-cycle*, whichever day
you pressed the button. Each issue's activity trail gets a *rolled over from
cycle N* line, and its Discord card is repainted like any other change. Move
one of those issues to another cycle by hand later and it stops being carried
over; it is simply in that cycle.

**Leaving them** is the other honest answer. An unfinished issue is not
FlaviBot's decision to make, and a team that opens the next cycle by hand and
picks what comes along, one issue at a time, is doing exactly what the
rollover does with more thought. Either way the completed cycle keeps the list
of what was still open when it closed, so the record of what that fortnight
did and did not do survives whatever happens to the issues afterwards.

Two things a completion refuses: a cycle that **has not started** — there is
nothing to freeze — and one that is already completed. Rollover only ever
targets an **open** cycle of the same project; if there is none yet, create it
first, or leave the issues and move them later.

#### Velocity

Above the list, once the project has completed a cycle, a **velocity** chart
lines the completed cycles up oldest to newest with two bars each: what was
**planned** — the issues in the cycle by the end of its first day — and what
was **completed**. Points, where the cycles carried them, sit beside the
issue counts, and each bar knows how many issues were added mid-cycle and how
many were carried over.

It is read across, not down. Three cycles that plan twelve and complete seven
are a team that plans for twelve and does seven, and the next cycle should
plan seven; a completed bar that keeps growing while the planned bar does not
is a team getting faster, or one starting to plan honestly. Because it is drawn
from the frozen records, it cannot be nudged after the fact — which is what
makes it worth looking at.

Cycles you never completed do not appear. If the chart is empty, that is why.

#### Patch notes

The **Shipped** tab of a cycle's page lists what the header calls *completed
in the window*: every issue of the project **completed** between the cycle's
start and its end, most recently closed first, whether or not it was ever in
the cycle. It is the changelog of the fortnight, and two buttons turn it into
one you can post:

| Button | Gives you |
| --- | --- |
| **Copy as Markdown** | The notes as one Markdown text, for a GitHub release, a doc, a forum post. Each issue links to its page on the dashboard |
| **Copy for Discord** | The same notes with Discord mentions and timestamps, already cut into messages of at most 2 000 characters at line breaks — paste them one after the other. Each issue links to its **Discord card** when it has one, since that is where the discussion was |

The notes are written the same way every time:

- A title — the cycle's name or number — and the window under it.
- One **section per label**, most-used label first. An issue carrying two
  labels appears under the first section it matches, once: an entry in two
  places reads as a duplicate, not as thoroughness. Issues carrying no label
  come last, under **Other changes**.
- **One line per issue**: the identifier, then the sentence, then who did it
  and any **merged** pull request from [GitHub](https://flavibot.xyz/docs/modules/tasks/08-github)
  — an open pull request on a shipped issue is a warning, not a link. The
  sentence is the issue's **title**, so a title written for the people who
  will read the changelog reads better here than one written for the board.
- A **Full changelog** link at the foot, which opens the dashboard on the same
  project, cycle and window, for anyone who wants the list behind the notes.

Two kinds of issue are **left out** on purpose. **Cancelled** issues are closed
but not shipped, so a cycle that closed ten and cancelled three writes seven
lines. **Sub-issues** fold into their parent: a shipped feature reads as one
line, not as its six pieces.

The same list is the **Shipped** filter on the [board and the
list](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#the-filter-bar) — completed
between two instants, whatever cycle the issue sits in — which is how you
write patch notes for a week, a month, or a cycle you never made: set the
window and copy.
Up to 500 issues go into one copy, which is more than any changelog should
carry.

#### Editing and deleting

Dates and name are editable at any time on an open cycle, including a running
one; stretching a cycle by a week is a normal thing to do and changes nothing
about the issues in it. Once a cycle is **completed** its dates are **frozen**
with the rest of its record — the name stays editable.

**Deleting** a cycle removes the cycle only. Its issues survive, having simply
stopped belonging to one, and land back in **No cycle** where you can put them
in the next. Numbering carries on from the highest cycle the project still has,
so deleting the most recent one hands its number to whatever you create next —
another reason to name a cycle you intend to refer back to. Deleting a
completed cycle deletes its record with it, and the velocity chart forgets it.

For the server-wide view of the same questions — how long things take, who
carries what, how the open pile moves week by week — see
[Insights](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#insights).

Next: [the Discord side](https://flavibot.xyz/docs/modules/tasks/05-discord-commands).

### The Discord side

Source: https://flavibot.xyz/docs/modules/tasks/05-discord-commands
Summary: The /task command, the issue card and its buttons, linking an issue, the thread, the direct messages, and what each refusal means.

Half of this module lives in Discord. Anyone can file an issue with `/task`
without opening the dashboard, every issue gets a **card** in a channel, and the
card carries the four things people actually do to an issue: move it, take it,
comment on it, open it.

Nothing here is a second, lesser copy of the dashboard. It is the same issues,
the same board, the same numbers — the card and the page are two windows on one
row in a database, and a change made through either shows in the other at once.

#### References

Every command that acts on an existing issue takes a **reference**, which is
the issue's identifier: `WEB-12`. Case does not matter, so `web-12` is the same
issue. A bare number is not a reference — see
[identifiers](https://flavibot.xyz/docs/modules/tasks/01-overview#identifiers).

You rarely have to remember one. **`reference` completes as you type**, from
this server's issues: open the box and it offers the issues assigned to you
first, then the server's most recent ones. Type `WEB-1` and it narrows to that
project's issues from `WEB-1` up; type a bare `12` and it finds the issue with
that number whichever project it is in; type words and it searches the titles.
Each line reads `WEB-12 · Fix the login loop`, and picking one fills in the
identifier — exactly what you would have typed. Issues that are **done** are
offered only when you name one by its identifier: the other two ways of asking
mean the work still on.

#### The commands

| Command | Options | What it does |
| --- | --- | --- |
| `/task create` | `project` (required), `title` (required), `description`, `priority`, `assignee`, `due` | Files an issue in that project's first column and posts its card |
| `/task list` | `project`, `state`, `assignee`, `team`, `mine` | The open issues, most recent first. `mine:True` is the shortcut for "assigned to me" |
| `/task mine` | — | Your own open issues, laid out like the [digest](#the-channel-digest): **Overdue** first, then **Due today**, then everything else by state, work in progress before work waiting. Dates are read on your server's timezone. Only you see it |
| `/task view` | `reference` (required) | One issue in full: state, priority, assignee, labels, due, cycle, estimate, time, team, anything attached from GitHub, the description, and a link to its card |
| `/task assign` | `reference` (required), `user` | Assigns it. Leave `user` out to clear the assignee |
| `/task state` | `reference` (required), `state` (required) | Moves it to another column of its project's board |
| `/task comment` | `reference` (required), `body` (required) | Adds a comment, mirrored into the issue's thread |
| `/task estimate` | `reference` (required), `points` | Sizes it, from the scale its project offers. Leave `points` out to clear the estimate |
| `/task time` | `reference` (required), `minutes`, `note` | Logs a stretch of work. Leave `minutes` out to read back what is logged |
| `/task team` | `reference` (required), `team` | Puts it on a team. Leave `team` out to take it off one |
| `/task request` | `reference` (required), `body` (required), `customer`, `important` | Records that somebody asked for that issue — the `customer` you name, or you — so they are told when it closes. Needs the server's [customers](https://flavibot.xyz/docs/modules/tasks/10-customers) switch |

`project`, `state`, `team` and `points` complete the same way, from what your
server actually has — `state` offers the columns of the project the command
already names (the `project` option on `/task list`, the reference's own
project on `/task state`), and `points` offers the rungs of the project's own
[scale](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#choosing-a-scale), and
nothing at all on a project that does not size its work. `due` is a date —
`2026-03-12` — and never a time of day.

`estimate` and `team` change the issue, so they need **Edit** — own or any.
`time` is the exception: the entry is **yours**, so **Log time** is enough to
log your own work, and only its author or somebody with **Moderate** can change
it afterwards.

`/task` carries no default permission, so **every member can use it**. That is
the point: the person who notices the broken thing should be able to write it
down. What each subcommand then does is decided by the member's tracker
[permissions](https://flavibot.xyz/docs/modules/tasks/01-overview#who-can-do-what) — `list` and
`view` are open to everybody, `create` needs **Create issues**, `comment`
needs **Comment**, `assign` is a **Claim** when you take a free issue and an
**Edit** otherwise, `state` is a **Move** into an open column or a **Close**
into a finished one, and `request` needs **Manage customers**. The default
floor gives every member all of these for their **own** issues; a role or
member row widens it. Managing projects, states, labels and cycles is done on
the dashboard. A server that would rather not have the command open to everyone
restricts it in **Configuration > Commands Settings**, like any other command.

Two **menu** entries sit beside the command, both named **Add as task
request**: one on a message, under right-click > **Apps**, and one in a
ticket's **⋯** menu. Each opens a small form asking which issue the ask belongs
to, and records it against the message's author or the ticket's opener — see
[customers and requests](https://flavibot.xyz/docs/modules/tasks/10-customers#filing-one).

#### The card

One message per issue: in the module's **default channel**, or in the channel a
`/task create` ran in. The bar down its left is the colour of the state the
issue is in, so a channel of cards reads as a board at a glance.

Example Discord message:

    color: "#f2c94c"
    title: WEB-12 · Landing page copy is out of date
    fields:
      - name: State
        value: In Progress
        inline: true
      - name: Priority
        value: 🔶 High
        inline: true
      - name: Assignee
        value: "@Nova"
        inline: true
      - name: Due
        value: 2026-09-18 (overdue)
        inline: true
      - name: Cycle
        value: Cycle 4 — Launch week
        inline: true
      - name: Labels
        value: design copy
    footer: WEB · Website · 3 comments
    timestamp: Today at 14:32

The pricing section still mentions the old tiers, and two of the screenshots
are from the previous dashboard.

**State**, **Priority** and **Assignee** are always there — an issue nobody has
taken reads *Unassigned* rather than going quiet about it. **Due**, **Cycle**,
**Estimate**, **Team**, **Time**, **Labels** and a `#2891` badge for anything
attached from [GitHub](https://flavibot.xyz/docs/modules/tasks/08-github) appear only when the
issue has them. The description is cut to its first 600 characters: the card is
a pointer to the issue, not a copy of it, and `/task view` and the dashboard
have the whole of it.

A description written in
[Markdown](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#markdown-mentions-and-checkboxes)
is carried onto the card as it was typed, and Discord renders the parts it
understands — bold, italics, lists, code. The rest, a task list's checkboxes
included, reads there as the plain text it is, which is another reason the card
is a pointer rather than the issue.

The due date is written as a plain `2026-09-18` rather than as one of Discord's
local timestamps, because a due date has no time of day — rendered in each
reader's own zone it would read as the 17th for half the server. It is marked
**overdue** once the day has passed — on the calendar of the **timezone set in
your server's settings**, the same one the dashboard board reads "today" in, so
the card and the board always agree — and stops being marked the moment the
issue closes.

The assignee is shown as a mention so you can see who it is at a glance, but
**nothing on a card ever pings**: a tracker is not the place a message decides
to notify somebody. Notifications are the DMs below.

Under it, four buttons:

| Button | What it does |
| --- | --- |
| **State** | Opens a menu of the project's columns, the current one pre-selected. Picking one moves the issue, exactly like dragging it on the board |
| **Assign** | Takes the issue if nobody has it, and drops it if you are the one holding it |
| **Comment** | Opens a box. What you write becomes a comment on the issue, in its thread and on the dashboard |
| **Open** | A link to the issue on the dashboard |

The buttons keep working for as long as the message exists — after a bot
restart, a month later, on an issue that has been through five states. They
carry the issue's id and nothing else, so there is no state to lose. A **closed
issue keeps them too**: *Done* is a column, not the end of the road, and
reopening an issue is picking another column from the same menu.

**Comment** is open to everyone who can see the card by default — it is on the
floor, because answering a question is not a change to the work. **State** and
**Assign** answer to the same
[permissions](https://flavibot.xyz/docs/modules/tasks/01-overview#who-can-do-what) as the
subcommands: by default the issue's **creator** and its **assignee** can move
and close their own; a **Contributor** role lets you move anybody's, and a
**Manager** role close anybody's. Anybody else pressing them gets a private
reply naming the permission they lack, and nothing happens to the issue.

If even reading them should be narrower than that, post the cards in a channel
your team can see and others cannot: the module puts them all in one channel
precisely so that who sees an issue is a channel permission.

Delete a card by hand and nothing is lost: the issue is untouched, and the next
change to it posts a fresh card in place of the one that went.

##### Turning cards off

A server that tracks in the dashboard, or reads the [digest](#the-channel-digest)
instead, can switch **Post a card for each new issue** off in the module's
settings. From then on a new issue gets no card and no thread, and `/task
create` says so rather than asking for a channel. Nothing already posted is
touched: existing cards stay, keep their buttons and keep updating. A card
deleted by hand is not posted again while the setting is off.

#### The channel digest

One message, kept up to date by the bot, that lists a project's **open issues
by state** — the view for the members who never open the dashboard. Pick a
channel and a project under **Channel digest** in the settings, save, and the
bot posts it and pins it.

Each state gets its own block, with the state's colour down the side, its name
and how many open issues it holds. Up to eight issues are listed per state,
most urgent first, each with its priority, its reference, its title and who has
it; a busier column says how many more it holds. Closed issues are never shown.
As on a card, the names are mentions that **never ping**.

The digest updates itself whenever an issue in the project is created, edited,
moved, archived or deleted — from the dashboard, from `/task`, from a card
button or from an automation — at most once every ten seconds, so dragging a
whole column across the board is one edit rather than twenty. A change nobody
could see in the digest (a comment, a description, time logged) does not edit
it at all, which is why its footer says when the **list** last changed rather
than when the bot last looked.

A few things worth knowing:

- **It carries no dates.** A digest that said "overdue" would be wrong at
  midnight with nothing to update it. Due dates are on the cards and the board.
- **Delete it and it comes back** with the next change. To remove it for good,
  clear the channel in the settings — the bot takes its message down.
- **Move it** by picking another channel: the old message is deleted and a new
  one is posted in the new channel.
- **One digest per server.** It shows one project; switching the project
  rewrites the same message.
- Renaming or recolouring a state updates the digest with the cards. Adding,
  deleting or reordering states shows up with the next issue change.

The bot needs **View Channel** and **Send Messages** in that channel to post it,
and **Pin Messages** (or **Manage Messages**) to pin it — without that it is
simply not pinned. If the bot cannot post, the settings save says so.

#### Linking an issue

The **Open** button is the link out of Discord, but an issue can also be linked
*into* Discord by anybody, because every issue has a page of its own:

```
/dashboard/123/tasks/WEB-12
```

Paste that into a channel, a thread or a commit message and whoever follows it
lands on the issue itself — its properties, its sub-issues, its relations and
its whole discussion — rather than on a board they then have to search.
**Copy link** in the dashboard's command palette is the fast way to get one.
The reader needs access to your server's dashboard, so inside Discord the
identifier `WEB-12` and `/task view` are what everybody can follow. See [the
address of an issue](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#the-address-of-an-issue).

#### The thread

With **Create a thread** on, FlaviBot opens a thread under each card, named
`WEB-12` and the issue's title. The thread is where the issue is discussed, and
comments written on the dashboard are mirrored into it, so somebody reading in
Discord sees the same conversation as somebody reading in a browser.

The channel then stays a list of cards rather than a wall of chat, which is
what makes a channel of a hundred issues readable at all.

Threads are best-effort: without **Create Public Threads** the card is posted
and works exactly as it should, and the only difference is that the discussion
has nowhere to be mirrored to. Turning the setting off stops new threads; the
ones already open stay where they are. Deleting a card's message takes its
thread with it, as Discord always does.

A thread archives itself after a week of silence, which is Discord's longest
setting — anyone posting in it brings it straight back.

#### The direct messages

| When | Who |
| --- | --- |
| An issue is assigned to somebody | The new assignee, unless **Notify the assignee** is off in Settings |
| An issue you follow changes state | You |
| A comment is posted on an issue you follow | You |
| You are `@mentioned` in a comment or a description | You, once — see [notifications](https://flavibot.xyz/docs/modules/tasks/01-overview#notifications) |
| An issue you **asked for** is closed | You, once — see [customers and requests](https://flavibot.xyz/docs/modules/tasks/10-customers#when-the-issue-closes) |

Each one is a card on the rail of the issue's state colour — the same colour
as its column on the board. It names the issue, says what happened and who did
it (with their avatar), and sums the issue up on one line: project, state,
priority, due date. A comment DM quotes the comment. Under it, **Open** goes to
the issue's card in the channel, or to the dashboard when it has no card, next
to **Mute this issue**. The one sent to somebody who **asked** for the issue
carries no mute toggle: they follow nothing, and it is sent once.

**Nobody is told about their own action.** Taking an issue yourself, moving one
yourself or commenting on one sends you nothing: you are looking at the result
already, and that is the single most common reason a bot ends up feeling noisy.

Following an issue comes from having been involved with it: you created it, it
was assigned to you, or you commented on it. Every one of these is a DM; no task
notification is ever posted in a channel, and none of them mentions a role or
`@everyone`. With your DMs closed you simply do not get them, and the card in
the channel is still the record.

#### When a command refuses

| What happened | Why | What to do |
| --- | --- | --- |
| The module is off here | It is disabled in Settings, or not enabled for your server yet | Turn it on under **Management > Projects & Tasks**, if the page is there |
| This bot does not handle tasks here | You used a bot that is not the module's **primary bot** | Use the primary bot, or change which one it is in Settings |
| No such issue | The reference names nothing, or names an issue in another server | Check the identifier; `/task list` shows the ones you can act on |
| You can't *(open issues / comment / move somebody else's issue / log time)* here, or You don't have the *(permission)* permission here | Your tracker permissions do not cover it — most often it is somebody else's issue, or the move is a **close** and you may only **move**. The reply names the permission, as the dashboard spells it | Comment on it instead, ask whoever is holding it, or ask a settings manager for the permission it names |
| That column is not on this board | The state you named belongs to another project, or was deleted | Pick from the completion list, which only offers this project's columns |
| This project requires an estimate before an issue can be started | The project asks for a size before work begins, and this issue has none | `/task estimate` first, then move it |
| No such label | The label was deleted while you were typing | Reload the picker on the dashboard |
| The project is full | It holds as many issues as the plan allows | Close and archive, or see [limits by tier](https://flavibot.xyz/docs/premium/06-limits-by-tier) |
| FlaviBot cannot post there | It lacks **View Channel** or **Send Messages** in the card channel | Grant them, or change the default channel |
| Something you typed was refused | A title over 200 characters, a due date that is not a date, a body over 4,000 | The answer names the field |

A failure to post the card never loses the issue: it is created, it is on the
board and in the list, and it simply has no message in Discord.

#### One bot per server

Issue cards, their threads and their DMs are the work of **one** bot, named in
Settings as the **primary bot**. A second bot posting the same card would
double every message and every DM, and neither could edit the other's.

You do not have to choose one to get started: in a server that has never used
the module, the bot that receives the first `/task create` claims the job, and
Settings is where you change your mind afterwards. A server running a single
FlaviBot never has to think about this at all.

Changing the primary bot leaves the cards already posted where they are: a
message belongs to the application that sent it, and the new bot cannot edit
them. New issues get their cards from the new bot; the old cards stay readable,
but their buttons now answer that this bot no longer handles tasks here. Switch
primary bots between projects rather than in the middle of one. The
[digest](#the-channel-digest) is the exception: the new bot posts its own, and
deletes the old one if it has **Manage Messages** in that channel.

Next: [views and shortcuts](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts).

### Views and shortcuts

Source: https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts
Summary: Board or list, grouping, swimlanes, WIP limits, saved views and the three built-in ones, the command palette, and every keyboard shortcut.

A board of twenty issues needs nothing but its columns. A board of two hundred
needs a way to ask it a question: *what is Ana carrying*, *what is urgent and
nobody has*, *what is actually left in this cycle*. This page is the half of
the module that answers those — the controls that re-slice the board, the views
that remember a slice worth keeping, the palette and the keyboard that drive
the whole page without a mouse.

All of it is on the dashboard, under **Management > Projects & Tasks**. There
is one page of issues, and **board** or **list** is something you choose in
**Display** rather than somewhere you go — so almost everything below works the
same either way, and where it does not, it says so.

#### Grouping

**Group by** decides what a column *is*. It lives in the **Display** popover at
the end of the filter bar, and it starts on **state**, which is the board
everybody pictures: one column per column of the project's workflow. Change it
and the same issues re-bucket by something else, with no issue touched and
nothing moved — grouping is a way of looking, not a change. It is also remembered, so
the grouping you pick is the one waiting for you tomorrow — and the **Display**
button reads *Grouped by assignee* while it is on anything but the state, so a
board never groups itself behind your back.

| Group by | One group per | Where the leftovers go |
| --- | --- | --- |
| **State** | Column of the project's workflow | Nowhere: every issue has a state |
| **Assignee** | Member holding at least one issue | An **Unassigned** group |
| **Priority** | Level, Urgent down to None | The **None** group |
| **Label** | Label in use | An **Unlabelled** group |
| **Cycle** | Cycle of the project | A **No cycle** group |
| **Team** | [Team](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time#teams) of the server | A **No team** group |
| **Project** | Project | Nowhere: every issue is in one |
| **None** | Nothing — one flat list of everything | — |

Three of them behave in a way worth knowing before you try them:

- **By label**, an issue carrying three labels appears in **three** groups. It
  is one issue shown three times, not three issues: change it in one of them
  and all three redraw. That is the honest way to draw "how much work does the
  *bug* label represent" when work is rarely only one thing.
- **By project**, you get one group per project. The page usually shows one
  project at a time, so usually that is a single column — but it is also what
  a board reading across several falls back to. A state and a cycle belong to
  one project, so **by state** and **by cycle** become **by project** while
  the board is wide, and bands by state or cycle are dropped rather than
  redrawn; every other grouping works as it does on one project. The
  **Display** popover says so where it happens. The two ways to get such a
  board are [Include
  sub-projects](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels#sub-projects)
  and a built-in view answering for [all
  projects](#the-three-you-already-have).
- **By team**, the **No team** column is the useful one rather than an
  afterthought: it is everything nobody has claimed, which is the column a
  team meeting is actually about.

Grouping drives the **list** as well, where the groups are headed bands of rows
instead of columns. Whatever the board is grouped by, the dependable ways to
change a property are the ones that name it: the issue's page, the pickers on a
list row, and the [palette](#the-command-palette). See [changing one
issue](#changing-one-issue).

#### Swimlanes

**Swimlane by**, in the same **Display** popover, lays a second dimension out
the other way: horizontal bands across the board, so every card sits at the
crossing of two questions at once. It starts at **none**, which is a board with
one band — the ordinary board — and it offers everything except whatever the
columns are already using, and except state and cycle while the board reads
across [several projects](#grouping). Swimlanes are a board idea, so the list
does not offer them.

The pairing that makes it click is **columns by state, lanes by assignee**: a
row per person, each row a small board of that person's own work, all of them
sharing the same columns. Standing back from it you can see who has four things
in progress and who has none, which is a question a flat board hides by design.

**Columns by state, lanes by priority** is the other one people keep: the
urgent band at the top, read first, every morning.

#### What a column header tells you

Three things, and the last two only when they apply:

- **The count** of issues in that column.
- **The sum of their estimates**, once anything in the column is sized — the
  same points the [cycle burn-up](https://flavibot.xyz/docs/modules/tasks/04-cycles#burn-up) is
  drawn from. A column reading *12 points* is telling you what it would cost to
  empty it.
- **The WIP limit**, when the state has one, as the count against the limit.
  Over it, the header warns.

A **WIP limit** — *work in progress* — is a number you put on a state to say
how much may be in that column at one time: *In Progress, at most three*. The
point of it is not the number, it is what happens when you reach it: the next
thing to do is to finish something, not to start something. A team that starts
six things and finishes none is the thing a limit is there to make visible.

FlaviBot **warns, and never refuses**. Going over a limit is allowed, it simply
shows. A tracker that blocks a drag at five o'clock on a Friday is a tracker
people stop using. Limits are set per state, in Settings, with [the rest of the
workflow](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels#the-workflow).

#### Saved views

A view is the filter bar, the grouping and the ordering, kept under a name.
Anything you find yourself rebuilding on a Monday — *my open bugs, grouped by
priority*, *everything in this cycle nobody has picked up* — is a view, and
after saving it, it is one click.

**Save view** sits on the filter bar. Saved views then appear as **pills**
above the board — beside the [Active / Backlog / All
issues](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#active-backlog-all-issues)
segments, which narrow whatever a pill restores rather than replacing it — and
each of them remembers four things:

| Remembered | Meaning |
| --- | --- |
| **The filters** | State, assignee, label, priority, team, estimate, time logged, GitHub, search, closed and archived |
| **The grouping** | What the columns or the bands are |
| **The ordering** | Manual, priority, due date, created, updated or title |
| **The view** | Whether it opens as a board or as a list |

**Manual** ordering is the one you made by hand: the order cards sit in inside
their column, which is the board's "what next" rather than a sort. The other
five are sorts, and apply everywhere the view does.

Clicking a pill restores all four at once; clicking it again takes you back to
the plain board. Each pill has a menu that renames it, changes its icon, makes
it shared or personal again, and deletes it, and the saved pills can be dragged
into the order you want them read in — the one you open every morning belongs
first.

##### The one you open on

The same menu carries **Make this my default view**: the view you land on every
time you open Tasks on this server. The pill that is your default is marked
with a star, and the same entry turns into **Stop making it my default** to
clear it. It is yours alone, even on a shared view — two people can keep
different defaults over the same pill.

A link wins over it. Any URL carrying `?view=` opens that view instead, which
is what a link copied out of the address bar does, so you can send someone "the
triage board" without changing where either of you lands tomorrow.

With a view selected, **Save view** becomes a choice: **update** the view you
are on with what you have just changed, or **branch** a new one from it and
leave the original alone.

A server may keep **30** saved views. At the limit, saving a new one is refused
— updating the one you are on still works, because it makes no new row.

##### Yours, or the server's

A view is either **yours** — nobody else has it, and nobody else's bar changes
when you make it — or **shared**, in which case it sits in everyone's bar,
marked with a small group icon. Either way it belongs to the server you made it
on, and its own menu moves it between the two.

Shared is how a team agrees on what "the triage board" means; renaming or
deleting a shared view changes it for everybody, so it is worth treating the
shared ones as furniture and keeping the experiments personal.

##### The three you already have

Three views exist on every server without anyone making them. They are worked
out for each reader rather than stored, are there in a server that has never
saved anything, and — having nothing to rename — carry no menu and sit first in
the row, drawn with a dashed outline:

| View | Shows |
| --- | --- |
| **My issues** | Everything assigned to you |
| **Created by me** | Everything you filed |
| **Subscribed** | Every issue you follow |

They answer across **every project** on the server, not only the one the
switcher is on: *what is mine* is not a question about one project. Lighting
one puts an **All projects** chip beside the project switcher, lit with the
view — press it to narrow the answer back to the project you are on, and press
it again to widen it. Each row names its own project through its identifier
(`WEB-12`), so which project a card came from is on the card. The narrowing
belongs to the view you pressed it on: open another pill and it answers wide
again.

**Subscribed** is the interesting one. You follow an issue by having been
involved with it — you created it, it was assigned to you, or you commented on
it — which is the same list that decides who gets a DM; see
[notifications](https://flavibot.xyz/docs/modules/tasks/01-overview#notifications). So *Subscribed*
is exactly "everything that would land in my inbox", which is a better answer
to *what should I look at* than any filter you would have built.

They are starting points, not a cage: change the filters on top of one and
**Save view** keeps your variation as a view of your own.

#### Insights

The board answers *what is there*. **Insights** — a tab in the tasks page
header, beside Cycles — answers *how does it go*: how long things take, who
carries what, how the open pile moves week by week, over **this project** or
**all projects**. It is read-only, anyone who can see the tracker can open it,
and nothing on it changes an issue.

It is two charts. The first is a **measure, sliced**:

| Measure | What a bar is |
| --- | --- |
| **Count** | How many issues |
| **Effort** | The sum of their estimates. Unsized issues add nothing, and the bar says how many were sized |
| **Lead time** | From filed to closed, averaged. Closed issues only |
| **Cycle time** | From the first time an issue entered a started column to when it closed, averaged. Only issues that did both |
| **Age** | How long the issues still open have been open, averaged |

The three time measures are shown as **days and hours**: under a day, plain
hours; above it, both — 61 hours reads **2d 13h**.

| Slice | One bar per |
| --- | --- |
| **State**, **state type** | Column, or type of column — backlog, unstarted, started, completed, cancelled |
| **Assignee**, **creator** | Member, with an **unassigned** bar |
| **Label** | Label. An issue carrying three labels counts under all three, the way [grouping by label](#grouping) shows it three times |
| **Priority** | Level |
| **Cycle**, **project**, **team** | Cycle, project or team, with a bar for the issues in none |

Every bar carries how many issues went into it, and it is worth reading before
trusting an average: a cycle time of ninety hours over two issues is an
anecdote, not a trend.

The second chart is **flow**: per day, week or month, how many issues were
**created**, **completed** and **cancelled** in the bucket, and, standing at
the end of it, three bands — **open and not started**, **in progress**,
**done** — that show whether the pile is growing, shrinking or merely being
churned. A band of in-progress that widens month after month is the same thing
a [WIP limit](#what-a-column-header-tells-you) warns about, seen from a
distance.

Both charts share a **window**: on when issues were **created** or on when
they **closed**, over a **range** that is a preset rather than a date input —
the last **30**, **90** or **365 days**, 90 unless you pick another — and a
**scope** of this project or all projects. Daily buckets are meant for the
short ranges; past about three months the flow chart switches to weeks.
Buckets are counted in **UTC**, like every chart in the module.

One thing to know before quoting a number from here: the charts read the
issues **as the rows stand today**. An issue reopened this morning leaves the
completed side of every past week, and an issue archived yesterday leaves the
charts entirely. That is the right answer to "how are things now" and the wrong
one to "what did we do in March" — for the second, a [completed
cycle](https://flavibot.xyz/docs/modules/tasks/04-cycles#completing-a-cycle) keeps a frozen record
and Insights does not.

#### The command palette

**⌘K** on a Mac, **Ctrl+K** everywhere else, opens the task palette for as long
as you are on the tasks page. It is one box that does two jobs.

**It finds issues.** Type part of an identifier or part of a title — `web12`,
`landing copy` — and it matches loosely, so a half-remembered word is enough.
**Enter** opens what is highlighted. With the box empty it offers the issues you
looked at recently, which is most of what you want it for.

**It acts on the issue the cursor is on**: change the state, assign it, set
the priority, add a label, put it in a
cycle, set the due date, **set the estimate**, **assign a team**, **start** or
**stop the timer**, **link a pull request**, archive it, delete it, copy its
link, open it in Discord. Type the action rather than reaching for the
property — `urg` then enter is the whole interaction.

Every picker it opens, and every property dropdown on the page, is headed by
what it is about to do — *Change status…*, *Change priority to…*, *Assign
to…*, *Set estimate to…* — with the keyboard shortcut that opens it shown on
the right and a **number** on each row. Typing the number picks that row, so
the four or five choices people make all day are two keystrokes rather than a
list to aim at.

It is **keyboard only** by design: arrows to move, **Enter** to take, **Esc**
to close. While it is open it owns the keyboard, so none of the single-letter
shortcuts below fire underneath it.

The dashboard has its own **⌘K** for navigating between pages, and on the tasks
page the task palette takes precedence. **Search the dashboard** at the bottom
of the list hands straight back to it, so nothing is lost by the tasks page
borrowing the key.

#### Keyboard shortcuts

The board answers the keyboard, and the fastest way to work through a triage
list is to never leave it: arrow down the column, `S` to move the issue on,
arrow again. The arrows move a **cursor** — one card at a time, outlined where
it rests — and the keys from `Enter` to `G` below all act on whatever the cursor
is on. They belong to the board and the list; an issue's own page is a separate
page, with no cursor and no palette on it.

| Key | What it does |
| --- | --- |
| `⌘K` / `Ctrl+K` | Open the command palette |
| `?` | The cheatsheet, without leaving the page |
| `/` | Jump to the search box |
| `C` | Create an issue |
| `Esc` | One step back: the composer, then the cursor, then the view, then the filters |
| `Enter` | Open the issue under the cursor |
| `E` | The same, and that page is where the title is edited |
| `A` | Assign it |
| `S` | Change its state |
| `P` | Change its priority |
| `L` | Add a label to it |
| `Shift+E` | Set its estimate |
| `T` | Assign it to a team |
| `Shift+T` | Start the timer on it, or stop the one that is running |
| `G` | Link a pull request or an issue on GitHub |
| `←` `→` `↑` `↓` | Move the cursor around the board |
| `⌘Enter` / `Ctrl+Enter` | Save the form you are in |

`A`, `S`, `P`, `L`, `Shift+E` and `T` open the palette already on that step, so
the six of them are one keystroke to a picker rather than six separate menus to
learn. `Shift+T` and `G` are not pickers and do not open one: the first starts
or stops the clock — you have at most one timer, so the key always means the
obvious thing — and the second opens the attach form, which asks for a
repository, a kind and a number and could not be a list of rows.

`E` and `Enter` both open the issue, which is where the title is edited: the
heading on that page **is** the field, so there is nothing a card could open in
place. `Shift+E` is the estimate rather than a second editor, and the shift is
what tells the two apart.

`Esc` gives back one thing at a time, in the order you opened them, so a reader
typing a title under three filters and a saved view does not lose all of it at
once: the composer first, then the cursor, then the selected view, then the
filters.

**None of the single keys fire while you are typing.** In a title, a
description, a comment or a filter box, `C` is the letter C, and the shortcuts
come back the moment you leave the field. The two with a modifier — the palette
and **save** — work everywhere, inside a form included, which is the whole
point of `⌘Enter`.

`?` is the one to remember. The rest of the table is on the page, one keypress
away, whenever you have forgotten it.

#### Changing one issue

Issues change one at a time, and the only real question is which route is
nearest to where you already are.

| Property | Where you change it |
| --- | --- |
| **State** | Drag the card to another column, the picker on a list row, `S`, the palette, or the issue's page |
| **Priority** | The picker on a list row, `P`, the palette, or the issue's page |
| **Assignee** | The picker on a list row, `A`, the palette, or the issue's page |
| **Labels** | `L`, the palette, or the issue's page |
| **Cycle**, **estimate**, **team**, **due date** | The palette, or the issue's page |

Three of those — state, priority and assignee — are editable from a **list
row** without opening anything: the chip on the row *is* the picker, and
clicking it never opens the issue. That is what makes the list the place to
work through forty issues in a sitting. The **palette** carries all of them and
needs no mouse at all. The **issue's page** carries everything, including the
ones nothing else offers.

**Archiving** is a button on a list row, which appears as you hover it, and
**Archive** in the palette. An archived row carries **Restore** in the same
place instead. See
[archiving](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#archiving-an-issue).

**Deleting** is at the foot of the issue's page, under everything it would
destroy, where it asks twice — the first press turns into the question and the
second does it. The palette's **Delete issue** does not ask: it deletes the
issue the cursor is on, at once. There is no undo either way.

A card you drag is moved the moment you drop it and put back if the server
refuses. Every other route waits: the row dims while the write is in flight,
and the board redraws from the server's answer — so a refusal leaves what was
already there, and says why.

#### How the page looks to you

**Display**, at the end of the filter bar, holds six choices, and every one of
them is **yours alone**: they are not settings, nobody else's page changes, and
there is no permission on them. There is no **Save** either — a click applies at
once, and the button carries a dot for as long as anything in there is off its
default.

| Choice | What it does |
| --- | --- |
| **View** | **Board** or **list**: the two drawings of the same issues |
| **Group by** | What the columns, or the list's bands, are |
| **Swimlane by** | The horizontal bands across the board. Board only |
| **Order by** | Manual, priority, due date, created, updated or title |
| **Density** | **Cosy**, the roomy default, or **compact**, which fits noticeably more on a screen |
| **Card properties** | What a board card shows under its title. Board only |

**View** leads the popover because it decides what the rest of it means. Set it
to **list** and the swimlanes and the card properties leave the popover
altogether: a list draws its own columns and has no bands, so both would be
choices that change nothing.

A card can carry its **priority**, **assignee**, **labels**, **due date**,
**estimate**, **identifier**, **cycle**, **project**, **sub-issue** count,
**comment** count, **team**, **time** and **GitHub** links — that is the order
the popover lists them in — and the first six of those, down to the
**identifier**, are on to begin with. Turning some off is worth trying on a
busy board: a team that never estimates and never sets a due date is reading
two empty rows on every card, and a card down to a title and an avatar is a
card you can see forty of.

The grouping, the density and the card properties follow you to another
machine. The view, the swimlane and the ordering are remembered in the browser
you set them in, which is usually what you want from them anyway — and both the
view and the ordering are things a [saved view](#saved-views) carries, so a
view is how you pin them down for good. **Reset** puts all six back to the
defaults, and clears your [default view](#the-one-you-open-on) with them.

Next: [estimates, time and
teams](https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time).

### Estimates, time and teams

Source: https://flavibot.xyz/docs/modules/tasks/07-estimates-and-time
Summary: Sizing issues on a scale, the two estimate settings, a time estimate against the time actually logged, the running timer, and putting an issue on a team.

A board made of titles answers *what*. This page is the three questions it
cannot answer: **how big is this**, **how long did it take**, and **whose work
is it**.

All three are optional and none of them is on to begin with. A server that
ignores every one of them has exactly the tracker it had before — that is the
point of leaving them off. Turn on the ones that answer a question somebody is
actually asking, and leave the rest alone.

#### Estimates

An **estimate** says how big a piece of work is. Not how long it will take:
how *big*. "Rewrite the pricing page" is a bigger thing than "fix the typo in
the footer", and everybody on the team knows that before anybody knows what
day either of them will happen on.

That is the whole reason estimates are **points** rather than hours. A point
is a size relative to the other issues on the board, so a team can agree that
something is a 5 without committing anybody to an afternoon. Hours are the
next section; the two are deliberately separate.

What points buy you, once a few issues carry them:

- A **column header** that sums them, so *In Progress* reads *12 points* and
  you can see what it would cost to empty it.
- A **cycle burn-up** drawn in points instead of issues, which is a far
  better picture of a fortnight when three of its issues are enormous and
  eleven are trivial. See [cycles](https://flavibot.xyz/docs/modules/tasks/04-cycles#burn-up).
- A **filter**: *everything unstarted and bigger than a 3*.

##### Choosing a scale

Points are numbers, but teams spell them in different ladders, and a ladder
that offers every number from 0 to 100 is a ladder nobody agrees on. So the
**project** declares which rungs it offers, in its own form under
[projects](https://flavibot.xyz/docs/modules/tasks/03-projects-states-labels#projects):

| Scale | What the picker offers | Reads as |
| --- | --- | --- |
| **No estimates** | — | The estimate control is hidden everywhere |
| **Linear** | 0, 1, 2, 3, 4, 5 | The number |
| **Exponential** | 0, 1, 2, 4, 8, 16, 32, 64 | The number |
| **Fibonacci** | 0, 1, 2, 3, 5, 8, 13, 21 | The number |
| **T-shirt** | 0, 1, 2, 3, 5, 8 | No estimate, XS, S, M, L, XL |

**No estimates** is where every project starts, including the ones that
existed before this page did. A ladder nobody chose has no business
decorating a board, so until somebody picks a scale there is no estimate
control to see and no empty row on any card.

Which of the other four is a matter of taste, and the taste is about how you
argue:

- **Linear** is the simplest thing that works, and it is the right answer for
  most servers. Five sizes, and the difference between a 4 and a 5 is small
  enough not to be worth a meeting.
- **Fibonacci** is the classic. The gaps widen as the numbers grow, which is
  an honest way of saying that nobody can tell a 12 from a 13 — past a point
  the only useful answer is "bigger than the last big one".
- **Exponential** widens harder still. It suits work that is genuinely either
  tiny or enormous with nothing in between.
- **T-shirt** replaces the argument about numbers with an argument about
  letters, which some teams find much easier. XS to XL, and the sums still
  work underneath because each size is a number in disguise.

##### Switching a scale changes nothing about the issues

A scale is a **display choice**, so you can change your mind on a Tuesday
afternoon with forty issues already estimated and nothing is rewritten,
converted or lost. That is deliberate: an estimate is stored as a plain
number, which is what lets the column headers and the burn-up add it up at
all.

The consequence is worth knowing before it surprises you. After a switch, an
issue can be carrying a value the new scale does not offer — a fibonacci
**13** under a t-shirt scale, an **8** under a linear one. **That is not an
error.** The issue shows the raw number, nothing refuses to save, and the next
time somebody opens the picker they get the new ladder. Re-estimate it if the
old number bothers you, or leave it: it was a real size when somebody chose
it.

##### The triangle

An estimate draws a small **triangle** that fills from the bottom, and it
fills **by rank rather than by value**: whatever sits at the top of the
project's ladder draws a full triangle. A fibonacci **21**, an exponential
**64**, a t-shirt **XL** and a linear **5** all read as *the biggest thing
this team files*, even though the numbers differ by a factor of thirteen.

Mapping it by value instead would make every t-shirt estimate look trivial
next to an exponential one, and the glyph would be useless the moment you
looked at two projects side by side. An unestimated issue draws the dashed
outline.

##### The two settings

Under the scale, the project's form carries two switches. Both are soft:
neither one goes back and changes an issue you already have.

**Allow a zero estimate** decides whether **0** is one of the rungs the picker
offers. It is on by default, because every scale above opens with a zero and
"this is nothing, and we know it is nothing" is a real answer. Turn it off in
a team that reads *0 points* as *nobody has estimated this yet* — the picker
then starts at 1, and an issue that already carries a 0 keeps it. It changes
what you are **offered**, never what an issue may **hold**.

**Require an estimate before an issue can be started** is the useful one, and
it is worth a paragraph because it is a rule about a *moment* rather than
about an issue.

With it on, an issue cannot **enter** a column whose type is **started** while
it has no estimate. Drag it there, pick the column on the issue's page or on a
list row, use the command palette, the card's **State** button in Discord or
`/task state`, and every one of those routes answers the same thing: *this
project requires an estimate before an issue can be started*. Size it and the
move goes through.

What it deliberately does not do is reach backwards. Ticking the box on a
project with six things already in progress leaves all six exactly where they
are — none of them is refused, hidden or flagged. A setting that froze a board
the moment it was switched on would be a setting nobody dares try. The same
goes in the other direction: nothing stops an unestimated issue being closed,
cancelled, archived or moved back to the backlog. It is a gate on *starting*,
which is the moment a team is deciding what to pick up, and that is the only
moment where the question "how big is this?" is actually worth asking.

The gate does nothing at all on a project whose scale is **No estimates**,
since there is nothing to estimate with. Turn the scale on first.

##### Where an estimate shows up

| Where | What you get |
| --- | --- |
| The [issue page](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#the-issue-page) | The picker, headed *Set estimate to…*, one row per rung |
| A board card | The triangle and the label, when **estimate** is one of your [card properties](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#how-the-page-looks-to-you) |
| A column header | The sum of the points in that column |
| The [filter bar](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#the-filter-bar) | A smallest and a largest, either end on its own |
| The [command palette](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#the-command-palette) | **Set estimate**, on `Shift+E` |
| Discord | `/task estimate`, and the estimate on the card |
| The activity trail | *changed the estimate from 3 to 5*, with who and when |

#### Time

Points say how big. **Time** says how long — and because those are two
different questions, they are two different things on the issue, neither one
derived from the other.

| | What it is | Who writes it |
| --- | --- | --- |
| **Time estimate** | How long somebody thinks this will take | Whoever is planning it, once |
| **Logged time** | How long it actually took | Whoever did the work, as they go |

Keeping them apart is what makes either one worth having. An estimate that
quietly turns into the actual is a number that can never be wrong, and
therefore never teaches anybody anything; an actual with nothing to compare it
against is a number nobody reads.

##### The time estimate

One property on the issue, in minutes, from 1 to 100,000 — the ceiling is
roughly sixty-nine days of non-stop work, and it exists to catch a form that
sent milliseconds rather than to be a rule about anything. Leave it empty on
the issues where it would be a guess; nothing requires it and no gate depends
on it.

##### The log

**Logged time** is a list rather than a counter. Each entry is one stretch of
work and carries:

| Field | Notes |
| --- | --- |
| **Minutes** | 1 to 100,000. Zero is refused — an entry that says nothing still appears in the list under somebody's name |
| **The day** | A calendar date, defaulting to today. "Ninety minutes on Tuesday" has no time of day, and giving it one would make it read differently in another timezone |
| **A note** | Optional, up to 200 characters: *pairing with Ana*, *stuck on the API* |
| **Who** | Filled in from whoever logged it. It is never a field you type |

A list rather than a number, for exactly the reason the
[activity trail](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#the-issue-page) is
one: a counter is a figure anybody can overwrite and nobody can explain. With
one row per stretch, the total is a sum you can take apart — *who did what, on
which day* — and one person can correct their own entry without touching
anybody else's.

The issue shows the **total** logged against the **estimate** where it has
one, so *how long so far* can be read against *how long we thought* without
anybody doing the arithmetic. An issue may carry **100** entries; past that,
the log is longer than the issue it belongs to and what you want is a second
issue.

**An entry is yours.** You can edit or delete the ones you logged, and only
those — changing somebody else's record of their own afternoon needs
**Moderate**: Manage Server, the **Manager** and **Admin** presets, or any role
or member row that carries it. Deleting an entry removes it from the total;
nothing else changes.

##### The running timer

Instead of logging a stretch afterwards, press **Start timer** and let
FlaviBot count. The issue shows it running and the number ticks in front of
you.

Three things about the timer that are worth knowing before you use it:

- **One timer, per person, per server.** Not one per issue. A person works on
  one thing at a time, and a product that let you run four timers at once
  would be a product that silently bills the wrong issue every time somebody
  forgot one.
- **Starting a second timer stops the first**, and the first is logged before
  the second begins. You do not lose the stretch you were in the middle of,
  and nothing is counted twice.
- **Stopping writes one entry**, rounded to the nearest minute with a floor of
  one, dated today. The entry is then an ordinary entry: edit its note, fix
  its minutes, delete it.

A running timer is **not** part of the logged total until it stops, which is
why the issue shows it separately. Nothing stops it for you — no end of day,
no idle timeout — so a timer left running overnight is a long entry to correct
in the morning rather than a number silently added behind your back. If you
would rather not think about it at all, skip the timer and log the minutes by
hand; the log does not care which way the entry arrived.

##### Where time shows up

| Where | What you get |
| --- | --- |
| The issue page | The estimate, the total, the log, and **Start timer** / **Stop timer** |
| A board card | The time, when **time** is one of your card properties |
| The filter bar | **Has time logged**, either way round |
| The command palette | **Start timer**, **Stop timer** |
| Discord | `/task time`, and the time on the card |
| The activity trail | One row per entry logged, with who and how long |

Nothing about time is ever announced: no DM, no channel message, no reminder
that your timer is running. A tracker that nags people about a clock is a
tracker people stop starting timers in.

#### Teams

A **team** is a named list of members with an optional free-text role next to
each one — your moderators, the art crew, the platform team. An issue can be
put on one.

##### One list, both modules

The teams here are **the same teams as the calendar's**. Not a copy, not a
second list that looks like it: one row, read by both. Make *Moderation* once
and it is there to invite to an event, to lay out an availability grid, and to
own an issue.

That means teams are created and edited in one place — the **Teams** tab of
the [Events](https://flavibot.xyz/docs/modules/events/05-teams-and-availability) page — and the
limits there are the limits here: **25 teams** per server, up to **100
members** each, a name of up to 64 characters. Renaming a team renames it
everywhere; deleting one removes it from everywhere.

Deleting a team does **not** delete the issues that were on it. They simply
stop being on a team, exactly as they would if you had cleared the field by
hand — disbanding a team widens its work, it does not destroy it.

##### A team is not a second assignee

An issue is still assigned to **one person**, and the team sits next to that
rather than instead of it. They answer different questions:

| | Answers | Can be empty |
| --- | --- | --- |
| **Assignee** | Who is doing this | Yes — and most issues start that way |
| **Team** | Whose work this is | Yes |

*Assigned to Ana, on the moderation team* is a normal issue. *On the
moderation team, unassigned* is the more useful one: it is work that belongs
to a group and has not been picked up by anybody in it yet, which is precisely
what a team wants to look at on a Monday.

Putting an issue on a team **notifies nobody**. Nobody on it is DMed, no role
is mentioned, and the card does not ping. Following an issue still comes from
being involved with it; see
[notifications](https://flavibot.xyz/docs/modules/tasks/01-overview#notifications).

##### Where a team shows up

| Where | What you get |
| --- | --- |
| The issue page | The **Team** property, one team or none |
| A board card | The team's name in its colour, when **team** is one of your card properties |
| **Group by** | One column per team, plus a **No team** column. See [grouping](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#grouping) |
| The filter bar | One team, or **No team** |
| Saved views | Remembered like every other filter |
| The command palette | **Assign team**, on `T` |
| Discord | `/task team`, `/task list team:…`, and the team on the card |
| The activity trail | *moved it to the platform team*, with who and when |

Grouping by team is what most servers turn teams on for. One column per
team, the **No team** column on the end holding everything nobody has claimed,
and a board that answers *what is each group carrying* without anybody writing
a report.

#### The three in Discord

Everything above is on the card and in `/task view`, and three subcommands set
them without opening the dashboard:

| Command | What it does |
| --- | --- |
| `/task estimate reference:WEB-12 points:5` | Sizes it. Leave `points` out to clear the estimate |
| `/task time reference:WEB-12 minutes:90 note:…` | Logs a stretch against it. Leave `minutes` out to read back what is logged |
| `/task team reference:WEB-12 team:…` | Puts it on a team. Leave `team` out to take it off one |

`estimate` and `team` need **Edit** like every other change — **Edit own
issues** by default, **Edit any issue** with a role or member row that carries
it — except a time entry, which is always your own and needs only **Log
time**. The full list is in
[who can do what](https://flavibot.xyz/docs/modules/tasks/01-overview#who-can-do-what), and the
rest of the Discord side is in
[the Discord side](https://flavibot.xyz/docs/modules/tasks/05-discord-commands).

#### When one of these refuses

| What it says | Why | What to do |
| --- | --- | --- |
| This project requires an estimate before an issue can be started | The project's **require an estimate** setting is on and the issue has none | Give it an estimate, then move it |
| The estimate must be a whole number between 0 and 100 | Something outside that range reached the server | Pick from the picker instead of typing |
| An issue can have at most 100 time entries | The log is full | Correct the entries you have, or split the work |
| Only the author or a moderator can change this time entry | It is somebody else's record of their own work | Ask them, or ask for **Moderate** |
| A time entry must be between 1 and 100,000 minutes | Zero, negative, or a number that was milliseconds | Retype the minutes |

#### Limits

| Thing | Limit |
| --- | --- |
| Estimate | 0 to 100 points, whatever the scale spells them as |
| Time estimate | 1 to 100,000 minutes |
| One time entry | 1 to 100,000 minutes |
| A time entry's note | 200 characters |
| Time entries on one issue | 100 |
| Running timers | One per person, per server |
| Teams on a server | 25, of up to 100 members each |
| Teams on one issue | One |

Next: [GitHub](https://flavibot.xyz/docs/modules/tasks/08-github).

### GitHub

Source: https://flavibot.xyz/docs/modules/tasks/08-github
Summary: Connecting a server to GitHub, linking repositories and what each toggle does, the magic words that attach a pull request, what a merge moves, and what is deliberately left out.

The issue lives here. The code lives on GitHub. Connecting the two means an
issue can say *this is the pull request that fixes me*, and — if you want it
to — move itself along the board when that pull request is merged.

This is optional, it is off until somebody sets it up, and it is worth setting
up only for a server where the work on the board is code somebody pushes. A
server running a moderation queue or a stream schedule should skip this page
entirely.

If you have never wired two tools together like this, the shape is: you
connect the **server** to a GitHub account once, you **link** the repositories
that matter, and from then on people write an issue's identifier into the pull
requests they open. Nobody has to press anything on the dashboard afterwards.

#### What it does, and what it does not

| | |
| --- | --- |
| Attach a pull request to an issue when somebody writes `WEB-12` in it | Yes, as soon as a repo is linked |
| Show the pull request, its number and its state on the issue and on its card | Yes |
| Move an issue when its pull request opens or merges | Only if you switch it on, per repository |
| Mirror GitHub issues into the tracker | Only if you switch it on, per repository |
| Mirror comments between an issue and its GitHub thread | **No** — see [what this version does not do](#what-this-version-does-not-do) |
| Create a branch from an issue | **No** |
| Sync with GitHub Projects | **No** |
| Push code, close a GitHub issue, or comment on GitHub | **Never**, under any setting |

That last row is the one to read twice. Everything on this page is FlaviBot
**listening** to GitHub. Nothing here writes anything back to your repository,
which is also why the access it asks for is read access.

#### Connecting the server

One connection per server, set up under **Management > Projects & Tasks**, in
**Settings**, by somebody with **Manage settings** (Manage Server, or the
**Admin** preset). It asks for three things:

| | What it is |
| --- | --- |
| **The account** | The GitHub user or organisation your repositories belong to |
| **A credential** | Either a **GitHub App installation**, or a **personal access token** as the fallback |
| **A webhook secret** | The password GitHub signs its deliveries with, so FlaviBot can tell a real one from a forged one |

The **GitHub App** is the proper way in: you install it on the account, pick
the repositories it may see, and GitHub tells FlaviBot who you are. The
**token** path exists for servers that would rather paste a fine-grained
personal access token — give it read access to the repositories you intend to
link and nothing more, because nothing here needs to write.

The token and the webhook secret are stored **encrypted**, and neither ever
comes back out: no route returns them, the dashboard never shows them again,
and there is no masked "last four" anywhere. What the page shows is whether a
credential is stored. Rotating one is pasting the new value over it; there is
nothing to reveal first.

A connection that GitHub starts refusing — a revoked token, an uninstalled App
— stamps the last thing it was told and the page says so, so a sync that
stopped working is visible rather than merely quiet.

##### The webhook secret is the whole trust boundary

FlaviBot receives what GitHub sends it, and anybody on the internet can send
something that claims to be from GitHub. The secret is what separates the two:
every delivery carries a signature computed with it, FlaviBot recomputes that
signature over the raw body, and **a delivery whose signature does not match
is refused before it is read**. Not parsed, not logged with its contents, not
acted on.

Two consequences, both deliberate:

- A connection with no secret stored accepts **nothing** inbound. The link
  still works from your side — repositories, manual attachments — but nothing
  arrives on its own.
- If the secret ever leaks, rotate it on both sides straight away. Somebody
  holding it could forge a delivery, and a forged delivery is somebody moving
  issues in your server.

A delivery about a repository this server has **not** linked is dropped
without an answer. It is not this server's business, and answering differently
would tell the sender what other people have linked.

#### Linking a repository

A connection on its own does nothing. **Link repository** adds one, by owner
and name — `flavibot/web` — and each linked repository carries its own
settings:

| Setting | Default | What it does |
| --- | --- | --- |
| **Project** | Every project | Which project this repository serves. Leave it on *every project* unless the repo maps to exactly one of them |
| **Default branch** | Empty | The branch the repo's work lands on, when it is not obvious |
| **Link pull requests** | **On** | Attach a pull request to the issues it names. This is the feature most people came for |
| **Sync issues** | Off | Mirror the repository's own GitHub issues into the tracker |
| **Move issues automatically** | Off | Let a pull request move the issues it names. See [the merge automation](#the-merge-automation) |
| **State a merged pull request moves to** | None | Which column a merge lands an issue in. With none chosen, a merge moves nothing |

The three switches are worth a sentence each. **Link pull requests** is the
one this page is mostly about, and it is on because it is the whole point:
without it, nothing attaches itself. **Sync issues** is the one that goes and
reads the repository's own issue list rather than waiting for somebody to
write an identifier — leave it off unless you actually want GitHub issues
appearing on your board, and note that it reads only: nothing you do here is
written back to GitHub. **Move issues automatically** is the board moving on
its own, and it has [a section of its own](#the-merge-automation) below.

FlaviBot remembers the repository by **GitHub's own id** rather than by its
name, so renaming a repository or transferring it to an organisation keeps
every link that hangs off it working.

Unlinking a repository removes it and the attachments made through it. The
issues themselves are untouched — they simply stop mentioning a pull request.

How many repositories a server may link depends on the plan; see
[limits](#limits) at the foot of this page.

#### Magic words

Once a repository is linked with **Link pull requests** on, nobody needs to
touch the dashboard again. Write an issue's **identifier** into a pull request
and FlaviBot attaches it.

FlaviBot reads three places: the **title** of a pull request, its **body**,
and the **message of a commit** pushed to the repository. In each it looks for
one of these words followed by an identifier:

| Words | What they do |
| --- | --- |
| `close`, `closes`, `closed`, `fix`, `fixes`, `fixed`, `resolve`, `resolves`, `resolved` | Attach the pull request to the issue **and** arm the [merge automation](#the-merge-automation) |
| `ref`, `refs`, `part of` | Attach only. The issue is never moved by this pull request |

So:

```
fixes WEB-12 — rewrite the pricing section
refs WEB-19, and part of WEB-4
```

The first line attaches `WEB-12` and arms the automation. The second attaches
`WEB-19` and `WEB-4`, and neither of those will ever be moved by this pull
request — which is exactly what you want for the issue a change merely touches
on the way past.

Three details worth knowing:

- **Case does not matter**, on the verb or on the identifier: `Fixes web-12`
  is the same as `fixes WEB-12`.
- **The words are whole words.** `prefix WEB-12` is not `fix`, and `WEB-123`
  is not `WEB-12`. A tracker that guessed here would attach the wrong issue
  quietly, which is worse than attaching nothing.
- **Attaching is not something that happens twice.** The same words in a
  title, in the body and in three commits are one attachment, not five.

You can also attach one **by hand**, from the issue itself or with **Link a
pull request** in the
[command palette](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#the-command-palette),
and that is how you attach a pull request whose author forgot, or a GitHub
*issue* rather than a pull request.

FlaviBot remembers **how** a link came to exist, and it matters: automation
only ever undoes its own work. A link a magic word made can be dropped by a
later pass over the same pull request; a link you made by hand is removed by a
person and by nothing else.

#### The merge automation

Off by default, per repository, under **Move issues automatically**. With it
on, and for issues attached by a **closing** word above:

| What happens on GitHub | What happens to the issue |
| --- | --- |
| A pull request naming it is **opened** | It moves to the project's first **started** column |
| That pull request is **merged** | It moves to the column you chose in **State a merged pull request moves to** |

And the list of things it will not do, which is longer and more important:

- **It never moves a closed issue.** Something already in a *Done* or
  *Cancelled* column stays there; a late merge does not reopen finished work.
- **It never moves an issue you attached by hand**, unless the pull request
  itself used a closing word. A link somebody made to say *these two are
  related* is not permission to move their issue.
- **`ref`, `refs` and `part of` never move anything**, in either direction.
  That is the entire difference between the two groups of words.
- **A pull request closed without being merged moves nothing.** Abandoning a
  branch is not finishing the work, and the issue is left where the team put
  it.
- **With no state chosen for a merge, a merge moves nothing.** The attachment
  still happens; the board simply waits for a person.
- **Nothing is ever written back to GitHub.** The GitHub issue is not closed,
  no comment is posted, no label is set, no branch is made.

A word on whether to turn it on at all. Automatic state changes are lovely
right up to the afternoon they move something you were in the middle of
talking about. The setting is per repository precisely so you can try it on
the repo where the workflow is tidy and leave it off on the one where it is
not. Choosing **In Review** rather than **Done** for a merged pull request is
the conservative setting most teams end up on.

#### What a linked issue looks like

An issue with something attached carries a banner at the top of its page —
*Issue synced with GitHub #2891* — and each attachment is a row naming the
repository, the number, the title GitHub last reported and its state: **open**,
**draft**, **closed** or **merged**. Clicking it opens the pull request.

The same thing shows more compactly elsewhere: a `#2891` badge on the board
card, once **GitHub** is one of your
[card properties](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#how-the-page-looks-to-you),
and on the issue's card in Discord and in `/task view`, where it appears
whenever the issue has something attached. The
[filter bar](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#the-filter-bar) can keep
only the issues that have something attached, or only the ones that have
nothing — *what merged this week* and *what is still only a conversation* are
both questions somebody asks on a Friday.

An issue may carry **25** attachments. A title FlaviBot has not heard about
yet — a pull request attached by a magic word seconds ago — shows as its number
until the next thing GitHub says about it fills the rest in.

#### What this version does not do

Three things that a GitHub integration could plausibly do and this one does
not. They are listed here rather than left to be discovered:

- **Two-way comment mirroring.** A comment on the issue does not appear on the
  GitHub pull request, and a comment on GitHub does not appear on the issue.
  The discussion stays where it was written: the dashboard and the issue's
  Discord thread on one side, GitHub on the other.
- **Creating a branch from an issue.** There is no *start work on this* button
  that makes a `web-12-…` branch for you. Make the branch the way you always
  make it, and name the issue in the pull request.
- **GitHub Projects sync.** A GitHub Project board and a FlaviBot board stay
  two separate boards. Nothing is copied between them in either direction, and
  moving a card on one does not move anything on the other.

None of the three is ruled out forever; they are simply not in this version,
and a page that implied otherwise would be worse than a page that says so.

#### When it refuses

| What it says | Why | What to do |
| --- | --- | --- |
| This server is not connected to GitHub | There is no connection yet, or it was removed | Connect it in **Settings** |
| A GitHub connection needs either an App installation or a token | Neither credential was given, or the only one stored was cleared | Install the App, or paste a token |
| That repository is already linked to this server | It is on the list already | Edit the one that is there |
| This server has reached its GitHub repository limit | The plan's cap | Unlink one, or see [limits by tier](https://flavibot.xyz/docs/premium/06-limits-by-tier) |
| An issue can have at most 25 GitHub links | The attachment list is full | Remove the ones that no longer matter |
| That GitHub issue or pull request is already linked | It is attached to this issue already | Nothing — the attachment you wanted exists |
| Repository not found | The repository belongs to another server, or was unlinked | Link it here first |

A delivery that fails its signature check is refused with no detail at all,
and shows up as a failed delivery in GitHub's own webhook log. That is the one
failure the dashboard cannot explain to you, by design: telling an unverified caller *why* it was refused is telling
it how to get it right next time.

When GitHub asks FlaviBot to slow down, FlaviBot slows down and catches up
afterwards; a burst of activity is not lost, it simply arrives over the next
few minutes.

#### Limits

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Linked repositories | 1 | 5 | 15 | Unlimited |

The rest are the same for everybody:

| Thing | Limit |
| --- | --- |
| GitHub connections | One per server |
| Attachments on one issue | 25 |
| Repository owner and name | 100 characters each |
| Branch name | 255 characters |

A linked repository is inbound traffic and outbound calls against a shared
rate limit, which is why it is capped more tightly than projects are. The
connection itself is not capped: there is one per server whatever your plan
is, and the free tier gets a repository to point it at.

Next: [doc pages](https://flavibot.xyz/docs/modules/tasks/09-doc-pages).

### Doc pages

Source: https://flavibot.xyz/docs/modules/tasks/09-doc-pages
Summary: The pages that live next to the issues — a project's docs and the server wiki, the tree they sit in, writing them in Markdown, how WEB-12 links both ways, history and restore, templates, archive versus delete, search, and who may do what.

An issue is one thing to do. A **page** is what is not: the spec the issues
came out of, the notes from Tuesday's meeting, the patch notes you wrote when
the cycle closed, the *how we do releases* that somebody asks for every month.
Pages live next to the board, in the same module, under the same permissions,
and they know about the issues around them — write `WEB-12` in one and the two
point at each other.

This is a small wiki, not a document suite. A page is a title and a body of
Markdown, it sits in a tree, it keeps its history, and that is the whole of
it. If you need comments in the margin, real-time co-editing or an exported
PDF, that is somebody else's tool; if you need a place for the writing that
does not fit in an issue's description, this is it.

#### Project pages and the server wiki

A page belongs either to **one project** or to **the server**.

| | Where it shows | For |
| --- | --- | --- |
| **Project pages** | The **Docs** view of that project, beside its board and its cycles | Specs, meeting notes, patch notes — writing about *this* work |
| **The server wiki** | **Docs** with no project chosen | Things that are true across projects: conventions, onboarding, how the team works |

Switching project switches the pages, as it switches everything else on the
page. A page can be **moved** between projects, or between a project and the
wiki, later — that is a [manage permission](#who-may-do-what), and it moves
the page's whole subtree with it.

#### The tree

Pages nest. A page can have a **parent**, and so can that one, down to **four
levels**: a *Releases* page holding *2026*, holding *September*, holding the
notes of one release. Deeper than that and the sidebar stops being a map of
anything, so the fifth level is refused.

Within a level pages keep the **order** you put them in, and a **pinned** page
sits **first** whatever its position — the one everybody opens goes at the
top, and stays there while the rest of the tree fills in below.

A page's parent must be in the **same project** (or, for a wiki page, the wiki)
and cannot be one of its own descendants; the form refuses a loop rather than
drawing one.

#### Writing a page

The body is **Markdown**, the [same
syntax](https://flavibot.xyz/docs/modules/tasks/02-board-and-issues#markdown-mentions-and-checkboxes)
as an issue's description and its comments, rendered the same way: headings,
lists, tables, quotes, code, links, checkboxes, no images and no HTML. If you
can write an issue you can write a page; there is nothing new to learn.

Two things a page does that a description does not, both about size: the body
may run to **100,000 characters**, and it is written in a full editor rather
than a box under a property list. It is saved when you press **Save**, not on
every keystroke, and a save that would overwrite somebody else's — they edited
the same page while you had it open — is refused with their version offered,
so nobody's afternoon is lost to whoever pressed the button second.

##### Identifiers become links

Write an issue's identifier in a page — `WEB-12`, exactly as you would in a
Discord message — and FlaviBot does two things:

- **On the page**, the identifier renders as a link that opens the issue.
- **On the issue**, the page appears under **Mentioned in**, so somebody
  reading `WEB-12` sees the spec it came out of and the meeting where it was
  discussed, without anybody having pasted a link in either direction.

Both follow the text: take the identifier out of the page and the page leaves
the issue's list on the next save. An identifier written inside a code block
is left alone, as a mention would be — somebody showing the syntax is not
citing an issue.

The two halves are worked out in different places, and it shows at the edges.
**Mentioned in** is resolved on the server, so only an identifier naming a
real issue of this server becomes a backlink; one that names nothing is simply
not one. The **link on the page** is drawn from the project **key** alone —
the page cannot look up every word it renders without a request per paragraph
— so `WEB-12` is a link wherever `WEB` is a project of this server, and one
written for an issue that was deleted or never filed opens a page saying there
is no such issue. An identifier whose key belongs to no project here, another
server's `OPS-4`, stays plain text.

Case does not matter — `web-12` is read as `WEB-12`, the same way the bot
reads a reference typed in Discord — and a page names at most a hundred
issues.

#### History

Every save that changes the text writes a **revision**: who, when, and the
page as it was. The page keeps its last **50**, and the list of them is on
the page itself. Open one to read it as it stood, and **Restore** to make it
the current text again — which is itself a save, and so writes a revision of
its own. Restoring never loses the version you restored *from*; it is always
one step behind.

A revision records the page's text, not its place in the tree: moving, pinning
and reordering a page leave the history untouched.

#### Templates

**New page** offers three shapes to start from, or a blank one:

| Template | What it lays out |
| --- | --- |
| **Spec** | The problem, what the change does, what it deliberately does not, the open questions, and a place to list the issues — which, written as identifiers, [link themselves](#identifiers-become-links) |
| **Meeting notes** | The date and who was there, the agenda, the decisions, and the actions — each a line waiting for the `WEB-` that turns it into a real issue |
| **Patch notes** | Headed sections for the shipped, the fixed and the known, ready for the [patch notes](https://flavibot.xyz/docs/modules/tasks/04-cycles#patch-notes) a cycle copies out |

A template is a starting text and nothing more. Delete what does not apply;
nothing holds you to the headings.

#### Archive and delete

**Archiving** a page hides it from the tree, from search and from every
**Mentioned in** list, and keeps everything: the text, the history, the place
it had — and it gives the page's slot back against the [cap](#limits), which
is why nothing here ever has to be deleted to make room. **Show archived**
brings the archived ones back into view, and un-archiving is one click. A page
archived with children under it takes only itself out of the tree: the pages
under it keep their place, and while their parent is away they show at the
**top level** rather than disappearing with it. Un-archiving puts the tree
back exactly as it was.

**Deleting** a page removes it and its history, for good. Its children are
**lifted to the top level** of their project — or of the wiki — and the issues
it mentioned simply stop listing it. There is no undo, which is why archive is the door on
the page and delete is the confirmation behind it.

#### Finding a page

The [command palette](https://flavibot.xyz/docs/modules/tasks/06-views-and-shortcuts#the-command-palette)
searches pages along with issues: type part of a title or a few words from the
body and the matching pages come back beside the matching issues, twenty at
most, best first. Archived pages are not searched. The **Docs** view's own
tree is the other way in, for the page you know the shape of but not the name.

#### Who may do what

The same permissions as the rest of the tracker, read as follows — see [who
can do what](https://flavibot.xyz/docs/modules/tasks/01-overview#who-can-do-what) for how they are
handed out:

| To | You need |
| --- | --- |
| Read pages, revisions included | **View the tracker** |
| Create a page, edit one, restore a revision, pin and reorder | **Create issues** — anyone who may file an issue may write a page |
| Archive, unarchive, delete, or move a page to another project or the wiki | **Manage projects** |

There is no *own page* right: a page is the team's, and whoever may write one
may write any of them. That is on purpose — a wiki nobody else can correct is
a wiki nobody reads — and the history is the safety net, since anything
overwritten is one restore away.

#### Live updates

A page changed by somebody else refreshes on your screen: the tree, the
**Mentioned in** list on an issue you are reading, and the page itself when
you are only reading it. An editor you are typing in is **not** replaced
under your hands; you are told the page moved on, and the conflict check at
save time is what keeps the two from silently overwriting each other.

#### Limits

The same for everybody:

| Thing | Limit |
| --- | --- |
| Live pages on the server | 500 |
| Title | 200 characters |
| Body | 100,000 characters |
| Depth of the tree | 4 levels |
| Revisions kept per page | 50, the oldest dropped |
| Search results | 20 |

At the page cap, **New page** answers that the server has reached its limit.
**Archiving frees a slot**, as archiving a project does: only the live pages
are counted, and an archived one keeps its text, its history and its place
against the day somebody needs it. So a wiki that has silted up is trimmed by
archiving what nobody opens any more — there is no reason to delete a page to
make room — or by merging the many small ones into a few that somebody will
actually read.

Next: [customers and requests](https://flavibot.xyz/docs/modules/tasks/10-customers).

### Customers and requests

Source: https://flavibot.xyz/docs/modules/tasks/10-customers
Summary: Who asked for a piece of work and how they hear when it ships — customers, including people who are not in your server, the requests filed on an issue, the four ways one is filed, the direct message an issue's close sends, folding a request into the right issue, important requests, merging, archiving, the limits and who may do what.

An issue says what the team is going to build. It does not say **who asked**
for it, how many of them did, or how to tell them once it is done — and on a
server where people ask for things, that is often the more useful half.

A **customer** is somebody the team tracks requests for: a member of your
server, somebody who is not in it at all, or another server entirely. A
**request** is one thing one of them asked for, kept in their own words, filed
on the issue that would answer it. Several requests land on the same issue —
that is the point, since *eleven people asked for this* is the number that
decides what gets built next — and when the issue closes, everyone who asked is
told, once.

This is a record of who asked, not a helpdesk. There is no queue to work
through, no status to keep in step and nothing to reply to: a request is a line
attached to an issue, and the issue is what the team actually works.

#### Turning it on

**Settings > Track customers and requests**, off by default. On, it adds three
things:

- **Customers** in the page header, beside Cycles and Docs: the list of them,
  and a panel for the one you pick;
- **Requests** on every issue, under its relations — who asked for this one;
- the direct message that goes out when an issue closes.

While the switch is off, none of it is read or written. The Customers button is
not drawn, no issue shows a Requests block, `/task request` and the two Discord
menu entries refuse with a line saying where to turn it on, and no closing DM is
ever sent. Turning it back off hides all of it and deletes none of it.

#### A customer

A customer **is** a Discord principal — any Discord user, or a whole server —
and your server holds exactly one row per principal. A customer is **not** a
member of yours: people ask by email, from a server of their own, or from one
they have since left, and every one of them is tracked here like anybody else.
Adding the same principal twice opens the row that exists rather than making a
second one, which is also what happens when somebody files a request in Discord
for a member who is already a customer: the row is reused, and never renamed
behind your back.

| | What it is |
| --- | --- |
| **Name** | What to call them. For anybody Discord can be asked about — a member, or an id you pasted — it fills itself in; for another server it is whatever you type, since FlaviBot is not in it and cannot read its name |
| **Status** | *Active*, *Prospect* or *Churned* — where they stand with you. The list filters on it |
| **Tier** | Your own word for how much they matter: *Free*, *Pro*, *Partner*. Nothing reads it but you |
| **Owner** | Who on the team looks after them |
| **Support channel** | The channel where you talk to them, noted on the row so whoever opens it knows where the conversation happens |
| **Notes** | Anything the team should know before answering them |
| **External ids** | How they are spelled elsewhere — a Stripe customer, a CRM record. Stored and shown, never called |

Every row carries three numbers: how many requests they have filed, how many of
those are [important](#important-requests), and how many sit on issues that are
**still open** — the last being the one worth reading, since it is what they are
still waiting for. The list puts the most-asked first, and **Show archived**
brings back the ones [put away](#archiving).

##### Adding one

**New customer**, on the Customers list, takes the three things a principal can
be:

- **a member of your server** — search the members, and the name fills itself
  in from what the server calls them;
- **anybody, by id** — paste a Discord user id and the bot looks up whose it
  is, showing the name and the face before you add them. An id it cannot look
  up is added all the same: the account may be gone, or Discord may simply be
  having a bad minute, and neither is a reason to refuse somebody you are being
  told about. You type the name, and the form says the id could not be looked
  up rather than pretending otherwise;
- **another server**, by id — FlaviBot is not in it, so nothing about it can be
  read and the name is yours to write.

Somebody who is not in your server is tracked exactly like anybody else and
costs one thing only: [the message an issue's close
sends](#when-the-issue-closes) is never written to them. The form says so while
you add them, and their row and panel carry a quiet marker beside the principal
afterwards, with the reason on hover.

Nothing files a request by itself. A customer row is a record, not a rule: a
request always comes from somebody filing one, through one of the four doors
below.

#### A request

One customer, one ask, in their words, up to **4,000 characters**. Beside the
text it carries:

- **the customer** it belongs to, or none — a request nobody is attached to is
  still a request;
- **where it came from**: added by hand, from a message, from a ticket, from a
  direct message, from a form, from an automation;
- **a link to where they asked**, so the conversation it came out of is one
  click away;
- **important**, the star;
- **the issue** it sits on — or a **project**, for something asked of a whole
  body of work rather than of one thing to do. Those read on the customer's
  panel, marked *On a project*; everything that files a request today files it
  on an issue.

#### Filing one

Four doors, one meaning. Each one needs [**Manage
customers**](#who-may-do-what), and each one records who asked and where they
asked it:

| Where | Recorded as having asked | Kept as the evidence |
| --- | --- | --- |
| **Add a request**, on the issue | The customer you pick, if any | The link you paste, if any |
| `/task request` | The `customer:` you name, or you | The channel it was typed in |
| **Apps > Add as task request**, on a message | The message's **author** | A jump link to that message |
| **Add as task request**, in a ticket's **⋯** menu | The member who **opened** the ticket | A link to the ticket's channel |

##### From the dashboard

Open the issue, and **Add a request** on its **Requests** block: the text, the
customer it belongs to, a link if you have one, and the star. The same block is
where a request is edited, starred, [folded](#folding-a-request-into-another-issue)
or deleted afterwards.

A request filed here names no requester of its own, so when the issue closes it
is the **customer** who is told — which works when the customer is a member of
your server, and tells nobody when it is another server or somebody outside
yours. Filing through one of the Discord doors always names a person.

##### `/task request`

```
/task request reference:WEB-12 body:… customer:@nova important:True
```

`reference` and `body` are required; `customer` defaults to **you**, since
somebody typing this in a support channel usually *is* the person asking; and
`important` is the star. The answer is private, and names the issue and the
customer it was recorded for. The channel it was typed in is kept as the link —
the command's own reply is private and has nothing to link to.

##### From a message

Right-click what somebody asked for, **Apps > Add as task request**. A small
form asks which issue it belongs to, with the **message's text already in it**,
editable — trimming a three-paragraph complaint down to the ask is the whole of
what the staff member contributes here. The request is filed against the
message's **author**, so they are the one told when it ships, and the jump link
to their message is kept.

##### From a ticket

**Add as task request** in the ticket options (**⋯**) menu does the same thing
with the ticket's opener as the requester and the ticket's reason already in the
form. The entry is only shown where the customers switch is on — a button that
always refuses is worse than no button — and it is ticket **staff** only on top
of the tracker's own permission, which is re-checked when the form is
**submitted**, not only when it is opened.

##### What they share

The three Discord doors **make a customer** out of a member who is not one yet,
on the spot, named after what the server calls them; a member who already is
keeps their row and everything on it, [archived](#archiving) or not. The
dashboard mints nobody: it offers the customers you have, and *no customer* is
a valid answer.

All four refuse the same things. A reference naming no issue of this server is
refused the way `/task view` refuses one — write `WEB-12`, the way people say
it out loud. At the [customer cap](#limits), or on an issue that already holds
two hundred requests, the door says so and files nothing.

#### When the issue closes

Moving an issue into a **completed** or a **cancelled** column sends one direct
message to everyone who asked for it:

- **completed** — what you asked for is done;
- **cancelled** — what you asked for is not going to be done.

Both are an answer, and which one is sent is decided by the column's **type**,
never by its name: a board whose cancelled column is called *Won't do* still
owes its requesters the sentence saying the work is not coming.

Who is told is who the request names — the message's author, the ticket's
opener, the member `/task request` named — and, for a request that names
nobody, the customer, when the customer is a member. Three are **not** told,
each for its own reason:

- **whoever closed it**: nobody is ever told about their own action, here as
  everywhere else in the tracker;
- **anybody already following the issue**: the same close DMed them the state
  change seconds ago, and one event should send one message, not two;
- **anybody who muted the issue**: a mute holds on every DM an issue sends.

It is sent **once**. Each request is stamped as told — the skipped readers'
requests included, since they have already heard everything the close had to
say — so reopening the issue and closing it again tells nobody twice. Several
requests from the same person folded onto one issue still send **one** message,
with a line saying how many of their asks it answers.

The card is the one every other task DM uses — the issue, what happened, and a
link to its card or to the dashboard — minus **Mute this issue**: its reader
follows nothing, and there is no stream to silence when the message is sent once
and never again. As with every task notification, nothing is posted in a
channel, nobody is `@mentioned`, and a member with DMs closed simply does not
get one.

One close tells at most **twenty-five** requesters — past that it is a channel
announcement rather than a DM list, and the card in the channel has changed for
everybody already. The ones over the line are marked as told all the same, so a
re-close does not go looking for them again. (Issue **followers** are a
different list, with its own ceiling of a hundred.)

A requester who is no longer a member of the server is not DMed at all. The id
on a request is whatever somebody typed, so the bot checks the person is
actually here before writing to them — and a lookup that cannot answer counts
as "not here": a DM to a stranger is the worse way to be wrong.

The same fence holds for somebody who was **never** here. A customer added by
id — from another server, from an email, from a support guild of their own — is
tracked in full and simply never written to; and another server has no inbox at
all, so a request that names nobody on a server customer tells nobody. None of
that is worth discovering afterwards, so it is shown before the fact: the
customer's row and panel say they are not messaged, and a request whose
requester cannot be reached is marked on the issue's **Requests** block, where
the "they have been told" mark would otherwise go.

#### Folding a request into another issue

People ask for the same thing on three different issues, and two of those turn
out to be duplicates. **Fold into another issue**, on the request's row, moves
it onto the issue that really covers the work.

What it keeps: the text, the customer, the source and its link, and the star.
What it remembers: **where it was first filed** — the row is marked as folded
in, and repeated folds keep the *first* issue, not the previous one. What it
clears: the stamp saying the person was told, so they hear about the issue that
actually ships the thing rather than about the duplicate that was closed on the
way.

Folding a request onto the issue it is already on does nothing at all.

#### Important requests

The star on a request says *this one matters* — the customer who will leave over
it, the ask that came with a deadline. It is counted per customer, and on the
board the issue's card shows **how many asked for it** and marks the count when
any of them is starred.

That is all it does. It changes no priority, sorts nothing and notifies nobody:
it is a flag for whoever reads the board, which is exactly what makes it safe to
use freely.

#### Merging two customers

The same people end up on two rows — a member added by hand, and the server they
run added later from a ticket; or simply two rows where the team wanted one.
**Merge** moves **every** request onto the customer you keep and archives the
one you merged, so nothing anybody asked for is lost, and the panel follows you
to the row that survives.

It cannot be undone, which is why it asks first. The merged row is **archived**
rather than deleted on purpose: the row is how a principal is recognised the
next time somebody files for them.

#### Archiving

Archiving a customer takes them out of the list and keeps everything: their
requests, their counts, their notes. **Show archived** brings them back into
view, restoring is one click, and the slot goes back against the
[cap](#limits) — which is why nothing here ever has to be deleted to make room.

A request filed for somebody whose row is archived lands on that **same** row
and does not wake it up; un-archiving is a deliberate act, and the dashboard is
where it is done.

A request is removed from an issue with **Delete**, behind a confirm, and it is
gone for good. It can also be **archived** instead: it keeps its text and its
place, stops counting against the issue's cap and is never told anything, and
the customer's panel shows archived requests behind **Show archived**.

#### Who may do what

Two permissions, handed out like all the others — see [who can do
what](https://flavibot.xyz/docs/modules/tasks/01-overview#who-can-do-what):

| To | You need |
| --- | --- |
| Read the Customers list, a customer's panel, and the requests on an issue | **View customers** |
| Add and edit a customer, merge or archive one, and file, edit, star, fold or delete a request — on the dashboard or through any of the Discord doors | **Manage customers** |
| Turn customers and requests on or off for the server | **Manage settings** |

A **reporter** sees them; a **contributor** keeps them. Reading a customer's
tier and notes is more than reading an issue, which is why it is not part of
*View the tracker*: a server can let everybody read the board and still keep who
asked for what to the people who answer them.

The bot refuses the same way everywhere. A member without **Manage customers**
reads the same sentence naming the same permission whether they used the
dashboard, `/task request`, the message menu or the ticket menu — and in
Discord, privately.

#### Live updates

A request filed, folded or deleted by somebody else updates the issue you are
reading, and the count on its card on the board. The Customers list refreshes while you are
looking at it, and is read again when you open it — a list nobody is watching
costs nothing.

#### Limits

The same for everybody:

| Thing | Limit |
| --- | --- |
| Customers on the server | 1,000 live ones — archiving frees a slot |
| Requests on one issue, or one project | 200 live ones |
| One request | 4,000 characters |
| Customer name | 80 characters |
| Tier | 24 characters |
| Notes | 2,000 characters |
| Link to where they asked | 500 characters |
| External ids on one customer | 20 |
| Requesters told when one issue closes | 25 |

At the customer cap, adding one answers that the server is full — archive the
ones you no longer track. At the request cap, the issue takes no more: file the
ask on another issue, which is usually a sign that the issue had become a
catch-all anyway.

### How tickets work

Source: https://flavibot.xyz/docs/modules/tickets/01-overview
Summary: How FlaviBot tickets work: panels, ticket channels and labels, the path a ticket takes from click to close, how many panels you get, and what the bot needs.

A **ticket** is a private channel between one member and your support team.
The member asks for it, FlaviBot creates it, staff answer in it, and when it is
over the conversation can be saved as a file.

Three objects are involved, and it helps to keep them apart:

| Object | What it is | Where it lives |
| --- | --- | --- |
| **Panel** | The settings, plus the message members click | Your dashboard, and one channel |
| **Ticket** | One conversation, one channel | A category in your server |
| **Label** | A tag on a ticket, picked at opening or added later | Attached to a panel |

#### The path a ticket takes

A ticket goes through six steps, and FlaviBot handles four of them.

1. A member clicks **Create a ticket** on your panel message.
2. FlaviBot checks the blocks: is this member blacklisted, is the panel within
   its business hours, do they already have a ticket on this panel, and can
   they afford it if you charge for one.
3. FlaviBot creates a channel inside the panel's category. Only the member, the
   support roles and FlaviBot can see it.
4. FlaviBot posts a welcome message with the buttons staff will use: **Close
   Ticket**, **Claim Ticket**, **Escalate**.
5. Staff answer, in Discord or from the dashboard.
6. Someone closes it. Depending on the panel, FlaviBot archives or deletes the
   channel, and a transcript can be sent to the member and to your log
   channel.

#### Where you configure it

Everything you tell FlaviBot about tickets is on the dashboard, under
**Requests > Ticketing**. That page holds your panels, the blacklist, and an
**Analytics** tab. Each panel has its own page with a **Configuration** tab and
a **Statistics** tab.

Members never need a command. Staff have a few, but the buttons cover the
common work.

#### How many panels you get

One panel is enough for most servers, and FlaviBot gives every plan at least
one. You need a second one when the two kinds of request should not land in the
same category, with the same team, or with the same rules.

| Plan | Panels |
| --- | --- |
| Free | 1 |
| Silver | 3 |
| Gold | 5 |
| Platinum | 20 |

#### What FlaviBot needs

The ticket system creates channels and rewrites their permissions, so FlaviBot
needs **Manage Channels** in your server. Without it, opening a ticket fails
and the member is told which permission is missing.

A few extras unlock optional behaviour, and each one is skipped quietly when
it is missing rather than breaking the ticket:

- **Manage Messages** to pin the welcome message
- **Create Private Threads** for the staff notes thread
- **Manage Webhooks** so dashboard replies show the staff member's name and
  avatar instead of FlaviBot's

Next: [setting up a panel](https://flavibot.xyz/docs/modules/tickets/02-panels).

### Setting up a panel

Source: https://flavibot.xyz/docs/modules/tickets/02-panels
Summary: Set up a FlaviBot ticket panel: its channels, the two messages, channel names, what happens on click, labels, business hours, pricing and response targets.

A **panel** is the message members click, plus every setting that applies to
the tickets it opens. Create one, fill it in, save, and FlaviBot posts the
message for you.

On the dashboard, open **Tickets**, then **Ticketing**, then **Add**. A panel
needs a name (up to 50 characters, unique on your server). The name is not
just a label: it is the title of the panel message members see, and it appears
on the ticket itself.

Screenshot: The Ticketing Panels card, listing one panel with its channel, category, ticket counts and rating

Each panel keeps its own counts and its own average rating. The **Analytics**
tab above breaks the same numbers down further.

Saving a panel is also what publishes it. Every save posts the panel message
in the channel you picked, or edits the existing one in place. If that message
was deleted in the meantime, a fresh one is posted. Until you pick a channel,
nothing is posted anywhere, so a half-configured panel is invisible to your
members.

#### Channel settings

**Ticket Panel Channel** is where the panel message goes. Members click the
**Create a ticket** button on it. If the channel you picked no longer exists,
or is of a type the bot cannot post in, the bot creates a text channel named
`open-a-ticket` and posts there instead, so the panel is never lost. Pointing
the panel at a forum channel works too: the panel is posted as a new thread.

**Logs Channel** is optional. When set, the bot posts a line there each time a
ticket opens and a card each time one closes, and it is where transcripts and
low ratings can be sent.

**Support Roles** is the important one. Up to **2 roles** per panel. Holders
of those roles can see every ticket the panel opens, claim them, escalate
them, close them, and use the ticket commands.

Leave Support Roles empty and the bot falls back to the **Manage Server**
permission for closing. Claiming and escalating stop being restricted at all
when no support role is set, so set them. Ticket channels are also created
without any staff access when the list is empty, which is rarely what you want.

#### The two messages

They are easy to mix up.

**Panel Message** is the public advert: an embed titled with the panel name,
described by the **Embed Description** you write (up to 1500 characters), and
a single **Create a ticket** button.

Example Discord message:

    color: "#947cea"
    title: Support
    timestamp: Today at 14:32

Something broken, a question about your subscription, a report to file: open
a ticket and a member of the team will answer here.

The title is the panel's name, so name the panel something your members will
understand. The colour is your server's embed colour, and a blurple **Create a
ticket** button sits under the embed.

**Ticket Message** is what greets the member inside their new ticket. It has
two parts, both up to 1500 characters:

- **Message Content**, posted above the card. Write `{author}` in it and it
  becomes a real mention that pings the member. That is the only placeholder
  this field understands.
- **Embed Description**, the body of the card that carries the staff buttons.

#### Ticket channel names

**Ticket Channel Name** decides what the created channel is called. The
default is `{status}-{username}`. Available placeholders:

| Placeholder | Becomes |
| --- | --- |
| `{status}` | The status emoji, always the one for a freshly opened ticket |
| `{username}` | The member's name |
| `{userid}` | The member's id |
| `{number}` | The ticket number for this panel, padded to four digits |
| `{panel}` | The panel name |

Spaces become dashes and everything is lowercased, because Discord requires
it. The name is set once, when the ticket is created, and is **never changed
afterwards**. That is deliberate: Discord only allows two renames per channel
every ten minutes, and a ticket that gets claimed, escalated and closed in a
row would blow through that. A claimed ticket therefore still shows the emoji
of a fresh one, so read the state from the dashboard, not from the channel
name.

#### What happens when the member clicks

**Create Behavior** offers three answers:

- **Create directly**: the channel is created immediately.
- **Ephemeral confirmation**: a private message asks them to confirm first.
  Useful to slow down accidental clicks.
- **Ask for reason**: a form opens and the member has to describe their issue,
  between 10 and 1000 characters. The reason is quoted in the welcome message
  and shown on the ticket's dashboard page, so staff know what it is about
  before reading anything.

The last two also expose **Show label selection**, which adds a dropdown so
the member picks what their ticket is about. With **Ask for reason** the
choice is required; with **Ephemeral confirmation** it is optional. Either way
a member can pick up to five.

#### Labels

**Labels** are the categories of your ticket system. Each one has a name (up
to 50 characters), a colour and an optional emoji, and you can define **up to
10 per panel**.

They do three jobs:

- They let the member say what they need before staff read a word.
- They are shown in the welcome message of the ticket.
- They filter the ticket list on the dashboard, and they break down the
  analytics by label.

Labels belong to a panel, so a "Billing" label on your support panel is a
different object from a "Billing" label on your applications panel. Staff can
add and remove them on an open ticket from the dashboard, and the ticket
channel gets a short notice each time.

#### Business hours

**Business Hours** restricts *opening*, not answering. Turn it on and mark the
open hours on the weekly grid, hour by hour. Click a day name to toggle the
whole day, or use the presets (office hours, weekdays, 24/7).

Hours are read in your server timezone, which is set in the main server
configuration, not here. Outside those hours the button still exists, but
clicking it returns the **Closed message** you wrote (up to 1500 characters)
and no ticket is created. Tickets already open are untouched.

#### Charging for a ticket

**Opening cost** takes coins from your server economy every time a member
opens a ticket on this panel. Leave it empty or at 0 to keep it free. It only
does anything when the Economy feature is enabled.

Three things are worth knowing before you use it:

- A member who already has a ticket open on the panel is **never** charged for
  the duplicate attempt: the refusal happens before the payment.
- If the channel cannot be created, the coins are given back automatically.
- Refunding on close is a switch in the Economy settings, not here.

#### First-response target

**First-response target** is the time you would like a first reply to take, in
minutes. It changes nothing at runtime: no reminder is sent and nothing is
enforced. Its only job is to give the Statistics tab a bar to measure against,
so you can see the share of tickets answered within it.

Once the panel is saved, read on: [what happens when someone opens a
ticket](https://flavibot.xyz/docs/modules/tickets/03-opening).

### When someone opens a ticket

Source: https://flavibot.xyz/docs/modules/tickets/03-opening
Summary: The checks FlaviBot runs when someone opens a ticket, the category, channel and permissions it builds, failures, opening one for a member, and blocking members.

A member clicks **Create a ticket**. Before anything is created, FlaviBot runs
four checks, in this order. The first one that fails stops the whole thing and
tells the member why.

1. **Are they blocked?** A blacklisted member is refused with a generic
   message, unless the entry was saved with its reason set to visible.
2. **Is the panel open?** Outside business hours, they get your closed message.
3. **Do they already have one?** One ticket per member per panel. They are
   pointed at the channel they already have, and that includes one that is
   closed but not yet deleted. Once the channel is gone they can open a new
   ticket, which takes the place of the old record.
4. **Can they pay?** Only when the panel has an opening cost. A member short
   on coins is told the price and their balance.

Depending on the panel's create behaviour, they may first have to confirm, or
fill in a reason and pick a label. Then the channel appears.

#### What the bot builds

**A category.** The panel's category, or a new one named `Tickets` if none is
set or the configured one is gone. A category the bot creates is denied to
`@everyone` and then saved back onto the panel, so the next ticket reuses it.

**The channel**, named from your format and numbered. Every panel counts its
own tickets, so the numbering is per panel.

**The permissions.** `@everyone` is denied View Channel. The member, the bot
and each support role get View Channel, Send Messages, Read Message History
and Attach Files.

**The welcome message.** Your message content with `{author}` turned into a
real ping, then a card holding the panel name (which links to the ticket's
dashboard page), your ticket description, the reason if one was given, the
labels if any were picked, and the staff buttons: **Close Ticket**, **Claim
Ticket**, **Escalate**, and a **⋯** menu. It is pinned unless you turned that
off.

Example Discord message:

    color: "#947cea"
    title: Support
    fields:
      - name: Reason
        value: My subscription renewed twice this month.
      - name: Labels
        value: 💳 Billing
    timestamp: Today at 14:32

Thanks for opening a ticket. Describe the problem in as much detail as you
can and someone from the team will be with you shortly.

The ping sits above the card, and the four buttons sit inside it, under the
text. Reason and Labels only appear when the panel asked for them.

Alongside that, and none of it blocking the ticket: a line in your log channel
if you set one, the private staff notes thread if the panel creates them
automatically, and the `ticket_open` event for the
[autoresponder](https://flavibot.xyz/docs/modules/autoresponder/02-triggers) if you built anything
on it.

#### When it fails

Channel creation is the step most likely to fail, and the bot repeats
Discord's own reason rather than guessing. The three you are most likely to
meet:

- The bot is missing **Manage Channels**.
- The category is full. Discord allows 50 channels per category, and a busy
  panel gets there. Delete closed tickets, or point the panel at a fresh
  category.
- The server is at its channel limit.

If a paid ticket fails at this point, the coins are refunded automatically.

#### Opening a ticket for a member

Sometimes support starts somewhere else and you want a ticket for it.

**From Discord**: right-click the member, then **Apps**, then **Create Ticket**. You get a private list of your panels as buttons. The ticket opens in the member's name only on panels set to **Create directly**; on panels that ask for a confirmation or a reason the ticket is opened in the name of the staff member who clicked. This needs the **Manage Server** permission.

**From the dashboard**: on the tickets list, the **Create Ticket** button asks
for a panel, a member, optional labels and an optional reason. The ticket gets
an extra note saying who created it from the dashboard.

Tickets opened either way behave exactly like a member's own, with one exception: a ticket opened from the dashboard is never charged. A ticket opened with the Discord Create Ticket context menu still charges the member it is opened for, and is refused outright if they cannot afford it.

#### How do I block someone from opening tickets?

Three ways in, one list out.

- The dashboard's **Blacklisted users** card, on the Ticketing page. It is the
  only one that can block for **selected panels only**, attach a reason, and
  choose whether the member sees that reason.
- `/ticket blacklist add`, with an optional reason and a `show_reason` switch.
  It blocks on every panel. `/ticket blacklist remove` lifts it and
  `/ticket blacklist list` shows who is blocked. These need **Manage Server**.
- The **⋯** menu inside a ticket, **Blacklist author**. Staff-only, blocks on
  every panel with no reason, and offers a Close button right after.

Blocking never touches the tickets someone already has open. Close those
yourself.

Next: [handling a ticket](https://flavibot.xyz/docs/modules/tickets/04-handling).

### Handling a ticket

Source: https://flavibot.xyz/docs/modules/tickets/04-handling
Summary: Handle a FlaviBot ticket: statuses, claiming, escalating, pulling people in, private staff notes, inactive tickets, and letting the assistant answer first.

Everything staff need day to day is on the welcome message FlaviBot posted at
the top of the ticket. The commands exist for the rest.

#### Statuses

| Status | Set when |
| --- | --- |
| **Open** | The ticket is created, or reopened |
| **In progress** | Someone claims it |
| **Waiting on user** | Staff reply **from the dashboard** |
| **Waiting on staff** | The opener replies **from the dashboard** |
| **Escalated** | Someone escalates it |
| **Closed** | It is closed, or its channel is deleted |

The two waiting statuses only move when the reply is sent from the dashboard.
Messages typed in Discord are recorded on the ticket's timeline, but they do
not change the status. If your team answers in Discord, treat **Open** and
**In progress** as your working states and lean on claiming.

#### Claiming

**Claim Ticket** marks the ticket as yours and sets it to In progress. Two
staff clicking at the same moment cannot both win: exactly one claim goes
through and the other is told who got it.

Claiming requires one of the panel's support roles when the panel has any.
When it has none configured, nothing is checked and anyone in the channel can
claim, which is one more reason to fill that field in.

A claimed ticket cannot be claimed by someone else, and there is no button to
hand it back. The one thing that moves a claim is escalation, below.

#### Escalating

**Escalate** is for handing a ticket to someone with more authority, and it is
staff only. Pick a user or a role from the two dropdowns. The target is given
access to the channel (including adding reactions), the status becomes
Escalated, and the ticket posts a card that pings them, with your reason if
you gave one.

Who ends up owning the ticket depends on what you picked:

- **A user**: the claim is transferred to them. The ticket is now theirs.
- **A role**: the ticket is assigned to that team, and the existing claim is
  released, unless the person who held it is a member of the role you escalated
  to.

Escalating again to a different target removes the previous target's access.

#### How do I pull someone into a ticket?

Sometimes a ticket needs a third party: the member who was reported, a
moderator from another team.

- `/ticket add user` and `/ticket add role` give them access.
- `/ticket remove user` and `/ticket remove role` take it away.

Support staff only, and the ticket author can never be removed this way. Each
change posts a short notice in the channel, and is recorded on the dashboard
timeline so it survives the channel being deleted. The ticket's dashboard page
can add and remove members too, though not roles.

#### Asking the member to close

`/ticket request-close` posts a card pinging the ticket author with a **Close
Ticket** button and a **Decline** button. Add a reason and it is shown in the
card.

Only the author can answer it. Accepting runs the normal close flow. Declining
opens a short form where they explain why they want to keep it open, between 5
and 500 characters. The request stays valid for a week.

#### Private staff notes

Every ticket can have a **private thread** for staff talk the member never
sees. It is created on the ticket channel as a private thread named
`📋 Staff notes`, and the panel's support roles are pinged into it. The opener
is not a member of it and cannot open it.

Notes go both ways: what you type in the thread appears on the ticket's
dashboard page, and a note written on the dashboard is posted into the thread.
Either way, the note is stored, so it is still readable after the Discord
channel has been deleted.

The panel decides when the thread is created:

- **Automatic**: on every new ticket.
- **On demand**: on the first dashboard note, or when a staff member picks
  **Create staff notes thread** in the **⋯** menu.
- **Off**: notes stay on the dashboard only.

Creating the thread needs the **Create Private Threads** permission. Without
it, nothing breaks, notes simply stay on the dashboard.

#### Inactive tickets

**Inactivity Settings** watch tickets nobody is talking in. Set a threshold
between 1 and 720 hours and choose what happens. The timer restarts on every
human message in the ticket.

- **Send reminder** posts a message in the ticket asking the member to reply
  if they still need help. It never closes anything.
- **Auto-close** posts a notice in the ticket saying it was closed for
  inactivity. Be aware that the closing itself is currently not going through,
  so the ticket stays open behind that notice. Until that is fixed, use the
  reminder and close by hand.
- **None** disables it.

#### Letting the assistant answer first

Platinum servers can put an AI assistant in front of their support team. It is
being rolled out gradually, so the **AI Assistant** card may not be on your
panel page yet.

The rule that makes it usable: **it only answers from the knowledge base you
give it**. When your entries do not cover the question, it says nothing at
all, and the ticket looks exactly like one without an assistant. It never
invents an answer, never promises a refund or a ban, and every reply carries a
notice saying it is automated and that a human will follow up. It replies in
the language the member wrote in, even when your entries are written in
another one.

**The knowledge base** is a list of question and answer pairs, the question up to 500 characters and the answer up to 4000, that you edit on the panel page. Entries can be switched off
without deleting them, and each one shows how many times it was actually used
to answer, which tells you what is dead weight. **General instructions** (up
to 8000 characters) is a free text box for what is not a question: your tone,
your opening hours, what your server is about.

**When it replies** is up to you:

- **Right away**, answering the reason the member typed when opening. A panel
  that never asks for a reason gives it nothing to work with, so pair this
  mode with **Ask for reason**.
- **On a button click**: an **Ask the assistant** button is added to the
  ticket. The member gets a real reply; a staff member clicking it instead
  gets a private suggested draft that nobody else sees.
- **If no staff replies in time**, between 1 and 1440 minutes after opening.
  It reads the reason plus everything the member has written since. Any human
  message from someone other than the opener cancels it.

It posts at most one public reply per ticket, whichever mode you use.

**What the member does with it**: the reply carries **Helpful** and **Not
helpful** buttons, and only the ticket author can press them. Two optional
follow-ups, both on by default: marking it helpful can add a **Close ticket**
button, and marking it not helpful can ping your support roles so a human
takes over.

**What staff do with a wrong answer**: the **Staff answer** button on the
reply opens a form. What you write is posted as the correct answer, and it is saved into the knowledge base paired with the reason the member gave when opening. A panel that does not ask for a reason has nothing to pair it with, so on those panels the answer is only posted, not learned.

**Learning from closed tickets**: when a ticket closes, the assistant writes a
short summary and a category for it (both visible on the dashboard), and if
the conversation contained a genuine question answered by a human, it proposes
that pair as a new entry, stripped of names and personal details. You choose
how those proposals are handled:

- **Review here**: they wait as suggestions on the panel page.
- **Review in a Discord channel**: they are posted to a channel you pick with
  Accept and Refuse buttons, for staff to decide.
- **Apply automatically**: they go straight into the knowledge base, no review.

A proposal is never made twice for a question you already have or already
refused. The **Detect duplicates** button finds near-identical entries that
crept in and offers to merge them.

Everything the assistant does draws on a daily token budget shown right under
the settings. When it runs out, the assistant simply stops answering until the
next day, and tickets carry on as normal.

Next: [closing, reopening and transcripts](https://flavibot.xyz/docs/modules/tickets/05-closing).

### Closing, reopening and transcripts

Source: https://flavibot.xyz/docs/modules/tickets/05-closing
Summary: Who can close a FlaviBot ticket, what closing does to the channel, reopening and deleting, channels deleted by hand, transcripts, and member feedback.

Closing a ticket is one click on **Close Ticket**, but FlaviBot does a fair amount
behind it. This page is what to expect, and how to configure it.

#### Who can close a ticket?

Staff always can. Staff means a holder of one of the panel's support roles, or,
when the panel has no support role set, anyone with **Manage Server**.

Those two are alternatives, not a stack: as soon as a panel has support roles,
they are the whole check. An administrator who does not hold one is treated
like any other member on this button, and a panel set to **Never** will refuse
them.

For the member who opened the ticket, the panel decides. **User Close
Permission** has three settings:

- **Always**: they can close their own ticket whenever they like.
- **When claimed**: only after a staff member has claimed it.
- **Never**: staff only.

#### What closing does

In every configuration, in this order:

1. The ticket is marked closed and the opener loses access to the channel.
2. The opener gets a direct message telling them their ticket was closed and
   by whom, with the transcript attached if you enabled it.
3. A card lands in your log channel, if you set one, naming the ticket, its
   author and who closed it, plus the transcript if you enabled that.
4. If feedback is on, a second direct message asks the opener to rate the
   support they got.
5. The `ticket_close` event fires for the
   [autoresponder](https://flavibot.xyz/docs/modules/autoresponder/02-triggers).
6. On Platinum servers where the assistant is available, it writes a short
   summary of the ticket and posts it to the log channel.

If the panel charged coins to open and the Economy setting for refunding on
close is on, the coins are given back. The refund matches what was actually
charged for that ticket, so changing the price afterwards cannot over-refund,
and closing then reopening then closing again refunds once.

A member with direct messages closed to your server simply does not get the
notice. Nothing else is affected.

#### What happens to the channel

**Close Behavior** picks between two endings.

**Archive only** (the recommended one) keeps the channel. The opener can no
longer see it, and a card offers staff two buttons: **Reopen Ticket** and
**Delete Ticket**. Nothing is lost until someone decides.

**Delete after delay** posts a card with a countdown and a **Reopen Ticket**
button, then deletes the channel. You set the delay in seconds, up to an hour.
Very small values are raised to five seconds, because the transcript and the
summary are read from the channel after the close and need a moment to finish.

Once the channel is deleted, the messages are gone from Discord. If you want a
record, turn on a transcript before you rely on this mode.

#### Reopening

**Reopen Ticket** gives the opener access back, sets the ticket to Open and
posts a notice in the channel. The conversation carries on where it stopped.

Reopening is not role gated. What limits it in practice is who can still see
the channel, and after a close that is your staff.

#### Deleting

**Delete Ticket** removes the channel permanently. The ticket keeps its page
on the dashboard with its timeline, its notes and its rating, so only the
Discord messages go.

Deleting is also what lets that member open a ticket on the panel again. A
member holds one ticket record per panel, so the next one they open replaces
the one you just deleted, and that older ticket then drops out of the list and
out of the analytics. Archiving instead of deleting keeps the record around,
at the cost of a channel that stays in your category and of the member not
being able to come back through the same panel.

#### When a channel is deleted by hand

If someone deletes a ticket channel in Discord instead of using the buttons,
the bot notices and closes the ticket so your list stays accurate.

That path is silent by design, because there is no channel left to work with:
**no transcript is produced, no closing message and no rating request are
sent**. Use the buttons if you care about any of those.

#### Transcripts

A transcript is a self-contained **HTML file** of the whole conversation, with
avatars, attachments, embeds and resolved mentions. Opened in a browser it
reads like the channel did.

Two switches on the panel, under **Transcript Settings**:

- **Send to User** attaches it to the closing direct message.
- **Send transcript to logs channel** posts it in your log channel.

There is a third way to get one: the ticket's page on the dashboard has a
**Transcript** entry in its actions menu that downloads the file on demand.
The member who opened the ticket can download their own from there too.

One thing to plan around: a transcript is built by reading the channel at the
moment it is asked for. The one generated at close time is captured before the
channel can be deleted, so it is complete. The dashboard download only works
while the channel still exists. Once it has been deleted there is nothing left
to build from, so if you use **Delete after delay**, turn on at least one of
the two switches above.

#### Feedback

Turn on **Enable Feedback** and the closing direct message is followed by a
rating request: five buttons, one to five stars. Only the member who opened
the ticket can answer, and only once.

Per rating value, you choose what happens. Each of these is a set of star
values, so you can treat one star differently from five:

- **Require a written reason**: the member must explain before the rating is
  accepted. Dismissing the form saves nothing at all.
- **Post to the log channel**: a card in your log channel with the score, the
  written reason if there is one, and who handled the ticket. The card is red
  for one or two stars and green for four or five.
- **Offer an external review**: the thank-you message invites them to review
  you elsewhere, with your own text and up to five link buttons.

Ratings that do not require a reason still offer an **Add a comment** button
afterwards, in case the member wants to say more.

**Feedback credited to** decides which staff member the score counts for in
the per-staff statistics: the last one who replied, the one who claimed, or
the one who closed. The credit is frozen when the rating is submitted, so
reassigning a ticket afterwards never moves a bad score onto somebody else.

Next: [the dashboard side](https://flavibot.xyz/docs/modules/tickets/06-dashboard).

### The dashboard side

Source: https://flavibot.xyz/docs/modules/tickets/06-dashboard
Summary: Answer tickets from the browser, read the timeline, and give your support team access without giving away the server.

Everything a ticket does in Discord can also be read and driven from the
FlaviBot dashboard, under **Tickets**. The section lists your panels, and **Open
tickets** takes you to the inbox.

#### The ticket list

Closed tickets are hidden by default and the newest come first, twenty per
page. Three filters narrow it down, and each accepts several values at once:

- **Status**, to see only what is waiting on you
- **Panel**, when you run more than one
- **Label**, which is why labels are worth setting up

**Include Closed** brings the archive back in. The **Create Ticket** button
opens one on a member's behalf.

#### A ticket's page

Click a row and you get the whole ticket in one screen.

**The conversation**, rendered as it looks in Discord: attachments, embeds,
replies, polls, system messages. It updates live while you have it open, so a
new message, an edit or a deletion shows up without refreshing. Scroll up to
load older messages.

**The reply box** sends a message into the channel, up to 2000 characters.
Your reply is posted through a channel webhook, so it carries your name and
your avatar rather than the bot's. Discord marks it with an APP tag, which is
unavoidable: no bot can post as a real account. If the bot is missing **Manage
Webhooks**, the reply still goes out, as a message from the bot that says it
came from you.

You can ping people from that box, but only the actual participants of the
ticket. Roles and `@everyone` are never pinged from the dashboard, whatever
the message contains.

**The sidebar** carries the ticket's identity: status, author, panel, when it
was opened, the reason they gave, who claimed it, who it was escalated to, its
labels (editable while the ticket is open), the members added to it, and on
Platinum the AI summary written when it closed.

**The actions**: **Claim** sits next to the status, and a single menu holds
**Escalate**, **Close**, **Reopen** and **Transcript**. They are the same
actions as the buttons in Discord and they run the same flow, so closing from
the browser still sends the transcript, the direct message and the rating
request.

**Internal notes** is the panel for things the member must not read. It is the
same conversation as the private staff notes thread in Discord: write here and
it appears there, write there and it appears here. Notes are stored, so they
outlive the channel.

**Activity** is the ticket's timeline: opened, first reply by each
participant, claimed, escalated, members and roles added or removed, notes,
status changes, the rating, reopened, closed. Like the notes, it survives the
channel being deleted, which is what makes deleting a ticket channel safe.

If the channel was deleted on Discord, the page says so and the ticket is
closed automatically. Everything except the messages is still there.

#### Analytics

The **Analytics** tab on the Ticketing page is a Silver feature. Without it
you still see the page, filled with a live preview from the demo server, so
you know what you would be getting.

It answers the questions a support lead actually asks: how many tickets and
how many are still open, the resolution rate and the median time to resolve,
satisfaction, how often tickets get claimed and escalated, first response time
and the share of tickets answered within your target. Below the numbers:
tickets over time, status breakdown, rating distribution, busiest hours,
volume per panel and per label, why tickets are opened and what people ask
about (extracted by the assistant), a support team table with a scorecard per
staff member, recent written comments, and backlog health with the oldest
ticket still waiting.

Each panel also has its own **Statistics** tab with the same view scoped to
that panel.

#### How do I give my support team access?

Answering tickets from the dashboard normally requires **Manage Server**,
which is far more than a support volunteer needs.

**Ticket Staff Dashboard Access**, on the Ticketing page, is the way around
that. Turn it on and anyone holding a panel's support role can open the ticket
dashboard and see **only the tickets of the panels their roles cover**. No
server settings, no other module, nothing else. The page gives you the link to
share with them.

The scoping is enforced when the request is made, not by the address, so
sharing the link with someone who has no support role gives them nothing.

### How forms work

Source: https://flavibot.xyz/docs/modules/forms/01-overview
Summary: How FlaviBot forms work: the path a response takes, where you build a form, whether answering needs a Discord account, who can answer and how many you get.

A **form** is a web page FlaviBot hosts for your server: you build it on the
dashboard, you share its link, and the answers come back to your staff in
Discord. This page answers what a form can hold, who is allowed to fill one in,
and how many you get.

Staff applications, feedback rounds, event sign-ups and ban appeals are all the
same object here, only configured differently.

#### The path a response takes

1. You build the form on the dashboard and switch it on.
2. You share its link. It looks like `https://flavibot.xyz/forms/123456789`,
   and the **Copy link** button on the forms list gives you exactly that.
3. Someone opens the link and signs in with Discord.
4. FlaviBot checks whether that person may answer, shows the questions, and
   takes their answers.
5. FlaviBot files the response on your dashboard and, if you named a staff
   channel, posts it there with review buttons on it.
6. A reviewer accepts, denies or dismisses it. Accepting or denying can add
   roles, remove roles and DM the respondent.

#### Where you build one

Everything is on the dashboard, under **Requests > Forms**. That page lists
your forms with their status, their response count, a switch each, and the
**Copy link** action.

There is no slash command for forms, and no module-level switch either: like
Custom Commands and Embed Messages, Forms lights up once you create your first
one. See [modules and
commands](https://flavibot.xyz/docs/getting-started/06-modules-and-commands).

#### Does answering a form need a Discord account?

Yes. The fill page signs the visitor in with Discord before it shows a single
question, even on a form open to everyone. That is what makes the rest
possible: one response per person, the server-membership checks below, the
right to edit your own answer later, and ban appeals tied to a real ban.

It also means the answers you read carry a real Discord user rather than a name
typed into a box, so accepting a response can hand that person a role straight
away.

#### Who can answer

Pick one mode, on the **Access** step of the builder:

| Mode | Who gets in |
| --- | --- |
| Anyone with a Discord account | Everyone signed in, member of your server or not |
| Server members only | Members of the server the form belongs to |
| Members with a required role | Members holding at least one role you list |
| Specific users only | The exact people you list, and nobody else |

Two rules apply on top of whichever mode you picked:

- **Blocked roles** shut out anyone holding one of them, even in a mode that
  would otherwise let them through. The exception is **Anyone with a Discord
  account**: that mode never looks the visitor up in your server, so FlaviBot
  cannot see their roles and the blocked list does nothing.
- **Minimum account age** turns away Discord accounts younger than the number
  of days you set. It reads the account's creation date, not the day they
  joined your server, so an old account that joined yesterday is not treated as
  an alt.

A form set to **Ban appeal** ignores the membership and role layer entirely,
since the person filling it in is banned. See
[responses and ban appeals](https://flavibot.xyz/docs/modules/forms/03-responses-and-appeals).

#### One response, or several

**One response per user** is on by default: a second submission is refused with
"You have already responded to this form."

**Let respondents edit their answer** is the companion switch. With it on,
someone who already answered reopens the link and sees their own answers,
changes them, and saves. Staff get a fresh post titled "Edited response", and
your dashboard keeps one row for that person rather than two.

Answers that were never submitted are kept in the respondent's own browser for
seven days, so closing the tab halfway through a long form costs nothing. If
you edit the form's questions in the meantime, that half-finished draft is
dropped rather than replayed against questions that moved.

#### How many forms you get

The cap counts forms that are **active**. A draft does not take a slot, but you
do have to be under the cap to create a new form at all, and switching a draft
on while you are at the cap is refused with an upgrade prompt.

Switching a live form off is not a way to make room. It stops new answers, but
the form stays active as far as the cap is concerned; only deleting a form
hands its slot back.

| Plan | Active forms |
| --- | --- |
| Free | 3 |
| Silver | 15 |
| Gold | 25 |
| Platinum | 50 |

Reading responses and the per-question statistics is free on every plan.
Premium adds three things: **CSV export** of the responses, **custom branding**
on the fill page, and **reviewer roles** that let people review without giving
them Manage Server.

Next: [building a form](https://flavibot.xyz/docs/modules/forms/02-building-a-form).

### Building a form

Source: https://flavibot.xyz/docs/modules/forms/02-building-a-form
Summary: The five steps of the FlaviBot form builder, the ten question types, showing a question only sometimes, pages, access, scheduling, and publishing.

The FlaviBot form builder is a five-step wizard: **Questions**, **Access**,
**Schedule and limits**, **Notifications and actions**, then **Review**. This
page walks through what each step holds, starting with the questions
themselves.

Nothing is published while you work. The builder keeps your unsaved changes
between visits and offers to restore them, it has undo and redo, and the form
only exists for respondents once you save it and switch it on.

#### What can you ask?

A form holds up to **50 questions**. Every one of them has a **label** (the
question itself), optional **help text** shown underneath, and a **Required**
switch.

The rest depends on the type you pick. There are ten:

| Type | The respondent | What you can set |
| --- | --- | --- |
| Short text | writes one line | minimum and maximum length, a validation pattern with your own error message |
| Long text | writes a paragraph | the same as short text |
| Single choice | picks one option | the options, shuffling them, and a branch per option |
| Checkboxes | ticks any number of options | the options, shuffling, minimum and maximum selections |
| Dropdown | picks one option from a list | the options, shuffling, and a branch per option |
| Rating scale | picks a point on a scale | the lowest and highest value, and a label for each end |
| Number | types a number | the minimum and maximum value |
| Email or URL | types one of the two | which of the two formats to require |
| Date | picks a day | the earliest and latest date allowed |
| Date range | picks a start day and an end day | the earliest and latest date allowed |

Every rule you set is checked twice: in the respondent's browser before they
can move on, and again on FlaviBot's side when the answers arrive, so a rule
cannot be talked out of.

A **validation pattern** is a regular expression the answer has to match. It is
compiled when you save, and a pattern that cannot be compiled is refused there
and then rather than silently rejecting every answer later.

#### How do you show a question only sometimes?

Two rules live on each question, and both read another question's answer:

- **Conditional visibility** shows the question only when that other answer
  matches.
- **Conditionally required** keeps the question visible but makes it required
  only when the other answer matches.

Both are written the same way: pick the question to depend on, pick a
condition — *Equals*, *Does not equal*, *Includes* or *Is one of* — and give
the value.

A question cannot depend on itself, cannot depend on a question that is not in
the form, and a chain of conditions cannot loop back on itself. Saving is
refused in all three cases, and the offending question is named.

#### How do you split a long form into pages?

**Add section break** starts a new page. A section can carry its own title and
description, and it decides where the form goes next under **After this
section**: *Continue to next section*, *Go to section* (any section you name)
or *Submit form*, which ends the form there.

Single choice and dropdown options can each carry a **Go to** of their own, so
the answer itself picks the next page. That branch is only offered on those two
types, since they are the only ones with exactly one answer to branch on.

A form holds up to 50 sections. If you would rather not draw the boundaries
yourself, leave the form as one section and turn on **Split into multiple pages
automatically**, which cuts it every *n* questions (8 by default). With section
breaks in place, they do the paginating and that switch has nothing left to do.

#### Who is allowed to answer?

The **Access** step decides that, and it is where a form is marked as a **ban
appeal** rather than a standard one. Both are covered on the other two pages of
this section: the access modes in [how forms
work](https://flavibot.xyz/docs/modules/forms/01-overview), ban appeals in [responses and ban
appeals](https://flavibot.xyz/docs/modules/forms/03-responses-and-appeals).

#### When does the form open and close?

The **Schedule and limits** step decides that. Its fields are checked when
someone opens the link rather than when you save:

- **Opens at**. Until that moment, whoever opens the link is told the form is
  not open yet.
- **Closes at**. From that moment on, the form is closed and stops taking
  answers.
- **Maximum total responses**. Once that many responses are in, the next person
  is told the form has reached its response limit. Someone editing an answer
  they already gave is not turned away by the cap.

#### Where do the answers turn up?

The **Notifications and actions** step is what connects the form back to
Discord.

- **Post submissions to channel** gives your staff an embed per response with
  the review buttons on it. Forum and media channels work too: pick an existing
  post to keep everything in one thread, or leave the post unset and FlaviBot
  opens a new one per submission.
- **Ping role on submission** mentions one role on that post.
- **Send the respondent a confirmation DM** sends them a receipt. It uses your
  **Confirmation message**, which is also the text shown on the page once they
  submit.
- **Actions on decision** are the roles to add and remove, and the DM to send,
  when a response is accepted or denied. What they do is covered in
  [responses and ban appeals](https://flavibot.xyz/docs/modules/forms/03-responses-and-appeals).
- **Branding** — an accent colour, a header image and a footer for the fill
  page. Premium only.

#### How do you publish a form?

The last step, **Review**, is a summary: the question count, how many are
required and how many are conditional, whether there is a cap or a closing
date, and where submissions will be posted. **Save form** writes it.

A new form is saved as a **draft**, which nobody can fill in yet. It goes live
from the switch on the forms list, and that is also where the active-forms cap
is checked: if you are already at your plan's limit, switching one on is
refused and you are pointed at the upgrade page. Turning the switch back off
closes the form to new answers and keeps everything you have collected — it
does not give the slot back, though: for that, delete the form.

Next: [responses and ban appeals](https://flavibot.xyz/docs/modules/forms/03-responses-and-appeals).

### Responses and ban appeals

Source: https://flavibot.xyz/docs/modules/forms/03-responses-and-appeals
Summary: Where a FlaviBot form response lands, who may review it, what accepting or denying does, the response statistics, and how ban appeal forms differ.

Once someone submits, FlaviBot files the response on your dashboard and, if you
asked it to, posts it into a staff channel with review buttons on it. This page
covers both review surfaces, what a decision actually does, and the one form
type with rules of its own: the ban appeal.

#### Where does a response land?

If the form has a **submit channel**, FlaviBot posts it there: the form's
title, who answered, their answers, and a row of buttons — **Accept**,
**Deny**, **Dismiss** and **View on site**. A ping role, if you set one, is
mentioned on that post. An edited answer arrives as a fresh post titled *Edited
response*; the earlier post stays where it is, while the dashboard keeps a
single row for that person rather than two.

Long answers are shortened on the post, since a Discord message has a size
limit and an essay belongs on the dashboard. **View on site** opens that exact
response with everything in it.

Everything is also on the dashboard, under **Requests > Forms** then **View
responses**, whether or not you configured a channel. That page has two tabs:

- **Responses** — the list, filtered by *All*, *Pending*, *Accepted* or
  *Denied*, 25 at a time. Opening one shows the full answers and the decision
  buttons, with room for a note.
- **Stats** — the same responses as aggregates, described below.

#### Who is allowed to review?

Anyone with **Manage Server**, plus whoever your dashboard access rules already
let in. That is true of both the buttons in Discord and the dashboard page.

On Premium you can also name **reviewer roles** on the form. Someone holding
one can accept, deny or dismiss a response from the buttons on the staff post
without being given Manage Server. That is a Discord-side grant only: the
dashboard's forms pages still ask for Manage Server, so a reviewer role hands
out the buttons, never the dashboard, and never the right to edit or delete the
form itself.

#### What does accepting or denying do?

The decision is recorded against the response with your name, plus your note if
you decided on the dashboard — the buttons in Discord take no note. Deciding
from the buttons also edits the staff post in place, to show the outcome and
who took it; deciding on the dashboard leaves that post exactly as it was,
buttons and all. Then the actions you configured on the form run:

| Decision | What happens |
| --- | --- |
| Accept | adds and removes the roles listed for an accepted response, and sends its DM |
| Deny | adds and removes the roles listed for a denied response, and sends its DM |
| Dismiss | records the decision and nothing else |

Failing to add one role does not abandon the others, and a respondent with DMs
closed to FlaviBot simply does not get the message; neither undoes the
decision.

Deciding twice is possible from the dashboard: reopen the response and change
it. The buttons in Discord do not re-decide — a response someone already
handled answers that it has already been dealt with, so two moderators clicking
at once cannot double up the roles. That is also the answer the still-visible
buttons give on a post whose response was decided on the dashboard.

#### What do the statistics show?

The **Stats** tab is free on every plan. It reads every response and reports,
per question:

- choice, checkbox and dropdown questions — how many times each option was
  picked, in the order you wrote them
- rating scales — how many people picked each point, and the average
- number questions — the smallest, largest and average answer
- date and date range questions — the earliest and latest date given
- text and email or URL questions — how many people answered, and nothing
  more, since there is no meaningful distribution to draw

**Export CSV** on the Responses tab is the Premium counterpart: one row per
response, with its id, the respondent, the status, the submission date and one
column per question.

#### What is a ban appeal form?

A form whose **Form type**, on the builder's Access step, is **Ban appeal**
instead of *Standard*. It is meant for people your server has banned, so it
skips the membership and role checks entirely — a banned member is not a
member — and applies a set of checks of its own instead.

Build it like any other form, then bind it on the dashboard under
**Moderation**, in the **Appeal form** field. From that point the **Appeal**
button in the ban DM points at your form instead of at the plain appeal link,
for both `/ban` and `/tempban`.

Someone opening it sees a **You were banned** banner with the date and the
reason, then your questions. The moderator who issued the ban is never shown.
If the reason is something you would rather not repeat back, **Hide the ban
reason** keeps the banner and drops the reason from it.

##### Who can file an appeal

The checks run in this order, and the first one that fails is what the
appellant is told:

| Check | Blocks the appeal when |
| --- | --- |
| Active ban | they are not actually banned on that server |
| Non-appealable reasons | the ban reason contains one of the words or phrases you listed |
| Appeal blacklist | a reviewer denied and blacklisted them on this server |
| One pending appeal | they already have an appeal waiting for a decision (on by default) |
| Cooldown | their last attempt was less than *n* days ago, whatever its outcome (7 days by default) |
| Waiting period | the ban is more recent than *n* days (0 by default, so no wait) |

##### Deciding an appeal

The staff post carries **Approve**, **Deny** and **Deny + Blacklist**. The
third one denies the appeal and bars that person from filing another appeal on
this server. That blacklist is kept per server rather than per form, so it
covers every ban-appeal form you run, not only the one they used. It is not
permanent: **Blacklisted appellants**, at the bottom of a ban-appeal form's
responses page on the dashboard, lists everyone currently barred and removes
them again. That panel needs Manage Server, since the bar spans every form.

None of the three touches the ban itself. Approving an appeal records your
decision and runs the form's accept actions; lifting the ban is still a
moderator's job, with [`/unban`](https://flavibot.xyz/docs/modules/moderation/02-sanctions). That is
deliberate: an appeal is a request to be looked at, not a self-service unban.

### Temporary voice channels

Source: https://flavibot.xyz/docs/modules/tempvoice/01-overview
Summary: FlaviBot temporary voice channels: a member joins one channel to get a room of their own, deleted once it empties. What happens, and what to set up.

A server with ten static voice channels is either half empty or completely
full, never right. Temporary voice channels fix that: you keep **one** channel
whose only job is to hand out rooms. A member joins it, gets a room of their
own, and that room is deleted the moment the last person leaves it.

Nobody needs a command and nobody needs to ask a moderator for a channel. The
member who created the room owns it, and owning it means they can rename it,
put a cap on it, lock it, hide it, and decide who may come in.

#### The five seconds after someone joins

1. A member joins the **creator channel**.
2. FlaviBot creates a new voice channel named after them, in the category you
   picked.
3. FlaviBot moves them into it.
4. A [control panel](https://flavibot.xyz/docs/modules/tempvoice/03-the-control-panel) is posted in
   that room's built-in text chat, addressed to them as the owner.
5. When the last member leaves the room, FlaviBot deletes it — bots left
   behind on their own do not keep it alive.

If FlaviBot cannot move the member into the room it just built (that needs
**Move Members**), it deletes the room again rather than leave an empty channel
behind that nobody ever entered.

#### What you need to set up

Everything lives on the dashboard, under **Server Management > Temp Voice
Channels**. There are no commands for this module: members drive it entirely
from the panel inside their room.

FlaviBot needs two permissions for the basic flow:

- **Manage Channels**, to create the room and to delete it afterwards.
- **Move Members**, to move the member into the room.

The panel needs to be able to post in the room's text chat, and its buttons
change the room's name and permissions, so a room that FlaviBot can create but
not manage will still refuse individual buttons. Every button that fails says
so to the person who pressed it.

#### Rules worth knowing before you start

- **One room per member per server.** Joining the creator channel while you
  already own a room does not give you a second one, FlaviBot moves you back
  into the room you already have.
- **A room lives as long as someone is in it**, not as long as its owner is in
  it. When the owner leaves and friends stay, the room stays, and anyone still
  inside can [claim it](https://flavibot.xyz/docs/modules/tempvoice/04-ownership-and-claiming).
- **A bot on its own does not count as someone.** A music bot left behind by
  the last human does not keep the room alive, it is disconnected along with
  it. Servers that park a bot in a room on purpose can turn that off per
  creator channel, see
  [rooms left to a bot](https://flavibot.xyz/docs/modules/tempvoice/02-creator-channels#rooms-left-to-a-bot).
- **Rooms are ordinary voice channels.** They count against Discord's limit of
  500 channels per server, and a server sitting at that cap simply stops
  getting new rooms.

#### Where to go next

- [Creator channels](https://flavibot.xyz/docs/modules/tempvoice/02-creator-channels), how to set
  one up and everything you decide about the rooms it makes.
- [The control panel](https://flavibot.xyz/docs/modules/tempvoice/03-the-control-panel), what each
  button does.
- [Ownership and claiming](https://flavibot.xyz/docs/modules/tempvoice/04-ownership-and-claiming),
  who is in charge of a room and how that changes hands.

If you want the bot to *react* to people moving around in voice rather than
give them rooms, that is a different module: the autoresponder has
`voice_join`, `voice_leave` and `voice_move`
[triggers](https://flavibot.xyz/docs/modules/autoresponder/02-triggers).

### Creator channels

Source: https://flavibot.xyz/docs/modules/tempvoice/02-creator-channels
Summary: Set up a FlaviBot creator channel: the settings for the rooms it hands out, the naming template, what a room inherits, running several, and the Health column.

A **creator channel** is a normal voice channel that exists only to be joined
and immediately left. Nobody ever talks in it: joining it is the gesture that
means *give me a room*.

Most servers name theirs something obvious, `➕ Create a room`, and put it at
the top of the voice category so it reads as a button.

#### How do I add a creator channel?

Open **Server Management > Temp Voice Channels** on the dashboard and press
**Add**. The form asks for six things.

**Trigger Voice Channel** (required). The channel members join. Only voice
channels are offered, and a channel can be the trigger for one entry only:
adding a second entry pointing at the same channel is refused.

**Category (optional)**. Where the new rooms are created. Leave it empty and
rooms land in the same category as the creator channel itself, which is what
you want most of the time.

**Channel Name Template** (required). How the room is named. See
[the naming template](#the-naming-template) below.

**User Limit**. How many people may sit in a new room, from 0 to 99, where `0`
means no limit. This is only the starting value, the owner can change it from
the panel afterwards.

**Control panel**. On by default. It decides whether the owner gets a
[control panel](https://flavibot.xyz/docs/modules/tempvoice/03-the-control-panel) posted in their
room, and whether it is drawn as **Buttons** or as a **Dropdown menu**.

**Delete rooms left to bots**. On by default. See
[rooms left to a bot](#rooms-left-to-a-bot) below.

Press **Create Config** and the entry is live immediately, no restart, no
waiting.

#### Rooms left to a bot

A room is deleted when the last person leaves it. **Delete rooms left to bots**
decides whether a bot counts as a person for that.

Leave it on, which is the default, and a music bot, a soundboard, or a recorder
still sitting in the room when the last human walks out does not keep the room
alive: the room goes, and the bot is disconnected with it. This is almost
always what you want, because a bot has no reason to leave on its own. Without
it a room that a bot was invited into outlives everybody, and you end up
cleaning those out by hand.

Turn it off when a bot sitting alone in a room is the point: a 24/7 radio, a
recording that must keep running while people step out, a lobby you want held
open. The room then survives until the bot itself disconnects, and it is
deleted the moment that happens.

Either way, a room with **one real person** in it is never touched, whatever
else is connected alongside them. When FlaviBot cannot establish whether an
occupant is a bot, it treats them as a person and leaves the room alone.

#### The naming template

The template is the room's name, with a few placeholders filled in from the
member who triggered it. Click a placeholder button under the field to insert
it.

| Placeholder | Becomes |
| --- | --- |
| `{username}` | the member's Discord username |
| `{user}` | the same thing, an alias for `{username}` |
| `{displayName}` | their nickname on your server, falling back to their display name, then their username |

The default is `{username}'s Channel`. Anything else in the template is kept
verbatim, so `🎮 {displayName}` and `Room of {username}` both work.

A few limits to keep in mind:

- The template itself is capped at 100 characters, and so is the finished name.
  A long username in a long template gets cut, it never fails.
- A template that resolves to nothing at all falls back to the member's
  username, because Discord refuses an empty channel name.
- Discord rejects some names outright (banned characters, for instance). When that happens no room is created and the entry's Health column reports the rejected channel-create request ("Discord rejected the channel-create payload as malformed"), without naming the template as the cause.

#### What the new room inherits

The room is created inside the category you chose, with **no permission
overrides of its own**. It does not copy the creator channel's permissions, it
simply follows the category it lands in. If you want every room private to a
certain role by default, set that up on the category, not on the creator
channel.

The user limit is applied at creation only when it is 1 or more. With `0` the
room is created without a cap.

#### Running several creator channels

The dashboard lets you add up to **10** entries, and the counter next to the
**Add** button shows how many you have used.

Several entries are how you get several kinds of room from one server: a `Games`
creator with a 5 person cap in the gaming category, a `Chill` creator with no
cap elsewhere, each with its own name template.

What does not change is the one room per member rule. A member who already owns
a room and joins a *different* creator channel is still moved back into the
room they own, they do not collect one of each.

#### Editing an entry

The pencil icon opens the same form again, with one exception: **the trigger
channel cannot be changed**. If you picked the wrong channel, delete the entry
and add a new one.

Edits apply to rooms created **from then on**. Rooms that already exist keep
the name and the limit they were born with, which is the point, their owners
may have changed both by hand already.

#### Deleting an entry

Deleting stops that creator channel from handing out rooms. It does **not**
delete the creator channel itself, which stays as a plain voice channel until
you remove it in Discord.

Be careful about timing. Rooms that are still open when you delete their entry
stop being tracked: FlaviBot will no longer delete them when they empty, and
their control panels answer *"This control panel belongs to a channel that no
longer exists"*. Those rooms become ordinary channels you have to clean up
yourself. Deleting an entry at a quiet hour, when no room from it is open, is
the clean way.

#### The Health column

Each entry has a Health badge in the table, and hovering it explains what it
means.

| Badge | Meaning |
| --- | --- |
| **Healthy** | rooms are being created normally |
| **N recent errors** | the last attempts failed, with the reason and how long ago |
| **Auto-disabled** | 10 attempts failed in a row, so the entry was switched off |

The reason is the HTTP failure Discord returned for the channel-create call: FlaviBot is missing permission to create channels (403), the trigger channel or category no longer exists (404), or Discord rejected the create request (400, which is what a bad name template or a server at the 500-channel cap looks like). A single success clears the counter, so an occasional hiccup
never accumulates.

An auto-disabled entry stays visible with its reason so you know what to fix.
Once the cause is fixed, delete the entry and add it again: a fresh entry starts
clean and begins creating rooms on the next join.

### The control panel

Source: https://flavibot.xyz/docs/modules/tempvoice/03-the-control-panel
Summary: Every button on the control panel FlaviBot posts in a temporary voice room: who may use it, rename, user limit, lock, hide, allow, block and kick.

The moment a room is created, FlaviBot posts a message called **Voice channel
controls** in that room's built-in text chat. It names the owner and carries
every control they have over the room.

The panel belongs to the room, so it needs no ids and no arguments: whatever
you press acts on the channel you are in. When the room is deleted, the panel
goes with it.

You can turn the panel off per creator channel, and choose whether it is drawn
as **Buttons** or as a **Dropdown menu**, on the
[creator channel](https://flavibot.xyz/docs/modules/tempvoice/02-creator-channels) entry. The two
styles offer exactly the same actions.

#### Who can use the control panel?

The **owner** of the room, and anyone whose permissions in that channel include
**Manage Channels**, which in practice means your moderators. Everyone else
gets *"Only the channel owner (or a moderator) can do that"*.

**Claim channel** is the one exception, it has its own rules and is covered in
[ownership and claiming](https://flavibot.xyz/docs/modules/tempvoice/04-ownership-and-claiming).

#### Rename

Opens a small form asking for the new name, up to 100 characters.

Discord itself allows only **two renames per ten minutes** on any channel, so
FlaviBot counts them and refuses the third up front rather than letting you
type a name that cannot be saved. The message says to try again later, and the
budget frees itself as the ten minutes pass.

#### User limit

Opens a form asking for a number between **0 and 99**. `0` removes the cap.
Anything that is not a number in that range is rejected with
*"The limit must be a number between 0 and 99"*.

#### Lock and Unlock

**Lock** takes **Connect** away from `@everyone` in this room. People already
inside stay, nobody new can join, and the room is still visible in the channel
list, which is what makes locking useful: others can see the room is busy.

**Unlock** gives Connect back.

Members you have explicitly allowed keep getting in while the room is locked,
see [Allow member](#allow-member-and-block-member) below.

#### Hide and Show

**Hide** takes **View Channel** away from `@everyone`, so the room disappears
from the channel list for members who have no explicit access to it.

**Show** puts it back.

Lock and Hide are independent and combine cleanly. A room that is both locked
and hidden and then unlocked stays hidden, unlocking does not quietly undo the
hiding.

#### Allow member and Block member

Both open a member picker, and both act on the one member you choose.

**Allow member** grants that member **View Channel** and **Connect** on the
room. It is the counterpart of Lock and Hide: lock the room, then allow the
three friends you actually wanted, and they can walk in while nobody else can.

**Block member** does the opposite, it denies them View Channel and Connect,
and if they are in the room at that moment, they are disconnected on the spot.
A blocked member cannot come back on their own.

You cannot block your way around the owner: picking the owner in **Block member** (or in **Kick member**) is refused with *"That would target the channel owner"*. **Allow member** has no such guard, picking the owner there just grants permissions they already have.

#### Kick member

Disconnects a member from the room. Nothing more: kicking does not stop them
from walking straight back in. Lock the room or block them if you want it to
last.

Kicking only works on someone who is actually in the room, and the owner cannot
be kicked.

#### Transfer ownership and Claim channel

These two change who the room belongs to and are covered on their own page,
[ownership and claiming](https://flavibot.xyz/docs/modules/tempvoice/04-ownership-and-claiming).

#### When a button does not work

Every action answers only to the person who pressed it, so a panel never spams
the room. If an action fails, the answer says *"Something went wrong doing
that. Check my permissions in this channel."*, which is almost always the real
cause: FlaviBot cannot rename a channel or edit its permissions without the
rights to do it there.

Two other answers are worth recognising:

- *"This control panel belongs to a channel that no longer exists"*, the room is
  no longer tracked by FlaviBot. It happens when the room's
  [creator channel entry was deleted](https://flavibot.xyz/docs/modules/tempvoice/02-creator-channels)
  while the room was still open.
- *"Join the channel first to claim it"*, the member the action needs in the room is not in it: either you pressed **Claim channel** from outside the room, or you tried to **Transfer ownership** to somebody who is not there.

### Ownership and claiming

Source: https://flavibot.xyz/docs/modules/tempvoice/04-ownership-and-claiming
Summary: Who owns a FlaviBot temporary voice room, the one-room-per-member rule, transferring ownership, claiming an abandoned room, and what happens when it ends.

Every room has exactly one **owner**: the member who joined the creator channel
and got the room built for them. The owner is named at the top of the
[control panel](https://flavibot.xyz/docs/modules/tempvoice/03-the-control-panel), and the panel
answers to them and to moderators (anyone with **Manage Channels** in that
room).

Ownership is not decoration. It decides who may rename the room, cap it, lock
it, hide it, and throw people out of it.

#### One room at a time

A member owns at most one room per server. Joining the creator channel while
you already own a room does not build a second one, FlaviBot moves you back
into the room you already have, whichever creator channel you joined.

If your room has vanished from Discord in the meantime, for instance a
moderator deleted it by hand, joining the creator channel builds you a fresh
one as normal.

#### Transfer ownership

**Transfer ownership** opens a member picker and hands the room to whoever you
choose. The panel is redrawn with the new owner's name, and from that click on
they are the one the buttons answer to.

The person you pick has to be **in the room**. Handing a room to someone who is
not there would leave it with an owner who is not around to use it, so FlaviBot
refuses and nothing changes.

Transferring gives up your own room, which means the one room rule no longer
holds you: rejoin the creator channel afterwards and you get a brand new room of
your own.

#### Claim channel

Rooms outlive their owners. The owner leaves, three people are still talking,
and the room now belongs to somebody who is not there. **Claim channel** is
how the people still inside take it over.

Claiming succeeds when both of these are true:

- **You are in the room.** Pressing it from anywhere else answers
  *"Join the channel first to claim it"*.
- **The owner is not.** While the owner is still connected the answer is
  *"The owner is still in the channel, you can only claim an abandoned
  channel"*.

There is no waiting period and no vote. The first person inside an abandoned
room who presses the button gets it, the panel is redrawn with their name, and
every other control is theirs from that moment.

Claiming a room you already own does nothing, it just confirms that it is
yours.

#### When the room ends

The room is deleted as soon as the **last** member leaves it, whether or not
the owner is that last member. There is no timer to configure and no grace
period: an empty room is a deleted room.

Everything in it goes with it, including the messages in its text chat and the
control panel itself. If people want to keep something that was posted in a
room, they have to move it out before the room empties.

### What Premium is

Source: https://flavibot.xyz/docs/premium/01-what-premium-is
Summary: What FlaviBot Premium changes: the three levels features sit behind, the two halves of a subscription, and where to find plans and your own subscription.

FlaviBot is free. Every module is available on a free server: music,
moderation, levels, tickets, giveaways, automations. What Premium changes is
**how much of each you get**, plus a set of settings that are visible but
locked until a server has it.

Nothing you already configured stops working because you did not pay. Premium
raises ceilings and opens options; it never takes a free feature away.

#### Three levels

Almost everything sits at one of three levels, and they show up on every
module page, so they are worth learning once.

- **Free.** Works everywhere, always.
- **Vote or Premium.** The command asks you to vote for FlaviBot on top.gg.
  Voting is free and counts for **12 hours**. Holding a subscription on your
  own account, or being in a server that has Premium, skips the ask entirely.
  `/volume`, `/seek`, `/loop`, `/shuffle`, `/filter`, `/lyrics`, `/tts`,
  `/jump`, `/removedupes` and `/mass-role` are in this group.
- **Server Premium.** The server itself needs Premium applied to it. Voting
  does not help here. `/24-7` and `/defaultplaylist` are in this group, and so
  are the dashboard cards that show a lock when you hover them.

The [music overview](https://flavibot.xyz/docs/modules/music/01-overview) uses the same three
words for the same three things.

#### The two halves of a subscription

Buying Premium gives you one subscription, and it does two different jobs.

1. It upgrades **your account**: vote prompts stop, your saved playlists stop
   being capped, and you get a supporter role on the support server.
2. It gives you **premium server slots**, which you spend on the servers you
   want upgraded. A slot is what makes a whole server premium for everybody in
   it.

That split is the single most common source of confusion, so it has its own
page: [user premium and server premium](https://flavibot.xyz/docs/premium/02-user-premium-and-server-premium).

#### Where to look

- **Plans and prices** are on the **Premium** page of flavibot.xyz, with a
  monthly and a yearly option.
- **Your own subscription** lives in the dashboard: switch the selector at the
  top from a server to your account (**User settings**) and open **Premium**.
  That page, titled *Premium Management*, shows your current plan, when it
  renews, which servers you have upgraded, all your subscriptions and your
  payment history.
- **In Discord**, `/premium activate` and `/premium remove` are the two
  commands, and they are covered in
  [activating premium on a server](https://flavibot.xyz/docs/premium/04-activating-premium).

#### Where to go next

- [User premium and server premium](https://flavibot.xyz/docs/premium/02-user-premium-and-server-premium),
  which half does what
- [Silver, Gold and Platinum](https://flavibot.xyz/docs/premium/03-tiers), what each plan adds
- [Activating premium on a server](https://flavibot.xyz/docs/premium/04-activating-premium)
- [Limits by tier](https://flavibot.xyz/docs/premium/06-limits-by-tier), the actual numbers

### User premium and server premium

Source: https://flavibot.xyz/docs/premium/02-user-premium-and-server-premium
Summary: A FlaviBot subscription belongs to your account, and premium on a server is a slot you spend from it. What each half unlocks, and which limits count where.

You buy **one** subscription. It is attached to your Discord account, and it
never upgrades a server by itself. To upgrade a server you spend one of the
**premium server slots** that the subscription gives you.

This is why someone can pay for Platinum and still see a locked card on a
dashboard: the subscription is real, the slot was simply never spent there.

#### What the subscription does for you

These follow your account into every server you are in, whether or not that
server has Premium:

- **Vote prompts stop.** Commands in the *vote or Premium* group run for you
  without asking you to vote.
- **Saved playlists.** 3 on a free account, unlimited from Silver upward. See
  [playlists](https://flavibot.xyz/docs/modules/music/10-playlists).
- **Premium server slots**: 2 on Silver, 4 on Gold, 6 on Platinum.
- **Custom bot slots**: 1 on Platinum, and you can buy more as an extra. See
  [setting up a custom bot](https://flavibot.xyz/docs/getting-started/07-custom-bot).
- **A supporter role** on the FlaviBot support server.

#### What a slot does for a server

Spending a slot on a server upgrades that server for **everyone in it**. The
members do not need their own subscription, and they do not need to vote.

A server with Premium gets, among other things:

- The music settings free servers cannot save at all: 24/7 mode, a default
  volume, default songs, a default playlist, a default autoplay mode, a default
  queue loop, auto shuffle, a shorter inactive disconnect timeout, a custom
  controller image and footer, and Smart Bot Routing. Filters are not one of
  these: `/filter` sits in the vote-or-Premium group, so it works on a free
  server for a member who voted, and premium simply removes the prompt.
- Much higher caps everywhere: queue length, autoresponders, ticket panels,
  embed messages, level role rewards, counting channels, server log channels,
  and the rest of [the table](https://flavibot.xyz/docs/premium/06-limits-by-tier).
- Options that are simply absent on a free server: invite alerts, the advanced
  giveaway settings, form branding and CSV export, per-role XP multipliers, a
  custom XP rate.
- Server Stats beyond the last 14 days.

#### The server's plan is the buyer's plan

A server does not have a plan of its own. Its level is read live from the
subscription of the person who activated it.

Two consequences worth knowing:

- If you upgrade from Silver to Gold, **every server you activated becomes Gold
  immediately**. You do not re-activate anything.
- If your subscription ends, those servers stop being premium, and their
  entries are cleared from your list. Subscribing again means activating them
  again.

#### One activation per server

A server holds a single activation, made by a single person. If someone else
already activated it, your own attempt answers *This server is already
premium*, and the person who did it is the only one who can remove it. There
is no way to stack two subscriptions on one server to get a higher tier.

#### Which is which

| Thing | Counted against |
| --- | --- |
| Vote prompts disappearing | your account |
| Saved playlists | your account |
| Premium server slots | your account |
| Custom bot slots | your account |
| Filters and the other vote-gated commands | either one, whichever you have |
| Songs allowed in the queue | the server |
| 24/7, default volume, default songs | the server |
| Autoresponders, tickets, forms, giveaways, logs | the server |
| Level rewards, card templates, XP rate | the server |
| Server Stats history | the server |

### Silver, Gold and Platinum

Source: https://flavibot.xyz/docs/premium/03-tiers
Summary: The three FlaviBot Premium plans on sale, Silver, Gold and Platinum, what each one adds over the plan below it, extra custom bots, and the retired Bronze tier.

Three plans are sold, each available monthly or yearly on the **Premium** page
of flavibot.xyz. Free is the baseline everyone starts from. Every plan
includes everything in the plan below it.

The exact numbers are on [limits by tier](https://flavibot.xyz/docs/premium/06-limits-by-tier);
this page is the shape of each plan.

#### Free

The whole bot, with small caps. Concretely: 3 autoresponders, 1 ticket panel,
1 starboard panel, 1 suggestion panel, 3 embed messages, 3 level role rewards,
3 card templates, 1 counting channel, 1 server log channel, 3 active forms, a
200 song queue, and Server Stats limited to the last 14 days.

Free servers cannot use 24/7 mode, default volume, default songs, a default
playlist, default autoplay, a default queue loop, auto shuffle, a shorter
inactive disconnect timeout, a custom controller image, Smart Bot Routing,
invite alerts, or the advanced giveaway options. Filters sit one level lower:
they work on a free server for a member who voted, and Silver removes the vote
prompt.

#### Silver

Silver is the jump from "capped" to "not thinking about caps".

- **2 premium servers.**
- **Music**: 24/7 mode, default volume, default songs, a default playlist,
  default autoplay, a default queue loop, auto shuffle, a shorter inactive
  disconnect timeout, a custom controller image and footer, a queue of up to
  100,000 songs, imported playlists read up to 10,000 songs, unlimited DJ
  permission entries, and unlimited saved playlists on your account. Filters and
  the other vote-gated commands stop asking anyone in the server to vote.
- **Smart Bot Routing**: with several FlaviBot accounts in the server, a music
  command is handled by the bot already in your voice channel, or by a free one
  if none is. It comes with a shared music prefix so every bot answers the same
  one.
- **Server Stats** with its full history instead of the last 14 days.
- **Levels**: a custom XP rate, per-role XP multipliers, and far more role
  rewards. See [levels and role rewards](https://flavibot.xyz/docs/modules/leveling/03-levels-and-role-rewards).
- **Invite tracker**: invite alert rules, the analytics graphs and the weekly
  leaderboard.
- **Giveaways**: advanced requirements, bonus entries, prize tiers and a claim
  window, scheduled and recurring giveaways, templates, and analytics. See
  [giveaways](https://flavibot.xyz/docs/modules/engagement/05-giveaways).
- **Forms**: CSV export of responses, custom branding, and reviewers chosen by
  role.
- **Autoresponders** may trigger on messages and reactions from *other bots*,
  which free rules ignore on purpose. See
  [the autoresponder overview](https://flavibot.xyz/docs/modules/autoresponder/01-overview).

#### Gold

Everything in Silver, plus:

- **4 premium servers.**
- **Server Bot Profile**: give the official FlaviBot a nickname, an avatar, a
  banner and an embed colour that apply to your server only. The page is
  **Bot Profile** in the dashboard sidebar.
- Higher caps across the board: 25 level role rewards instead of 10, 25
  autoresponders instead of 15, 5 ticket panels instead of 3, 10 server log
  channels instead of 5, and so on.

#### Platinum

Everything in Gold, plus:

- **6 premium servers.**
- **One custom bot**: your own Discord application, with your name, avatar and
  status, running FlaviBot's features. The full walkthrough is
  [setting up a custom bot](https://flavibot.xyz/docs/getting-started/07-custom-bot).
- **A daily AI budget for tickets**: 200,000 tokens per server per day for the
  ticket assistant's first reply. That assistant is still being rolled out, so
  it is not available on every server yet.
- **Unlimited giveaways and giveaway templates**, and an economy ledger with no
  history cut-off.
- The highest caps everywhere else: 50 autoresponders, 50 level role rewards,
  20 ticket panels, 500 embed messages, 50 card templates.

#### Extras

On top of a Platinum subscription you can buy **extra custom bots**, one
subscription line whose quantity you change from the **Premium** page. Extras
require a Platinum subscription taken through Stripe; they are not available
through the other payment provider.

Adding one charges the prorated amount immediately. Removing one puts a credit
on your next invoice. Changing plan to something that cannot keep an extra
asks you to confirm before removing it.

#### About Bronze

An older **Bronze** tier still exists for the people who bought it, and it
still shows in their dashboard. It is no longer sold and is not part of the
plans above.

### Activating premium on a server

Source: https://flavibot.xyz/docs/premium/04-activating-premium
Summary: Buying FlaviBot Premium does not upgrade a server by itself. Activate it from Discord or the dashboard, check it worked, read the refusals, and move it.

Buying a FlaviBot subscription gives you slots. Activating spends one of them on a
server. Until you activate, the server is still a free server.

#### The two ways

**From Discord.** In the server you want to upgrade, run:

```
/premium activate
```

The bot answers `Premium (Gold) activated on this server!` with your own plan
name. To undo it later, `/premium remove` in the same server.

**From the dashboard.** Open that server's dashboard and hover any card
showing a lock, for example **Smart Bot Routing** on the Music page or **Bot
Profile** in the sidebar. The overlay has an **Activate Premium** button when your current plan already covers that card, an **Upgrade to <plan>** link when the card needs a higher tier than you hold (Bot Profile, for instance, needs Gold), and a link to the plans when you have no subscription at all.

Both do exactly the same thing, and the effect is immediate. Right after a
purchase, give the subscription itself up to a minute to register before you
activate.

#### Who can activate premium on a server?

Anyone holding an active subscription with a free slot. You do **not** need
Manage Server, and you do not need to be the server owner: you are spending
your own slot, not the server's.

Two things that trip people up:

- It is the **Discord account you are using** that matters, not the server. If
  you subscribed with one account and run the command from another, the bot
  says you have no plan.
- The person who activates keeps control of the activation. Nothing removes it
  when they leave the server, and nobody else can remove it for them.

#### How do I check premium is active on a server?

The server appears under **Your Premium Servers** on your **Premium** page in
the dashboard (switch the top selector to your account), with the date you
activated it. The locked cards on that server's dashboard unlock at the same
time.

#### When it refuses

| What you see | What it means |
| --- | --- |
| *You do not have a premium server plan* | the account you are signed in as has no active subscription |
| *You are restricted to 2/2 premium server* | every slot is spent; remove one, or move up a plan |
| *This server is already premium* | an activation already exists here, possibly someone else's |
| *This server is not premium* | on `/premium remove`, there is nothing to remove |
| *The premium has not been activated by you* | someone else activated this server; the reply names them |

The dashboard shows the same three refusals as toasts: *You don't have an
active premium subscription*, *You've reached your premium server limit*, and
*This server already has premium active*.

#### How do I move premium to another server?

There is no move button, and there does not need to be one. Remove the
activation from the old server, then activate on the new one. The slot is free
again the moment you remove it, so the two steps take seconds.

Removing is covered, along with everything else that changes a subscription,
in [changing and cancelling](https://flavibot.xyz/docs/premium/05-changing-and-cancelling).

### Changing and cancelling

Source: https://flavibot.xyz/docs/premium/05-changing-and-cancelling
Summary: Take FlaviBot Premium off a server, change plan, deal with a failed payment, cancel your subscription, and see what happens when the last one ends.

Everything on this page happens on the **Premium** page of your FlaviBot dashboard
(switch the top selector from a server to your account) or on the **Premium**
page of flavibot.xyz.

#### How do I take premium off a server?

Two ways, same result:

- `/premium remove` in that server. Only the person who activated it can do
  this; anyone else is told who did, and asked to take it up with them.
- The **Remove** button next to the server in the *Your Premium Servers* table
  on your Premium page. The confirmation is blunt on purpose: the server loses
  every premium feature immediately.

The slot is free again straight away, so this is also how you move premium
from one server to another.

#### How do I change my Premium plan?

Pick the plan you want on the **Premium** page of the site. If you already have a **Stripe** subscription, it is modified in place rather than started again; a Tebex subscription cannot be changed in place, so picking a plan there starts a new subscription and the old one has to be cancelled on Tebex.

- **Upgrading** charges the prorated difference right away, and the new plan
  only takes effect once that payment goes through. A declined card leaves you
  on your current plan.
- **Downgrading** applies straight away, with no charge that day. The unused
  time on the old plan becomes a credit on your next invoice. Nothing goes back
  to the card; the payment provider does not refund a downgrade.
- **Switching monthly to yearly** is charged like an upgrade. Going the other
  way is a credit, like a downgrade. Either way the renewal date is reset so it
  reflects the new period.
- If the new plan cannot keep an extra you bought, you are told how many extras
  will be removed and asked to confirm before anything changes.

Downgrading below the slot count you are using does not pick servers for you.
Sort out which servers you want to keep before you drop a plan.

#### When a payment fails

A renewal that fails puts your subscription into payment recovery. You will
see a banner on every dashboard page, a card on the Premium page, and you get
a direct message with a button that takes you straight to the payment.

While that lasts, plan changes and extras are blocked: *Settle your pending
payment before changing your plan*. Pay the open invoice or update your payment
method, and everything unblocks. Ignore it and the subscription is eventually
cancelled by the provider.

#### How do I cancel my Premium subscription?

Use the **Manage** button on your Premium page. Where it takes you depends on
where you bought:

| Bought through | Manage takes you to |
| --- | --- |
| Stripe | Stripe's billing portal, where you cancel and manage your payment method |
| Tebex | Tebex's recurring payments page |

After you cancel, the subscription shows as **ending** and everything keeps
working until the end of the period you already paid for. There is no partial
month.

A subscription that arrived through Discord itself, or one granted by the team,
shows in the table with its own provider and no Manage button. Ask on the
support server if you need one of those changed.

Refunds are handled by hand: if something is wrong, contact the team on the
support server within 14 days of the purchase.

#### What happens when it ends

When your last active subscription ends:

- Every server you had activated stops being premium, and the entries disappear
  from your list. Subscribing again means running `/premium activate` again.
- Premium-only behaviour stops. 24/7 mode no longer holds the voice connection,
  and the locked cards go back to being locked.
- A **Server Bot Profile** is reset: the nickname, avatar and banner set for
  that server are removed from Discord and the profile is deleted.
- Custom bots you are no longer entitled to are switched off, most recently
  created first, and disconnected from Discord.

What you configured is not wiped. Autoresponders, panels, templates and levels
settings stay where they are; you simply cannot go on adding beyond the free
caps until you subscribe again.

### Limits by tier

Source: https://flavibot.xyz/docs/premium/06-limits-by-tier
Summary: Every FlaviBot Premium limit per plan in one place, from music, levels and economy to automation and engagement, plus the features that are simply on or off.

The reference table for FlaviBot Premium. Bronze is left out because it is no longer sold; see
[Silver, Gold and Platinum](https://flavibot.xyz/docs/premium/03-tiers) for what each plan is for.

Remember which column applies to what: the *saved playlists*, *premium servers* and *custom bots included* rows count against your account; everything else counts against the server the limit is being checked on. That split is explained in
[user premium and server premium](https://flavibot.xyz/docs/premium/02-user-premium-and-server-premium).

#### Servers and bots

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Premium servers | 0 | 2 | 4 | 6 |
| Custom bots included | 0 | 0 | 0 | 1 |

#### Music

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Songs in the queue | 200 | 100,000 | 100,000 | 100,000 |
| Songs read from an imported playlist | 500 | 10,000 | 10,000 | 10,000 |
| Saved playlists (your account) | 3 | Unlimited | Unlimited | Unlimited |
| Server playlists | 2 | Unlimited | Unlimited | Unlimited |
| DJ permission entries | 2 | Unlimited | Unlimited | Unlimited |

#### Levels and cards

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Level role rewards | 3 | 10 | 25 | 50 |
| Per-role XP multipliers | 0 | 3 | 10 | 25 |
| Custom card templates | 3 | 10 | 20 | 50 |
| Layers per card template | 15 | 25 | 35 | 50 |
| Rank card role overrides | 1 | 5 | 10 | 25 |

#### Economy

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Shop items | 5 | 15 | 30 | 100 |
| Shop categories | 2 | 5 | 10 | 25 |
| Earning rules | 2 | 5 | 10 | 20 |
| Sales running at once | 1 | 3 | 5 | 10 |
| Custom replies per earning rule | 3 | 10 | 25 | 50 |
| Market listings per member | 2 | 5 | 10 | 25 |
| Ledger history on the dashboard | 30 days | 90 days | 365 days | Unlimited |

#### Automation and utilities

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Autoresponders | 3 | 15 | 25 | 50 |
| Installed presets with repeatable rows | 3 | 10 | 25 | Unlimited |
| Rows per repeatable block in a preset | 10 | 25 | 25 | 25 |
| Data store variable names | 25 | 150 | 400 | 1,000 |
| Shortest interval for a scheduled autoresponder | 1 hour | 10 minutes | 5 minutes | 1 minute |
| Automated messages | 2 | 10 | 15 | 25 |
| Shortest interval for an automated message | 1 hour | 10 minutes | 5 minutes | 1 minute |
| Embed messages | 3 | 10 | 15 | 500 |
| Sticky messages per channel | 1 | 5 | 10 | 25 |
| Counting channels | 1 | 10 | 25 | 50 |
| Channels in a mode list | 20 | 500 | 500 | 500 |
| Server log channels | 1 | 5 | 10 | 15 |
| Active forms | 3 | 15 | 25 | 50 |
| Ticket panels | 1 | 3 | 5 | 20 |
| Starboard panels | 1 | 3 | 5 | 20 |
| Suggestion panels | 1 | 3 | 5 | 20 |

#### Engagement

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Giveaways running at once | 5 | 25 | 50 | Unlimited |
| Saved giveaway templates | 0 | 5 | 15 | Unlimited |
| Bonus-entry role rules per giveaway | 0 | 10 | 20 | 30 |
| Invite alert rules | 0 | 3 | 10 | 25 |
| Active events | 3 | 15 | 50 | Unlimited |

#### Projects and tasks

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Projects | 1 | 10 | 25 | Unlimited |
| Issues per project | 100 | 2,000 | 10,000 | Unlimited |

Projects counts the **active** ones: archiving a project frees its slot and
keeps its issues. Issues are counted per project, closed ones included. See
[how projects and tasks work](https://flavibot.xyz/docs/modules/tasks/01-overview#limits).

#### Elsewhere

| | Free | Silver | Gold | Platinum |
| --- | --- | --- | --- | --- |
| Server Stats history | 14 days | Full | Full | Full |
| AI tokens per day for tickets | 0 | 0 | 0 | 200,000 |

#### On or off

These are not numbers, they are switches. Everything below is off on a free
server and on from **Silver** upward, except the last two.

- The music settings a free server cannot save at all: **24/7 mode**, **Default
  Volume**, **Default Songs**, **Default Playlist**, **Default Auto Play**,
  **Default Loop Queue**, **Auto Shuffle**, a shorter **Inactive Disconnect
  Timeout**, and the **Controller Customization** image and footer
- Smart Bot Routing
- A custom XP rate
- Invite analytics and the weekly invite leaderboard
- Giveaway bonus entries, advanced requirements, prize tiers and claim window,
  scheduling and recurring giveaways, saved templates, analytics
- Form branding, reviewers chosen by role, CSV export of responses
- Autoresponders triggering on other bots' messages
- **Server Bot Profile**: Gold and Platinum only
- **A custom bot**: Platinum only

**Audio filters are not in that list.** They are not a server switch you turn on:
`/filter` is one of the *vote or Premium* commands, so it works on a free server
for a member who has voted, and a premium server removes the prompt for
everyone. The full vote-or-Premium set in Music is `/filter`, `/volume`,
`/loop`, `/shuffle`, `/jump`, `/seek`, `/fastforward`, `/rewind`,
`/removedupes`, `/lyrics`, `/tts` and `/announcechannel set|reset`. See [audio
filters](https://flavibot.xyz/docs/modules/music/05-filters).

#### One exception

A subscription can carry a raise granted by the team, usually extra premium
servers or extra custom bots. When that applies to you, the number your
dashboard shows is the real one, not the one in these tables.
