Fluxta

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 classManifest contextonTrigger 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");
  • key must match a sources[] entry whose resolve is "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" });
  • type must match an events[].type in the manifest. An undeclared type is dropped.
  • payload keys should match the Event Source's declared fields[] — 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 id or title, a non-boolean connected — 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 });
});

On this page