Custom actions
Custom Actions allow you to tap into how Lumia Stream calls actions from the inside.
Custom Actions are mainly meant for things that we haven't added as a Helper function yet. For instance, actions for Spotify, Twitch, Streamer.Bot etc are all actions that we do not have a have helper functions for. This is where an action will step in.
You can use actions by just passing in one object, or an array of objects. If you pass in an array of actions then any action that can be awaited will be wait until the promise has been resolved.
This documentation for every action we use in Lumia can get extremely broad, so we will give examples for different actions, but if you get stuck please visit our Discord to ask us any questions.
Let's get started:
Generic Action
actions([ { base: "lumia", type: "tts", value: { message: "Hello" }, variables: {} } ]);:
Before we begin, let's dissect this.
base: will be the base of actions that can be used with Custom Code. There are two groups of bases.
System bases (built into Lumia):
delay, lumia, overlay, api, commandRunner, inputEvents
Note: the canonical system bases are
lumiaandoverlay. The older spellingslumiaActionsandoverlayActionsstill run but are deprecated — preferlumia/overlay. The input base isinputEvents(plural).
Integration bases — every connected integration is also a valid base. These include (and more are added over time):
twitch, youtube, facebook, tiktok, kick, discord, obs, slobs, meld, spotify, youtubemusic, nowplaying, vlc, voicemod, streamerbot, mixitup, vtubestudio, midi, osc, artnet, mqtt, serial, websocket, broadlink, hue, lifx, nanoleaf, govee, wled, wiz, tplink, tuya, yeelight, elgato, streamdeck, touchportal, loupedeck, homeassistant, switchbot plus any installed plugin (use the plugin's id as the base).
type: Every base has different action types. For instance, the base lumia has chatbot, tts, setStreamMode, toggleStreamMode and many more. The full type lists for the system bases are below; integration and plugin types vary per integration — the easiest way to discover the exact base, type, and value for an integration action is to configure that action once in Lumia's normal Action editor.
value: can sometimes be an object and sometimes a string, it all depends on the base and type
variables: allows you to send in different variables for each action. But do note that the variables that are already on the command/alert will also be spread on to this variables object. Variables are not required
To pause between actions, insert a dedicated delay step: an entry whose type is "delay" with the milliseconds in delay (e.g. { type: "delay", delay: 1000 }). It can carry any base and runs in order with the rest of the list. (An inline delay on a non-delay action is not applied by the runner — use a delay step.)
Where actions live: the unified actions lane
Inside a Lumia command or alert, every action — whatever its base (a lumia system action, an overlay action, a twitch action, an integration, a plugin) — lives together in one ordered list on the command:
{
"actions": {
"before": [{ "base": "lumia", "type": "tts", "value": { "message": "Hi" } }],
"after": [{ "base": "twitch", "type": "clip", "value": { "title": "Clutch!", "duration": 30 } }],
"waitForActions": true
}
}
beforeruns before the command/alert's main effect (its light / overlay reaction);afterruns after it.waitForActions(optional): whentrue, each action is awaited so the list runs in strict order before the command continues; when omitted orfalse, the list fires without waiting.- Every entry is one action object of the shape
{ base, type, value?, delay?, args? }— the same object you pass to the custom-codeactions()helper.delayholds the milliseconds for atype: "delay"step;argscarries the extra payload used by imported Streamer.bot actions.
This unified actions lane is the current model, and the one the AI action creator and the Streamer.bot / Mix It Up importers target. It replaces the older per-source command fields (lumiaActions, overlayActions, api, commandRunner, inputEvents) and the per-integration lanes (command.<integration>.before/after — e.g. command.wavelink, command.streamfog, whose entries have no base), all of which are deprecated — within the unified lane the source is simply each entry's base.
Passing an array of actions
async function() {
// Runs the TTS, then sends a chatbot message
await actions([
{ base: "lumia", type: "tts", value: { message: "Hello {{username}}" } },
{ base: "lumia", type: "chatbot", value: { message: "Welcome {{username}}" } }
]);
done();
}
System base type reference
These are the built-in type values for each system base.
base: "lumia" — callCommand, callRandomCommand, chatbot, tts, setStreamMode, toggleStreamMode, setFuzeAudioSensitivity, playAudio, writeToFile, setConnection, updateVariable, updateCounter, appendToVariable, unappendFromVariable, saveLocal, addToUserlevel, removeFromUserlevel, addToRestrictionsList, removeFromRestrictionsList, setFolder, setAlert, setAlertVariation, setCommand, setChatbotCommand, setTwitchPointsCommand, setTwitchExtensionCommand, setKickPointsCommand, setChatMatchCommand, setTwitchPointValue, setLoyaltyPointValue, setUserLoyaltyPoint, setTwitchExtensionBitsValue, setAutomation, setVoicecommands, sendToDiscordWebhook, sendToDiscordWithMediaWebhook, sendToWebhook, sendToPrinter, raffleEntry, raffleRemoveEntry, raffleGetWinner, raffleStart, raffleStop, raffleEnd, viewerQueueEntry, viewerQueueLeave, tournamentEntry, tournamentRemoveEntry, tournamentUpdatePoints, tournamentStart, tournamentEnd, viewerQueuePlayPause, viewerQueueEndQueue, viewerQueuePickPlayer, backToDefault, replayLastEventListEvent, runLastQueueItem, resumeQueue, pauseQueue, removeCurrentQueueItem, clearQueue, clearCooldowns, resetSession, cleanAll, refreshSettings, addSongRequest, skipSongRequest, clearSongRequestQueue, delay, setColor, accessory, plus the control-flow types conditional, randomGroup, loop, stop (see Control-flow actions below)
base: "overlay" — alertTrigger, alertEvent, setOverlayVisibility, setLayerVisibility, setLayerPosition, setLayerSize, setTextContent, setImageContent, setVideoContent, setAudioContent, setLayerVolume, playPauseMedia, setContent, sendShoutout, sendCustomOverlayContent, sendGameTrigger, sendGameUpdate, takeScreenshot, spinwheelReset, spinwheelAddItem, spinwheelRemoveItem, sendHfx, hudOverlayChange, hudToggle, hudVolumeSet, hudOpacitySet, timerIncrement, pollTrigger, pollStart, pollResetVotes, pollSetTimer, pollAddItem, pollRemoveItem, delay
base: "api" — get, put, post, patch, delete, delay. value is { url, method, rawBody?, headers?, body?, timeout?, delay? } — headers and body are arrays of "Key=Value" strings, never objects (e.g. headers: ["Authorization=Bearer abc"]); rawBody is a JSON string and wins over body.
base: "commandRunner" — app/file, shell command, delay
base: "inputEvents" — keyboard, mouse, delay. The key/mouse data is flat on the action, not under value:
keyboard:{ base: "inputEvents", type: "keyboard", keyboardValue: { value: "ctrl+j", valueType: "combination" } }—valueTypeiscombination(a hotkey likectrl+shift+f5) orinput(type the literalvalueas text); optionallongPress,cpm.mouse:{ base: "inputEvents", type: "mouse", mouseValue: { x: 0, y: 100, clickEvent: "left", moveType: "set" } }—clickEventisleft/right/none; optionalx1,y1,doubleClick,mouseSpeed.
delay step — an entry with type: "delay" pauses the list; put the milliseconds in delay (e.g. { type: "delay", delay: 1000 }). duration is accepted in place of delay, and the step runs under any base.
Tip: most of the
lumiaandoverlayactions already have dedicated helper functions (tts,chatbot,overlaySetTextContent, etc.) inhelper-functions.md. Reach foractions()mainly when you need an integration action that does not have a helper yet.
Control-flow actions
Four action types add branching and repetition inside an action list: conditional (if / else), randomGroup (run one random branch), loop (repeat) and stop (end the list). The runner matches them by type alone, so any base works, but write base: "lumia". Nested actions are ordinary action objects of any base (including other control-flow actions), and they run in order against the same variables as the rest of the list: a variable set by an action inside a branch is visible to the actions after the block.
| type | value | what it does |
|---|---|---|
conditional | { if: [<condition rows>], then: [<actions>], else: [<actions>] } | runs then when the conditions pass, otherwise else (optional); an empty if counts as passing |
randomGroup | { groups: [{ name: "<label>", weight: 1, actions: [<actions>] }] } | picks exactly one group per run, weighted, and runs its actions |
loop | { mode: "count", count: "3", actions: [<actions>] } | runs actions repeatedly; see the three modes below |
stop | {} | ends the current action list, including everything after the enclosing blocks |
Condition rows (conditional and loop while mode)
A condition row is { variable: "<variable name>", operator: "<operator>", value: "<compare to>", conditionComparison: "&&" } — the same rows a command's own conditions use.
variableis a bare variable name, without braces ("username", not"{{username}}"). It is looked up first among the list's own variables (event/command variables likemessageorusername, plus anything set by earlier actions), then among your global Lumia variables. Dot paths reach into objects, e.g."data.user.name".valueaccepts template tokens like{{username}}."true"/"false"and plain numbers are compared as booleans / numbers, and a JSON array or object is parsed.conditionComparisonjoins a row to the one before it and is ignored on the first row."&&"rows bind tighter than"||"rows, soA && B || Cmeans(A and B) or C. A missing value counts as"&&".- A row with no
variableor nooperatorcounts as passing. If the variable doesn't exist, every operator exceptis-empty/not-emptyfails.
| operator | passes when |
|---|---|
equals | loose match: text is compared case-insensitively, "5" equals 5, "true" equals true |
strict-equals | exact match, same type and case ("5" in the value is a number, so a text variable "5" does not match) |
not-equals | the opposite of equals |
contains | text contains the value (case-insensitive), or a list contains it as an item, or an object has it as a key or key: value pair. A comma-separated value requires every item |
not-contains | the opposite of contains |
greater-than / less-than | numeric comparison; for a list or object it compares the item / key count; two non-numeric texts compare alphabetically |
is-empty / not-empty | the variable is (or isn't) missing, null, "", [] or {}; value is ignored |
regex | the value, as a regular expression (case-sensitive, no flags), matches the variable |
randomGroup
Each group's chance is its weight divided by the total weight. weight must be a positive number; a missing, zero, negative or text weight (such as "2") counts as 1. name is only a label. A group with an empty actions list is a valid "do nothing" outcome, and a randomGroup with no groups does nothing.
loop
value is { mode, count, list, if, actions }; only the fields for the chosen mode are read. Iterations run one after another, each waiting for the previous one to finish. Inside the body, {{loop_index}} is the current iteration number starting at 0, and in list mode {{loop_item}} is the current item. A loop with no actions does nothing.
| mode | reads | runs |
|---|---|---|
count | count: "5" | that many times; the number is rounded down and limited to 0–1000, and anything that isn't a number runs 0 times |
list | list: "red, green, blue" | once per item; a JSON array (["a","b"]) is parsed, anything else is split on commas; items are trimmed, empty items are dropped, and each item is text |
while | if: [<condition rows>] | while the conditions pass, checked before every iteration; with no condition rows the loop is skipped entirely instead of running forever |
- Every mode stops after 1000 iterations.
countandlistaccept template tokens, including your global Lumia variables (list: "{{my_list}}",count: "{{my_count}}"). The list's own variables (event/command variables and anything set earlier in the list) win over a global variable with the same name. A variable holding an array is used as that array, so items that contain commas stay whole, and object items arrive in{{loop_item}}as JSON text. A token that doesn't resolve becomes empty, which means 0 iterations.- A
whileloop only ends if something in its body changes the condition, for example anupdateCounter/updateVariableon the global variable it checks, or acodestep returning a new value withdone({ variables }). Put a delay step in the body so it can't spin through all 1000 iterations instantly. loop_indexandloop_itemare ordinary variables, not scoped to the loop: they keep their last value after the loop ends, and a nested loop overwrites the outer loop's values.
stop
stop ends the action list it runs in. Inside a conditional, randomGroup or loop it also ends every enclosing block and the actions after them (a stop inside a loop body ends the loop and the rest of the list). A code step that calls done({ shouldStop: true }) does the same. It only ends that one list: in a command, a stop in actions.before does not stop actions.after or the command's own light / overlay effect, and in custom code await actions([...]) simply returns early while your code keeps running.
async function() {
await actions([
{
base: "lumia",
type: "conditional",
value: {
if: [{ variable: "message", operator: "is-empty", value: "", conditionComparison: "&&" }],
then: [
{ base: "lumia", type: "chatbot", value: { message: "Usage: !hype <text>" } },
{ base: "lumia", type: "stop", value: {} }
],
else: []
}
},
{
base: "lumia",
type: "randomGroup",
value: {
groups: [
{ name: "Common", weight: 3, actions: [{ base: "lumia", type: "chatbot", value: { message: "Hype!" } }] },
{ name: "Rare", weight: 1, actions: [{ base: "lumia", type: "tts", value: { message: "{{username}} says {{message}}" } }] }
]
}
},
{
base: "lumia",
type: "loop",
value: {
mode: "list",
list: "red, green, blue",
actions: [
{ base: "lumia", type: "chatbot", value: { message: "Color {{loop_index}}: {{loop_item}}" } },
{ base: "lumia", type: "delay", delay: 1000 }
]
}
},
{
base: "lumia",
type: "loop",
value: {
mode: "while",
if: [{ variable: "hype_level", operator: "less-than", value: "5", conditionComparison: "&&" }],
actions: [
{ base: "lumia", type: "updateCounter", value: { value: "hype_level", message: "1", operator: "+" } },
{ base: "lumia", type: "delay", delay: 500 }
]
}
}
]);
done();
}
Common lumia action examples
Most lumia actions have a dedicated helper already (chatbot, tts, playAudio, setVariable, etc.) — prefer those. Reach for actions() for the engagement / system features that have no helper. The value fields are not uniform across types, so if you need a type not shown here, set that action up once in the normal action editor to read off its fields. Verified examples:
async function() {
// Text to speech (volume and speed are percentages; speed is 20-200 where 100 is the voice's normal pace)
await actions([{ base: "lumia", type: "tts", value: { message: "Hello {{username}}", volume: 100, speed: 130 } }]);
// Song requests
await actions([{ base: "lumia", type: "addSongRequest", value: { value: "never gonna give you up" } }]); // a search term or url
await actions([{ base: "lumia", type: "skipSongRequest" }]);
// Raffle (ends_after is in seconds; raffleStop / raffleEnd take no value)
await actions([{ base: "lumia", type: "raffleStart", value: { title: "My Raffle", auto_end: true, ends_after: 120 } }]);
// Viewer queue
await actions([{ base: "lumia", type: "viewerQueueEntry", value: { value: "{{username}}" } }]);
// Stream mode and connections
await actions([{ base: "lumia", type: "setStreamMode", value: { on: true } }]);
await actions([{ base: "lumia", type: "setConnection", value: { value: "obs", on: true } }]); // on:true enables, false disables
// Counter (operator is one of + - * /)
await actions([{ base: "lumia", type: "updateCounter", value: { value: "deaths", message: "1", operator: "+" } }]);
// Lights — set every light to red (lights: {} = all lights; power: false turns them off)
await actions([{ base: "lumia", type: "setColor", value: { rgb: { r: 255, g: 0, b: 0 }, lights: {}, power: true } }]);
// Smart plug / key light — id/brand/brandOrigin come from your connected devices (brandOrigin 7 = key light)
await actions([{ base: "lumia", type: "accessory", value: { accessories: [{ id: "<device id>", brand: "<brand key>", brandOrigin: 7, state: { on: true, brightness: 100, temperature: 6000 } }] } }]);
done();
}
Engagement & system lumia actions
These lumia actions drive Lumia's engagement features (raffles, tournaments, viewer queue, song requests) and system controls. None of them has a dedicated helper, so call them through actions(). The action shape is { base: "lumia", type: "<type>", value: { ... } } — the fields in the tables below go inside the inner value object. Text fields accept template tokens like {{username}}.
Enable / disable commands, alerts, folders & automations
on: true enables, on: false disables.
| type | value | targets |
|---|---|---|
setCommand | { value: "<command name>", on: true } | a chat command |
setChatbotCommand | { value: "<command name>", on: true } | a chatbot command |
setTwitchPointsCommand | { value: "<command name>", on: true } | a Twitch channel-points reward command |
setTwitchExtensionCommand | { value: "<command name>", on: true } | a Twitch extension command |
setKickPointsCommand | { value: "<command name>", on: true } | a Kick points command |
setChatMatchCommand | { value: "<chat-match id>", on: true } | a chat-match command (by id) |
setAlert | { value: "<alert key>", on: true } | an alert (its pathKey) |
setAlertVariation | { value: "<alert key>", variation: "<variation id>", on: true } | one variation of an alert |
setFolder | { value: "<folder id>", on: true } | a command/alert folder |
setAutomation | { value: "<automation name>", on: true } | an automation/timer (matched by name) |
setVoicecommands | { on: true } | the voice-commands input as a whole |
setFuzeAudioSensitivity | { value: 50 } | Fuze audio sensitivity (number) |
Variables, local storage & files
| type | value | notes |
|---|---|---|
appendToVariable | { value: "<variable name>", message: "<item>", unique: true } | append an item to a list variable; unique skips it if already present |
unappendFromVariable | { value: "<variable name>", message: "<item>" } | remove an item from a list variable |
saveLocal | { value: "<key>", message: "<value>" } | persist a value (same store as the {{save_local}} / {{load_local}} chat functions) |
writeToFile | { value: "<file path>", message: "<content>", on: true } | write text to a file; on: true appends instead of overwriting |
For plain variables prefer the
setVariable/getVariable/deleteVariablehelpers inhelper-functions.md.
Userlevels & restrictions
| type | value | notes |
|---|---|---|
addToUserlevel | { value: "<userlevel id>", message: "<username>" } | add a user to a userlevel |
removeFromUserlevel | { value: "<userlevel id>", message: "<username>" } | remove a user from a userlevel |
addToRestrictionsList | { message: "<username>" } | restrict a user (no value) |
removeFromRestrictionsList | { message: "<username>" } | unrestrict a user |
Raffles
| type | value | notes |
|---|---|---|
raffleStart | { title: "<title>", preset: "<raffle id>", auto_end: true, ends_after: 5 } | all optional; preset seeds from a saved raffle, ends_after is in minutes (default 30), auto_end turns on the timer |
raffleEntry | { value: "<username>" } | needs a started raffle |
raffleRemoveEntry | { value: "<username>" } | |
raffleStop | {} | stop accepting entries |
raffleGetWinner | {} | pick a winner (raffle must be stopped first) |
raffleEnd | {} | end the raffle |
Tournaments
| type | value | notes |
|---|---|---|
tournamentStart | { preset: "<tournament id>" } | start from a preset, or a new tournament if omitted |
tournamentEntry | { message: "<username>", value: "<avatar url>", extra: "<team>", platform: "twitch" } | message is the username; needs a running tournament with signups open |
tournamentRemoveEntry | { message: "<username>", platform: "twitch" } | |
tournamentUpdatePoints | { message: "<username>", points: "+100" } | points uses the modifier syntax (+ - * / =); defaults to = (set) when no modifier |
tournamentEnd | {} |
Viewer queue
| type | value | notes |
|---|---|---|
viewerQueueEntry | { value: "<username>" } | needs a started queue |
viewerQueueLeave | { value: "<username>" } | |
viewerQueuePickPlayer | { value: 1, mode: "single" } | pick N players; mode is single, bulkfirst, or bulkrandom (default single, 1 player) |
viewerQueuePlayPause | { on: true } | true plays, false pauses |
viewerQueueEndQueue | {} |