---
title: "Live Update"
description: "Live bidirectional connection to Designer data"
url: https://developer.disguise.one/api/session/liveupdate/
---

# Live Update API

The **Live Update API** is a websocket-based interface that enables web widgets to receive real-time updates on data held by Designer and request changes back to the main software. This documentation outlines the API’s connection lifecycle, message protocol, subscription mechanisms, and error handling. It also explains how changes are serialised using JSON and sent to clients.

## Using the interface

From a high level, the live update interface provides a mechanism to look up relevant objects in a Designer project and subscribe to its properties. This is done through a mix of Designer and Python expressions. We use Designer expressions to “find” a relevant object in the project, and then Python expressions to read (and potentially write) relevant data from it.

Some examples may be constructive:

| Object | Property | Description |
| --- | --- | --- |
| `track:track_1` | `object.lengthInBeats` | The length of “track 1” in beats (seconds). |
| `track:track_1` | `object.layers` | The layers associated with the track. |
| `track:track_1` | `object.getLeafLayers(VariableVideoModule)` | Retrieves all leaf layers of a specific type from the object. |
| `screen2:screen_1` | `object.description` | `"screen 1"` - the user-visible name of the object. |

### Higher-level framework integrations

We recommended using higher-level framework-specific interfaces to work with the functionality provided by this API. By working with this API directly, a developer must consider details relating to data management and integrity, which is largely solved by higher-level interfaces. Badly behaved clients which leak subscriptions have the potential to cause harm to productions.

The API overview below is useful for users of higher-level interfaces as well as those interested in implementing the WebSocket API directly.

Disguise have [provided a Vue composable](https://github.com/disguise-one/vue-liveupdate) for this reason.

## API Overview

The Live Update API provides a bidirectional communication channel via websockets, allowing:

-   **Realtime Subscriptions:** Clients can subscribe to updates on specific resources.
-   **Dynamic Data Changes:** Clients can issue commands to modify resource properties.
-   **Partial Updates:** The API supports partial modifications, merging new fields with existing data.

### Subscriptions

Subscriptions are built from a base `object`. Objects are referenced using Designer [expression syntax](https://help.disguise.one/designer/configuration/expressions/accessing-resources.html#identifying-resources).

The object serves as the jumping off point for any number of `properties`. Each property accesses the `object` using Python syntax, e.g. `object.description` to get the user-visible name of the object. The `object` variable is provided by the object expression in order to make property access less repetitive & marginally more efficient.

This object and set of properties are combined into a single [subscribe message](#subscribe) sent to the Director. Any number of subscribe and unsubscribe messages can be made in a given session, the only limit being performance and memory. Subscription and unsubscription is fast. As a general guideline, unsubscribe from data which is not visible to the user, e.g. when scrolling on a larger page.

Assuming that both the `object` and `property` are correctly specified, a [subscription list](#subscriptions-list) will be returned. The subscription list identifies the unique combination of object and property by a unique, never-repeating `id`.

The `id` is an integer value which is used by all subsequent messages to identify the property. This avoids repeating the potentially extremely long object & property names for every update and makes the subscription model of the API clear.

Every subscribe or unsubscribe action returns the full current status of all subscriptions for the client.

If a given client subscribes to the same property multiple times, they use the same `id`. Each subscription will need to be matched with a corresponding unsubscription for that property id, until the subscription is truly removed. This is achieved via a reference counting mechanism within Designer running on the Director.

This id reuse is useful when different components on a client page may subscribe to the same value, as the data is shared effectively between them, and resources are conserved. The reuse means it is not advisable to implement a client interface using the `id` as a primary key for anything related to subscriptions.

### Property updates

When a value is newly subscribed (this does not include a resubscription to the same value), and as the underlying resource values change, updates are [pushed to the client](#values-changed-notification).

The `id` is used to identify the property. Using `id` as a key into a local store of values is a reasonable idea, as the id will not change over the lifetime of the WebSocket connection.

### Property setting

Clients can update resource properties by sending a `set` command with the corresponding `id` and a new value.

The designer-internal live update system will distribute the changes throughout the session to all connected Designer software.

Changes made through set commands are undoable and persistent on the Director.

Not all values can be set. For example, a subscription like `{ object: 'track:track_1', 'properties': [ 'object.getLeafLayers(VariableVideoModule) ] }` will return all video layers on the named track, but will not allow the client to change any of those layers. Attempting to do so will result in an error. There is no way to know if setting a property will work before trying it, due to the complexities of the property expressions.

If a property value is a JSON object (e.g. `screen2:screen_1` / `object.offset` -> `{ x: 0.0, y: 3.0, z: 0.0 }`) it is possible to only set one field of the property. e.g. `{x: 4.0}` - this ensures that the other values are preserved, and is sometimes useful if a subscription over a complex object was made but the interface focuses on editing individual fields one-by-one. This ensures that fields the user wasn’t actively editing don’t get reverted due to edits by other components or clients.

### Error Handling & Concurrency

-   **JSON Parsing & Validation:** Invalid JSON or missing required fields (e.g., missing `"object"` or `"properties"` in a subscribe message) will result in an error response.
-   **Data Type Checks:** For unsubscribe messages, if non-integer IDs are provided, the system will notify the client while still processing valid IDs.

### JSON value serialisation

-   **Basic values:** Depending on the value type (e.g., integer, float, string), the appropriate JSON value is written.
-   **Structures:** For complex types, the function recursively constructs the JSON structure using these rules.
-   **Resources:** Resources within Designer are converted to a JSON object with keys `"uid"`, `"path"`, and `"type"`. `uid` is serialised as a hex string (`"0x0123456789abcdef"`)
-   **Arrays:** An array is sent, and the elements are serialised as normal.
-   **Tuples:** A Python tuple is sent as a JSON array, and the elements are serialised as normal.
-   **Dictionaries:** A Python dictionary is returned as a JSON object, where the values in the dictionary are serialised according to this table. This allows for more complex queries in a single property subscription.

## Advanced Property Expressions

Property paths are evaluated as Python expressions in an `eval` context. The variable `object` is bound to the resolved subscription object, and all Designer types are available (via `from d3 import *`). This means property paths support the full range of Python expressions — including method calls, list comprehensions, dictionary comprehensions, and chained property access.

### Chaining through resource references

When a subscription returns an object that contains resource references (such as a `Layer` or `Track`), those references are automatically resolved to their full Designer objects. This means you can chain property access through them.

For example, if `object.layer` returns a Layer reference, you can access the layer’s timing directly:

```python
object.layer.tStart
object.layer.tEnd
object.layer.tLength
```

### List comprehensions

When a property returns a list, you can use Python list comprehensions to transform, filter, or enrich the results in a single subscription.

**Filtering:** Return only items matching a condition:

```python
[m for m in object.getMappedMediaOfType(VideoClip, 0) if m.layer.tStart < 30.0]
```

**Enriching:** Add computed fields to each item:

```python
[{"media": m, "layerStart": m.layer.tStart, "layerEnd": m.layer.tEnd} for m in object.getMappedMediaOfType(VideoClip, 0)]
```

### Dictionary comprehensions

You can use dictionary comprehensions to reshape results into key-value structures:

```python
{m.mappedResource.uid: m.mappedResource.path for m in object.getMappedMediaOfType(VideoClip, 0)}
```

This would return a JSON object mapping each resource UID to its path.

### Worked example: media schedule with layer timing

The `MappedMediaDomain` provides `getMappedMediaOfType()` which returns all sequenced media of a given type. By default, each entry includes `contentSequencingStart` and `contentSequencingEnd` — but `contentSequencingEnd` reports `1.7976931348623157e+308` (the maximum float value) when the clip is the last in its sequence, meaning “unbounded”. In practice, such a clip stops when its parent layer ends.

Using a list comprehension, you can build a richer subscription that includes the layer timing alongside the media details:

```js
const subscribeMessage = { subscribe: {
    object: "domain:MappedMedia",
    properties: [
        '[{"resource": m.mappedResource, "track": m.track, "layer": m.layer, "layerStart": m.layer.tStart, "layerEnd": m.layer.tEnd, "layerLength": m.layer.tLength, "contentStart": m.contentSequencingStart, "contentEnd": m.contentSequencingEnd} for m in object.getMappedMediaOfType(VideoClip, 0)]'
    ]
} };
socket.send(JSON.stringify(subscribeMessage));
```

Each element in the returned array will contain the mapped resource, its track and layer references, the layer’s start/end/length times (in beats), and the content sequencing boundaries. When `contentEnd` is the maximum float value, use `layerEnd` as the effective end time.

All times are in **beats** (track time), consistent with Designer’s timeline coordinate system.

### Worked example: reading and setting keyframe values

Layers contain field sequences that hold keyframes for each animatable property. You can navigate this hierarchy using chained method calls to read — and set — individual keyframe values.

The path to a keyframe value follows the chain:

```text
Layer → findSequence(name) → .sequence → .key(index) → .v
```

Where:

-   **`findSequence(name)`** finds a `FieldSequence` by its decorated name (e.g. `"pos.x"`, `"pos.y"`, `"scale"`)
-   **`.sequence`** accesses the underlying `KeySequence` containing the keyframes
-   **`.key(index)`** returns the keyframe at the given zero-based index
-   **`.v`** is the keyframe’s value (a float for numeric fields)

To subscribe to the first keyframe of a layer’s X position:

```js
const subscribeMessage = { subscribe: {
    object: "track:track_1.getLeafLayers()[0]",
    properties: [
        'object.findSequence("pos.x").sequence.key(0).v'
    ]
} };
socket.send(JSON.stringify(subscribeMessage));
```

Because `.v` is a writable field, you can update it using a `set` command with the subscription’s `id`:

```js
const setMessage = { set: [
    { id: 123, value: 42.5 }
] };
socket.send(JSON.stringify(setMessage));
```

This sets the first X-position keyframe to `42.5`. Changes made through set commands are undoable and persistent on the Director.

You can also use a comprehension to subscribe to all keyframe values at once:

```python
[{"t": object.findSequence("pos.x").sequence.key(i).localT, "v": object.findSequence("pos.x").sequence.key(i).v} for i in range(object.findSequence("pos.x").sequence.nKeys)]
```

## Connection Lifecycle

This section focuses on the mechanics and syntax of the JSON messages passed between the client and the Director. The client initiates a connection to `ws://<director:port>/api/session/liveupdate`.

### Incoming messages

These messages are passed from the client to the Director.

### Subscribe

A subscribe message must include the target object and a list of property paths:

```js
{ "subscribe": {
    "object": "objectPath",
    "properties": ["propertyPath1", "propertyPath2"]
} }
```

For each property listed, a new subscription is created (using the provided object path). If the required fields are missing or the properties value is not an array an error is sent.

A subscribe message can also include the optional configuration object which supports configuring the frequency of updates received for the given properties subscribed through this request:

```js
{ "subscribe": {
    "object": "objectPath",
    "properties": ["propertyPath1", "propertyPath2"],
    "configuration": { "updateFrequencyMs": 1000 }
} }
```

Each property will send independent updates at most once every cycle determined by the duration in milliseconds specified in the configuration `updateFrequencyMs` property.

### Unsubscribe

Unsubscribe messages support either a single subscription ID or an array of IDs:

```js
{ "unsubscribe": { "id": 123 } }
```

or

```js
{ "unsubscribe": { "ids": [123, 456] } }
```

Non-integer values are detected and reported as errors. No further updates are sent after this message is processed.

### Set

The set command allows the client to update values for subscribed resources:

```js
{ "set": [
    { "id": 123, "value": <value> },
    { "id": 456, "value": <value> }
] }
```

The server locates the subscription by ID and updates its current value using a partial merge strategy. That is, if multiple set commands modify different fields of the same object, later commands merge with previous changes.

## Outgoing messages

These messages are passed from the Director to the client.

### Errors

Errors are reported using a JSON-formatted error like so:

```js
{ "error": <message> }
```

### Subscriptions list

When the set of subscribed properties changes, a full list of all subscribed properties is sent from the server. This is to avoid the client having to do any bookkeeping to keep the current set of subscriptions straight.

```js
{ "subscriptions": [
    { "id": <unique id integer>, "objectPath": <subscribed object>, "propertyPath", <subscribed property> }, ...
]}
```

### Values Changed Notification

When a new subscription is made or when values subsequently change within Designer, the server notifies clients with the following JSON structure:

```js
{ "valuesChanged": [
    { "id": 123, "value": <value>, "changeTimestamp": 257.1089659, "messageTimestamp": 265.5967602 },
    { "id": 456, "value": <value>, "changeTimestamp": 257.1089659, "messageTimestamp": 265.5967602 }
] }
```

-   **changeTimestamp**: indicates at which time in seconds relative to the start of the session was this property modified.
-   **messageTimestamp**: indicates at which time in seconds relative to the start of the session was this message sent.

## Examples

### Example 1: Establishing a Connection and Subscribing to a Resource

```js
// Open a live update websocket connection
const socket = new WebSocket('ws://director:80/api/session/liveupdate');

// Once connected, subscribe to a Track resource
socket.onopen = () => {
    const subscribeMessage = { subscribe: {
        object: "track:track_1",
        properties: ["object.lengthInBeats", "object.layers"]
    } };
    socket.send(JSON.stringify(subscribeMessage));
};

// Listen for incoming updates
socket.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('Update received:', data);
};
```

### Example 2: Sending a Set Command to Update a Value

```js
// Assume the subscription id is 123 from a previous subscribe command
const setMessage = {
    set: [
        { id: 123, value: 42.0 }
    ]
};
socket.send(JSON.stringify(setMessage));
```

### Example 3: Unsubscribing from a property.

```js
// Unsubscribe from a single subscription
const unsubscribeMessage = { unsubscribe: { id: 123 } };
socket.send(JSON.stringify(unsubscribeMessage));
```

### Examples: subscribing to graphs

#### Subscribing to a local graph:

```js
const subscribeMessage = { subscribe: {
    object: "subsystem:MonitoringManager.findLocalMonitor(\"fps\")",
    properties: ["object.seriesAverage(\"Actual\", 1)"]
} };
socket.send(JSON.stringify(subscribeMessage));
```

The second argument in the `seriesAverage()` function is the number of samples to average. By using a value of 1, we end up obtaining the latest value in the series.

#### Subscribing to a remote graph:

```js
const subscribeMessage = { subscribe: {
    object: "subsystem:MonitoringManager.findRemoteMonitor(\"IYPVM:d3\", \"fps\")",
    properties: ["object.seriesAverage(\"Actual\", 1)"]
} };
socket.send(JSON.stringify(subscribeMessage));
```

### Examples: subsribing to RenderStream status data

#### Receive statuses for all machines subscribed to streams within a workload:

```js
const subscribeMessage = { subscribe: {
    object: "subsystem:RenderStreamSystem",
    properties: ["object.getWorkloadReceiveStatuses(<workload ID>)"]
} };
socket.send(JSON.stringify(subscribeMessage));
```

How to obtain workload ID:

1.  Use a web request to `http://127.0.0.1/api/v1/renderstream/workloads` to list all workloads with their IDs
2.  Right-click on the title bar of the ‘Cluster Workload’ widget and select ‘Copy UID’ from the popup menu.

#### Receive statuses for all machines subscribed to a particular render node (machine):

```js
const subscribeMessage = { subscribe: {
    object: "subsystem:RenderStreamSystem",
    properties: ["object.getStreamReceiveStatuses(<workload ID>, <machine name>)"]
} };
socket.send(JSON.stringify(subscribeMessage));
```

How to obtain render node machine name:

1.  In Disguise:
    -   Open RenderStream layer and right-click on the **Workload** field.
    -   With the workload running, look at the **Instances** section of the **Cluster Workload** widget.
        -   The names of the render node machines are listed in the first column (**Machine** column)
2.  In Disguise, alternative:
    -   Look at the Cluster Pool used in a particular RenderStream layer: machines are listed by name.
3.  REST API:
    -   Using the `http://127.0.0.1/api/v1/renderstream/clusters` endpoint exposes the machines from the Cluster Pool as well.

#### All workload instances in a workload:

```js
const subscribeMessage = { subscribe: {
    object: "subsystem:RenderStreamSystem",
    properties: ["object.getWorkloadInstances(<workload ID>)"]
} };
socket.send(JSON.stringify(subscribeMessage));
```

This gives a list of all workload instances.  
Note that the health status for each workload instance is a computed property so it needs to be accessed by subscribing to the `health` property of a single instance like in the next example.

#### Health information about a single workload instance:

```js
const subscribeMessage = { subscribe: {
    object: "subsystem:RenderStreamSystem",
    properties: ["object.getWorkloadInstance(<workload ID>, <instance index>).health()"]
} };
socket.send(JSON.stringify(subscribeMessage));
```

#### Failover status of a machine;

```js
const subscribeMessage = { subscribe: {
    object: "getByUID(<machine UID>)",
    properties: ["object.hasTakenOverFailedMachine()"]
} };
socket.send(JSON.stringify(subscribeMessage));
```

OR

```js
const subscribeMessage = { subscribe: {
    object: "Machine:\"<machine name>\"",
    properties: ["object.hasTakenOverFailedMachine()"]
} };
socket.send(JSON.stringify(subscribeMessage));
```
