Skip to content

Vibe coding presets ​

A preset is a handful of text files and a script. That is an unusual thing for a piece of hardware to be, and it is what makes an AI agent - Claude Code, Codex, or the assistant built into the app - a practical way to build one. You say what the preset should do; the agent writes the files, puts them on the controller, looks at what happened, and fixes what it got wrong.

This page describes how that works:

  • what closes the loop,
  • what to set up,
  • how an external agent such as Claude Code drives the app,
  • how to phrase a request to the built-in assistant,
  • what feedback the agent gets, including the Lua tests added in 5.0.0,
  • and why a retrieval corpus makes the whole thing cheaper.

Most of this needs the desktop app

The watched folder, the request channel an external agent drives, the built-in AI panel and the reading of screenshots and logs from disk all live in the Electra One app, the offline desktop build. The browser editor has none of them: it cannot touch your disk, and it has nowhere to keep an API key. Everything below that is not plainly about the controller or the command line assumes the desktop app.

You can vibe code with the browser editor too, and the results are good: the agent writes the files and you import them yourself. The desktop app is far better at it, because it does that carrying for you - the agent applies its own change, looks at the screen, reads the log and corrects itself.

Why the loop closes here ​

An agent writing code for most hardware works blind. It produces a file, hands it over, and waits for a human to load it, look at the device and report back. The human is the AI agent's only sense organ, and every cycle costs a person's attention, time, and it is prone to human errors.

Four properties of the Electra One remove that.

A preset is text. preset.json is the preset, main.lua is its script, router.lua is the router's, and the rest of the slot is a few more JSON files. There is no build, no binary, no project database: a diff is readable, a change is a file write, and git handles the history.

The controller reports. It prints its own log, it answers a screenshot request with what is on the display, it reports the SysEx it exchanges, and it runs Lua handed to it on demand. It also carries a debugger - a script can be stopped on a line while the instrument plays, and its variables, call stack and threads read there - and it reports its run-time information: the memory, the overruns, and the System stats with the processor load and the latencies of the last minute. Between them, a script's execution can be followed precisely rather than inferred, and an agent does not have to reason about what probably happened.

The firmware keeps the hard parts. Timing, the MIDI queues, the parameter map and the display are the firmware's job. A generated script is wrong in the ways scripts are wrong - a bad offset, a float where an integer was needed - not in the ways real-time code is wrong. That is the difference between a bug an agent can find from a log and one it cannot.

It is also the difference in what you end up with. What is being built here is not a flaky browser editor held together by Web MIDI, at the mercy of a tab, a garbage collector and whatever else the machine is doing. It is a preset on a dedicated instrument, with the timing accuracy, the knobs and the screen of real hardware - because that is what it is running on. The agent writes the part that describes your instrument; everything that has to be exact was written long before it got there.

The work is verifiable on the device. From firmware 5.0.0 a preset can carry test files, so "does it still send CC 74 inverted?" is a question with an answer, asked of the real controller, in seconds.

Coding agentClaude Code in the folderor the built-in AI panelpreset filesand requestsElectra One appwatches the slot folder,uploads and reads backuploadover USBElectra Oneruns the preset,its Lua and its testsReference filesLua API · formats · exampleswhat comes back, every cycleScreenshotsDevice logSysEx trafficLua debuggerTest results
The agent writes, the app applies, the controller answers. Nothing in the return path needs a person.

What to set up ​

The desktop app. Install the Electra One app and open the preset you want to work on. Leave the CONTROLLER, Current slot page showing: the watched folder is driven from there, and a request made while that page is closed is never answered.

A folder for the slot. Make an empty folder - it will very likely become a git repository - and point the app at it: Settings, Watched folder, Choose folder, then tick Watch a folder for this slot. Every file in the folder is a file of the slot. preset.json, main.lua, router.lua, performance.json, remoteMap.json, data.json, overrides.json and any further .lua file, which is loaded as a module. Anything else - a README, notes, a subfolder - is left where it is and never transferred.

The folder's own instructions. Tick Write CLAUDE.md into the folder and the app writes a short guide explaining the folder's protocol to whatever agent is opened in it. It is written only when it is missing, so anything you add to it survives; delete it and the current one comes back. This is off by default, because a folder that is somebody's repository should not acquire files nobody asked for.

Git. Not required, and worth it from the first commit. An agent that can see a diff can be asked what it changed, and a bad afternoon is a git checkout rather than an argument.

Nothing, for the reference material. The app carries it and hands it over when an agent asks - see What the agent has to know below. It is the single largest difference between an agent that writes a working preset in one pass and one that writes plausible nonsense, and it is the part you do not have to arrange.

An API key, for the built-in panel. The AI tab runs on your own Anthropic key, saved on the Settings page of the desktop app. Nothing is billed through Electra One and no key leaves the machine. An organisation-level key also needs a Workspace ID next to it — see Workspace ID, for an organisation-level key.

The command line, optionally. electraone-cli speaks the SysEx protocol directly: it uploads files, runs Lua, drives the debugger, takes screenshots and runs test files, all without the app. An agent with a terminal can use it as a second, independent hand on the hardware - and it is the only way to run tests unattended.

How an external agent drives the app ​

The watched folder is the whole integration. The agent edits files in it like any other repository, and asks for something to happen by writing one small file.

A request is .electra/request.json, with a new id each time:

json
{ "version": 1, "id": "2026-09-23-01", "action": "push" }
ActionWhat the app does
pushreads the folder, writes every changed file to the slot, reloads the preset, then runs the test files
reloadreloads the preset without writing anything
testruns main-tests.lua and router-tests.lua from the folder on the controller, writing nothing
screenshotphotographs the controller's screen into .electra/screen.png
referencewrites the reference files the app carries into .electra/reference/

Write it atomically - to a temporary name, then rename onto request.json - or the app may read half a file. Then wait for .electra/status.json to carry your requestId with a state that is no longer busy.

What comes back lives in the same directory:

FileWhat it holds
.electra/status.jsonthe request being answered, busy then done or error, with a result per file
.electra/log.txtwhat the controller has printed, newest last
.electra/sysex.logthe messages the app has exchanged with the controller
.electra/screen.pngthe controller's screen, as of the last screenshot
.electra/tests.jsonthe last test run: the controller's result document per file, with a summary

Three things about this are worth knowing before an agent surprises you with them.

Editing is safe; applying is deliberate. Nothing written in the folder reaches the controller on its own. A change waits until something applies it - a push request, or Apply changes in the slot browser.

Applying is additive. The folder is what you are writing to the slot, not a copy of it. Files the slot has and the folder does not are left running on the controller; nothing in the folder deletes anything from the device.

preset.json must parse. If it does not, the app refuses to apply anything at all, rather than leave the controller with a preset it cannot open.

The request channel is also the honest limit of the integration: it is driven by the slot browser, so the app has to be open, on that page, with a controller connected. An agent working on a preset while the app is closed is writing files, not testing them.

The built-in assistant, and how to ask it for things ​

The AI tab in the Tools pane is a different arrangement of the same idea. It is not a chat window beside the editor: it holds tools that read and change the open project directly - get_project, get_slots, add_control, update_control, get_lua, set_lua, add_device - and tools that reach the hardware: upload_project_to_device, get_device_log, run_lua, list_device_presets, import_preset_from_device. It can also read the SysEx log the app has collected and look at image files in your Downloads and Desktop folders, which is how "here is a photo of what my synth's panel says" works.

Because it edits the project you have open, a prompt for it is shaped differently from one you would write in a chat somewhere else.

Do not paste the project. It reads the project itself, and a pasted copy is tokens spent on something it can fetch. "Explain what this preset does" is a complete prompt.

Name the place. Which page, which slot, which device, which channel. Add four dials to page 2 of the Mini layout, CC 20 to 23 on device 1 leaves nothing to be guessed at; add some dials buys you a guess about the page and another about the layout.

Ask for one change, then look. The panel shows a tool log beside the answer. Reading it after each step is how you catch a wrong slot id while it is still one control rather than a page.

Ask it to check on the hardware. The editor is not the controller. A preset can transfer perfectly, hold every control and still draw nothing, because a custom control is painted by Lua and a paint callback that throws leaves a blank rectangle - and that error appears in the controller's log and nowhere else. Upload it and read the device log is the difference between working and appearing to work.

Give it the instrument's documentation as a file. A SysEx implementation chart, dropped on the panel as an attachment, is what turns "write a preset for my synth" from a guess into a transcription. Attachments are cached, so a long manual is paid for once in a conversation rather than on every turn.

Say what to do when it is unsure. The assistant asks rather than guessing when something is genuinely ambiguous. If you would rather it chose and told you, say so.

The same phrasing works for Claude Code in the folder, with one addition: it has a shell, so it can be told how to verify. Push it, take a screenshot, and tell me whether the labels fit is a complete instruction to an agent that can write request.json and read a PNG.

A concrete request can finish in one turn

A request specific enough leaves nothing for the assistant to ask back. One message describing an arpeggiator — hold notes from a source, send them to a destination, tempo and pattern on their own knobs, routing on its own page — was enough for the panel to write the Lua, upload it, read the log, write and run four tests, patch a default it found missing, and confirm the controller clean, for $0.968 on Sonnet 5. See Building an Arpeggiator in One Prompt for the full session.

What the agent can see ​

Everything below is available to an agent without a human in the loop. It is the part of this setup that does not exist for most hardware, and it is worth being explicit about what each one is good for.

The controller's log. .electra/log.txt in the folder, get_device_log in the panel, logger listen on the command line. This is where Lua errors surface - the paint callback that threw, the nil indexed, the argument the graphics call rejected. It is the first thing to read after a push and the last thing people think of.

An empty log is not a clean log. The controller logs its own load every time it reads a preset, so no lines at all means the log did not arrive, not that nothing went wrong. Treat a silent log as unknown.

The screen. A screenshot request, or electraone screenshot get -o screen.bmp --take. Layout is the class of problem an agent cannot reason its way to: a label that collides with a curve, a value that runs off the edge, a custom control drawn at half the size it expected. A picture settles it in one cycle.

The SysEx traffic. .electra/sysex.log, or get_sysex_log in the panel. What actually went out, byte for byte - which is how you find out that the knob sends the right CC on the wrong channel.

Lua on demand. run_lua in the panel, lua exec on the command line. The firmware's own answer to "does this control resolve, what are its bounds, is the callback attached". Faster than a push, and it reads the live preset.

The debugger. Breakpoints on the running controller, the call stack, locals, upvalues, globals, the threads. Driven over the CLI's debug group, so an agent can stop a script at a line and read its variables rather than adding a print() and uploading again. The human-facing version of this is Debugging a preset's Lua.

The tests. The newest of these, and the one that changes how the loop behaves rather than just what it can see.

Tests, and what they change ​

A preset can carry main-tests.lua and router-tests.lua beside its scripts. A test file declares cases; the controller runs the file in the preset's own Lua state, with outgoing MIDI held back and recorded, and answers with a JSON document saying which cases passed and why the others did not. The full description is in Lua tests.

lua
test.case("cutoff sends CC 74 inverted", { approved = "2026-09-22" }, function()
  test.turn(1, 100)
  local sent = test.sent()
  test.count(sent, 1)
  test.eq(sent[1].controller, 74)
  test.eq(sent[1].value, 27)
end)

A run is started from the Preset tests and Router tests tabs of the editor's Lua pane, from the command line with electraone lua test --file main-tests.lua, or over SysEx. The command line exits 0 on a clean run and 1 on a failure, which is the shape a script - or an agent - wants.

The desktop app puts the run inside the loop. Every push from the watched folder runs the folder's test files after the reload, and answers with the outcome under tests in status.json - counts, the failing cases with their messages, and one boolean, regression, that is true when a case marked approved went red. A push that causes a regression answers state: "error" even though every file was written: the files being on the controller is not the same as the change being right. A test request runs the files without writing anything, and the full result documents land in .electra/tests.json. The built-in panel has the same arrangement as tools: run_lua_tests, upload_project_to_device running them after the load, get_lua_tests and set_lua_tests for the file itself, and approve_lua_test - which is the only way an approval gets written, and which the assistant is told to call only on the user's word. set_lua_tests refuses a text that adds or moves an approval on its own.

Three things follow for anyone building presets this way.

A generated script gets a regression suite for free. The single most expensive failure in a long agent session is the fix that breaks something settled three prompts ago. Tests turn that from something you notice next week into a red line in the next run.

The approval convention makes a test a contract. A case may carry approved, with the date somebody confirmed on the instrument that the behaviour is right. Passing is not approval: a passing case has only shown that the script does what the case says, not that the case says the right thing. Approval is added by the person who played the instrument and heard it, never by the author of the code - which, in this workflow, means never by the agent. An approved case that turns red is a bug in the change, not a test to be updated.

Run them before you change anything. Green before and red after is the change you just made. Red before is something else, and finding that out first saves a long hunt for a bug that was already there.

The practical rhythm, then: ask for the behaviour, let the agent write the script and the cases, try it on the instrument yourself, and approve the cases that are right. From that point the agent can work faster than you can watch, because the cases you approved are watching for you.

What the agent has to know ​

A language model already knows a great deal about MIDI, about Lua, and about controllers in general. It does not know this controller: the exact spelling of its Lua calls, which of them takes a colon and which a dot, what happens to an argument out of range, which fields preset.json accepts and which it silently drops, what the firmware pushes into each callback, and which numbers are counted from zero here and from one there. A guess in any of those places is a script that loads and then does nothing, and finding out why costs a cycle.

So the reference material is not a nicety, and it is not something you have to find: the desktop app ships it. Six files, generated from the firmware itself and from the developer pages, carried inside the application rather than left in your project folder:

IdWhat it covers
preset-luathe complete preset Lua API: environment, lifecycle, every callback with the arguments the firmware really pushes, every library and object, the traps, and runnable recipes
router-luathe router sandbox: the watch filter, the message and port objects, the preset-side router table, the limits
lua-teststhe test files, the test library, the result document, and the approval rules an assistant is expected to follow
preset-jsonthe .epr preset format as the firmware reads it: every field, its default, its enforced range, what a bad value does
project-jsonthe editor's project document, and where the things people expect to find in it actually live
controllerhow to read the controller itself: the run-time information and System stats with what each number means, the log, screenshots, Lua on demand, the test runner on the wire, and the debugger

Each file states the firmware version and the commit it was generated at, and that line is the contract: a corpus that does not match the firmware on the desk will produce scripts that fail in ways nobody can explain.

An agent in the watched folder asks for them. A reference request writes them into .electra/reference/, with an index saying what each one covers:

json
{ "version": 1, "id": "2026-09-23-02", "action": "reference" }

Add "name": "preset-lua" for one of them rather than all six. They are written where nothing else goes - the control directory already carries a .gitignore of * - so a repository does not acquire half a megabyte of machine-facing markdown, and nothing appears at all until something asks. This is the one request the app answers by itself, so it works whether or not the Current slot page is open; the CLAUDE.md the app writes into the folder says all of this, which means an agent opened there finds it without being told.

The built-in panel reads them through tools. list_reference gives the files and their sections, search_reference says which section covers a name, and read_reference returns one section. It is told to look a call up before writing it rather than to remember it - which is why the panel can be right about an API of this size without carrying it in every prompt.

Why looking things up is cheaper than guessing ​

It is tempting to read a large reference as an expense. It is the opposite, and the arithmetic is worth doing once.

A guessed call costs the write, the push, the failed run, the log, the diagnosis and the correction - six exchanges, each carrying the whole conversation so far, and at the end of it the agent has learned one signature. A looked-up call costs the retrieval. Wrong turns are the expensive part of an agent session; the corpus exists to remove them.

Retrieval beats carrying the manual, too. An API this size does not fit in a prompt - the preset Lua reference alone is a quarter of a megabyte - and a fixed bundle small enough to fit is the same bundle whether the question is about the graphics library or the colour of a pad. Reading one section when it is needed costs a tool call and is accurate about all of it.

What is carried should be carried in a stable prefix. Material that sits in a system prompt, an attachment, or a file read early in a session is billed in full once and at a fraction on every turn afterwards; material that is re-fetched or re-pasted each turn is billed in full each time. So ask for the reference files at the start of a session, not in the middle of each task.

Where it stops ​

Worth knowing before you plan an afternoon around it.

  • The request channel needs the app open on Current slot, with the controller connected. There is no headless mode of the app; for unattended work, use the command line.
  • The app does not receive every line the controller prints. A quiet log is unknown, not clean.
  • The panel's screenshot tools read images from your Downloads and Desktop folders. Asking the controller for its screen is a folder request or a CLI call.
  • Tests need the preset under test loaded on the controller: send it first, then run.

See also ​

Electra One proudly uses Lua and ArduinoJson.
For support contact info@electra.one · © 2019-2026 Electra One