# Ableton.js: driving Ableton Live from Node, Deno or the browser

> Ableton.js exposes Ableton Live to JavaScript through a MIDI Remote Script that opens a WebSocket on the local machine, with typed getters and setters for song state, property listeners, an envelope protocol with UUID correlation, an ETag cache and optional HMAC-SHA256 authentication. It is MIT licensed and works with Live 11 or later.

**leolabs/ableton-js** — Control Ableton Live with Node.js

- Repository: https://github.com/leolabs/ableton-js
- Stars: 532 · Forks: 73
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/leolabs-ableton-js

## The shape of the integration

Ableton.js lets you control Ableton 11 or later using Node.js, Deno, or a browser, and the README is upfront that the package is still a work in progress whose goal is to expose all of Ableton's MIDI Remote Script functions to TypeScript. That framing tells you the architecture. There is no closed protocol reverse engineered from the outside, no library that talks to Live through some private channel. Instead, Ableton's own remote scripting API is the mechanism, and the project provides a MIDI Remote Script that acts as a server, plus a TypeScript client that talks to it. The consequence is that coverage follows Live's own API surface, so a function the remote script does not expose is a function this library cannot call, and a new Live release may add surface that takes time to appear here. The project is MIT licensed, the primary language is TypeScript, the release history is steady with v5.0.4 from 2026-10-12, v5.0.3 from 2026-09-21 and v5.0.2 from 2026-09-19, and the last push to the repository was on 2026-10-16. The package is published on npm as ableton-js.

## Installing it means putting a script inside Live

Ableton.js requires Ableton Live 11 or later, and the setup has a step that is unusual for a JavaScript library because it involves copying files into the application. To use the library you need to install and activate the MIDI Remote Script in Ableton, which means copying the midi-script folder of the repository to Ableton's Remote Scripts folder and renaming it to AbletonJS. The Remote Scripts folder is usually at ~/Music/Ableton/User Library/Remote Scripts. After starting Ableton Live you add the script to your list of control surfaces, in the preferences, which is what activates it. The repository carries npm scripts that automate the whole sequence on macOS, and the launch scripts are the clearest description of the steps involved.

```json
    "ableton:clean": "rm -f midi-script/AbletonJS/*.pyc",
    "ableton:copy-script": "set -- ~/Music/Ableton/User\\ Library/Remote\\ Scripts && mkdir -p \"$1\" && rm -rf \"$1/AbletonJS\" && cp -r \"$(pwd)/midi-script\" \"$1/AbletonJS\" && rm -rf \"$1/AbletonJS/_Framework\"",
    "ableton11:launch": "set -- /Applications/Ableton*11* && open \"$1\"",
    "ableton:logs": "tail -n 50 -f ~/Library/Preferences/Ableton/*/Log.txt | grep --line-buffered -i -e AbletonJS",
    "ableton11:start": "yarn ableton:kill; yarn ableton:clean && yarn build:static && yarn ableton:copy-script && yarn ableton11:launch && yarn ableton:logs",
```

The copy step removes the previous copy before copying and then deletes the _Framework directory from the installed copy, which is the kind of detail that shows the script has been iterated against a real installation. The start script chains the whole flow, killing any running Live instance, cleaning stale bytecode, building the static bundle, copying the script, launching the application and then tailing the log file filtered for the script's own output.

## The port and the password, and how authentication works

The MIDI Remote Script opens a WebSocket server on 127.0.0.1:39031 by default, and the bind address and port can be changed in midi-script/Config.py through the WEBSOCKET_HOST and WEBSOCKET_PORT settings. The JavaScript client has to use the same port, passed as new Ableton({ port }), which means a configuration change on the script side has a matching change on the client side and a mismatch shows up as a connection failure rather than as a subtle bug. Authentication is optional and configured on the script side by setting PASSWORD in Config.py to a string and passing the same value to new Ableton({ password }). Auth is off when PASSWORD is None or empty, so a default install listens without any credential at all. When it is enabled, the plugin sends a per-connection salt and the client authenticates with HMAC-SHA256, so the password itself is never sent in plaintext. The README is careful about the limit of that protection, stating that the WebSocket is still unencrypted after login with ws://, and that binding to loopback is preferable if possible. That is the right advice and it is worth taking literally, since a local WebSocket with a salted challenge response still passes every subsequent message in the clear on the loopback interface.

## The API is a class with async getters, setters and listeners

The library exposes an Ableton class which lets you control the entire application, and the README suggests instantiating it once and using TypeScript to explore the available features, which is a sensible way to discover an API that mirrors Live's own object model. The example shows the shape of a session.

```ts
import { Ableton } from "ableton-js";

// Log all messages to the console
const ableton = new Ableton({ logger: console });

const test = async () => {
  // Establishes a connection with Live
  await ableton.start();

  // Observe the current playback state and tempo
  ableton.song.addListener("is_playing", (p) => console.log("Playing:", p));
  ableton.song.addListener("tempo", (t) => console.log("Tempo:", t));

  // Get the current tempo
  const tempo = await ableton.song.get("tempo");
  console.log("Current tempo:", tempo);

  // Set the tempo
  await ableton.song.set("tempo", 85);

  // Commands started in the same tick are sent together
  const tracks = await ableton.song.get("tracks");
  await Promise.all(
    tracks.map((t) => t.addListener("name", (n) => console.log(t.raw.id, n))),
  );
};

test();
```

The three verbs are consistent across the object model. A get is awaited and returns the current value, a set is awaited and applies a change, and an addListener registers a callback that fires when the property changes. A listener on a collection element, as on each track, works the same way, and the raw property is available on the object for identification, which matters when you are tracking individual tracks by their id. The bundling behaviour is the detail that makes the API pleasant in practice, since commands started in the same JavaScript tick, for example through Promise.all, are combined into a single WebSocket round trip rather than sent one after another.

## The protocol is JSON envelopes with UUIDs and an ETag cache

The protocol section explains what actually goes over the socket. Each message is a JSON text frame containing the required data and a UUID so that a request and its response can be associated. The Remote Script is the server and it supports connections from multiple clients, and the routing is worth noting, since command replies and property listener updates are sent only to the client that issued the command or subscribed. Removing a listener, or disconnecting, drops that client's subscriptions only, although Live itself still uses a single listener per property until the last subscriber leaves. The request envelope carries one or more commands, each with a namespace, a namespace id for addressing a specific track or device, a command name and arguments.

```js
{
  "uuid": "1", // Envelope id
  "commands": [
    {
      "ns": "song", // The command namespace
      "nsid": null, // The namespace id, for example to address a specific track or device
      "name": "get_prop", // Command name
      "args": { "prop": "current_song_time" }, // Command arguments
      "etag": "4e0794e44c7eb58bdbbbf7268e8237b4", // MD5 hash of the data if it might be cached locally
      "cache": true // If this is true, the plugin will calculate an etag and return a placeholder if it matches the provided one
    }
  ]
}
```

The response is a JSON object with a data array holding one result per command, an event of result or error, and the same UUID that was sent.

```js
{
  "data": [{ "ok": true, "data": 0.0 }], // Per-command results
  "event": "result", // This can be 'result' or 'error'
  "uuid": "1" // The same UUID that was used to send the envelope
}
```

The error handling is granular in a useful way. A failed command appears as an ok of false with an error string in its slot, while other commands in the same envelope still run, and a top-level event of error means the whole envelope failed, for example because of authentication or a malformed payload. The caching scheme is built around that etag field. Certain props are cached on the client to reduce repeated payloads, and the plugin generates an MD5 hash of the prop, calls it an ETag, and sends it along with the data. The client stores both the ETag and the data in an LRU cache and sends the latest stored ETag when the same prop is requested again, and if the data still matches the plugin responds with a placeholder object and the client returns what it already had.

## Events, the static server, and the project that proves it works

There are a handful of events for lower level insight, and they cover the lifecycle of the connection and the traffic on it.

```ts
// A connection to Ableton is established
ab.on("connect", (e) => console.log("Connect", e));

// Connection to Ableton was lost,
// also happens when you load a new project
ab.on("disconnect", (e) => console.log("Disconnect", e));

// A raw message was received from Ableton
ab.on("message", (m) => console.log("Message:", m));

// A received message could not be parsed
ab.on("error", (e) => console.error("Error:", e));

// Fires on every response with the current ping
ab.on("ping", (ping) => console.log("Ping:", ping, "ms"));
```

Two of those deserve a note. The disconnect event also fires when you load a new project, which is behaviour a caller has to handle, since a setlist or show script will see the connection drop whenever a set is loaded rather than only when Live quits. And the ping event reports the current ping on every response, which is the cheapest available measure of whether Live is busy. Ableton.js also serves files located in midi-script/static on the same port as the WebSocket, so you can host your own web app from within Live, and by default that folder contains a bundled version of the library and a simple index.html that lets you use the browser console to try the API without writing anything. The README then carries a sponsored message pointing to AbleSet, a setlist manager built with the library, which allows managing and controlling setlists from any device, reordering songs, adding notes to them and getting an overview of the current state of the set. That is the project's own proof that the API covers a substantial real application, and it is a good model for what to build on top of it. The repository layout matches a mature TypeScript project, with a src/ directory, a midi-script/ directory, hooks/, test-fixtures/, biome.json for linting and formatting, vite and vitest configurations, two tsconfig files and a yarn.lock.

## Where Ableton.js is the wrong choice

The first limit is that it needs Live itself. There is no offline mode and no way to exercise the client without the application running with the remote script installed and activated, so any test suite or CI pipeline for your own project has to account for a licensed copy of Live and a running instance, which is a real obstacle for continuous integration. The second is the transport, since everything travels over a local WebSocket with no encryption after login, and the README's own advice is to bind to loopback, which means the design assumes a single user on a single machine. A remote control scenario, with Live on one host and the controlling program on another, would need a tunnel and would then be running an unencrypted protocol across it. The third is the completeness caveat, since the goal is to expose all of Ableton's MIDI Remote Script functions and the package is described as a work in progress, so coverage will be uneven across the Live feature set, and the failure mode is a missing method rather than a clear error. The fourth is that the project is maintained by one person in what the README presents as spare time, so the release cadence and the response to Live version changes are a single point of failure. The fifth is the mismatch with Live's own surface for controller work, since a MIDI Remote Script in Live is designed to be driven by a physical controller's input and this library drives the same API from a program, which is powerful for automation but is not a substitute for a controller script when the goal is a playable surface.

## Conclusion

Adopt Ableton.js if you want to script or control Ableton Live from a program, for instance a setlist manager or a live performance tool, and you can install a MIDI Remote Script into Live's user library. Skip it if your automation needs are met by Live's own MIDI mapping, since this is a programmatic interface rather than a controller surface. Verify first that you are on Live 11 or later, that the WebSocket port in midi-script/Config.py matches the port you pass to the client, and that you set a password if anything other than your own machine can reach that port.

## FAQ

### How do I install Ableton.js and connect it to Ableton Live?

You need Ableton Live 11 or later. Copy the midi-script folder from the repository into Ableton's Remote Scripts folder, usually at ~/Music/Ableton/User Library/Remote Scripts, and rename it to AbletonJS, then add the script to your list of control surfaces in Live's preferences. The script opens a WebSocket on 127.0.0.1:39031 by default, and on macOS the repository's yarn ableton11:start script automates the copy, launch and log tailing.

### How does the Ableton.js protocol work?

It uses a WebSocket to talk to the MIDI Remote Script. Each message is a JSON text frame with a UUID so a request and its response can be associated, and a request envelope carries one or more commands, each with a namespace, a namespace id, a name and arguments. Commands started in the same JavaScript tick are combined into a single round trip, and a failed command returns an ok of false in its own slot while the others still run.

### Does Ableton.js support authentication and caching?

Authentication is optional, configured by setting PASSWORD in midi-script/Config.py and passing the same value to new Ableton({ password }), and it is off when the value is None or empty. When enabled the plugin sends a per-connection salt and the client authenticates with HMAC-SHA256, though the WebSocket itself remains unencrypted. Caching uses an MD5 hash of a property as an ETag, stored with the data in an LRU cache on the client, so an unchanged property returns a placeholder instead of the payload.

## Sources

- [Issues](https://github.com/leolabs/ableton-js/issues)
- [leolabs/ableton-js on GitHub](https://github.com/leolabs/ableton-js)
- [License: MIT](https://github.com/leolabs/ableton-js/blob/master/LICENSE)
- [README](https://github.com/leolabs/ableton-js/blob/master/README.md)
- [Releases](https://github.com/leolabs/ableton-js/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/leolabs-ableton-js
