Fluxta

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

manifest.json
{
  "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.

On this page