Skip to content

Notation syntax

The patchflow notation is line-oriented. Blank lines are insignificant. Lines starting with // are comments. Everything else is either a connection, a module header, a parameter, or a voice marker.

A connection is a line that starts with -:

- <source endpoint> <operator> <target endpoint>

An endpoint has the shape Module (Port) or Module.Section (Port). The port is always the last parenthesized token in the endpoint.

- Oscillator (Out) -> Filter (In)
- MATHS.CH 1 (OUT) >> MATHS.CH 2 (In)

Port and module names are matched case-insensitively with surrounding whitespace trimmed — OUT, Out, and out all refer to the same port. The original capitalization is preserved for rendering.

Internal whitespace inside a name is not collapsed for lookup. A module declared as Low Pass won’t match a reference to LowPass or Low Pass (note the double space). Keep your spacing consistent between the declaration and every connection that references it.

Add a trailing comment with // — it becomes a small label printed near the cable’s midpoint:

- LFO (Out) >> VCA (CV) // tremolo, slow

Any [key=value, ...] segment appended before the annotation is parsed into graphvizExtras on the connection. patchflow itself doesn’t render these, but they’re preserved in the graph for downstream tools.

- Clock (Out) c> Sequencer (In) [weight=5]

Any module name you use in a connection becomes a stub block unless you declare it with a header line. A declaration lets you attach parameters, a subtitle, or section breakdowns.

Filter:
* Cutoff: 2 kHz
* Resonance: 0.6

The header ends with : and must be the full module name. Parameters follow, each starting with *.

Add a bracketed subtitle after the name:

Filter [Low Pass]:
* Cutoff: 2 kHz

Many modules have multiple functional channels. Declare the parent with parameters keyed by channel, and address each section with dot-notation (Module.Section) inside connections:

MATHS:
* CH 1: Cycle ON
* CH 2: Attenuverter ~2 o'clock
- MATHS.CH 1 (OUT) >> MATHS.CH 2 (In)

Each section is laid out as its own panel, side-by-side with any sibling sections. If every parameter on the parent matches a section name (as above), the parser migrates those params to their corresponding section panels and removes the parent block entirely (unless the parent is also directly wired). So MATHS above becomes two sibling panels labeled “CH 1” and “CH 2”, each carrying its own parameter plate — no outer “MATHS” panel wraps them.

A voice marker tags every block declared after it with a voice name. The parser records each voice in graph.voices and each block’s voice field, which is useful if you want to post-process the graph — highlighting a voice, extracting it, or driving a legend.

VOICE 1:
- Osc1 (Out) -> Mixer (In1)
- Env1 (Out) >> VCA1 (CV)
VOICE 2:
- Osc2 (Out) -> Mixer (In2)
- Env2 (Out) >> VCA2 (CV)

Any line starting with // is ignored. Trailing // text on a connection line becomes an annotation (see above).

// ── voice 1 ──
- Osc1 (Out) -> Mixer (In1) // saw wave

parse() never throws on ordinary syntax issues. Instead it returns a ParseResult with errors and warnings arrays of ParseDiagnostics, each pointing to the offending line and column. The render() convenience function does throw, but only when the final graph has no modules and no connections — because there’s nothing to draw.

See the API reference for the full diagnostic shape.