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:

TriggerAdds
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

WriteWhat it doesExample
:uppercasePuts the whole value in capitals.ada -> ADA
:lowercasePuts the whole value in lower case.Ada -> ada
:titleCapitalises the first letter of every word.ada lovelace -> Ada Lovelace
:trimRemoves 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

WriteWhat it doesExample
:numberAdds thousands separators so a big number stays readable.1000000 -> 1,000,000
:roundRounds a decimal to the nearest whole number.12.7 -> 13

Dates and durations

WriteWhat it doesExample
:durationTurns a number of seconds into words.300 -> 5 minutes
:from_nowTurns a number of seconds into a live Discord countdown that keeps ticking after the message is posted.300 -> in 5 minutes
:relativeShows a date as how long ago or how far ahead it is, in the reader's own language.a timestamp -> 3 days ago
:datetimeShows a date with its time, in each reader's own timezone.a timestamp -> 1 January 2026 00:00
:dateShows a date without its time, in each reader's own timezone.a timestamp -> 1 January 2026
:timeShows 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.

WriteWhat it doesExample
:joinWrites 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
:countHow 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:

WriteWhat 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

WriteWhat it doesExample
:escapeCancels 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.

Created on · Last updated on