Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
name: Build Docusaurus
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
Comment thread
Julusian marked this conversation as resolved.
- uses: actions/setup-node@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
name: Build Docusaurus
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
Comment thread
Julusian marked this conversation as resolved.
- uses: actions/setup-node@v6
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
env:
CLICOLOR: 1
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand All @@ -22,7 +22,7 @@ jobs:
name: Prettier Format Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
Expand Down
313 changes: 313 additions & 0 deletions for-developers/module-development/api-changes/v2.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
---
title: API 2.1 (Companion 5.0+)
sidebar_position: -210
description: 'Overview of API 2.1 changes (abort signals, action results, graphics overhaul with gauges, preset alternatives, affectedProperties, internal actions and feedback-driven local variables in presets).'
---

API 2.1 builds on [API 2.0](v2.0.md) 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:

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

After:

```ts
{
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](v2.0.md#expression-handling-in-options).

## 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.

```ts
// 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.

Add `hasResult: true` to your action definition and return a `JsonValue` from the callback:

```ts
{
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 // a number, string, boolean, null, array, or object
},
}
```

In Companion's UI, the user will have the option to nominate a local or custom variable to receive the returned value after each execution.

:::tip

This pairs well with the expression support introduced in [API 2.0](v2.0.md#automatic-expression-parsing). 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.

```ts
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 }`

```ts
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'`:

```ts
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](v2.0.md)) 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](#layered-presets) look with a [simple](../connection-basics/presets.md#simple-button-preset-definitions) fallback for hosts which don't support all of the functionality of the module API (such as Bitfocus Buttons). List the variants most-preferred first:

```ts
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](../connection-basics/presets.md#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:

```ts
{
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](#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 `id`s must be unique within a single action or feedback definition, and config field `id`s 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`:

```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.
Loading