Automation Templates
Template variables let you weave live data — the viewer's name, the current song, the stream title, a custom variable you set — into an automation action or a bot command response. You wrap a variable name in curly braces, and Lumio replaces it with the matching value at runtime.
Lumio has two template surfaces, and they use different brace styles and different variable sets:
| Surface | Braces | Variable set |
|---|---|---|
| Automation actions (the automation builder) | double — \{\{displayName\}\} | the large reference below |
| Bot command responses (Response Type Template) | single — \{user\} | seven built-ins, see Bot command variables |
Most of this guide covers the automation engine. If you are editing a !command response, jump straight to Bot command variables.
Who needs this guide?
- Anyone building automation actions that send chat messages, Discord posts, or overlay alerts.
- Anyone writing a bot command whose response should include the caller's name or the current song.
- Anyone debugging a template that resolves to an empty string or leaks a literal
\{\{name\}\}into chat.
Where Templates Are Used
The double-brace engine resolves variables in these automation action fields:
- Chat Message — the message body.
- Discord Post — the message body.
- Overlay Alert — the alert message.
- Webhook Call — the URL, every header value, and the request body.
- Set Variable — the value you assign.
Bot command responses are handled by the separate single-brace engine described below. Timed messages (the Timed Messages bot module) send their configured lines verbatim — they do not resolve variables.
Syntax: \{\{variable\}\}
Variables are wrapped in double curly braces:
Hello {{displayName}}, welcome to the stream!
Rules:
- The name is letters, digits and underscores —
camelCaseis the convention. - Dotted paths are allowed:
\{\{event.type\}\},\{\{discord.serverName\}\},\{\{var.myCustomVar\}\},\{\{event.raw.someField\}\}. - Single curly braces (
\{name\}) are left untouched by the automation engine. In a bot command response, single braces are exactly what you want — see Bot command variables. - Replacement is done left-to-right in one pass. The resolved value is not re-parsed, so a variable whose value happens to contain
\{\{otherVar\}\}will appear literally.
Quick Start
| Template | Typical output |
|---|---|
Welcome {{displayName}}! | Welcome Rapha! |
{{username}} cheered {{cheerAmount}} bits! | rapha cheered 500 bits! |
Now playing: {{songName}} by {{songArtist}} | Now playing: Blinding Lights by The Weeknd |
Stream: {{streamTitle}} ({{category}}) | Stream: Valorant Ranked (Valorant) |
Uptime: {{uptime}} / {{liveViewerCount}} viewers | Uptime: 2h 30m / 1234 viewers |
Shout out to {{var.lastRaider}}! | Shout out to someStreamer! |
Order #{{orderNumber}} — {{orderTotal}} {{orderCurrency}} | Order #1082 — 42.00 EUR |
Resolution Rules
There are three cases, and knowing which applies saves a lot of debugging time.
- Known variable, data present → replaced with the value.
- Known variable from a permission-scoped group, data missing → replaced with an empty string. This covers every variable listed under
chat:userinfo,events:readandspotify:readbelow. The rest of the message still renders, which is often why you see weird double-spaces (" "where you expected a value). - Everything else that does not resolve → left as-is, braces and all. That includes typos (
Hello {{unknwn}}!staysHello {{unknwn}}!) and the ungated groups: a\{\{channelName\}\},\{\{botName\}\},\{\{date\}\},\{\{sceneName\}\},\{\{orderNumber\}\},\{\{discord.serverName\}\}or\{\{var.something\}\}with no value behind it stays visible in the output rather than vanishing.
Rule 3 is worth internalising: an unset custom variable does not disappear — \{\{var.lastRaider\}\} will be posted to chat literally until something has written a value into lastRaider.
The empty-string rule for the gated groups is what lets you write defensive templates like:
Currently playing {{songName}}{{songArtist}}
If Spotify isn't connected, nothing renders for those variables. If it is, you get Blinding LightsThe Weeknd (you'll want to add a separator — see Tips).
Permission Scopes
Every variable belongs to a group, and each group is associated with a permission. The groups matter for two reasons: they tell you which part of Lumio a variable's data comes from, and they decide whether a variable that fails to resolve becomes an empty string (gated groups) or stays literal in the output (ungated groups — see rule 3 above).
| Variable group | Associated permission |
|---|---|
Channel, Bot, Time, Shopify, OBS, Discord, Custom (var.*) | none |
User (username, displayName, avatarUrl, userId) | chat:userinfo |
| Stream, Channel Status, Follower, Sub, Cheer, Raid, Redemption, Tip, Hype Train, Poll, Prediction, Event metadata | events:read |
Spotify (songName, songArtist, …) | spotify:read |
Automations run under the account that owns them, not under the viewer who triggered them, so a variable's value never depends on who typed in chat.
Variable Reference
Ungated groups (no associated permission)
Channel
| Variable | Description |
|---|---|
\{\{channelName\}\} | Display name of the channel. |
\{\{channelUrl\}\} | URL to the channel page. |
\{\{platform\}\} | Platform slug (twitch, youtube, kick, trovo). |
Bot
| Variable | Description |
|---|---|
\{\{botName\}\} | Bot account's username. |
\{\{commandPrefix\}\} | Current command prefix (for example !). |
Time
| Variable | Description |
|---|---|
\{\{date\}\} | Current date in the configured timezone. |
\{\{time\}\} | Current time. |
\{\{timestamp\}\} | Unix timestamp. |
\{\{timezone\}\} | Configured timezone name. |
Shopify (from Shopify order events)
| Variable | Description |
|---|---|
\{\{orderNumber\}\} | Order number. |
\{\{orderTotal\}\} | Order total. |
\{\{orderCurrency\}\} | ISO currency code. |
\{\{productName\}\} | Product name from the order. |
OBS (from the OBS integration)
| Variable | Description |
|---|---|
\{\{sceneName\}\} | Current scene. |
\{\{bitrate\}\} | Current streaming bitrate. |
\{\{fps\}\} | Current frames per second. |
\{\{droppedFrames\}\} | Dropped frames counter. |
Discord (dotted paths)
| Variable | Description |
|---|---|
\{\{discord.serverName\}\} | Discord server display name. |
\{\{discord.channelName\}\} | Discord channel name. |
\{\{discord.memberCount\}\} | Member count at event time. |
Custom variables (var.*)
Values written by a Set Variable action, or captured by the automation's trigger. A Keyword trigger with capture groups exposes them here, and a Command trigger fills \{\{var.command\}\} with the command name and \{\{var.args\}\} with everything the caller typed after it. There is no hard list — any name under var.* looks up the matching entry.
| Variable | Description |
|---|---|
\{\{var.anyName\}\} | Value of the named custom variable. If nothing has set it, the placeholder is left in the output unchanged. |
User group — chat:userinfo
Context: info about the user who triggered the event (the viewer who followed, subscribed, sent the bits, ran the command, etc.). When the triggering event carries no user, these resolve to empty strings.
| Variable | Description |
|---|---|
\{\{username\}\} | Login / handle. |
\{\{displayName\}\} | Display name (may include capitalization / unicode). |
\{\{avatarUrl\}\} | Profile picture URL. |
\{\{userId\}\} | Platform user ID. |
Event group — events:read
Stream snapshot:
| Variable | Description |
|---|---|
\{\{streamTitle\}\} | Current stream title. |
\{\{category\}\} | Current category/game. |
\{\{gameName\}\} | Legacy alias for \{\{category\}\} — kept for backward compatibility. |
\{\{viewerCount\}\} | Viewer count at event time. |
\{\{uptime\}\} | Stream uptime. |
\{\{streamUrl\}\} | Link to the live stream. |
\{\{streamPreviewUrl\}\} | URL of the preview thumbnail image. |
Channel-status (multi-platform live state):
| Variable | Description |
|---|---|
\{\{isLive\}\} | true / false. |
\{\{liveViewerCount\}\}, \{\{liveCategory\}\}, \{\{liveTitle\}\}, \{\{livePlatform\}\} | Current live data aggregated across connected platforms. |
Follower:
| Variable | Description |
|---|---|
\{\{followerName\}\} | Display name of the new follower (alias for \{\{displayName\}\} scoped to follower events). |
Subscription:
| Variable | Description |
|---|---|
\{\{subTier\}\} | Tier 1, Tier 2, Tier 3, or the platform equivalent. |
\{\{subMonths\}\} | Cumulative subscription months. |
\{\{isGift\}\} | true / false. |
\{\{giftRecipient\}\} | Display name of the gift-sub recipient (only when isGift=true). |
Cheer / bits:
| Variable | Description |
|---|---|
\{\{cheerAmount\}\} | Bits cheered. |
\{\{cheerMessage\}\} | Message sent with the cheer. |
Raid:
| Variable | Description |
|---|---|
\{\{raidViewers\}\} | Number of incoming raiders. |
\{\{raidFrom\}\} | Name of the raiding channel. |
Channel-point redemption:
| Variable | Description |
|---|---|
\{\{rewardName\}\} | Reward title. |
\{\{rewardCost\}\} | Channel-point cost. |
\{\{rewardInput\}\} | Viewer-supplied text, if the reward requires input. |
Tip / donation:
| Variable | Description |
|---|---|
\{\{tipAmount\}\} | Numeric amount. |
\{\{tipCurrency\}\} | ISO currency code. |
\{\{tipMessage\}\} | Viewer's tip message. |
Hype train:
| Variable | Description |
|---|---|
\{\{hypeTrainLevel\}\} | Current level. |
\{\{hypeTrainTotal\}\} | Total contributions so far. |
\{\{hypeTrainProgress\}\} | Progress toward the next level. |
Poll / prediction:
| Variable | Description |
|---|---|
\{\{pollTitle\}\}, \{\{pollChoices\}\}, \{\{pollWinner\}\} | Live poll state. |
\{\{predictionTitle\}\}, \{\{predictionOutcomes\}\}, \{\{predictionWinner\}\} | Live prediction state. |
Event metadata (dotted paths):
| Variable | Description |
|---|---|
\{\{event.type\}\} | Event type, e.g. twitch:follower, youtube:superchat. |
\{\{event.platform\}\} | Source platform slug. |
\{\{event.raw.<field>\}\} | Any raw field of the source event — useful for platform-specific data not yet surfaced as a first-class variable. |
Spotify group — spotify:read
| Variable | Description |
|---|---|
\{\{songName\}\} | Current track title. |
\{\{songTitle\}\} | Legacy alias for \{\{songName\}\}. |
\{\{songArtist\}\} | Track artist(s). |
\{\{songAlbum\}\} | Album name. |
\{\{songUrl\}\} | Spotify track URL. |
\{\{songAlbumArt\}\} | Album art image URL. |
\{\{albumArt\}\} | Legacy alias for \{\{songAlbumArt\}\}. |
\{\{songDuration\}\} | Track duration. |
\{\{songProgress\}\} | Current playback position. |
\{\{isPlaying\}\} | true / false. |
\{\{shuffleState\}\} | true / false. |
\{\{repeatState\}\} | off, track, or context. |
\{\{spotifyDevice\}\} | Active playback device name. |
\{\{spotifyPlaylist\}\} | Active playlist name, if any. |
Spotify must actually be connected and playing for these to have values.
Bot command variables (single braces)
Bot command responses do not use the double-brace engine. In the command editor, set Response Type to Template and write your Response Text with single braces. The editor shows the available names inline as a hint.
| Variable | Description |
|---|---|
\{user\} | Display name of the viewer who ran the command. |
\{channel\} | Name of the channel the command was used in. |
\{platform\} | Platform the command came from, e.g. twitch. |
\{count\} | How many times this command has been used. |
\{uptime\} | Stream uptime, e.g. 2h 30m. Empty when the stream is offline. |
\{song\} | Currently playing Spotify track. Requires the Spotify Now Playing feature on your plan and a connected Spotify account. |
\{random:a,b,c\} | Picks one item at random from the comma-separated list. |
Anything else in single braces — a typo, or a name that is not in this list — is replaced with an empty string, so a mistyped variable silently disappears from the message rather than showing up literally.
If you need any of the richer variables from the reference above in a chat command, set Response Type to Automation instead and point the command at an automation whose Chat Message action carries the double-brace template.
Recipes
Welcome message with username
Bot command, Response Type Template:
Welcome to the stream, {user}! Enjoy your stay :)
The equivalent inside an automation's Chat Message action:
Welcome to the stream, {{displayName}}! Enjoy your stay :)
Shoutout template
Go check out {{var.lastRaider}} at https://twitch.tv/{{var.lastRaider}} — they raided us with {{raidViewers}} viewers!
\{\{var.lastRaider\}\} is whatever value you stored in the custom variable lastRaider — typically via a Set Variable action earlier in the same automation, on a raid trigger. Until something writes it, the placeholder stays visible in the message.
Now-playing announcement
In an automation's Chat Message action:
🎵 Now playing: {{songName}} — {{songArtist}} [{{spotifyDevice}}]
When Spotify is paused or disconnected, the fields resolve to empty strings, producing 🎵 Now playing: — [] — so use a Condition step that only sends the message when \{\{isPlaying\}\} equals true.
For a plain !song chat command, the single-brace \{song\} variable is simpler:
🎵 Now playing: {song}
Sub announcement
{{displayName}} just subscribed at {{subTier}} for {{subMonths}} months! 🎉
Pair this with a subscription event trigger, which is what populates the sub variables.
Tip thank-you (with currency)
Thank you {{displayName}} for the {{tipAmount}} {{tipCurrency}} tip! Your message: "{{tipMessage}}"
Reading an uncommon event field
Every event has a raw map containing platform-native fields. If Lumio doesn't expose a first-class variable for what you need, reach into event.raw:
New Kick follow from {{event.raw.user_slug}}
Use this as a fallback; first-class variables should be preferred for portability across platforms.
Random picks
In a bot command template, \{random:a,b,c\} picks one of the listed items directly. The automation engine has no \{\{random\}\} built-in — there, use a Random logic node to branch, or a Set Variable action to store a value and reference it as \{\{var.pickedUser\}\} in a later step.
Tips & Best Practices
- Always add separators. Missing data resolves to empty string, so
\{\{songName\}\} by \{\{songArtist\}\}becomesbywhen Spotify is paused. A small conditional upstream avoids the awkward output. - Prefer
\{\{displayName\}\}over\{\{username\}\}for user-facing messages —displayNamepreserves capitalization and localized characters;usernameis the raw handle. - Use
\{\{event.raw.*\}\}sparingly. It ties your template to a specific platform's payload. When a field starts mattering to more than one command, request a first-class variable. - Custom variables are your escape hatch. Anything not covered by a built-in (accumulated counters, last caller) can live in
\{\{var.*\}\}and be updated by a Set Variable step — but seed it before you reference it, or the placeholder shows up literally. - Try it on a quiet channel first. Neither editor renders a template preview, so the first real output you see is the one your viewers see. Run the automation manually, or point the command at a test channel, before you rely on it live.
- Legacy aliases exist for compatibility.
\{\{songTitle\}\}=\{\{songName\}\},\{\{albumArt\}\}=\{\{songAlbumArt\}\},\{\{gameName\}\}=\{\{category\}\}. Prefer the canonical names in new templates.
Troubleshooting
My template output contains a literal \{\{name\}\}
Two things produce this:
- A typo or a name that doesn't exist —
\{\{usrname\}\}instead of\{\{username\}\}, or\{\{viewerName\}\}, which isn't a real variable (use\{\{displayName\}\}). Cross-check the Variable Reference. - An ungated variable with nothing behind it — the Channel, Bot, Time, Shopify, OBS, Discord and
var.*groups stay literal rather than collapsing to an empty string. An unset\{\{var.something\}\}or an\{\{sceneName\}\}with OBS disconnected will land in chat as-is.
The output is missing where I expected data
A variable from the chat:userinfo, events:read or spotify:read groups resolved to an empty string. Possible causes, in order:
- The variable category isn't populated for this trigger. A follower event doesn't populate
\{\{cheerAmount\}\}— it's simply absent. - Source feature not connected or idle.
\{\{songName\}\}needs Spotify connected and playing. - Wrong group for the trigger.
\{\{displayName\}\}needs an event that carries a user; a timer trigger has none.
Why isn't \{name\} (single braces) working?
Check which surface you are on. Automation actions need double braces; bot command responses need single braces and Response Type set to Template — a command left on Response Type Text sends its body verbatim, braces and all.
Can I nest variables?
Not directly. The replacement pass doesn't recurse. If you need composition, pre-compute the value into a custom variable with an automation step, then reference \{\{var.composed\}\} in the final template.
It worked once and now resolves to nothing
The Spotify, OBS and stream-snapshot variables read live state. \{\{songName\}\} is empty whenever nothing is playing, \{\{uptime\}\} is empty while you are offline, and \{\{sceneName\}\} stays literal while OBS is disconnected — the template itself is fine.
\{\{event.raw.someField\}\} is always empty
The field name doesn't exist in this event's raw payload. Raw fields are platform-native and differ per platform and per event type, so a name that works for a Kick follow won't necessarily exist on a Twitch follow. Watch the Events page while the event fires to see what a real payload for that trigger looks like.
Related Docs
- Roles & Permissions — how roles and permissions are organised.
- Automations — triggers, actions, and the automation builder.
- Bot Commands — creating and overriding
!commands. - Chat — how to send chat messages from the dashboard.