| name | ableton-extensions |
| description | Scaffold, write, build, and package Ableton Live extensions with the Ableton Extensions SDK (@ableton-extensions/sdk, TypeScript). Use when creating or editing an Ableton Live extension — adding context-menu actions, modal/progress dialogs, or code that manipulates tracks, clips, MIDI notes, devices, tempo, audio rendering, or selections in a Live Set. |
Ableton Extensions SDK
Build tools that run inside Ableton Live. An extension is a Node/TypeScript module bundled to a single CommonJS file that Live's Extension Host loads. It registers commands and wires them to context-menu actions, then reads and mutates the Live Set through a typed object model (tracks, clips, notes, devices, tempo, scenes).
Extensions are an offline editing / automation layer. They are not real-time processors and not devices in the signal chain: they cannot process the live audio or MIDI stream. The SDK has no note-on/off, MIDI-input, or stream callbacks (the only on* callbacks are dialog/registration lifecycle internals) — an extension only acts when the user triggers a command, then mutates the data model. For real-time MIDI/audio transformation (e.g. "play one key, hear a chord"), the right tool is a MIDI/audio effect device in the track chain: Live's built-in MIDI effects (the Chord device does exactly one-note→chord — set Shift voices to e.g. +4, +7) or a Max for Live device — not an Extension. Extensions complement those by transforming clips/notes at edit time, batch-editing the Set, and adding context-menu tools, dialogs, and rendering.
Local SDK location
The SDK distribution lives wherever you unpacked it — referred to below as <sdk-dir>. It contains:
docs/ — pre-built HTML documentation (getting-started, essentials, development, design)
api/ — TypeDoc HTML API reference
examples/ — 7 runnable examples (read these for canonical patterns)
ableton-create-extension-<version>.tgz — the scaffolding tool
ableton-extensions-cli-<version>.tgz — the build/run/package CLI
ableton-extensions-sdk-<version>.tgz — the SDK library
This skill was written against SDK 1.0.0-beta.0 (API version "1.0.0"); newer builds may ship a different <version> — check the actual .tgz filenames in <sdk-dir> and substitute it wherever it appears below. Requires Node.js >= 24.14.1.
Reference files (load as needed)
- reference/scaffolding.md — create a project, the generated file layout,
manifest.json / package.json / build.ts / tsconfig.json, the extensions-cli run|package commands, the .env / EXTENSION_HOST_PATH, debugging, log file locations, packaging to .ablx.
- reference/api.md — the full object model:
ExtensionContext, handles, transactions, Song/Track/Clip/MidiClip/AudioClip/Device/Scene, MIDI NoteDescription, enums (WarpMode, GridQuantization), context-menu scopes, selection types.
- reference/recipes.md — copy-paste code for every common task: context menus, modal dialogs (with the WebView bridge + Ableton-themed CSS), progress dialogs with cancellation, creating/editing clips and notes, audio import/render, transactions.
The examples/ folder is the source of truth for idiomatic code — prefer reading the closest example to the task over guessing.
The entry point — always this shape
src/extension.ts exports activate. Call initialize once, then register commands and menu actions synchronously:
import { initialize, type ActivationContext } from "@ableton-extensions/sdk";
export function activate(activation: ActivationContext) {
const context = initialize(activation, "1.0.0");
context.commands.registerCommand("myext.doThing", (arg: unknown) => {
});
context.ui.registerContextMenuAction("AudioClip", "Do Thing", "myext.doThing");
}
context (the ExtensionContext) is the root of everything:
context.application.song — the Live Set (tempo, tracks, scenes, …)
context.commands — registerCommand(id, cb) / executeCommand(id, …)
context.ui — registerContextMenuAction, showModalDialog, withinProgressDialog
context.resources — importIntoProject(path), render audio from a track
context.environment — storageDirectory, tempDirectory, language
context.getObjectFromHandle(handle, Class) — turn a Handle into a typed object
context.withinTransaction(() => …) — group mutations into one undo step
Two concepts that trip people up
Handles are not objects. Live passes commands lightweight Handles ({ id: bigint }). Resolve to a typed object with context.getObjectFromHandle(handle, Track). Handles go invalid when the object is deleted, moved in the arrangement, or the Set changes — never cache objects or handles across operations.
Commands receive scope-dependent args. Object scopes ("AudioClip", "MidiTrack", "ClipSlot", "Scene", …) pass a single Handle. Selection scopes pass a structured object: "AudioTrack.ArrangementSelection" / "MidiTrack.ArrangementSelection" pass an ArrangementSelection (time_selection_start, time_selection_end, selected_lanes: Handle[]); "ClipSlotSelection" passes { selected_clip_slots }. Match the scope you register to the arg type you cast.
Standard workflow
- Scaffold (new project):
npx file:<sdk-dir>/ableton-create-extension-<version>.tgz <dir> — interactive prompts for name, author, Live path, UI support. See reference/scaffolding.md.
- Write
src/extension.ts — register commands + menu actions; see reference/recipes.md.
- Run in Live:
npm start (builds with esbuild, then extensions-cli run loads it into the Extension Host). Enable Developer Mode first in Live's Preferences → Extensions.
- Debug:
console.log/warn/error → ExtensionHost.txt (paths in reference/scaffolding.md). In VS Code, F5 / "Debug Extension".
- Package:
npm run package → a distributable .ablx archive users drop into Live.
Conventions that matter
- TypeScript, ESM project, CJS bundle.
package.json is "type": "module"; build.ts bundles to format: "cjs", platform: "node". Keep it that way — the Extension Host loads CommonJS.
- Namespace command IDs (
"myext.action") to avoid collisions with other extensions.
- Most mutations are async (
Promise). Batch related edits in context.withinTransaction(() => promises) then await Promise.all(...) so the user gets one undo step and Live updates once.
- Narrow handle types defensively with
instanceof (e.g. resolve to Clip, then check clip instanceof AudioClip) — a menu scope can match objects the code must reject.
- HTML dialogs are inlined by esbuild (
loader: { ".html": "text" }) and passed as a data:text/html, URL — there's no web server.