File Format (.loomflow)
A harness project is saved as a single <name>.loomflow file — an encrypted, self-contained container that bundles the document and all of its device images. The file is opaque and not meant to be hand-edited; this page documents the logical structure stored inside it: the top-level shape, the version/migration mechanism, and a field-by-field listing of every entity. For saving, sharing and sync, see Collaboration.
Two rules govern what is stored:
- Inputs only. Anything derivable — wire lengths, the cut list, the BOM, a connection’s signal — is computed on the fly and never written to the file.
- Logical vs. cosmetic. Manufacturing data (nodes, connections, bundles) is separate from view data. Canvas positions live under
views, keyed by element id.
Understand the top-level shape
Section titled “Understand the top-level shape”Keys are always written in this order:
| Key | What it holds |
|---|---|
schemaVersion | Integer format version (currently 3); the loader migrates older files up on open |
meta | Document name and units — lengthUnit ("mm" or "in"), gaugeUnit ("AWG" or "mm2") |
palette | Named colors; the decode key for every hex color/stripe value in the document |
signals | Logical nets: name, default color/gauge, optional voltage and current ratings |
nodes | Connectors, terminals (ring/fork lugs), splices, and layout-only junctions — each with its cavities |
connections | Physical conductors: two endpoints (node + cavity ids), color, gauge, optional cable/twist membership |
cables | Multi-core / shielded cable groupings of connections |
twists | Twisted pairs/triples of connections |
bundles | Layout trunk segments between two nodes, carrying physical length |
mates | Connector pairs that plug together (zero-length routing junctions) |
parts | Connector part templates, copied in from the library so the file opens without it |
notes | Free-text canvas annotations |
devices | Layout-only enclosures/PCBs that connectors mount onto |
harnesses | User-given names for detected isolated harness groups |
views | Cosmetic positions (schematic, layout) and wire drag points (edgePoints) |
Every entity id is a short random string, stable for the life of the entity — ids are never regenerated. Because library parts are copied into parts (with mfr/pn provenance) when assigned, a saved project is fully self-contained and opens without your library files.
Read an annotated excerpt
Section titled “Read an annotated excerpt”A trimmed document — a fragment of a small vehicle harness:
{ "schemaVersion": 3, "meta": { "name": "Example — small vehicle harness", "lengthUnit": "mm", "gaugeUnit": "AWG" }, "palette": [ { "hex": "#D32F2F", "name": "Red" } // decode key for all color fields ], "signals": [ { "id": "sig_gnd", "name": "GND", "color": "#1B1B1B", "defaultGauge": "10" } ], "nodes": [ { "id": "n_clt", "kind": "connector", // connector | terminal | splice | junction "label": "CLT", "partId": "p_dt06_2s", // -> parts[].id "breakoutLength": 20, // fan-out tail added to wires ending here "cavities": [ // array order IS the pin order { "id": "c_clt_1", "label": "1", "signalId": "sig_clt" }, { "id": "c_clt_2", "label": "2", "signalId": "sig_sgnd" } ] } ], "connections": [ { "id": "w2", "endA": { "node": "n_ecu", "cavity": "c_ecu_gnd" }, "endB": { "node": "n_gnd", "cavity": "c_gnd" }, "color": "#1B1B1B", "stripe": "#F5F5F5", "gauge": "10" // no length field: length is derived from the bundle route + breakouts } ], "bundles": [ { "id": "b1", "nodeA": "n_batt", "nodeB": "n_ecu", "length": 600 } // physical lengths live here ], "views": { "schematic": { "n_clt": { "x": 450, "y": -450 } }, // cosmetic positions, keyed by node id "layout": { "n_clt": { "x": 390, "y": -240 } } }}Know how versioning and migration work
Section titled “Know how versioning and migration work”schemaVersion is bumped whenever the shape changes. On open, the app walks a migration chain from the file’s version up to the current version:
| Migration | What changes |
|---|---|
| v1 → v2 | notes added; the cosmetic single-offset wireOffsets view data was replaced by edgePoints waypoints (old offsets are dropped — they were cosmetic only) |
| v2 → v3 | devices added |
Migration happens in memory on load; the file on disk is only rewritten when you save. A file with a newer version than the app refuses to open with a message telling you to update the app.
The loader is also forgiving of older or partial documents: collections that are absent are filled with empty arrays, and missing units default to mm/AWG. Fields the app doesn’t recognize are preserved under the _unknown convention (per entity and at the top level) so data written by another version survives a round-trip.
How the document is stored
Section titled “How the document is stored”Inside the container the document is written in a canonical form — the same design always serializes to the same bytes, regardless of the order you built it in: collections are sorted by id, object keys follow a fixed order per entity type, and cosmetic canvas positions live in a separate views block keyed by element id. This keeps saves stable and the file compact. It is an internal detail — the .loomflow container is encrypted and opaque, so you never read or diff these bytes directly.
Get data in and out
Section titled “Get data in and out”The .loomflow container is not a public, hand-editable format — the app is the way in and out:
- Derived outputs (cut list, BOM, validation) are not stored — the app computes them from the inputs. Get them via the export functions.
- Device background images are bundled inside the
.loomflowfile (devices[].image.assetnames the entry), so a project is a single portable file with no sidecar folder to carry along. - To bring designs in from other tools, see Importing; every field the app reads and writes is in the field reference below.
Look up every field
Section titled “Look up every field”Unless marked required, every field is optional and simply absent when unset. Any extra fields you (or another tool) add are preserved untouched across load and save.
| Field | Meaning |
|---|---|
name | Document display name |
lengthUnit | "mm" or "in" — the unit for every length in the file (bundle lengths, breakouts, overrides) |
gaugeUnit | "AWG" or "mm2" — how wire sizes are entered and displayed |
sourceFormat | Provenance when the document was imported from another tool (name + version), kept for lossless round-trip export |
palette[]
Section titled “palette[]”Each entry is { "hex": "#D32F2F", "name": "Red" }. The array keeps the order you arrange in the app (it is never re-sorted), and it is the decode key for every other hex color in the document — a color with no palette entry displays as its raw hex.
signals[]
Section titled “signals[]”| Field | Meaning |
|---|---|
id, name | Required |
color, stripe | Default insulation / tracer color (hex) inherited by new wires on this net |
defaultGauge | Gauge inherited by new wires on this net unless overridden per wire |
type | "power" or "data"; absent = unspecified. Feeds the power/derating analysis |
voltage | Nominal system voltage in volts — enables percentage voltage-drop checks on power nets |
nominalCurrent, maxCurrent | Continuous / peak current in amps |
notes | Free text |
nodes[]
Section titled “nodes[]”All node kinds share these fields:
| Field | Meaning |
|---|---|
id, kind, cavities | Required. kind is "connector", "terminal", "splice", or "junction" |
label | Display name |
breakoutLength | Fan-out tail from the trunk to this node’s contacts, added to every wire ending here; a wire end can add more via its endpoint’s extraLength |
excludeFromBom | true = documented but not purchased — the BOM skips this node entirely |
deviceId | Mounts the node on a device (devices[].id); mounted nodes move with the device on the layout |
Kind-specific fields:
connector—partIdpoints at a template inparts. Thecavitiesarray order is the pin order.terminal— a ring/fork/eyelet lug with exactly one cavity.terminalType("Ring","Fork", …) andinternalPn(your organization’s part number).splice— a join point where all of its cavities are electrically common.internalPn(e.g. a splice band part number).junction— a layout-only branch point in the bundle tree, created by Insert branch point on a bundle. Nothing electrical happens here — no BOM line;cavitiesis always empty.
Each entry in cavities:
| Field | Meaning |
|---|---|
id | Required |
label | Pin designation, e.g. "A1" — generated by the part’s designation strategy, or set manually |
signalId | The intended net for this contact (signals[].id) |
contactPn | The crimp contact loaded in this specific cavity, overriding the part’s default contactPn — for mixed-size housings (a size-12 power cavity next to size-20 signal cavities). Should be one of the part’s contactOptions |
gauge | Intended wire size for this cavity, free-form (e.g. "18 AWG", "0.5 mm2") — documents the plan before a wire is drawn. An attached wire’s own gauge takes precedence. Feeds the ampacity check |
connections[]
Section titled “connections[]”| Field | Meaning |
|---|---|
id, endA, endB | Required. Each endpoint is { "node": <node id>, "cavity": <cavity id>, "extraLength": <number> }; extraLength adds a service loop or strip allowance at that end |
color, stripe | Insulation / tracer color (hex); default to the net’s colors |
gauge | Overrides the net’s defaultGauge |
ident | Wire identification: { "colors": [...], "text": "..." } — up to 3 hex band colors and/or a printed label |
signalId | Usually derived from the endpoint cavities; may be set explicitly |
internalPn | Your organization’s part number for the wire stock |
role | "conductor" (the default) or "shield" (a cable’s shield/drain conductor) |
cableId, twistId | Membership in a cable / twisted group |
lengthOverride | Manual length; when set, the route-derived length is ignored |
cables[]
Section titled “cables[]”| Field | Meaning |
|---|---|
id, connectionIds | Required. The member conductors |
partNumber, internalPn | The manufactured cable’s part numbers |
shielded | true = the jacket has a foil/braid shield, even if no drain conductor is drawn yet |
shieldConnectionId | The connection (with role: "shield") representing the drain |
twists[]
Section titled “twists[]”| Field | Meaning |
|---|---|
id, connectionIds | Required. The twisted conductors (pair, triple, …) |
turnsPerLength | Twist rate |
bundles[]
Section titled “bundles[]”| Field | Meaning |
|---|---|
id, nodeA, nodeB | Required. The two ends of the trunk segment |
length | Physical length in meta.lengthUnit. A wire’s length is the sum of the bundle lengths along its route, plus breakout tails |
sleeving | Loom / convolute type — produces a BOM line with the total covered length |
color | Loom/cover color (hex, palette-decoded), drawn on the layout trunk |
The bundle graph is expected to be a tree: every wire routes along the unique path of bundles between its two endpoints. Cycles or missing paths show up as validation issues, not file errors.
mates[]
Section titled “mates[]”{ "id": ..., "nodeA": ..., "nodeB": ... } — two connectors that plug together, treated as a zero-length routing junction.
parts[]
Section titled “parts[]”Reusable connector templates. Assigning a part from the library copies it in here, so the document opens correctly on a machine without the library installed.
| Field | Meaning |
|---|---|
id, numberOfCavities | Required |
name | Display name |
color | Housing color (hex) |
gender | e.g. "Male", "Female" |
currentRating | Housing/system continuous current rating in amps |
designation | Cavity label generator (see below) |
face | Mating-face cavity arrangement (see below) |
mfr, pn, series | Library provenance: manufacturer, housing part number (e.g. "DT06-4S"), product series |
internalPn | Your organization’s part number for the housing |
contactPn | Default crimp contact part number — the BOM counts one per occupied cavity |
contactOptions | Compatible contacts copied from the library, each { "pn", "internalPn", "currentRating", "gaugeRange" } — this is what makes the per-cavity contact picker and the ampacity check work without the library present |
cavityContacts | Per-cavity default contact part numbers, in pin order (from a mixed-size housing); length equals the cavity count. A cavity’s own contactPn still overrides; where neither is set, contactPn applies |
wedgelockPn, backshellPn, sealPn | Accessory part numbers — each becomes a BOM line per connector |
plugPn | Cavity plug for unused positions — the BOM counts one per empty cavity on a sealed housing |
Designation strategy (designation) generates cavity labels:
{ "type": "sequential" }— labels each cavity in pin order;rowLabelspicks the scheme ("sequential"= 1, 2, 3… — the default;"alphabetical"= A, B, C…;"custom"= your own list incustom).{ "type": "grid", "rows": ..., "columns": ..., "rowLabels": ..., "columnLabels": ... }— row-major grid labels likeA1, A2, …, B1, …. Rows default to alphabetical, columns to numeric; either can be a custom list.
Face layout (face) describes the physical cavity arrangement on the mating face — what the connector looks like head-on, as drawn in the layout view. It is distinct from designation, which only generates labels. Positions are assigned to cavities in pin order:
{ "shape": "grid", "rows": 2, "columns": 4 }— row-major, top row first, left to right. The last row may be partial (rows × columnsmay exceed the cavity count).{ "shape": "rows", "counts": [8, 7, 8] }— per-row cavity counts, top to bottom, each row centered. Covers triangles (a 3-way =[1, 2]) and staggered faces. Counts sum to the cavity count.{ "shape": "circle", "rings": [12, 8, 1] }— a round shell; each entry is one ring of cavities, outermost first, laid clockwise from 12 o’clock. A final1is a center contact. Rings sum to the cavity count.
notes[]
Section titled “notes[]”{ "id": ..., "text": ... } — free-text canvas annotations. A note’s position is stored in the views maps under its id, just like a node.
devices[]
Section titled “devices[]”| Field | Meaning |
|---|---|
id, size | Required. size is { "w": ..., "h": ... } in layout canvas units |
label, mfr, partNumber, internalPn | Identity; a device with a partNumber appears as a BOM line item |
excludeFromBom | true = documented but not purchased |
image | Background image: { "asset": "<name>", "w": ..., "h": ... }. The image bytes are bundled inside the .loomflow container under the asset name; w/h are its natural pixel size |
Devices are layout-only — they never affect wire routing or lengths.
harnesses[]
Section titled “harnesses[]”| Field | Meaning |
|---|---|
id, name, members | Required. members is a snapshot of the harness group’s node ids taken when you named it. The app detects isolated harnesses automatically (connected groups of bundles) and uses members to re-match each name to the right group as the design evolves — best overlap wins. The snapshot is refreshed only when you rename, so normal editing leaves it untouched |
internalPn | Your organization’s part number for the finished assembly |
schematicandlayout— one position map per canvas view: id →{ "x": ..., "y": ... }, keyed by node, note, or device id. Schematic positions are grid-snapped for stable, predictable layout.edgePoints— connection or bundle id → an array of{ "x", "y" }drag waypoints the curve is routed through. Purely organizational: dragging a wire out of the way never changes the manufacturing data.