Fluxta

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.

manifest.json
{
  "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" }]
}
apps/plugin/src/index.ts
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

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

apps/plugin/src/index.ts
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:

apps/editor/src/main.tsx
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:

apps/editor/src/App.tsx
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.

manifest.json
{
  "events": [
    {
      "type": "milestone",
      "name": "Count reached a milestone",
      "fields": [
        { "key": "itemId", "name": "Button", "type": "number" },
        { "key": "total", "name": "Total", "type": "number" }
      ]
    }
  ]
}
apps/plugin/src/index.ts
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.

On this page