Skip to main content

API 2.0 (Companion 4.3+)

This is a major series of changes — the first breaking changes since Companion 3.0, over three years in the making.

If you are not ready to tackle these changes, there is no rush. Many of the changes improve module functionality, but API 1.x remains fully supported and there are no plans to remove that support.

Required changes

Dropping support for Node 18

Node 18 stopped receiving security updates in early 2025, so we will not support it for API 2.0.

This also allows us to use newer language and platform features and to clean up the dependency tree.

If your module is set to use the node18 runtime in its manifest, it will need to be updated to node22.

@companion-module/base is now ESM

As part of dropping support for Node 18, this library is now written in the newer ESM syntax. For TypeScript users, update your tsconfig to use "moduleResolution": "nodenext", or, if using our tsconfig presets, extend @companion-module/tools/tsconfig/node22/recommended-esm.

tip

For most modules this will have no impact; you shouldn't need to change a JavaScript module.

Minimum @companion-module/tools version

In order to build your module correctly, you need to be using @companion-module/tools v2.7.1 or later

We will soon be releasing a v3.0.0 which will optimise the build process further and should be a drop in replacement.

Remove runEntrypoint method

The runEntrypoint method has been removed. Instead, change your main/default file to use a default export. Additionally, any upgrade scripts should be exported as a named UpgradeScripts constant.

Before:

class ModuleInstance extends InstanceBase {}

runEntrypoint(ModuleInstance, [someScript]) // or runEntrypoint(ModuleInstance, UpgradeScripts)

After:

export default class ModuleInstance extends InstanceBase {}

export const UpgradeScripts = [someScript] // or export { UpgradeScript }

If not using ESM syntax:

module.exports = class ModuleInstance extends InstanceBase {}

module.exports.UpgradeScripts = [someScript] // or similar

TODO: test this

Required manifest type property

To help us differentiate connection modules from surface modules, modules must now add a "type": "connection" to their companion/manifest.json.

If you don't already, we also recommend defining the $schema property to give your editor suggestions.

{
"$schema": "../node_modules/@companion-module/base/assets/manifest.schema.json",
"type": "connection",
"id": "your-module-name"
}

Additional runtime checks for modules

To help with the migration to the new API, Companion performs additional checks on your module and writes warnings to the module debug log. Keep an eye on these messages — Companion acts as a helpful second pair of eyes for things you may have missed.

Updates to setVariableDefinitions

To better match the other similar methods, setVariableDefinitions no longer expects an array of definitions, and instead expects an object

Before:

this.setVariableDefinitions([
{ variableId: 'variable1', name: 'My first variable' },
{ variableId: 'variable2', name: 'My second variable' },
])

After:

this.setVariableDefinitions({
variable1: { name: 'My first variable' },
variable2: { name: 'My second variable' },
})

Automatic expression parsing

One of the most requested features in Companion has been using variables in every action or feedback field. More recently, support for expressions has also been requested.

This has been challenging because it requires changes to upgrade scripts and may break assumptions modules have made about options. It also risks degrading the user experience for fields that expect specific values.

Because of this, only modules updated to API 2.0 will have expression support, so we can be confident modules won't break.

The parseVariablesInString method has been removed

Companion now automatically parses variables in input fields according to the option definitions and whether the field is in expression mode.

See below for details on how expressions are handled in options.

We believe there is no longer any reason to call parseVariablesInString directly, so it has been removed. If you have a valid use case, please let us know, we are open to restoring it.

Expression handling in options

As part of automatic parsing, Companion also executes expressions in action and feedback options.

Currently this applies to each field you define when:

  • When a field is of type textinput and has useVariables: true, variables will be parsed
  • Any field that does not have disableAutoExpression enabled and that the user has toggled into expression mode

When executing expressions in option values, Companion will ensure the computed value is valid according to the field definition. This means:

  • For a number field, the value will be clamped to the min and max you provide
  • For a dropdown field, the value must be a valid option (unless useCustom: true, in which case any string is accepted)
  • Similar behaviour applies to other field types

If this validation fails and we can't auto-correct it (eg clamping the number), then the action or feedback will be skipped, as if it was disabled. We do this so you don't receive values you are unprepared to handle. However, sometimes you may want to accept any value (for example, your callback may interpret 1 and 'ch1' both as 1). By setting allowInvalidValues: true on the option field, you will receive these non-standard values.

Some options don't make sense as expressions — for example, an option that chooses between on/off/toggle. Set disableAutoExpression: true on the field to disable expression support for that field.

tip

You can set an expressionDescription to show guidance beneath the expression input field explaining what values are valid.

If you use subscribe or unsubscribe on actions, consider setting the optionsToMonitorForSubscribe property to avoid unnecessary calls when unrelated options change. Some values may change multiple times per second and cause extra work for your module.

Using sensible values for options

Now that many fields can be toggled into expressions by the user, take a moment to consider if the values used by any dropdown fields are user friendly.

Many modules are using values here that make sense for how they talk to the device, but are not at all user friendly. Some examples:

  • Some modules are embedding osc path fragments
  • Some are doing channel1=0, channel2=1

Consider that the user will need to remember and write these. They will appreciate obvious and simple values.

If you want to be really friendly, you could use allowInvalidValues: true and try to be tolerant of typos.

tip

Now is the best time to address these, as you can assume that your upgrade script won't need to handle any expression values. If you do it later, you will need to handle expressions which are hiding the values being provided.

Expression handling in upgrade scripts

warning

This expression support introduces a breaking change for existing upgrade scripts.

Unfortunately, expression support means your upgrade scripts must be prepared for any field to be an expression. Because we cannot know whether an upgrade script was written before or after expression support, we require all upgrade scripts to handle the new shape for all inputs.

The key change is the shape of the options object. Previously you might have received:

options: {
a: 1,
b: "something",
}

You will now receive:

options: {
a: { isExpression: false, value: 1 },
b: { isExpression: false, value: "something" },
}

In future updates to your module some of those could be expressions, for example:

a: { isExpression: true, value: "$(local:a) + 1" }

Going forward, upgrade scripts must handle this new shape. You can safely assume that any upgrade scripts you write during this update (or prior) will not encounter isExpression: true for existing stored values. When writing future upgrade scripts, review the docs on upgrade scripts for advice on handling the new format.

For feedbacks, the isInverted property now uses the same shaped object.

Real example

Below is a real-world upgrade script pattern demonstrating how to walk actions/feedbacks and update numeric options while preserving expressions.

/**
* Since Companion 4.3, users can provide every value as an expression
* To make this easier for them, ensure numbers are 1 based (not 0 based)
*/
const offsetZeroBasedNumbers: CompanionStaticUpgradeScript<VideoHubConfig> = (_ctx, props) => {
const result: CompanionStaticUpgradeResult<VideoHubConfig, undefined> = {
updatedConfig: null,
updatedSecrets: null,
updatedActions: [],
updatedFeedbacks: [],
}

const offsetValue = (options: CompanionMigrationOptionValues, key: string) => {
if (!options[key]) return

if (options[key].isExpression) {
options[key].value = `${options[key].value} + 1`
} else {
options[key].value = Number(options[key].value) + 1
}
}

for (const action of props.actions) {
const rule = fixupActionsForExpressions.find((r) => r.basicName === action.actionId)
if (!rule) continue

for (const prop of rule.numberProps) {
offsetValue(action.options, prop)
}

result.updatedActions.push(action)
}
for (const feedback of props.feedbacks) {
const rule = fixupFeedbacksForExpressions.find((r) => r.basicName === feedback.feedbackId)
if (!rule) continue

for (const prop of rule.numberProps) {
offsetValue(feedback.options, prop)
}

result.updatedFeedbacks.push(feedback)
}

return result
}

See the upgrade-scripts documentation for more patterns and helpers.

For years many modules performed their own variable parsing in action and feedback fields. Now that any field can be an expression, some options would benefit from clearer field types or removal of duplicate actions/feedbacks.

Cleaning up now improves the user experience by reducing confusion and providing better hints about valid values.

For example, if you have number fields implemented as textinput so users could type variables, change them to a number field and use an upgrade script to convert previous inputs to expressions. We provide a helper FixupNumericOrVariablesValueToExpressions:

const result1 = FixupNumericOrVariablesValueToExpressions({ isExpression: false, value: '1' })
// returns: { isExpression: false, value: 1 }

const result2 = FixupNumericOrVariablesValueToExpressions({ isExpression: false, value: '$(local:abc)' })
// returns: { isExpression: true, value: '$(local:abc)' }

const result3 = FixupNumericOrVariablesValueToExpressions({ isExpression: false, value: '$(local:abc)$(local:def)' })
// returns: { isExpression: true, value: 'parseVariables("$(local:abc)$(local:def)")' }
tip

Doing this cleanup now is easier than doing it later, since you don't yet need to handle stored isExpression: true values.

Referencing expressions from `isVisibleExpression

When a field's value comes from an expression, you cannot reference it from another field in an isVisibleExpression.

We disallow this because it can lead to fields being hidden when they are actually needed later as the computed expression value changes, which would confuse users and prevent them from editing required fields.

Presets overhaul

Presets have been reorganised in this release to make large preset sets easier to manage and display.

For some time we've found presets can be repetitive and benefit from additional structure. API 1.8 (Companion 3.3) introduced a text preset type to help with layout; the new approach formalises that idea into proper groups.

Overhauling preset definition structure

Some modules have many presets, and categories alone are no longer sufficient to present them clearly.

Previously some modules used the text definition to break large blocks into well-described sections. That structure is now formalised: presets should use a hierarchy of sectionsgroupsdefinitions.

To support this, setPresetDefinitions has been reworked and now expects two parameters: structure and presets.

The presets parameter should be an object listing preset definitions:

const presets = {
something: {
type: 'simple',
name: 'My preset',
style: { ... },
steps: [ ... ],
},
another: {
type: 'simple',
name: 'My preset',
style: { ... },
steps: [ ... ],
}
}
tip

Make sure to change the type property to simple. We intend to add additional types in a future release. Also remove the old category property.

Using this object for presets lets you define a preset once and reference it in multiple places within the structure (see Preset templating).

The structure array describes how to present these presets in the UI. For example:

const structure = [
{
id: 'a',
name: 'Main A',
// optional description
definitions: [
{
id: 'b',
type: 'simple',
name: 'Output 1',
// optional description
presets: ['something', 'another'],
},
],
},
]

This replaces the old category property that was previously defined on each preset. The new structure lets you present presets with more organisation and give each section or group its own description.

If you were using the text definition type before, replace those entries with groups.

tip

More group types are supported — see Preset templating for details.

Local variables in presets

Companion 4.1 added support for user-defined local variables. While actions and feedbacks could already use these variables, presets could not define them.

Presets can now declare 'user value' local variables:

localVariables: [
{
variableType: 'simple',
variableName: 'output',
startupValue: 1,
},
],

This makes presets easier for users to edit by providing a single place to adjust values instead of editing multiple actions and feedbacks.

tip

This pairs nicely with the template preset group, enabling simple templating within presets.

Preset templating

Modules often produce many similar presets that differ only by a few values.

To help, we now support templating in presets via a template group type that overrides local variable values.

For example:

// The template definition in the presets object (named 'mute-template')
{
type: 'simple',
name: 'Mute input X',
style: { ... },
steps: [ ... ],
localVariables: [
{ variableType: 'simple', variableName: 'input', startupValue: 0 },
],
}

// A template group that instantiates the template with different values
{
id: 'mute',
name: 'Mute Input',
type: 'template',
presetId: 'mute-template',
templateVariableName: 'input', // The name of the local variable being templated
templateValues: [
{ name: 'Mute input 1', value: 1 },
{ name: 'Mute input 2', value: 2 },
]
}

Companion will render this in the UI as two presets generated from the template, for example:

{
type: 'simple',
name: 'Mute input 1',
style: { ... },
steps: [ ... ],
localVariables: [ { variableType: 'simple', variableName: 'input', startupValue: 1 } ]
},
{
type: 'simple',
name: 'Mute input 2',
style: { ... },
steps: [ ... ],
localVariables: [ { variableType: 'simple', variableName: 'input', startupValue: 2 } ]
}

We hope this simplifies preset generation logic and reduces presets' data size, improving memory usage and performance in Companion.

Dropping support for absolute delays in presets

Support for absolute delays in presets has been removed; all preset delays are now interpreted as relative.

This matches Companion's behaviour, which only supports relative delays through the internal: Wait action and avoids confusing translations.

TypeScript reworking

The TypeScript types in the library have received significant attention to help you strongly type various aspects of your module.

InstanceBase<T> generic change

The InstanceBase class remains generic, but it no longer expects a TConfig. Instead it expects a type that describes your config, actions, feedbacks, and variables.

If no type is specified, the default is:

export interface InstanceTypes {
config: JsonObject
secrets: JsonObject | undefined
actions: Record<string, CompanionActionSchema<CompanionOptionValues>>
feedbacks: Record<string, CompanionFeedbackSchema<CompanionOptionValues>>
variables: CompanionVariableValues
}

In your code you can extend this interface to get the same behaviour as before:

export interface MyTypes extends InstanceTypes {
config: MyConfig
}

or you can write your own from scratch.

Strongly typed actions and feedbacks

With the generic change on InstanceBase, types now propagate through to related types such as:

  • CompanionActionDefinition<CompanionOptionValues>
  • CompanionFeedbackDefinition<CompanionOptionValues>

On the InstanceBase class, the definition of setActionDefinitions (and similar methods) will now match the types you declare in your InstanceTypes, providing stronger typing and better IDE assistance.

For example:

const act: CompanionActionDefinition<{ num: number }> = {
name: 'My First Action',
options: [
{
id: 'num',
type: 'number',
label: 'Test',
default: 5,
min: 0,
max: 100,
},
],
callback: async (event) => {
console.log('Hello world!', event.options.num)
},
}

In this example TypeScript knows that event.options.num is a number and that a value such as event.options.other does not exist.

tip

You need to ensure that the generic and your option types match; we cannot do that automatically.

Combined with the Automatic expression parsing, this should ensure that the types you declare are what you actually receive and can tidy up a lot of logic.

InstanceTypes also defines variables, which is propagated and gives you type hints when updating variable values.

Miscellaneous changes

New logging utility

To improve logging in your module, you can now produce a full range of log levels without passing around your class instance.

Loggers created this way work from anywhere in your module and route to the same destination as the instance methods.

These loggers can be created with an optional prefix, which appears in the log output and helps produce structured logs.

import { createModuleLogger } from '@companion-module/base'

const logger = createModuleLogger('SomePrefix')

logger.error('something happened!')

Control over UI order of actions and feedbacks

Companion sorts your action and feedback definitions by name when displaying them to the user.

If this is undesirable, a new sortName property allows you to control sorting. When set, sortName is used instead of name; its value is not shown to users.

Clarifying this.checkFeedbacks() usage

In very old versions of Companion, it was expected that modules should call this.checkFeedbacks() without any arguments in order to trigger a check of all the feedbacks.

Many versions back, it became possible to supply the types of feedbacks as an argument, to allow for only rechecking a subset of the feedbacks upon each call.

Due to this dual behaviour, it is easy for a module to call this.checkFeedbacks() without realising it was bad practice, especially with all the existing code showing exactly that.

To clarify the intended usage, this older behaviour has been removed. With a new this.checkAllFeedbacks() method being added instead.

// before
this.checkFeedbacks()

// after
this.checkAllFeedbacks()

// this is recommended and remains valid
this.checkFeedbacks('my-feedback')
tip

We strongly recommend using checkFeedbacks with one or more arguments whenever possible, as it reduces the amount of work your module will do whenever a feedback changes

Usages of this.checkFeedbacksById(...) are unaffected by this change, and remain valid.

Feedback lifecycle simplification

Because Companion now automatically parses variables in feedback options, the subscribe method became confusing: any option change could trigger unsubscribe, subscribe, and callback in sequence.

The lifecycle has been simplified by removing subscribe and simplifying when unsubscribe is called.

Now, callback is invoked whenever the feedback should run and is the only method called when the feedback is added or its options are updated. unsubscribe is called only when the user deletes or disables the feedback, allowing you to do cleanup when the feedback becomes inactive.

To help track state, previousOptions is now provided to the callback. This contains the values from the previous call so you can detect relevant changes without tracking state yourself.

Learn callback return value changes

If you implement a learn callback in your actions or feedbacks, expectations for its return value have changed.

Previously you were expected to return all options; now you should only return the options being "learnt" in this call.

If you continue returning all options you may overwrite expressions the user has entered in other fields.

Replace action optionsToIgnoreForSubscribe with optionsToMonitorForSubscribe

As part of the expression changes, optionsToIgnoreForSubscribe has been replaced with an allowlist optionsToMonitorForSubscribe.

This reduces the chance of forgetting to update it when you add new options. When not set, it is treated as if all options are monitored.

Replacing required property on some option field types

The required property has been unclear for some time.

Instead, the textinput field now supports a minLength property, which offers similar behaviour but greater control.

Bonjour query IPv6 support

Bonjour queries can now opt into IPv6 support by setting the addressFamily field in a query. This can be ipv4, ipv6, or ipv4+6.

Convert 'isVisible' functions to 'isVisibleExpression'.

Since API 1.12 (Companion 4.0), it has been possible to define isVisible expressions using the isVisibleExpression property.

The old function-based isVisible approach is no longer supported and will be ignored.

tip

When used in actions or feedbacks, expressions are only allowed to reference fields that cannot be an expression. See Automatic expression parsing for more details.

Examples

"isVisible": (opts) => !!opts.otherField
"isVisibleExpression": `!!$(options:otherField)`
"isVisible": (opts) => opts.otherField == 1
"isVisibleExpression": `$(options:otherField) == 1`