Action File Reference (.faction)

Action File Reference (.faction)

.faction files are JSON documents that describe deterministic sequences of control changes. Fusor Studio runs them in three places:

  • Pad Mode. Each pad triggers an action (Pad Mode).
  • Pre-run queue. A fusor's pre-run actions run before its automation starts (Pre-run Actions).
  • Library → Actions. Standalone manual run for testing or one-off setup.

This page is the canonical reference for the format itself. For the authoring UI — opening an action for editing, the Notes/Code tabs, the pop-out Step Editor, and pack assignment — see Action Authoring Overview, Action Editor, and Pack Authoring.


File location and naming

Action files carry the .faction extension and live under your library's actions/ folder. Filenames are surfaced as-is, so choose them for readability in the Actions list. The folder layout, how subfolders are treated, and where packs sit relative to all this are covered in Action Authoring Overview.


Top-level shape

Every action file is a JSON object with four required top-level fields:

{
  "format": "faction",
  "version": 1,
  "meta": { ... },
  "steps": [ ... ]
}
FieldTypeNotes
formatstringMust be "faction".
versionnumberMust be 1. Future versions are not yet defined.
metaobjectSee Meta block.
stepsarrayOrdered list of step objects. See Step types.

A file that fails this shape is rejected by the validator and won't load.


Meta block

The meta object carries human-readable metadata and runtime hints.

FieldTypeRequiredNotes
namestringYesDisplay name shown in the Library and prerun list.
shortIdstringYesShort name used to identify this action on the status line. 1–12 characters, from letters, digits, space, dot, underscore, and hyphen. See Short ID.
descriptionstringNoFree-form summary.
actionIdstring (UUID)AutoStable identifier. Written on first save, preserved on overwrite and on rename. This is what a fusor's actions.before[].ref points at, so changing it strands every fusor using the action. Don't hand-edit.
appsstring[]NoDJ apps this action targets (["traktor"], ["traktor", "mixxx"], etc.). Edited via the multi-select picker on the action's Notes tab. Informational; doesn't gate execution.
createdISO timestampAutoPreserved on overwrite.
modifiedISO timestampAutoRewritten on every save.
anticipatedDurationMsnumber | nullNoAuthor override for the prerun duration badge. Required when the action contains WAIT_UNTIL and you want the badge to show a real number instead of unknown. Must be a non-negative finite number.
preflightobjectNoPer-DJ-app setup notes, keyed by app tag with an optional default. Values must be strings. The Pre-flight field on the action's Notes tab edits default; per-app keys are hand-edit only. See Action Authoring Overview.

Example:

"meta": {
  "name": "Reset Incoming EQ",
  "shortId": "ResetInEQ",
  "description": "Set all incoming EQ bands to unity",
  "actionId": "sample-reset-eq-001",
  "apps": ["traktor"],
  "anticipatedDurationMs": 150,
  "created": "2026-04-11T00:00:00.000Z",
  "modified": "2026-04-11T00:00:00.000Z"
}

Short ID

When an action fails, the status line has to say which action — and it has about 40 characters for the whole message. name is free-form and often too long to fit, so the status line uses shortId instead:

ACT2-Prerun aborted: ResetInEQ

Pick something you would recognise at a glance mid-transition. It doesn't have to be unique, but two actions sharing a short ID are indistinguishable in a status message, which defeats the point.

A file without a valid shortId fails validation and will not run. If you open such an action in the Step Editor, the field is pre-filled with a suggestion derived from name and the action is marked unsaved — saving it is enough to make the file valid, but the suggestion is a truncation, not a good name. Replace it.


Step types

The steps array runs top-to-bottom. Each step is one of six types: SET, WAIT, WAIT_UNTIL, CAPTURE, RESTORE, IF. Step type values are case-sensitive.

Steps run one at a time, in the order you write them. There is no step type that sends two control changes simultaneously: MIDI carries one message at a time, so a group of changes always goes out one after another. To change several controls together, write consecutive SET steps.

SET

Send a control change.

{ "type": "SET", "addr": "deck.incoming.eq.high", "value": "UNITY" }
FieldNotes
addrControl address. May be a deck address (deck.a.eq.high), a role address (deck.incoming.* or deck.outgoing.* — see Role addresses), or any non-deck control (master.auto, mixer.crossfader). Read-only telemetry addresses are rejected at validation.
valueEither a number or a symbolic name.

SET is non-blocking — the runtime queues the MIDI message and moves on. A small per-SET overhead (~10ms) is charged to duration estimates.

WAIT

Pause for a fixed duration.

{ "type": "WAIT", "ms": 50 }
FieldNotes
msPositive number of milliseconds.

WAIT is abortable: aborting the action resolves the wait immediately.

WAIT_UNTIL

Pause until a control's value satisfies a predicate, or fail after a timeout.

{
  "type": "WAIT_UNTIL",
  "addr": "deck.incoming.transport.sync",
  "predicate": "== ON",
  "timeoutMs": 2000
}
FieldNotes
addrAddress to watch. The runtime uses live telemetry.
predicatePredicate string. See Predicates.
timeoutMsPositive number. If the predicate isn't satisfied in time the action fails.

WAIT_UNTIL fails immediately while the engine is in Blind Mode — telemetry can't arrive, so the runtime fails fast rather than waiting out the timeout.

CAPTURE

Save the current values of one or more controls into a named snapshot.

{
  "type": "CAPTURE",
  "name": "eq_before",
  "addrs": [
    "deck.incoming.eq.high",
    "deck.incoming.eq.mid",
    "deck.incoming.eq.low"
  ]
}
FieldNotes
nameSnapshot name. Used to restore later. Session-scoped, not action-scoped — see below.
addrsNon-empty array of addresses. Read-only telemetry addresses are rejected at validation (since RESTORE would fail).

CAPTURE is a one-shot read of the current values via the Control Surface API.

Snapshots outlive the action that captured them. They live on the engine for as long as Fusor Studio is connected, so a RESTORE in a different action can name a snapshot an earlier action captured. That is what makes a pre-run / post-run pair possible: the pre-run action remembers the state, the transition runs, the post-run action puts it back. Bypass the Crossfader and Put It Back builds exactly that pair.

Two consequences worth knowing. Names are global, so capturing a name a second time overwrites the first snapshot — pick names that say what they hold (xfader_assigns, not before). And a snapshot survives until the engine is disposed, which means a RESTORE can succeed against a capture taken much earlier in the session; if you want a guaranteed-fresh reading, capture it in the same run.

RESTORE

Restore a previously captured snapshot.

{ "type": "RESTORE", "name": "eq_before" }
FieldNotes
nameSnapshot name. Must match a CAPTURE that has already run — in this action or in any earlier one this session.

Restoring a snapshot that was never captured (typo, the capture step was skipped by an IF branch, or the pre-run half of a pair never ran) fails the action with a status-bar message.

IF

Run a branch of steps based on a control's current value.

{
  "type": "IF",
  "addr": "master.auto",
  "predicate": "== ON",
  "then": [
    { "type": "SET", "addr": "master.auto", "value": "OFF" },
    { "type": "WAIT", "ms": 50 }
  ],
  "else": [ ]
}
FieldNotes
addrAddress to read.
predicatePredicate evaluated against the current value.
thenNon-empty array of steps run when the predicate is true.
elseOptional array of steps run when the predicate is false.

Any step type is allowed inside then/else, including nested IF. The runtime reads the address once at the start of the IF — it doesn't re-evaluate during the branch.


Symbolic values

SET values and predicate right-hand sides can be symbolic names instead of raw numbers. Symbolic names come from each control's value semantics — the named points that make sense for that control (UNITY, CUT, BOOST, ON, OFF, MIN, MAX, etc.).

{ "type": "SET", "addr": "deck.incoming.eq.high", "value": "UNITY" }
{ "type": "SET", "addr": "deck.incoming.eq.high", "value": 0.5 }

Both lines do the same thing for an EQ band. The symbolic form is the recommended idiom — it's stable across mapping changes and self-documents intent.

If a symbolic name isn't defined for the address, validation fails with a message listing the available names. Numeric literals always work as a fallback.


Predicates

Predicates are strings of the form <operator> <value>:

OperatorMeaning
==Equal to
!=Not equal to
>=Greater than or equal
<=Less than or equal
>Strictly greater
<Strictly less

Examples:

"== ON"
"!= OFF"
">= 0.5"
"< 0.1"

The right-hand side may be a symbolic name (resolved per address) or a numeric literal. Predicates are used by WAIT_UNTIL and IF.


Role addresses

Fusor's runtime distinguishes two deck roles: incoming (the deck being mixed in) and outgoing (the deck being mixed out). Actions can target a role directly without caring which physical deck it currently maps to:

{ "type": "SET", "addr": "deck.incoming.eq.high", "value": "UNITY" }

When the runtime executes this step, the role resolves to the current physical deck (deck.a.* or deck.b.*) via the Control Surface API. Roles swap on transition completion, so the same .faction file works on either side of the mix without rewriting.

Use a literal deck address (deck.a.eq.high) only when you specifically need that physical deck regardless of role.


Nesting

IF is the only step type that contains other steps. Its then and else arrays accept any step type, including a further IF. There is no nesting depth limit, and the validator checks nested steps by the same rules as top-level ones.


Duration estimation

The prerun duration badge sums an estimate of each step's runtime. The rules:

  • SET, CAPTURE, RESTORE~10ms each (per-step overhead for MIDI bus latency + host echo).
  • WAIT — exactly ms.
  • WAIT_UNTILunknown (no estimate). If the action contains a WAIT_UNTIL with no meta.anticipatedDurationMs override, the badge shows unknown.
  • IF — sums both branches conservatively (a planner can't know which branch will run, so it charges both).

If meta.anticipatedDurationMs is set, that value wins unconditionally. Use the override when an action contains WAIT_UNTIL and you have a reliable real-world estimate.


Validation errors

The most common rejection messages, from the validator and from the Code tab's parser:

ErrorWhat it means
Invalid format: expected "faction"Wrong or missing format field.
Unsupported version: NOnly version: 1 is recognized.
meta.name is required and must be a stringThe display name is missing.
meta.anticipatedDurationMs must be a non-negative number, null, or absentOverride field is malformed.
SET requires "value"Numeric or symbolic value missing.
SET "value" must be a string (symbolic) or numberWrong type (e.g. true, null, or an object).
WAIT requires "ms" (positive number)Zero or negative durations are rejected.
invalid step type "<type>"The type isn't one of the six step types. The error lists the valid ones. Check spelling and capitalization.
Unmatched "END" — no IF block is openA block-end marker with no IF open above it, usually left behind when you delete an IF line but not its END. Parsing stops at that line, and the message says how many later lines went unparsed.
Cannot SET read-only address "<addr>" (telemetry only)The address is incoming-only (telemetry). Pick the writable counterpart.
Cannot CAPTURE read-only address "<addr>" (telemetry only, RESTORE would fail)Same — telemetry addresses can't be snapshotted.
Invalid predicate format: "<str>"Predicate doesn't match <operator> <value>.
Cannot resolve symbolic value "<name>"The name isn't defined for that address. The error lists the available names.

Validation runs at file load and again before execution — a malformed action never reaches the runtime.


Worked example: capture / cut / restore

A demo action that snapshots the incoming EQ, cuts all three bands, waits two seconds, then restores the originals:

{
  "format": "faction",
  "version": 1,
  "meta": {
    "name": "Capture-Restore EQ Demo",
    "shortId": "EQCapRest",
    "description": "Capture incoming EQ state, cut all bands, wait 2 seconds, restore original values"
  },
  "steps": [
    {
      "type": "CAPTURE",
      "name": "eq_before",
      "addrs": [
        "deck.incoming.eq.high",
        "deck.incoming.eq.mid",
        "deck.incoming.eq.low"
      ]
    },
    { "type": "SET", "addr": "deck.incoming.eq.high", "value": "CUT" },
    { "type": "SET", "addr": "deck.incoming.eq.mid",  "value": "CUT" },
    { "type": "SET", "addr": "deck.incoming.eq.low",  "value": "CUT" },
    { "type": "WAIT", "ms": 2000 },
    { "type": "RESTORE", "name": "eq_before" }
  ]
}

This is the smallest pattern that exercises every snapshot mechanic: CAPTURE reads the snapshot in one shot, the three SET steps cut the bands, WAIT blocks for the demo gap, and RESTORE writes the captured values back.