Skip to main content

API 2.1 (Companion 5.0+)

API 2.1 builds on API 2.0 with new capabilities and some TypeScript-level type improvements. There are no runtime breaking changes.

If you haven't migrated to API 2.0 yet, start there — it is a prerequisite for 2.1.

TypeScript: type safety for subscribe hooks

When using a subscribe callback on an action, optionsToMonitorForSubscribe is now required in the TypeScript types rather than optional.

Before:

{
name: 'My Action',
options: [...],
callback: async (action) => { ... },
subscribe: async (action) => { ... },
// optionsToMonitorForSubscribe was optional but recommended
}

After:

{
name: 'My Action',
options: [...],
callback: async (action) => { ... },
subscribe: async (action) => { ... },
optionsToMonitorForSubscribe: ['field1', 'field2'], // now required when subscribe is present
}

This is a TypeScript-only change. There is no runtime impact; if your module does not use TypeScript, or you already set optionsToMonitorForSubscribe on every action that uses subscribe, no changes are needed.

For a refresher on why optionsToMonitorForSubscribe matters with expressions, see the API 2.0 guide.

Abort signals

Companion now passes an AbortSignal through the callback context of actions, feedbacks, and learn operations, so long-running work can be cancelled when its result is no longer needed. Each callback receives a context.signal:

  • Actions — aborts when the result of this execution is no longer needed (for example, the user aborted the actions currently running on a button).
  • Feedbacks — aborts when a recheck of the feedback is queued while it is still executing, meaning the in-progress result is about to be superseded.
  • Learn — aborts when the user cancels an in-progress learn before it finishes.
// Action
callback: async (action, context) => {
await myDevice.send(action.options.command, { signal: context.signal })
},

// Feedback
callback: async (feedback, context) => {
const value = await myDevice.query(feedback.options.channel, { signal: context.signal })
return value > 0
},

// Learn
learn: async (action, context) => {
const response = await fetch('http://device.local/settings', { signal: context.signal })
if (context.signal.aborted) return undefined

const data = await response.json()
return { setting: data.value }
},

Respecting the signal is optional. If you do choose to honour it, stop your work and throw — the thrown error is ignored, and for feedbacks a fresh check is then performed. If a callback is short or synchronous, you can safely ignore context.signal entirely.

Action callbacks can return a result value

Actions can now declare that they produce a result. When an action returns a value, Companion can store it in a local or custom variable of the user's choosing, making it available to subsequent actions in the same sequence.

The action's callback may return (possibly asynchronously) any result value that can be represented in JSON. The action definition must also specify hasResult: true so that the returned result is not ignored. For example, assuming the getLevel function returns a number or null, an action definition exposing this value could look like:

const actions = {
// ...other actions...
read_current_value: {
name: 'Read current value',
options: [
{
id: 'channel',
type: 'number',
label: 'Channel',
default: 1,
min: 1,
max: 64,
},
],
hasResult: true,
callback: async (action, context) => {
const level = await myDevice.getLevel(action.options.channel)
return level
},
},
}

You'll also need to update any TypeScript schema defined for the action by adding result: <JsonValue subtype>:

type ModuleActions = {
// ...other actions...
read_current_value: {
options: {
channel: number
}
result: number | null
}
}

In Companion's UI for configuring the action, the user can then choose a local or custom variable to store the result in after each execution.

tip

This pairs well with the expression support introduced in API 2.0. The stored result can be referenced in later action options via the variable name the user chose.

Graphics overhaul

Layered presets

A new layered preset type is available. It lets modules describe button graphics using the same element-based system Companion uses internally, without needing to produce raw image buffers via advanced feedbacks.

Available element types:

  • Text — formatted text label
  • Image — raster image
  • Box — filled or stroked rectangle
  • Line — line segment
  • Circle — filled or stroked circle
  • Gauge — value-driven bar/ring meter
  • Group — a named collection of the above
  • Composite — a reusable element defined by your module (see below)

Existing simple presets are unaffected and continue to work as before. The layered type is an addition for cases where richer graphics are needed.

tip

We recommend continuing to use simple presets whenever possible, for compatibility with Bitfocus Buttons

Composite elements

Modules can now define reusable composite drawing elements using setCompositeElementDefinitions. A composite is a named component built from simpler drawing elements. They can be referenced in layered presets, and users can also add them to their own buttons through the Companion UI.

this.setCompositeElementDefinitions({
'signal-indicator': {
name: 'Signal Indicator',
// ...element composition
},
})

This is intended as a more structured replacement for the "produce a bitmap in an advanced feedback" pattern, keeping your graphics logic encapsulated and reusable.

Composite element definitions and layered preset data are strictly validated on receipt, so invalid definitions produce clear warnings in the module debug log rather than silently corrupting button graphics.

Presets: reference Companion's internal actions and feedbacks

Presets can now reference a small, reserved set of Companion's built-in internal actions and feedbacks alongside your module's own, using internal:* ids. Companion translates each of these to the matching internal action/feedback when the preset is imported onto a control, resolving any references (such as the button location) to the control the preset is placed on.

The internal actions available in presets are:

  • internal:wait{ time: number } — wait for an amount of time (ms)
  • internal:customLog{ message: string } — write a message to the Companion log
  • internal:abortButton{ skipReleaseActions?: boolean } — abort the actions currently running on this button
  • internal:localVariableSet{ name: string; value: string } — set one of this button's local variables

The internal feedbacks available in presets are:

  • internal:checkExpression{ expression: string } — boolean, driven by an expression
  • internal:buttonPushed{ treatSteppedAsPressed?: boolean } — boolean, true while the button is pressed
  • internal:buttonCurrentStep{ step: number } — boolean, true when the button is on the given step

In addition, you can use the logic/flow building blocks, which nest other actions or conditions via named children groups:

  • internal:actionGroup{ executionMode?: 'inherit' | 'concurrent' | 'sequential' }, children: { default: actions }
  • internal:logicIf — children: { condition: conditions; actions; elseActions? }
  • internal:logicWhile — children: { condition: conditions; actions }
  • internal:logicOperator (feedback) — { operation: 'and' | 'or' | 'xor' }, children: { default: conditions }
steps: [
{
down: [
{ actionId: 'my-recall', options: { preset: 1 } },
{ actionId: 'internal:wait', options: { time: 500 } },
{
actionId: 'internal:logicIf',
options: {},
children: {
condition: [
{ feedbackId: 'internal:checkExpression', options: { expression: '$(this:step) == 1' } },
],
actions: [
{ actionId: 'internal:customLog', options: { message: 'On first step' } },
],
},
},
],
up: [],
},
],

All of these require a module built against API 2.1 (2.1.0 or newer). The host drops, with a warning, any internal preset entry that the module is too old to use, so the catalog can grow in future API versions without older modules being able to emit ids they predate.

Presets: feedback-based local variables

Presets can declare local variables on a button via the localVariables field. As well as the existing simple variables (a fixed startup value), you can now define a variable whose value is driven by the live result of a feedback, using variableType: 'feedback':

localVariables: [
{
variableType: 'simple',
variableName: 'last_recalled',
startupValue: '',
},
{
variableType: 'feedback',
variableName: 'current_level',
feedbackId: 'get-level', // one of your module's feedbacks
options: { channel: 1 },
},
],

The feedback's evaluated value is exposed as the named local variable, so it can be referenced from expressions elsewhere on the button. Pair this with value feedbacks (see API 2.0) to surface device state as a variable without the user wiring it up themselves.

Presets: alternatives

A single preset id can now offer several variants using type: 'alternatives'. The host picks the best variant it supports, so the user only ever sees one copy of the preset. This is the clean way to ship a rich layered look with a simple fallback for hosts which don't support all the functionality of the module API (such as Bitfocus Buttons). List the variants most-preferred first:

presets['play'] = {
type: 'alternatives',
variants: [
{
type: 'layered',
name: 'Play',
elements: [
/* ... */
],
steps: [
/* ... */
],
feedbacks: [],
},
{
type: 'simple',
name: 'Play',
style: { text: 'Play', size: '18', color: 0xffffff, bgcolor: 0x000000 },
steps: [
/* ... */
],
feedbacks: [],
},
],
}

See Preset alternatives for details.

Advanced feedbacks: declare affectedProperties

Advanced feedbacks (type: 'advanced') should now declare which button style properties they modify, via the new affectedProperties field:

{
type: 'advanced',
name: 'Status Colour',
options: [
{
id: 'active',
type: 'checkbox',
label: 'Active',
default: false,
},
],
affectedProperties: ['bgcolor', 'color'],
callback: async (event) => {
return event.options.active
? { bgcolor: '#00cc00', color: '#ffffff' }
: { bgcolor: '#cc0000', color: '#ffffff' }
},
}

Companion uses this to configure a more accurate set of style overrides for the button.

From API 2.1 onwards, Companion will emit a warning in the module debug log for any advanced feedback that does not declare affectedProperties. Boolean and value feedbacks are unaffected.

tip

Where possible, prefer boolean or value feedbacks, or the new layered presets, over advanced feedbacks. Advanced feedbacks are intended only for cases that cannot be expressed any other way.

Smaller additions

A few smaller, opt-in additions also landed in 2.1:

  • sortSelection on multidropdown fields — set sortSelection: true on a multidropdown input field to have the UI keep the selected values in the same order they appear in the choices array (any custom values are sorted alphabetically at the end).
  • Duplicate option ids are rejected — option ids must be unique within a single action or feedback definition, and config field ids must be unique across the config. From 2.1, Companion drops the later duplicates and logs a warning in the module debug log, rather than letting fields silently overwrite each other.

Node 26 support

Modules can now declare node26 as their runtime, by setting runtime.type in companion/manifest.json:

{
"runtime": {
"type": "node26",
"api": "nodejs-ipc",
"apiVersion": "0.0.0",
"entrypoint": "../main.js"
}
}

node22 remains supported, but we encourage modules to update when they can. Node 26 is an LTS release and is suitable for new modules or modules that want to adopt the latest platform features.