Examples
Two complete plugins.
Both examples below show every file that carries plugin logic: the manifest, the
background process, and the editor. Project scaffolding — package.json, build
configs, the editor's index.html — is left out, unchanged from what
fluxta create generates. Both assume that layout, where apps/plugin builds
to index.mjs and apps/editor builds to editor/.
A minimal plugin
The smallest thing that does something useful: one Action, no editor, no settings. It writes a line to the log when a button is pressed.
{
"name": "hello",
"apiVersion": 1,
"version": "1.0.0",
"title": "Hello",
"description": "Writes a line to the log when triggered.",
"main": "index.mjs",
"author": "Jane Doe",
"license": "MIT",
"repository": "https://github.com/janedoe/fluxta-hello",
"minAppVersion": "0.1.0",
"maintainers": ["janedoe"],
"platforms": ["windows", "macos", "linux"],
"categories": ["utilities"],
"actions": [{ "name": "Say Hello", "type": "say-hello" }]
}import { Plugin, StandaloneAction } from "@fluxta/sdk/api";
class SayHello extends StandaloneAction {
type = "say-hello"; // === the manifest's `type`
onTrigger() {
console.log("Hello!");
}
}
const plugin = new Plugin();
plugin.registerAction(new SayHello());
await plugin.connect();A complete plugin
A per-button counter. It shows most of what a plugin can do: an Action with settings, a Run Variable, a Value Source an If/Else can branch on, and an editor that configures it.
The user can build "press → increment by 5 → if the count is over 100, play a sound".
Manifest
{
"name": "counter",
"apiVersion": 1,
"version": "1.0.0",
"title": "Counter",
"description": "Counts presses, per button.",
"main": "index.mjs",
"author": "Jane Doe",
"license": "MIT",
"repository": "https://github.com/janedoe/fluxta-counter",
"minAppVersion": "0.1.0",
"maintainers": ["janedoe"],
"platforms": ["windows", "macos", "linux"],
"categories": ["utilities"],
"actions": [
{
"name": "Increment",
"type": "increment",
"context": "item",
"editor": "editor/index.html",
"produces": [{ "key": "total", "name": "New total", "type": "number" }]
},
{
"name": "Reset",
"type": "reset",
"context": "item"
}
],
"sources": [
{
"key": "count",
"name": "Count",
"type": "number",
"resolve": "query",
"context": "item"
}
]
}Both Actions are "context": "item" because a count belongs to one button.
count is a query source for the same reason: there is no single value to
push, so Fluxta asks for the one belonging to the button being evaluated.
total is declared "number", which is what lets a later If/Else compare it
with "greater than".
Background process
import { ItemAction, ItemTriggerContext, Plugin } from "@fluxta/sdk/api";
/** Counts per button. `itemId` is the key, so two buttons never collide. */
const counts = new Map<number, number>();
type IncrementSettings = { step?: number };
class Increment extends ItemAction<IncrementSettings> {
type = "increment";
onTrigger(ctx: ItemTriggerContext<IncrementSettings>) {
// Settings are whatever was saved last, possibly by an older version.
const step = Number(ctx.settings.step);
const amount = Number.isFinite(step) ? step : 1;
const total = (counts.get(ctx.itemId) ?? 0) + amount;
counts.set(ctx.itemId, total);
// Declared in produces[], so a later step can read it.
return { total };
}
}
class Reset extends ItemAction {
type = "reset";
onTrigger(ctx: ItemTriggerContext) {
counts.delete(ctx.itemId);
}
}
const plugin = new Plugin();
plugin.registerAction(new Increment());
plugin.registerAction(new Reset());
// Answers "what is the count on this button?" whenever a condition asks.
plugin.registerSource({
key: "count",
resolve: (ctx) => counts.get(ctx.itemId) ?? 0,
});
// Register everything first — handlers are wired up inside connect().
await plugin.connect();Editor
One provider at the root connects the page to Fluxta:
import { ActionEditorProvider } from "@fluxta/sdk/ui";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ActionEditorProvider>
<App />
</ActionEditorProvider>
</StrictMode>,
);The form itself is then just the settings:
import { EditorPage, NumberField, useActionSettings } from "@fluxta/sdk/ui";
type Settings = { step?: number };
export default function App() {
const { values, set } = useActionSettings<Settings>({ step: 1 });
return (
<EditorPage>
<NumberField
label="Step"
value={values.step}
onChange={set("step")}
min={1}
hint="How much each press adds."
/>
</EditorPage>
);
}useActionSettings loads what was saved and hands back what is on screen when
the user presses Save. Doing that by hand is where the most common editor bug
lives: a save handler registered once captures the values from the first render
and keeps saving those, whatever the user types afterwards.
Adding an automation
To make the same plugin react to the outside world rather than a button, declare an Event Source and announce it.
{
"events": [
{
"type": "milestone",
"name": "Count reached a milestone",
"fields": [
{ "key": "itemId", "name": "Button", "type": "number" },
{ "key": "total", "name": "Total", "type": "number" }
]
}
]
}const total = (counts.get(ctx.itemId) ?? 0) + amount;
counts.set(ctx.itemId, total);
if (total % 100 === 0) {
plugin.emitEvent("milestone", { itemId: ctx.itemId, total });
}
return { total };The user can now build an automation on "Count reached a milestone" and
reference {{counter.milestone.total}} in any step of it.