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 any bot command response, automation action, or timed message. The templating engine is tiny and predictable: you wrap a variable name in double curly braces, and Lumio replaces it with the matching value at runtime.
Who needs this guide?
- Anyone writing bot commands and wanting the response to include the caller's name.
- Anyone building automation actions that send chat messages, Discord messages, or tips announcements.
- Timed-message authors who want dynamic content ("Currently playing
\{\{songName\}\}on\{\{platform\}\}"). - Anyone debugging a template that resolves to an empty string or leaks the literal
\{\{name\}\}into chat.
Where Templates Are Used
The same engine powers every string field in the app that promises "variables are supported":
- Bot commands — the response body of
!commandentries (cross-platform and per-platform overrides). - Automation actions — the message field of "Send chat message", "Send Discord message", "Shout out user", and similar action nodes.
- Timed messages — bot modules that post periodic messages.
- Welcome messages / subscriber greetings — the body of automated greetings.
If a field says it accepts templates, it uses the exact rules below.
Syntax: \{\{variable\}\}
Variables are wrapped in double curly braces:
Hello {{displayName}}, welcome to the stream!
The parser uses the regex \{\{(\w+(?:\.\w+)*)\}\}, which means:
- The name must match
\w+(letters, digits, underscore) —camelCaseis the convention. - Dotted paths are allowed:
\{\{event.type\}\},\{\{discord.serverName\}\},\{\{var.myCustomVar\}\},\{\{event.raw.someField\}\}. - Single curly braces (
\{name\}) are not interpreted as variables — they are left untouched. Some older Lumio documents show single braces for illustrative purposes; the actual engine requires double braces. - 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, permission granted → replaced with the value.
- Known variable, data missing or permission denied → replaced with an empty string. The rest of the message still renders, which is often why you see weird double-spaces (
" "where you expected"{{someVar}}"). - Unknown variable (typo, not a real variable) → left as-is, braces and all:
Hello {{unknwn}}!staysHello {{unknwn}}!. This is deliberate so you can spot typos in output.
The empty-string-on-missing-data rule 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 Gating
Each variable category has a permission requirement. If the executing context lacks the permission, the variable resolves to an empty string (rule 2 above).
| Variable group | Required 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 |
Bot contexts run with the account's permissions, not the calling viewer's — so a chat command defined on an account that lacks spotify:read will never resolve \{\{songName\}\}, even if Spotify is connected. Grant the permission to the role assigned to the account's bot, or add it to your Owner/Administrator role.
Writer's convenience: automations save-time scanning. When you save an automation or bot command that uses
\{\{songName\}\}, Lumio runsrequired_permissions()against your template and can warn you which permissions the executing account needs.
Variable Reference
Always available (no permission required)
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.*)
User-defined variables you set via the automation builder or the custom-variables settings page. There is no hard list — any name under var.* looks up the matching entry.
| Variable | Description |
|---|---|
\{\{var.anyName\}\} | Value of the named custom variable, or empty string if unset. |
Requires chat:userinfo
Context: info about the user who triggered the event (the viewer who followed, subscribed, sent the bits, ran the command, etc.). If the bot account lacks chat:userinfo, 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. |
Requires 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. |
Requires 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 for these to have values — the permission alone is not enough.
Recipes
Welcome message with username
Bot command body:
Welcome to the stream, {{displayName}}! Enjoy your stay :)
Requirements: chat:write (to send), chat:userinfo (to resolve \{\{displayName\}\}).
Shoutout template
Go check out {{var.lastRaider}} at https://twitch.tv/{{var.lastRaider}} — they raided us with {{raidViewers}} viewers!
Requirements: chat:write, events:read (for \{\{raidViewers\}\}). \{\{var.lastRaider\}\} is whatever value you stored in the custom variable lastRaider (typically via a "Set Variable" automation action on the raid trigger).
Now-playing command
🎵 Now playing: {{songName}} — {{songArtist}} [{{spotifyDevice}}]
Requirements: chat:write, spotify:read. When Spotify is paused or disconnected, the fields resolve to empty strings, producing 🎵 Now playing: — [] — so guard it:
🎵 {{isPlaying}} · {{songName}} — {{songArtist}}
or use a conditional automation step that only sends the message when \{\{isPlaying\}\} equals true.
Sub announcement
{{displayName}} just subscribed at {{subTier}} for {{subMonths}} months! 🎉
Requirements: chat:write, chat:userinfo, events:read.
Tip thank-you (with currency)
Thank you {{displayName}} for the {{tipAmount}} {{tipCurrency}} tip! Your message: "{{tipMessage}}"
Requirements: chat:write, chat:userinfo, events:read.
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}}
Requirements: events:read. Use this as a fallback; first-class variables should be preferred for portability across platforms.
Random-user picker
Lumio doesn't ship a \{\{random\}\} built-in. The idiomatic pattern is to use an automation action to pick a random value, store it in a custom variable, then reference \{\{var.pickedUser\}\} in the next step's template. Custom variables have no permission gate.
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 (random picks, accumulated counters, last caller) can live in
\{\{var.*\}\}and be updated by an automation step. - Test with the preview. The bot-command and automation editors render a preview with sample context so you can see exactly which variables resolve.
- Legacy aliases exist for compatibility.
\{\{songTitle\}\}=\{\{songName\}\},\{\{albumArt\}\}=\{\{songAlbumArt\}\},\{\{gameName\}\}=\{\{category\}\}. Prefer the canonical names in new templates.
Troubleshooting
My template output contains literal \{\{name\}\}
That's the "unknown variable" rule. Either the name is a typo (\{\{usrname\}\} instead of \{\{username\}\}), or it doesn't exist (\{\{viewerName\}\} isn't a real variable — use \{\{displayName\}\}). Cross-check the Variable Reference above.
The output is missing where I expected data
The variable is known but resolved to 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. - Permission missing. The bot account's role doesn't include
spotify:read/events:read/chat:userinfo. Fix: add the permission to the role, or assign a role that already has it. - Source feature not connected.
\{\{songName\}\}needs an active Spotify connection;\{\{sceneName\}\}needs OBS connected.
Why isn't \{\{name\}\} (single braces) working?
Lumio's engine only recognizes double braces. Single braces are left alone so that message content containing JSON or code snippets isn't accidentally rewritten. Convert \{name\} to \{\{name\}\}.
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.
The preview shows the right value but chat shows empty
Preview uses a sample context; chat uses the live one. If preview renders \{\{songName\}\} but chat shows empty, Spotify isn't playing at that moment — or you lack spotify:read on the bot's account.
\{\{event.raw.someField\}\} is always empty
Either the field name doesn't exist in this event's raw payload (platform-specific), or you lack events:read. Check a sample event under Dashboard → Events — click a row and inspect the raw fields.
Related Docs
- Roles & Permissions — which role grants which variable's permission.
- Automations — triggers, actions, and the automation builder.
- Bot Commands — where most templates live.
- Chat — how to send chat messages from the dashboard.