Variables
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 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, orgroup(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.
