Plugin API
Everything the background process can do.
The API for the background process, the Node.js program Fluxta starts from the
manifest's main.
import { Plugin } from "@fluxta/sdk/api";Lifecycle
Create one Plugin, register everything, then connect once:
import { Plugin } from "@fluxta/sdk/api";
import { SwitchScene } from "./actions/switch-scene";
const plugin = new Plugin();
plugin.registerAction(new SwitchScene());
await plugin.connect();Plugin
new Plugin()
Picks up the credentials Fluxta hands the process when it starts it. There is nothing to pass in and nothing to configure.
It throws when those credentials are absent, which is what happens if the background process is run directly from a terminal instead of being started by Fluxta.
connect
connect(): Promise<void>
Opens the connection to Fluxta and resolves once it is established.
Register everything before calling this: the handlers for triggers, sources,
and options are wired up inside connect().
The connection reconnects on its own after an ordinary network failure. Authorization, protocol-version, and policy failures are final — the promise rejects and the SDK does not retry. Handle that rejection; an unhandled one takes the process down, and Fluxta does not restart a crashed process.
onTerminal
onTerminal(listener: (error: Error) => void): () => void
Observes a final disconnect that happens after a successful connect. Nothing is retried after this; the process should exit and let Fluxta start a fresh one. Returns an unsubscribe function.
Actions
An Action is a class extending one of two base classes. Which one to pick has to
match the context declared in the manifest.
| Base class | Manifest context | onTrigger receives |
|---|---|---|
ItemAction<T> | "item" | ItemTriggerContext<T> — has itemId |
StandaloneAction<T> | "standalone" (the default) | StandaloneTriggerContext<T> — no itemId |
T is the shape of that Action's saved settings.
registerAction
registerAction(action): void
Registers an Action instance. Call before connect().
The instance's type must equal the Action's type in manifest.json. If they
differ — or the manifest does not declare the Action at all — Fluxta never routes
a trigger to it, with no error anywhere.
import { ItemAction, ItemTriggerContext } from "@fluxta/sdk/api";
type Settings = { channel?: string };
export class ToggleMute extends ItemAction<Settings> {
type = "toggle-mute"; // === `type` in manifest.json
async onTrigger(ctx: ItemTriggerContext<Settings>) {
const muted = await toggleMuteFor(ctx.itemId, ctx.settings.channel);
return { muted }; // a later step can read this
}
}onTrigger
onTrigger(ctx)
Called when the user's Action list reaches the Action.
Return an object of values to hand to later steps of the same Run — see Run Variables — or return nothing. Anything that is not a plain object is treated as no values.
If the handler throws, the error is logged and the step produces no values. The Run continues either way; a failing Action never breaks the user's whole Action list.
Handlers run concurrently
Steps within one Run are sequential, but separate Runs are not. Two buttons
pressed at once, or two Events firing at once, put two onTrigger calls in
flight simultaneously. Guard any shared state Actions touch.
An Action can never change how its button looks. Title, icon, colour, and state are host-owned steps the user places and can see.
ItemTriggerContext
ItemTriggerContext<T>
Prop
Type
StandaloneTriggerContext
StandaloneTriggerContext<T>
The same, without itemId.
Value Sources
A Value Source is a fact an If/Else step branches on. There are two flavours, and
which one to implement is decided by resolve in the manifest.
Push: setVariable
setVariable(key, value): void
For a source declared with "resolve": "variable", the default. The plugin
pushes the value whenever it changes, and Fluxta holds it.
plugin.setVariable("currentScene", "Live");keymust match asources[]entry whoseresolveis"variable". An undeclared key is dropped.- The value lives as long as the process does. It is dropped on disconnect, so push again after reconnecting.
There is no registration step for push sources.
Pull: registerSource
registerSource(source): void
For a source declared with "resolve": "query". Fluxta asks the plugin for the
value at the moment a condition is evaluated, which is how a per-button fact
works: the call says which button it is asking about.
const timers = new Map<number, number>();
plugin.registerSource({
key: "running", // === `sources[].key` in manifest.json
resolve: (ctx) => timers.has(ctx.itemId),
});Call before connect().
Source
Prop
Type
SourceContext
Prop
Type
The value is absent if resolve returns null, throws, takes longer than
Fluxta's roughly one-second query timeout, or the key is not registered — and
likewise if the plugin is offline. An absent value makes the condition false and
takes its else branch. It is never a spurious then.
Options Sources
An Options Source fills a dropdown while the user is editing, so they pick a real scene or input instead of typing one.
registerOptions
registerOptions(options): void
Call before connect(). The key must match an operand.options or
params[].options value somewhere in the manifest.
plugin.registerOptions({
key: "scenes",
resolve: async () => {
const scenes = await obs.listScenes();
return scenes.map((scene) => ({ value: scene.name, label: scene.name }));
},
});Options
Prop
Type
OptionsContext
Prop
Type
There is deliberately no itemId — the list is global, so it resolves even for a
button that has not been saved yet.
OperandOption
type OperandOption = {
value: JsonValue; // a scalar matching the declared type; this is what gets stored
label: string; // what the user reads
};If the plugin is offline or slow, or resolve throws or returns nothing, the
list is empty. Whatever value is already stored stays selectable regardless, so a
saved condition never quietly loses its operand.
Events
emitEvent
emitEvent(type, payload): void
Announces that something happened. Every Event the user built on this Event Source runs its Action list.
plugin.emitEvent("chat-message", { user: "alice", text: "hello" });typemust match anevents[].typein the manifest. An undeclared type is dropped.payloadkeys should match the Event Source's declaredfields[]— those are what users filter on and what later steps can read.- Fire-and-forget: nothing is awaited, and the plugin learns nothing about what ran.
Plugin settings
Plugin-wide settings, shared by every placement of every Action, stored as JSON.
getPluginSettings
getPluginSettings(): Promise<{ event: string; payload: JsonValue }>
Fetches the current settings. The value is on payload.
const settings = await plugin.getPluginSettings();
const config = settings.payload;setPluginSettings
setPluginSettings<T extends JsonValue>(data: T): Promise<void>
Replaces the stored settings wholesale — there is no merge.
await plugin.setPluginSettings({ host: "127.0.0.1", port: 4455 });onPluginSettings
onPluginSettings<T>(listener): Promise<() => void>
Subscribes to the settings. The listener fires immediately with the current value and again on every change, which covers most uses. Resolves to an unsubscribe function.
const unsubscribe = await plugin.onPluginSettings<Config>((config) => {
reconnectTo(config.host, config.port);
});Validate what you read
Stored settings are whatever was written last, possibly by an older version of the plugin. They are not guaranteed to match the current type.
External Connections
The services this plugin is connected to outside Fluxta — an OBS instance it drives, a Twitch account it publishes through. The app's toolbar shows them as a status indicator; see Connections.
setConnections
setConnections(connections): void
Reports the complete list, in the order the indicator should show them. Replaces the previous list: a connection missing from it is removed.
plugin.setConnections([
{ id: "main", title: "OBS Studio — Main", connected: true },
{ id: "backup", title: "OBS Studio — Backup", connected: false },
]);Prop
Type
- An empty array is a valid report: the plugin has no connections.
- There is no registration step and nothing to declare in
manifest.json. - An entry the host cannot read — an empty
idortitle, a non-booleanconnected— drops that whole snapshot. The plugin keeps running. - The SDK re-sends the last list after every reconnect. See Connections for the full behaviour.
Editor messaging
Arbitrary JSON, live, between the background process and any open editor of the same plugin. The format is yours to define; see Messaging.
sendToEditor
sendToEditor<T extends JsonValue>(data: T): void
Sends to this plugin's open editors. Received there via onReceiveFromPlugin.
plugin.sendToEditor({ event: "connection", status: "connected" });onReceiveFromEditor
onReceiveFromEditor<T>(listener): () => void
Receives what an editor sent with sendToPlugin. Returns an unsubscribe
function.
plugin.onReceiveFromEditor<{ event: string }>((data) => {
if (data.event === "getStatus") {
plugin.sendToEditor({ event: "status", connected: obs.isConnected });
}
});A complete background process
import { Plugin, ItemAction, ItemTriggerContext } from "@fluxta/sdk/api";
type Settings = { scene?: string };
class SwitchScene extends ItemAction<Settings> {
type = "switch-scene";
async onTrigger(ctx: ItemTriggerContext<Settings>) {
const previous = await obs.currentScene();
await obs.switchTo(ctx.settings.scene);
return { previousScene: previous };
}
}
const plugin = new Plugin();
plugin.registerAction(new SwitchScene());
plugin.registerSource({ key: "connected", resolve: () => obs.isConnected });
plugin.registerOptions({
key: "scenes",
resolve: async () =>
(await obs.listScenes()).map((s) => ({ value: s, label: s })),
});
plugin.onTerminal(() => process.exit(1));
await plugin.connect();
await plugin.onPluginSettings<{ host: string }>((config) => {
obs.connect(config.host);
});
obs.on("sceneChanged", (scene) => {
plugin.setVariable("currentScene", scene);
plugin.emitEvent("scene-changed", { scene });
});