Op vocabulary
Everything goofi can do, it does through these 57 ops. The editor, the CLI, a test and an MCP agent are all transports over the same entry point.
The registry is a phrase tree and is rendered as one here: group words carry
their own docs, and a leaf op authors only its final word. Every
phrase below works as a CLI command (goofi node param edit …), as a /control message, and as an MCP tool.
An argument schema is name:type, with ! marking required and [] a list. Positional says the argument may be given without its
name on a command line, in the order shown.
agent
agent list readThe agents the config offers to launch, and the ones goofi has running.
returns {instances: [{id, harness, state, exit_code}], agents: [{name, command}], config_error: string | null}
agent start effectLaunch a config-listed agent on a PTY with the patch workspace as its cwd, under a login shell — a command that cannot launch fails on the PTY itself. Read its terminal at /term/<instance_id>. An unknown name is refused with the config's list.
namestringrequired positional
returns {instance_id: string}
agent stop effectStop a running agent (SIGTERM, then SIGKILL), or dismiss one that already exited. The shell's undo stack dies with it; the exit code arrives on harness_changed.
instancestringrequired positional
returns {ok: true}
control
control list readEvery control panel and the group it draws, and every group holding a widget: each element with its value, its widget (`control`), its lock and what it follows (`source`).
returns {panels: [{panel, group}], groups: {group: {lock, elements: [{name, element, type, value, control, lock, source?}]}}}
control add writeBear a widget in a control panel's group: a global of the kind's own type, carrying the widget. `kind` is knob/slider/number/field/toggle/dropdown. `element` is minted `knob0`, `knob1`, … when not given, and the cell is the first free one when `x`/`y` are not. Works on a config-locked group and leaves it locked — a control panel owns its group — as ONE undo step.
groupstringrequired positionalkindstringrequired positionalelementstringxfloatyfloatwfloathfloatvalueanyminfloatmaxfloatstepfloatoptionsjson
returns {name, control} — the global's full name and the widget as stored
control edit writeChange a widget: `name` renames the element (every expression reading it follows), and the rest re-shape the widget, its range, its options or its cell. Through the lock, as `control add` is.
groupstringrequired positionalelementstringrequired positionalnamestringkindstringminfloatmaxfloatstepfloatoptionsjsonxfloatyfloatwfloathfloat
returns {name} — the element's full name after the edit
control remove writeDelete a widget and the global under it. Through the lock, as `control add` is.
groupstringrequired positionalelementstringrequired positional
returns {removed: true}
control source writeMake a widget FOLLOW one producer output, `node.slot`, with `index` picking one number out of a wide frame — a MIDI controller's `cc` is 128 of them — so a knob on a controller drives the widget. An empty reference clears it. Through the lock, as `control add` is.
groupstringrequired positionalelementstringrequired positionalreferencestringrequiredindexint
returns {source: {reference, index} | null}
dir
dir list readList a directory on the goofi host — the save/load browser's read.
pathstringpositional
returns {path, parent, entries: [{name, dir}], roots}
global
global list readEvery patch global — what an expression can read and the global writes can set — each with the lock that holds it (its own and its group's together), and every group that carries a lock. The `system` group is goofi's own: config-locked for life, and `system.goofi_home` is the machine's value, never saved into a patch.
returns {globals: [{name, type, value, lock: {config, value}, control?, source?}], groups: {group: {lock}}}
global entry
global entry add writeCreate a patch global. `name` is `group.element` — every global is in a group. `type` is one of float/int/bool/string; a name the patch already holds is refused — `global entry edit` changes one. `control` makes it a control-panel element: {kind, min, max, step, options, x, y, w, h}, where kind is knob/slider/number/field/toggle/dropdown and must be able to draw the type.
namestringrequired positionaltypestringrequiredvalueanyrequiredcontroljson
returns {value} — the value as stored, type-coerced
global entry edit writeChange an existing global's value, type-coerced to the type it holds. The type is immutable, because every expression reading a global depends on it: re-typing is a remove and an add. A value-locked global refuses the edit, and so does the machine's own (system.goofi_home). `control` sets the control-panel widget and its place, `null` clears it, and giving one makes `value` optional — which is what a panel sends when it moves a widget; a config-locked global refuses it.
namestringrequired positionalvalueanycontroljson
returns {value} — the value as stored, type-coerced
global entry remove writeDelete a patch global. A config-locked one refuses, and a system global always is.
namestringrequired positional
returns {removed: true}
global entry source writeMake a global FOLLOW one producer output, `node.slot`, at that producer's rate: the manager writes the global on every frame that changes it, and nobody else may set it until the source is cleared with an empty reference. `index` picks one number out of a frame wider than one — a MIDI controller's `cc` is 128 of them — and a frame that holds one number needs none. The reference follows a node rename exactly as a param's does. A config-locked global refuses; a value-locked one holds its value and takes nothing.
namestringrequired positionalreferencestringrequired positionalindexint
returns {source: {reference, index} | null}
global entry lock writeLock or unlock one global on its own account: `config` freezes its name, its widget and its place in a group, `value` freezes its value alone. An axis not named keeps what it has, and its group's lock holds it besides. The system group's locks are goofi's own.
namestringrequired positionalconfigboolvaluebool
returns {lock: {config, value}} — the global's own lock as stored
global entry rename writeRename a global, and rewrite every expression that reads it. `to` is a full `group.element`, so one op both renames an element and moves it to another group.
namestringrequired positionaltostringrequired positional
returns {name} — the name as stored
global group
global group rename writeRename a group, moving every member with it and rewriting every expression that reads one. A config-locked group refuses, and the system group always does.
fromstringrequired positionaltostringrequired positional
returns {group} — the group as stored
global group lock writeLock or unlock a whole group, reaching every member: `config` freezes every name, widget and the membership itself — nothing is added, renamed or removed — and `value` freezes every value. An axis not named keeps what it has. A control panel's edit mode is this lock, inverted. The system group's lock is goofi's own.
groupstringrequired positionalconfigboolvaluebool
returns {lock: {config, value}} — the group's lock as stored
layout
layout inspect readThe arrangement as a tree: every tab, split and panel with its id, order and share of its parent. How a caller discovers the ids every layout op addresses. `tab` narrows it to one tab; no arg = all of them.
tabstringpositional
returns {text: string}
layout move writeMove a layout entry — a panel, a whole split's subtree, or a tab; one op per drag gesture, so a drop is one undo step. With `--beside` (and `--side`, `--ratio`) it lands beside that panel. With `--in` it lands inside that split, at `--index` among its children. Bare, a TAB moves to `--index` in the strip, and anything else wraps onto a tab of its own, labelled `--name`. Taking a tab's last panel takes the tab with it.
entrystringrequired positionalbesidestringsidestringratiofloatinstringindexintnamestring
returns {id, tab, text} — what was moved, the tab it is on, and the arrangement as `layout inspect` draws it
layout remove writeClose a layout entry: a panel, a whole split's subtree, or a tab and every panel on it. Its space goes to its siblings; a tab keeps its last panel, and the last tab stays.
entrystringrequired positional
returns {text} — the resulting arrangement, as `layout inspect` draws it
layout panel
layout panel add writeA fresh empty panel. With `--beside` it divides that panel, on its `left`/`right`/`top`/`bottom` (`--side`, default right), taking `--ratio` of its space (default half). Bare, it lands on a new tab at `--index` in the strip, labelled `--name` — minted (`Tab 2`, `Tab 3`, …) unless you give one.
besidestringsidestringratiofloatnamestringindexint
returns {id, tab, text} — the born panel, the tab it is on, and the arrangement as `layout inspect` draws it
layout panel edit writeEdit a PANEL's content: its type, its state, or both in one call and one undo. State MERGES key by key — send only what changes, and null to clear a key. A new type clears the old type's state, so send both together to rebind. `type` is one of: empty (a placeholder with no content yet — what a fresh split births); node-editor (the patch canvas — nodes, wires and sub-patches); parameters (the parameters of one node, with ranges and expression bindings); viewer (live frames from one output slot, drawn by `state.kind`); metadata (frame metadata from one output slot (sfreq, channels, shape)); console (the patch log; a bound node filters it to that node); globals (the patch globals, which any expression can read); control (knobs, sliders and fields over one group of globals); agent (a terminal on an agent harness, running in the patch workspace). A viewer panel's `state.kind` is one of: line (a time plot: one series (1-D) or one per channel (C, N)); image (a bitmap: (H, W), or (H, W, C) for 1–4 channels); trajectory (a phase portrait over pairs of rows of a (D, N) frame); topomap (a scalp map of one scalar per channel); string (the text of a STRING slot); table (the rows of a TABLE slot); a STRING or TABLE slot ignores it and uses its own.
panelstringrequired positionaltypepanel_typestatejson
returns {text} — the resulting arrangement, as `layout inspect` draws it
layout tab
layout tab edit writeRelabel a TAB. Its id and every panel on it stand; the strip index is where it sits, which `layout move` owns.
tabstringrequired positionalnamestringrequired
returns {text} — the resulting arrangement, as `layout inspect` draws it
layout split
layout split edit writeSet the shares of ALL of a SPLIT's children at once, in child order — what a resize drag commits. Renormalized to fill the slot.
splitstringrequired positionalfractionfloat[]required
returns {text} — the resulting arrangement, as `layout inspect` draws it
layout viewpoint
layout viewpoint edit effectStore where this client is looking — active tab, maximize, camera, each panel's sub-patch path. ONE stored value, replaced whole, last writer wins; persisted in the `.gfi`, never converged, never dirtying.
valuejsonrequired
returns {ok: true}
library
library list readThe node library: every registered type with its slots, params, docs and availability — what `node add` can build.
returns {types: [{type, engine, tags, doc, input_slots, output_slots, params, available}]}
library get readONE library entry in full: the palette fields plus where the type came from and its source text. Copy a node into the patch workspace to modify one.
typestringrequired positional
returns the `library list` entry plus {language, tier, provenance, path, source}
library refresh effectRe-read the shipped and patch node directories; live instances of a changed type restart onto the new code. Call after writing a node file.
returns {added: [type], changed: [type], removed: [type]}
link
link add writeWire `from` (an output, as `uid/slot`) to `to` (an input). Refuses a dtype mismatch, naming both ends; refuses an end that names no node — so a reply means the wire is really there. A link never crosses a sub-patch boundary, and the two acts that look like it are ordinary links in different scopes. From the OUTSIDE you wire a node to the sub-patch's facade, naming a port's uid as the slot; the wire is stored against the PORT, whether or not anything is behind it yet. From the INSIDE you wire a port to a member, both of them in that sub-patch. A port carries one slot, `value`, on both of its sides.
fromendpointrequired positionaltoendpointrequired positional
returns {from, to, dtype} — the wire as made, with a facade endpoint resolved to the PORT it named.
link remove writeRemove one wire, addressed by both of its endpoints — a boundary port's inner wire included. Idempotent, like `node remove`.
fromendpointrequired positionaltoendpointrequired positional
returns {removed: bool} — false when there was no such wire
node
node state readRead one node: its params (values, ranges, expression bindings), each output slot's name and kind and whether the node is emitting on it, and its error. `slot` narrows to one output; `--no-params` and `--no-error` drop a section. The FRAMES are not here: `node snapshot` reads one raw, and `/data/<node>/<slot>` streams them exactly as a viewer sees them.
nodeuidrequired positionalslotstringparamsboolerrorbool
returns {text: string}
node snapshot readThe output's latest frame, RAW and once — the analysis read, addressed `uid/slot`. A facade or a boundary port resolves to the stream behind it, exactly as a viewer's does. It reads the cache the slot's reducer already keeps, so it never wakes the node and never touches the viewers' shared stream. ARRAY answers base64 NPY; STRING and TABLE answer plain JSON, a table's ARRAY members as NPY again. A slot asked about before anything was cached answers `{frame: null}` with the reason — asking is also what opens the slot's feed, so ask again after the node's next emit.
outputendpointrequired positional
returns {meta, npy_b64} for ARRAY; {meta, value} for STRING/TABLE; {frame: null, reason} before the first cached frame
node add writeCreate a node of `type`. `inst_id` births it inside that sub-patch; absent = root. `name` is a letter then letters or digits and not a Python keyword — one that is taken or illegal is refused, never silently swapped — and an omitted one is minted. Each `--param` is one birth param, self-addressed: `{"name": "group/param", …}` carrying `node param edit`'s fields — inside a JSON flag under bash, spell nested strings with ESCAPED double quotes (`"nd(\"other\").out.sfreq"`); a single-quoted `nd('x')` inside a single-quoted shell token loses its quotes silently. `member_uid` asks for a CHOSEN uid, so a caller rebuilding a graph it already knows — or wiring a batch it is still building — keeps its uid-keyed bindings; naming one the patch already holds answers with that node rather than a second one. The boundary types (InArray/InString/InTable/InAudio/OutArray/OutString/OutTable/OutAudio) create a PORT of the sub-patch named by `inst_id`, which is required for them. A port is a node in every way an op can see — it is named, moved, wired and removed by the same ops — but it never runs, so it takes no params. To COPY a node rather than build one, read it with `nodes copy` and put it back with `nodes paste`.
typestringrequired positionalposfloat2namestringinst_iduidmember_uiduidparamjson[]
returns {uid, name, input_slots, output_slots, params} — the node as born, so it can be wired and tuned without a follow-up read. `name` is what nd() addresses it by.
node edit writeEdit a node's own record: rename it, move it, set viewers — any of them, in one step and one undo. An omitted field is left alone. Params are `node param edit`'s. A sub-patch boundary port takes every field: its name is in the one namespace nd() reads, so a collision is refused exactly as a leaf's is, and its `value` slot takes a viewer exactly as a leaf's output does. A `name` is a letter then letters or digits, and not a Python keyword, for every kind of node. An expression reads a name as an ATTRIBUTE — a sub-patch's slot in `nd('chain').out.drain` — and a reference spells `name.slot`, so one that cannot be read there breaks every source naming it, and the rewrite that follows the NEXT rename can no longer find what it broke. Each `--viewer` is one slot's inline view, `{"slot": "out", "kind": …, "settings": …}`, merged slot by slot so only the slots named move; `{"slot": "out", "clear": true}` removes that slot's stored view. `kind` is one of: line (a time plot: one series (1-D) or one per channel (C, N)); image (a bitmap: (H, W), or (H, W, C) for 1–4 channels); trajectory (a phase portrait over pairs of rows of a (D, N) frame); topomap (a scalp map of one scalar per channel); string (the text of a STRING slot); table (the rows of a TABLE slot).
nodeuidrequired positionalnamestringposfloat2viewerjson[]
returns {ok: true}
node remove writeDelete whatever the uid names — a leaf, a boundary port or a whole sub-patch. A sub-patch takes everything inside it, to any depth: nested sub-patches, their members and their ports. A port of an enclosing sub-patch that exposed the deleted node STAYS, unwired — a port is a node, and it outlives what was behind it exactly as an unconnected node outlives the cable it lost. Idempotent: a uid naming no node succeeds having deleted nothing, and says so.
nodeuidrequired positional
returns {removed: bool} — false when the uid named nothing
node restart effectRespawn a node in place, keeping its uid, name, params, links and scope. Recovery, not an edit — `setup()` runs again.
nodeuidrequired positional
returns {ok: true}
node editor effectOpen a node's own editor window — a plugin's GUI — on the machine goofi runs on, never in the page; `--no-show` closes it. Only a type whose palette row says `editor: true` has one, and a machine with no display has none.
nodeuidrequired positionalshowbool
returns {changed: bool} — false when the editor was already open, or already closed
node param
node param edit writeSet ONE param, addressed `group/param`. `value` is coerced to the param's declared type — a fraction into an int rounds, a value of the wrong kind falls back to that type's zero; the declared min/max are the editor's range, NOT a clamp. A param has ONE active source, named by `mode`: `constant` (the value), `expression` (Python over nd(), globals and me, at control rate), or `reference` (one producer output spelled `node.slot`, no Python, at the producer's rate). Giving an `expression` or a `reference` implies its mode, so binding one is a single flag; the other two are RETAINED across a mode switch, an empty text clears that text (and the mode, if it was the active one), and a mode or trigger given alone edits what is already there. A `value` on a driven param switches it to `constant`. A reference's producer slot must match the param: a number or bool references an ARRAY or AUDIO output holding one element, a string references a STRING output. `triggers` defaults false, and that is almost always right: a binding re-evaluates on its own — when a referenced node emits, when a referenced param (`nd('x').params.<group>.<param>`, `me.params.…`) is edited, or on each of the node's own runs for a ref-less one — and the node reads the fresh value on its next normal run. `triggers: true` ALSO wakes the node's process() on every evaluation, making the reference its clock. Reach for it only when the node would otherwise not run (a trigger input with no wire into it) and you want the referenced node to drive it. Never on a ref-less expression (`t`, `globals.x`): that free-runs the node at its common.max_frequency.
nodeuidrequired positionalparamparam_addrrequired positionalvaluestringexpressionstringreferencestringmodestringtriggersbool
returns {value, error} — the value as STORED, with its bind error: a compile failure, an unknown producer, or a slot of the wrong kind.
node param refresh effectAsk a node to re-enumerate a refreshable string param's options (a device or stream picker), addressed `group/param`. The scan runs on the node's own thread, so this reply only says the request was dispatched — read the fresh options back with `node state`.
nodeuidrequired positionalparamparam_addrrequired positional
returns {ok: true} — the options land on the node; `node state` reports them
node param pulse effectFire a pulse param once, addressed `group/param`: a reset, a trigger, a clear. A pulse holds no value, so this is a request to the node, never an edit of the document.
nodeuidrequired positionalparamparam_addrrequired positional
returns {ok: true} — the pulse was dispatched to the node's own thread
nodes
nodes inspect readRead one scope as a mermaid flowchart — nodes, sub-patches, boundary ports and wires. No arg = the root scope. Scope-wide and nothing more: what is broken is the whole patch's business, so `session status` answers that.
scopeuidpositional
returns {text: string}
nodes copy readRead `nodes` and everything they hold — a sub-patch's members, their ports and the nested sub-patches below them, to any depth — as a self-contained fragment. A link rides only when BOTH its ends are in the fragment. The shape is the `.gfi`'s own, so a fragment is a patch's worth of nodes in the format a patch is written in, and `nodes paste` is what puts one back.
nodesuid[]required positional
returns {doc: {nodes, links}} — the fragment, keyed by the uids it was read from
nodes paste writeAdd a `nodes copy` fragment on FRESH uids and fresh names, so it lands beside whatever it was copied from rather than colliding with it. `pos` shifts the whole fragment by that offset; `inst_id` puts its roots inside that sub-patch, absent = root. A record naming a scope that is IN the fragment keeps the shape it was copied with. One command, so it is one undo step.
docjsonrequiredposfloat2inst_iduid
returns {rename: {old_uid: new_uid}} — every record's uid in the fragment mapped to the one it was created at
nodes group writeCollapse nodes into a new sub-patch, returning its instance uid. `nodes` must share one scope, and one of them may itself be a sub-patch. Every wire that ends up CROSSING the new boundary mints a port to carry it, so nothing is disconnected and nothing stops running; a wire buried in a nested member mints a port there too, so it can reach the new boundary.
nodesuid[]required positionalposfloat2
returns {inst_id: uid}
nodes ungroup writeDissolve a sub-patch, returning its members to the parent scope. Its ports go with it and every wire they carried stands, because a port keeps its wire against the node behind it. A port of an ENCLOSING sub-patch that exposed one of these follows down onto what it exposed.
subpatchuidrequired positional
returns {ok: true}
op
op list readEvery op this server speaks: its name, its arguments (`!` marks a required one), what it does, what it answers, and its kind — a `write` is undoable and may ride in a batch, an `effect` runs alone.
returns {ops: [{op, args, positional, kind, doc, result}]}
op complete readWhat can come NEXT on a partial command line — the shell completion read. Each candidate is a word with a one-line doc: a group or op word mid-phrase, a flag once the op is named, or a value for a flag with a known vocabulary (a panel type, a live node's uid). The line's last word, when partial, filters the candidates.
linestringpositional
returns {text: string} — one candidate per line, `word<TAB>doc`
session
session status readThe session's identity AND its health: which instance this is, where the patch lives, whether it differs from disk, and every standing error with how long it has stood. One read for `is my patch healthy, and have I saved it`.
returns {instance_id, save_path: string | null, workspace, dirty: bool, errors: [{node, path, error, standing}]}
session state readThe whole replicated document, exact and ATOMIC: nodes, links, globals and arrangement in one read — what every client mirrors, read without the sync protocol that carries it. ONE `nodes` map carries leaves, sub-patch facades and boundary ports alike, each naming its scope, and a port's inner wire is in `links` like any other cable. Narrowing is the caller's: pipe it through `jq`.
returns {nodes, links, globals, arrangement} — nodes and globals keyed by id, links a list.
session manifest readThe open patch as YAML — the manifest a `.gfi` holds, diffable and versionable.
returns {yaml: string}
session save effectPack the patch and its workspace to a `.gfi`. With no `path` it saves to the patch's home — refused when the patch has never been saved — and a given `path` becomes the new home.
pathstringpositional
returns {path: string}
session load effectReplace the open patch, losing unsaved work. `path` names a `.gfi` and brings its workspace with it; `--content` is an inline YAML manifest and carries no workspace. Exactly ONE of the two — the empty patch is `session new`. `adopt` (default true) decides whether a loaded FILE becomes the patch's home, which is what a later silent save overwrites; `/patch.gfi` passes false, because the file a browser upload came from lives on the user's machine and the staged copy this reads is deleted immediately.
pathstringpositionalcontentstringadoptbool
returns {ok: true, layout_warning: string | null}
session new effectReplace the open patch with the empty one, losing unsaved work. The undo history is cleared, so this cannot be taken back.
returns {ok: true, layout_warning: string | null}
undo · redo · compound
undo effectUndo this actor's last graph command. Each actor — a browser tab, a shell, the MCP — has its own stack.
returns {changed: bool, can_undo: bool, can_redo: bool}
redo effectRedo this actor's last undone graph command.
returns {changed: bool, can_undo: bool, can_redo: bool}
compound effectRun several steps in order as ONE undo step and one settled decision: viewers see no intermediate document, and the unsaved dot moves once. `ops` is a list of `{op, payload}`; a step is a read or an undoable write, and an effect is refused — it runs as its own call. A refused step takes back the ones that already landed, so the call either happens whole or not at all. A read step sees the earlier steps' writes on the GRAPH — but the document settles only when the batch does, so `session state` and `session status` inside a batch answer the document the batch found. Read them after it, not inside it.
opsjsonrequired
returns the steps' own replies, as a bare JSON list in order
This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.