Run Variables
Pass a value from one Action to later steps.
A Run is one execution of an Action list, top to bottom. Run Variables are how one step passes something to a later one: an Action returns a value, and the steps after it can read it.
They live for exactly that Run. Nothing persists, and two overlapping Runs never see each other's values.
Declare, return, reference
Declare it
{
"actions": [
{
"name": "Switch Scene",
"type": "switch-scene",
"produces": [
{ "key": "previousScene", "name": "Previous scene", "type": "string" }
]
}
]
}name is what users see in the picker. type is one of "string", "number",
or "boolean" — there are no object or array Run Variables.
Return it from onTrigger
async onTrigger(ctx: StandaloneTriggerContext<Settings>) {
const previous = await obs.currentScene();
await obs.switchTo(ctx.settings.scene);
return { previousScene: previous };
}The user references it
In a later step's settings, the address goes in double braces:
{{obs.switch-scene.previousScene}}The address is <plugin>.<action-type>.<key> — the plugin's name, the Action's
type, and the variable's key. A later If/Else can also branch on it
directly, picking it from a list rather than typing anything.
What gets dropped
Fluxta validates every returned value against the declaration and drops what does not fit:
- a key that is not in
produces[] - a value that is not a string, number, or boolean
- a value whose type disagrees with the declared
type - everything, if the Action declares no
produces[]at all
Each drop is logged as a warning by the app, not reported back to the plugin. A
Run Variable that never shows up is therefore almost always a mismatch between
what was declared and what was returned — a number declared as "string", or a
key renamed in one place only.
Reading references
When an Action's settings contain a reference, it is already resolved by the
time onTrigger sees them. Nothing needs parsing and nothing needs opting into:
// saved: { "message": "hi {{twitch.chat-message.user}}!" }
// arrives: ctx.settings.message === "hi alice!"Two substitution rules decide which type arrives:
// A string that is EXACTLY one reference keeps the value's type:
// saved: { "amount": "{{counter.read.value}}" }
// arrives: ctx.settings.amount === 42 (a number, not "42")
// A reference mixed with other text becomes a string:
// saved: { "label": "count: {{counter.read.value}}" }
// arrives: ctx.settings.label === "count: 42"Substitution walks nested objects and arrays, so any settings shape works. A value containing no reference anywhere arrives byte-identical.
A reference to something that was never produced resolves to an empty string. It is never an error, and the Action still runs. If a setting arrives empty when a value was expected, check that the producing Action really runs before this one in the user's list.
An unclosed {{ is left alone as literal text.
Values from an automation
Inside an Event's Action list, the Event's own
fields are readable the same way. The middle part of the address is the Event
Source's type instead of an Action's:
{{twitch.chat-message.user}}Most references users write pull something out of the event that fired.
Values the user keeps
A Run Variable is gone when the run ends. A value that has to outlive it — the id of a message posted when a stream started, needed again to edit that message when the stream ends — lives in a Global Variable the user manages in the app, and a template reads it the same way:
{{variables.3f2a9c1e-…}}The address holds the Variable's id, not its name, so renaming the Variable keeps the reference working; the picker below lists every Global Variable by name, so nobody types the id. The value is the one current when the Action runs, and a reference to a deleted Variable resolves to an empty string, like any other absent reference.
Give users a picker
Users should not have to memorise addresses. Fluxta works out which variables are readable at an Action's position and pushes the list to its editor.
The quickest route is TemplateField from the
UI Kit, a text field with the
picker already built in:
<TemplateField
id="message"
label="Message"
value={message}
onChange={setMessage}
editor={editor}
/>To build a custom one, read the list and subscribe to changes — Fluxta pushes a new one whenever the user reorders steps or edits an Event's subscriptions:
import { useSyncExternalStore } from "react";
// Outside the component, so React subscribes once rather than on every render.
const subscribe = (onChange: () => void) =>
editor.onRunVariablesChange(onChange);
const getSnapshot = () => editor.getRunVariables();
export function useRunVariables() {
return useSyncExternalStore(subscribe, getSnapshot);
}
// referenceTo(variable) gives you the "{{...}}" text to insert.The list is empty until the first push arrives, shortly after the editor mounts,
and a listener hears only the pushes that come after it subscribed.
useSyncExternalStore reads the list again once it has subscribed, so a push
that landed in between still shows up. Seeding useState with
getRunVariables() and subscribing in a useEffect looks equivalent but is
not: the effect runs after the render that read the list, and a push in that gap
leaves the picker empty until the next one. Outside React, subscribe first, then
read.
An empty list is an ordinary situation, not an error: an Action on a plain button is not reacting to anything, so nothing produced values for it to read.