Skip to content

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:

  1. 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.
  2. Logical vs. cosmetic. Manufacturing data (nodes, connections, bundles) is separate from view data. Canvas positions live under views, keyed by element id.

Keys are always written in this order:

KeyWhat it holds
schemaVersionInteger format version (currently 3); the loader migrates older files up on open
metaDocument name and units — lengthUnit ("mm" or "in"), gaugeUnit ("AWG" or "mm2")
paletteNamed colors; the decode key for every hex color/stripe value in the document
signalsLogical nets: name, default color/gauge, optional voltage and current ratings
nodesConnectors, terminals (ring/fork lugs), splices, and layout-only junctions — each with its cavities
connectionsPhysical conductors: two endpoints (node + cavity ids), color, gauge, optional cable/twist membership
cablesMulti-core / shielded cable groupings of connections
twistsTwisted pairs/triples of connections
bundlesLayout trunk segments between two nodes, carrying physical length
matesConnector pairs that plug together (zero-length routing junctions)
partsConnector part templates, copied in from the library so the file opens without it
notesFree-text canvas annotations
devicesLayout-only enclosures/PCBs that connectors mount onto
harnessesUser-given names for detected isolated harness groups
viewsCosmetic 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.

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 } }
}
}

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:

MigrationWhat changes
v1 → v2notes added; the cosmetic single-offset wireOffsets view data was replaced by edgePoints waypoints (old offsets are dropped — they were cosmetic only)
v2 → v3devices 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.

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.

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 .loomflow file (devices[].image.asset names 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.

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.

FieldMeaning
nameDocument 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
sourceFormatProvenance when the document was imported from another tool (name + version), kept for lossless round-trip export

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.

FieldMeaning
id, nameRequired
color, stripeDefault insulation / tracer color (hex) inherited by new wires on this net
defaultGaugeGauge inherited by new wires on this net unless overridden per wire
type"power" or "data"; absent = unspecified. Feeds the power/derating analysis
voltageNominal system voltage in volts — enables percentage voltage-drop checks on power nets
nominalCurrent, maxCurrentContinuous / peak current in amps
notesFree text

All node kinds share these fields:

FieldMeaning
id, kind, cavitiesRequired. kind is "connector", "terminal", "splice", or "junction"
labelDisplay name
breakoutLengthFan-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
excludeFromBomtrue = documented but not purchased — the BOM skips this node entirely
deviceIdMounts the node on a device (devices[].id); mounted nodes move with the device on the layout

Kind-specific fields:

  • connectorpartId points at a template in parts. The cavities array order is the pin order.
  • terminal — a ring/fork/eyelet lug with exactly one cavity. terminalType ("Ring", "Fork", …) and internalPn (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; cavities is always empty.

Each entry in cavities:

FieldMeaning
idRequired
labelPin designation, e.g. "A1" — generated by the part’s designation strategy, or set manually
signalIdThe intended net for this contact (signals[].id)
contactPnThe 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
gaugeIntended 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
FieldMeaning
id, endA, endBRequired. Each endpoint is { "node": <node id>, "cavity": <cavity id>, "extraLength": <number> }; extraLength adds a service loop or strip allowance at that end
color, stripeInsulation / tracer color (hex); default to the net’s colors
gaugeOverrides the net’s defaultGauge
identWire identification: { "colors": [...], "text": "..." } — up to 3 hex band colors and/or a printed label
signalIdUsually derived from the endpoint cavities; may be set explicitly
internalPnYour organization’s part number for the wire stock
role"conductor" (the default) or "shield" (a cable’s shield/drain conductor)
cableId, twistIdMembership in a cable / twisted group
lengthOverrideManual length; when set, the route-derived length is ignored
FieldMeaning
id, connectionIdsRequired. The member conductors
partNumber, internalPnThe manufactured cable’s part numbers
shieldedtrue = the jacket has a foil/braid shield, even if no drain conductor is drawn yet
shieldConnectionIdThe connection (with role: "shield") representing the drain
FieldMeaning
id, connectionIdsRequired. The twisted conductors (pair, triple, …)
turnsPerLengthTwist rate
FieldMeaning
id, nodeA, nodeBRequired. The two ends of the trunk segment
lengthPhysical length in meta.lengthUnit. A wire’s length is the sum of the bundle lengths along its route, plus breakout tails
sleevingLoom / convolute type — produces a BOM line with the total covered length
colorLoom/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.

{ "id": ..., "nodeA": ..., "nodeB": ... } — two connectors that plug together, treated as a zero-length routing junction.

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.

FieldMeaning
id, numberOfCavitiesRequired
nameDisplay name
colorHousing color (hex)
gendere.g. "Male", "Female"
currentRatingHousing/system continuous current rating in amps
designationCavity label generator (see below)
faceMating-face cavity arrangement (see below)
mfr, pn, seriesLibrary provenance: manufacturer, housing part number (e.g. "DT06-4S"), product series
internalPnYour organization’s part number for the housing
contactPnDefault crimp contact part number — the BOM counts one per occupied cavity
contactOptionsCompatible 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
cavityContactsPer-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, sealPnAccessory part numbers — each becomes a BOM line per connector
plugPnCavity 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; rowLabels picks the scheme ("sequential" = 1, 2, 3… — the default; "alphabetical" = A, B, C…; "custom" = your own list in custom).
  • { "type": "grid", "rows": ..., "columns": ..., "rowLabels": ..., "columnLabels": ... } — row-major grid labels like A1, 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 × columns may 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 final 1 is a center contact. Rings sum to the cavity count.

{ "id": ..., "text": ... } — free-text canvas annotations. A note’s position is stored in the views maps under its id, just like a node.

FieldMeaning
id, sizeRequired. size is { "w": ..., "h": ... } in layout canvas units
label, mfr, partNumber, internalPnIdentity; a device with a partNumber appears as a BOM line item
excludeFromBomtrue = documented but not purchased
imageBackground 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.

FieldMeaning
id, name, membersRequired. 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
internalPnYour organization’s part number for the finished assembly
  • schematic and layout — 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.