Automation scripts
On this page
The Java extension exposes a newline-terminated text-command/JSON-response protocol on loopback. Scripts can use it without the Python MCP adapter. Offline composition, export and read-only Nitro evidence are Python facilities, not native socket commands. The MCP adapter adds their schemas and reviewed workflow state alongside live validation, shaping and recovery.
Version 0.2.0 names the extension/Python package. Protocol 3 is the socket contract; public Controller API 25 is the extension dependency. Private native operations are supported only on Bitwig Studio 6.1.1 and remain capability-gated. The MCP layer exposes 90 tools, not 90 interchangeable raw socket commands.
Live-script prerequisites
- Install and enable Bitwig Grid Bridge in Bitwig.
- Keep Bitwig running with the target project open.
- Confirm that
127.0.0.1:8765is not occupied by another process. - Read capabilities and selected-device state before mutating anything.
Extension reload after copying the JAR/.bwextension is asynchronous. Wait for
read-only capabilities/readiness from the expected version before any write,
then refresh all identities. Never probe readiness or retry a timeout with a mutation.
The protocol is loopback-only. It has no remote transport, cloud service, or authentication layer.
Run the included example
From a source checkout or release bundle:
python examples/automation/grid_bridge_demo.py inspect
python examples/automation/grid_bridge_demo.py graph
inspect works for any selected device and reports the exposed-control surface. graph requires a supported selected Grid.
The example also demonstrates reversible mutations:
python examples/automation/grid_bridge_demo.py sweep --index 2 --minimum 0.2 --maximum 0.8 --duration 4
python examples/automation/grid_bridge_demo.py insert-fx-grid --position after --keep
Omit --keep to let the example restore or undo its temporary change. Read the command output and verify the Bitwig state before running another operation.
Device insertion must first read the current selection_token; the wire form
is insert POSITION UUID SELECTION_TOKEN, not the old three-token command.
See the guarded device sequence below.
Protocol shape
Open a TCP connection to 127.0.0.1:8765. Send one UTF-8 command line, not a
JSON request; read one JSON response line. Protocol 3 examples:
capabilities
state
graph-state
song-state
Check both the response's ok field and operation-specific status. In
particular, an import receipt can be accepted by the bridge while still
queued, native_rejected or unverified. A closed socket, malformed response
or missing ok is not success.
Keep the exchange serialized: one request, one response. The extension schedules Bitwig API work on Bitwig's host thread before returning.
Minimal Python client
import json
import socket
def request(command: str):
if not command or "\n" in command or "\r" in command:
raise ValueError("Expected one command line")
encoded = command.encode("utf-8") + b"\n"
if len(encoded) > 1024 * 1024:
raise ValueError("Command exceeds client limit")
with socket.create_connection(("127.0.0.1", 8765), timeout=65.0) as connection:
with connection.makefile("rwb") as stream:
stream.write(encoded)
stream.flush()
line = stream.readline(8 * 1024 * 1024 + 1)
if not line.endswith(b"\n") or len(line) > 8 * 1024 * 1024:
raise RuntimeError("Missing, incomplete or oversized bridge response")
response = json.loads(line)
if not isinstance(response, dict) or response.get("ok") is not True:
raise RuntimeError(f"Bridge request failed: {response!r}")
return response
capabilities = request("capabilities")
state = request("state")
print(capabilities)
print(state)
Open a new connection for a small one-off request, as above. A longer script may reuse one connection, but it must preserve request/response order and close the connection on any framing or timeout error.
Safe script structure
A mutation script should have five explicit phases:
- Read capabilities and current state.
- Validate selected device, revision, IDs, indexes, and value ranges.
- Record enough state to undo or restore the intended change.
- Mutate once.
- Read back and compare the observable result.
Do not turn a failed check into a warning and continue. A bridge rejection protects the user's project from stale or unsupported operations.
Guarded device edits
Read inspect and devices after selecting the intended track/device.
devices exposes a 16-sibling window with window_complete, native
identities and selection_token, not a complete recursive chain. Selected-device
properties also expose instance_id, track_id, selection_token and
selection_guard_available/reason. Public state/inspect reads may retain null
private identities; a null token cannot authorize native device edits.
Resolve the exact UUID through device-catalog, review the change, then use
one of these descriptive wire forms with freshly observed values:
insert POSITION UUID SELECTION_TOKEN
device-remove SELECTION_TOKEN
Positions are before, after, start, end and replace. All require a
selected track; before/after/replace also require a selected device. Start/end
work on empty tracks. Replacement removes the need to stack a new instrument
after an unintended import-created Organ. Removal targets the exact selected
native device. The document-thread edit revalidates identities and selection;
re-read the sibling window afterward. Do not replay an ambiguous mutation.
MCP equivalents are grid_insert_device(position, device_id, expected_selection_token) and grid_remove_device(expected_selection_token),
each with confirm: true or explicitly authorized cooperative: true.
Exposed-control automation
The selected-device response contains exposed parameter indexes and values. Those indexes belong to the current selected-device snapshot.
For sweeps or staged changes:
- use native exposed-control values 0–1 (
set INDEX VALUE); MCP uses 0–128; - use a monotonic clock, not accumulated
sleepdurations, for timing; - limit write frequency so Bitwig's host thread remains responsive;
- restore the original value in
finallyunless the caller explicitly asks to keep it; - stop when selection or state no longer matches the initial target.
The included sweep example implements these guardrails. Prefer it over copying an unbounded write loop.
Graph automation
Graph operations require graph_available: true. Each operation must use data from the latest graph response:
- package IDs from the live catalog;
- instance IDs from the live graph;
- native parameter IDs and types from the live graph;
- input and output indexes from the relevant module snapshot;
- coordinates from the current graph layout;
- the exact reviewed graph revision via
guard REVISION GRAPH_COMMAND....
After insertion, disconnection, parameter changes, or undo, request the graph again. Never issue a series of dependent graph mutations from the first snapshot.
For an explicitly intended destructive clear, preserve the reviewed graph,
authorize guard REVISION graph-clear, and re-read afterward. MCP
grid_clear_graph requires expected_revision and authorization and preserves
a before snapshot; that evidence is not an automatic restore operation.
Native parameter IDs are exact printable strings, not uppercase symbols. Protocol 3 encodes them separately from values:
import base64
def token(text: str) -> str:
return base64.urlsafe_b64encode(text.encode("utf-8")).decode("ascii")
def resolve_native_value(module_id: str, parameter_id: str, display_value: str):
return request(
f"graph-resolve-value {module_id} {token(parameter_id)} {token(display_value)}"
)
Use observed IDs and a compatible musical display such as 65 ms. Resolution
does not mutate: review its value and revision, then authorize the guarded
graph-set MODULE PARAMETER_B64 VALUE separately. Booleans use true/false;
enum numbers come from live options. Read actual native value/display afterward.
See Protocol and lifecycle for all command forms and limits.
Offline full-song delivery
From the project's Python environment, this creates a real full-duration type-1 MIDI and manifest without a running Bitwig instance:
from pathlib import Path
from bitwig_mcp_server.track_workflow import TrackWorkflow
workflow = TrackWorkflow(output_dir=Path.home() / "Music" / "BitwigGridBridge")
review = workflow.start(
"Minimal techno, 138 BPM, D minor, 4/4, 4 minutes, 8-bar phrases",
density=0.3,
motion=0.35,
seed=42,
)
print(review["plan"]) # Review sections, roles, constraints and bounded routes.
artifact = workflow.export(review["session_id"], review["revision"])
print(artifact["midi_path"], artifact["manifest_path"], artifact["artifact_sha256"])
The producer workflow documents the accepted grammar and limits. This is MIDI composition, not rendered audio or installed instruments. Synth recipes and modulation routes are intentions, not applied Grid graphs or MIDI CC automation. The example's review authority belongs to this Python process: a separately started MCP adapter cannot adopt its session/export. For MCP delivery, start, review and export in that adapter.
For live delivery, prefer grid_track_import through the same MCP session that
owns the reviewed export. It checks revision, artifact digest, observed project
token and one-attempt authority. Raw song-import does not create that Python
review history. Native import submits a private derived MIDI with tempo/meter
metadata removed, reports its distinct hash, and inserts after the selected
native target at beat 0. The transformation is not a general raw-MIDI sanitizer:
it removes tempo/meter events; notes/markers describe compiler-generated input.
Set global tempo/meter explicitly. Resolve each imported role by fresh flat
bank index (not native position), then call song-arranger INDEX or MCP
grid_inspect_arranger(track_index). Hidden group children, FX and master are
included in the bank; inspection covers only that track's primary arranger list.
Explicit transport seek uses song-transport-seek PROJECT_TOKEN POSITION_BEATS
or authorized grid_song_transport with operation: "seek" and
position_beats. Its finite range is 0–24576 quarter-note beats; recording
blocks the write. Read back grid_song_state.position_beats, then authorize
play separately. This selects the controller-owned project's position, not
an application-wide PlayFromStart action, and never changes tempo/meter.
Global arranger-loop control uses song-transport-loop PROJECT_TOKEN false
(literal true/false) or authorized grid_song_transport with
operation: "loop" and boolean enabled. It refuses recording and changes
only the global enabled flag, not loop start/duration or any clip flag.
Read arranger_loop_enabled, arranger_loop_start_beats and
arranger_loop_duration_beats from fresh song state. All three participate in
the token; refresh it before another write. A submitted receipt is unverified.
Check availability, truncation and metadata/count/detail completeness, clip
bounds, loop geometry and source-note counts/spans. Times are quarter-note
beats, not meter-dependent beats or ticks. For linear scores, disable enabled
loops with the observed clip native_id, current loop_enabled and fresh
project token using:
song-clip-loop PROJECT_TOKEN TRACK_INDEX CLIP_ID EXPECTED_ENABLED false
CLIP_ID is a signed 32-bit current-document ID; EXPECTED_ENABLED is the
observed true/false flag. MCP grid_set_clip_loop uses track_index,
native_clip_id, expected_loop_enabled, enabled, expected_project_token
and authorization. It guards project/recording context, deadline, track/clip
identity and expected state; only the loop boolean changes. Re-inspect flags,
unchanged source counts/spans and clip geometry. A 690-beat score was observed
as 692-beat clips with a 576-beat Motif loop; source summaries do not expand
that loop and are not playback/audio proof.
Inspect role devices before installing instruments: MIDI import can create
default Organ devices. Use the guarded replacement sequence above, not
unintended instrument stacking. Re-inspect the device chain and actual arranger,
then listen; none of these steps follows from a submitted receipt.
Native project checkpoint
Prefer authorized grid_song_save with a fresh expected_project_token and
optional nonempty output_dir parent root. It calls
TrackWorkflow.save_project(expected_project_token, output_dir=None, *, confirm=False);
direct Python callers must set confirm=True only after authorization.
No composition session/revision/hash is involved: this checkpoints the current
controller-owned native project, not the offline MIDI/manifest.
For direct protocol clients the command is:
song-save PROJECT_TOKEN DIRECTORY_B64
Allocate and retain a fresh private empty directory before sending; encode
its resolved absolute UTF-8 path with URL-safe base64. MCP does this with a
bitwig-project-* child under the configured/explicit output root. Native
validation rejects noncanonical/symlink/nonempty paths and destinations inside
existing Bitwig project folders. The host writer can replace files; this
strategy avoids existing destinations but is not an atomic no-clobber API.
Observe recording: false, capabilities.project_save: true and
project_save_pending: false. Submission rechecks token, same native document,
recording and deadline on the UI thread through exact Bitwig 6.1.1 private
bindings (API 25/protocol 3); it does not invoke an application-wide Save action.
This is ordinary native save, not Collect and Save: external assets can remain
external. The async callback checks for
DIRECTORY/PromptTrack/PromptTrack.bwproject, returning
status: "native_save_completed" or requires_observation. Completion and
file presence do not prove content equivalence, asset collection or audio.
The native pending gate clears at callback/submission failure, not client
timeout. MCP retains output_directory and retry_performed: false on
bridge/native error, with native_response when available; raw clients must
retain their path themselves. Inspect fresh state, pending and that directory
without replaying or deleting possibly in-use output. A cleared pending flag
is not a success receipt. See checkpoint results.
For optional read-only installed DSP evidence, see Nitro evidence. Catalog discovery needs no keys; inspection requires authorized local keys and the pinned optional dependency. Neither operation establishes audible behavior.
Long-running scripts
For a script that watches or changes Bitwig over time:
- use a bounded socket timeout;
- reconnect only for read-only probes;
- never automatically retry a mutation after an ambiguous timeout;
- log the request command, response status, selected-device identity, and revision;
- keep logs, native identities, generated exports, captures and project/evidence files local, outside tracked examples and release inputs;
- handle
SIGINTand restore temporary parameter values where possible; - rate-limit state reads and writes;
- exit when Bitwig or the bridge restarts instead of carrying stale IDs forward.
A 30-second deadline rejects native submissions that expired while queued, but a timeout can still follow an already-submitted mutation. Re-read state and determine the outcome; song and patch workflows intentionally deny ambiguous replay rather than treating a reconnect as permission to retry.
When to wrap a script as an MCP workflow
Keep a script direct when it has a narrow, deterministic contract and an operator reviews its output. Add an MCP tool only when the operation needs to be discoverable and safely orchestrated by an agent.
An MCP-facing operation also needs:
- a precise JSON input schema;
- validation in the Python adapter and Java bridge;
- explicit mutation authorization;
- structured error mapping;
- tests for rejected and successful boundaries;
- producer and agent documentation;
- a recovery path.
See the Agent workflow playbook for orchestration patterns and Data and safety for identity and recovery contracts.
Troubleshooting
Connection refused
The extension is not listening. Confirm Bitwig is running, the extension is installed and enabled, and no stale Bitwig instance is holding the port. Restart Bitwig after replacing the extension file.
Request times out
Stop the script. Do not retry a mutation. Check Bitwig's controller log, then run a read-only capabilities request from a fresh connection.
graph_available is false
The connection is healthy, but the selected device does not expose the supported Grid graph surface. Select the intended Grid or use exposed-control commands.
State changed during a run
Abort and re-read. Track selection, selected-device identity, graph revision, port indexes, and instance IDs are live state, not durable references.