Value Sources
Expose a fact users can branch on.
If/Else is a built-in step: the user picks something to test, an operator, and a value, and the steps below take one branch or the other. A Value Source is a plugin contributing something to test — "is OBS connected", "which scene is live", "is a timer running on this button".
Push or pull?
There are two kinds, and the choice is mostly made for you:
Push ("resolve": "variable") | Pull ("resolve": "query") | |
|---|---|---|
| Who acts | The plugin calls setVariable on change | Fluxta calls resolve when it asks |
| Knows the button | No | Yes — ctx.itemId |
| Good for | Global facts the plugin already observes | Per-button facts, and anything expensive |
Use push when the value is already known. An event handler that fires when the scene changes can push it, and the value is then read from memory with no round trip.
Use pull when the answer depends on the button, or when computing it eagerly would be wasteful. A per-button timer cannot be pushed, because there is no single value: the answer differs per button, and only pull is told which one.
Push is the default when resolve is omitted.
Push
Declare it, then keep it up to date:
{
"sources": [
{ "key": "currentScene", "name": "Current scene", "type": "string" }
]
}obs.on("sceneChanged", (scene) => {
plugin.setVariable("currentScene", scene);
});There is no registration call for a push source.
Pushed values are dropped on disconnect
The value lives as long as the process does. After a reconnect Fluxta holds nothing until the plugin pushes again, so push current state on connect, not only on change:
await plugin.connect();
plugin.setVariable("currentScene", await obs.currentScene());A setVariable whose key is not declared in sources[] is dropped.
Pull
Declare it with "resolve": "query" and register a handler:
{
"sources": [
{
"key": "running",
"name": "Timer running",
"type": "boolean",
"resolve": "query",
"context": "item"
}
]
}const timers = new Map<number, Timer>();
plugin.registerSource({
key: "running",
resolve: (ctx) => timers.has(ctx.itemId),
});
await plugin.connect(); // register first, then connectresolve may be async, and it runs while the user's Run is waiting on the
condition. Fluxta gives it about one second before treating the answer as
absent, so read state already held rather than making a network call.
"context": "item" says the source is about a specific button, which is what
makes ctx.itemId meaningful.
Parameters
A source can take inputs the user fills in next to the condition — "is the scene named this active":
{
"key": "sceneActive",
"name": "Scene is active",
"type": "boolean",
"resolve": "query",
"params": [
{ "key": "scene", "name": "Scene", "editor": "select", "options": "scenes" }
]
}They arrive as ctx.args, always as strings:
plugin.registerSource({
key: "sceneActive",
resolve: async (ctx) => {
const { scene } = ctx.args as { scene?: string };
return (await obs.currentScene()) === scene;
},
});"editor": "select" with an options key gives the user a dropdown of real
values instead of a text box — see
operand options. Any other editor value
is a plain text input.
When no answer arrives
The value is absent if the plugin is offline, resolve throws, resolve
returns null, the answer takes longer than the timeout, or the key was never
registered. An absent value makes the condition false, and the Run takes the
else branch.
This is deliberately one-directional: an unavailable plugin never produces a
spurious then. Nothing fires because a plugin was slow.
The user's mental model has to account for it. "If connected → do the thing" behaves correctly while the plugin is down; "if not connected → do the thing" does not fire at all, because absent is not the same as false. Prefer phrasing a source so the interesting case is the positive one.
What users can compare
The declared type decides which operators the user gets:
type | Operators |
|---|---|
"number" | Equals, Not Equals, Greater Than, Greater Than or Equal, Less Than, Less Than or Equal |
"string" | Equals, Not Equals |
"boolean" | Equals, Not Equals |
So declare a count as "number", not "string", or "more than 5" is not
something the user can express.
Return a value matching the declared type. Fluxta compares by JSON type, and a mismatch is treated as absent.