# Molding > Author parametric Revit families (.rfa) as a JSON document. Molding is a node-graph > editor whose output, FamilyDefinitionJSON, is executed by a C# Revit add-in that > builds a fully-constrained .rfa. This file is the complete spec for generating that > JSON programmatically or with an LLM — enough to author complex families the way an > expert Revit family modeler would. Read this whole file before generating a family. Every field name is camelCase. Units are millimetres for lengths and radians for angles. The JSON must be fully serializable (no comments, no trailing commas, no functions). ## Pipeline ``` FamilyDefinitionJSON (you author this) → C# Revit runner (net48, Revit 2021+) → .rfa — fully-constrained parametric family → Revit scene (Revit's own solver flexes it; no plugin at runtime) ``` The runner generates the constraint apparatus (ReferencePlane + Dimension + AssociateElementParameterToFamilyParameter) so a coordinate that references a parameter becomes a real Revit constraint. Changing that parameter in Revit reflexes the geometry. ## Top-level shape ```json { "meta": { "template": "metric-generic-model", "name": "MyFamily" }, "parameters": [ ParameterJSON, ... ], "types": [ FamilyTypeJSON, ... ], // optional — size variants "nodes": [ NodeJSON, ... ] } ``` - `meta.template` — the Revit .rft template slug. Examples: `metric-generic-model`, `metric-door`, `metric-generic-model-wall-based`, `metric-generic-model-floor-based`. Adaptive families (adaptivePoint / adaptiveLoftForm / curveByPoints) require `metric-generic-model-adaptive`. The live list comes from `/api/templates`. - `meta.name` — output family name. - `parameters` — the inputs that drive geometry (see Parameters). - `types` — optional named preset value-sets (see Family types). - `nodes` — the geometry and elements. Shapes and curves may appear in any order relative to the forms that reference them (resolved by key). Transforms and arrays (`linearArray`, `radialArray`, `mirror`, `move`, `copy`, `rotate`, `scale`) must have their `source` form declared earlier in the list — the runner creates geometry in array order, and a transform whose source hasn't been created yet is silently skipped. ## Coord — the coordinate grammar Every position / size / depth / radius / angle field is a `Coord`, and a Coord is ONLY one of three things: ``` Coord = number // a fixed value: mm for lengths, radians for angles (45, 0, 1.5708) | "ParamKey" // follow a parameter — the field tracks that parameter's value | "-ParamKey" // the same parameter, negated (mirror across the origin) ``` A Coord is NEVER an expression. You cannot write `"Width / 2"` in a coordinate field. All arithmetic lives in a parameter's `formula`; to use `Width / 2`, declare a parameter `HalfWidth` with `formula: "Width / 2"` and reference `"HalfWidth"`. This is the single most important rule — a field is a literal number or one parameter key, nothing else. CRITICAL: a literal is a bare JSON number (`45`), NEVER a quoted string. `"45"` is read as a reference to a parameter named `45` (and fails as an unknown parameter) — write `45` for constants and `"Key"` only for parameter references. (Note: a parameter's own `args.input` / `args.formula` / `meta.min` ARE strings, e.g. `"input": "900"`; that is the one place numbers are quoted. In NODE args, a numeric Coord is unquoted.) - `Point2D` = `[Coord, Coord]` (x, y) - `Point3D` = `[Coord, Coord, Coord]` (x, y, z) Each element of a point is itself a Coord, so `["HalfWidth", 0]` and `[0, "-HalfHeight", "Z"]` are valid. The leading `-` negates only a parameter reference, never a literal. ## Parameters ```json { "key": "Width", // unique; referenced by Coord strings and formulas "type": "length", "isInstance": false, // false = type parameter (default); true = per-instance "shared": false, // true = shared parameter (cross-family / schedules) "group": "dimensions", // Properties-palette group "args": { "input": "900", // default value (a literal); editable unless a formula is set "formula": "Height / 2" // if present: computed, not user-editable, may reference other keys }, "meta": { "label": "Width", "min": "600", "max": "3000", "step": "1" } } ``` A parameter has EITHER `args.input` (a plain default the user can change) OR `args.formula` (derived, locked). Derived parameters are how you do math: declare them once and point geometry fields at their key. ### Parameter types (and units) `length` (mm) · `number` (dimensionless) · `angle` (radians) · `integer` · `boolean` (Yes/No) · `text` · `multilineText` · `material` (a Revit material name) · `area` (mm²) · `volume` (mm³) · `force` · `mass` · `slope` · `speed` · `currency` · `url`. Numeric types (length/number/angle/area/volume/integer/force/mass/slope/speed/currency) accept formulas. `boolean` uses `true`/`false` only. String types (text/url/multilineText/material) carry a literal value — no formula. ### Parameter groups `dimensions` · `constraints` · `construction` · `graphics` · `materials` · `identity` · `text` · `data` · `general` · `structural` · `electrical` · `mechanical` · `plumbing` · `analysis`. (Convention: driving sizes → `dimensions`; derived helpers → `constraints`.) ### Formula grammar Operators: `+ - * / % ** ^` (`**` and `^` are both power), comparison `== != < > <= >=`, and the ternary `cond ? a : b` for conditionals. Logical `&& || !` are NOT accepted by the validator (Revit's formula language has no such operators) — express a choice with a ternary instead, e.g. `hasGlass ? GlassThickness : 0`. Built-ins: `Math`, `Number`, `Boolean`, `isNaN`, `parseFloat`, `parseInt`. A formula may reference any other parameter key. Examples: ``` Width / 2 Height * 0.5 Thickness + Projection hasGlass ? GlassThickness : 0 Math.max(600, Math.min(Width, 1200)) Math.sin(Angle) * Reach // Angle is radians DegAngle * Math.PI / 180 // convert a degrees parameter to radians size < 600 ? 1 : size < 1200 ? 2 : 3 // nested ternary (e.g. shelf count) ``` ### instance vs type `isInstance: true` → editable on every placed copy in the project (e.g. a door `Width`). `isInstance: false` → set per family type, shared by all instances of that type. ## Family types (size variants) ```json "types": [ { "name": "0800 x 2100mm", "values": { "Width": 800, "Height": 2100, "Thickness": 45 } }, { "name": "0900 x 2100mm", "values": { "Width": 900, "Height": 2100, "Thickness": 45 } }, { "name": "1200 x 2400mm", "values": { "Width": 1200, "Height": 2400, "Thickness": 50 } } ] ``` Each type sets values for the parameters named in `values`. The user picks a type when placing the family. Geometry is one graph; types only vary parameter values (no per-type node sets). Values are `number` for numeric params, `true`/`false` for boolean, `string` for text. Material params cannot be set via types (ElementId, not supported). ## Profiles and references Forms are built from closed loops. A `profile` (or `bottomProfile` / `topProfile`) is an ARRAY of loop-node keys: the first key is the outer loop, any further keys are holes. ```json "profile": ["OuterRect", "HoleCircle"] // a rectangle with a circular hole ``` Closed-shape nodes usable in a profile array: `rectangle`, `circle`, `ellipse`, `polygon`, `slot`, `loop`. A `loop` is a freeform closed shape assembled from ordered open-curve segments via `segments: [curveKey, ...]`. (`filledRegion` is an annotation node — it has its own `profile` field but is NOT usable as a shape inside a form profile.) A form/curve `path` is a SINGLE key (string) pointing at a curve segment or a loop. ## Node reference (exact args) Notation: `name(len)` = Coord in mm; `name(rad)` = Coord in radians; `name?` = optional; `[...]` = array of node keys; `Point2D`/`Point3D` as above. `meta?` may carry `label`. Forms additionally accept `meta.subcategory` (string) and `meta.visibility` (see below), plus `modifiers` (see Modifiers). ### Segment — open curves (consumed by `loop.segments` or a form `path`) - `line` { start: Point2D, end: Point2D } - `arc` { start: Point2D, mid: Point2D, end: Point2D } // 3-point arc - `ellipseArc` { center: Point2D, radiusX(len), radiusY(len), startAngle(rad), endAngle(rad) } - `spline` { points: Point2D[] } // interpolated through points; minimum 2 points ### Shape — closed loops (form profiles) - `rectangle` { width(len), height(len), center?: Point2D, cornerRadius?(len) } - `circle` { radius(len), center?: Point2D } - `ellipse` { radiusX(len), radiusY(len), center?: Point2D } // radii are NOT flexible (see Limitations) - `polygon` { sides(integer), radius(len) /*circumradius*/, center?: Point2D } - `slot` { length(len), radius(len), center?: Point2D } // stadium; length = total outer edge-to-edge; straight section = length−2×radius - `loop` { segments: [segmentKey, ...] } // freeform from ordered segments ### Form — 3D solids/voids (all accept `isSolid?`=true, `meta`, `modifiers`) - `extrusion` { profile: [...], depth(len), startOffset?(len) } - `blend` { bottomProfile: [...], topProfile: [...], depth(len), bottomOffset?(len) } // loft between 2 shapes - `revolution` { profile: [...], axisStart: Point2D, axisEnd: Point2D, startAngle?(rad)=0, endAngle?(rad)=2π } - `sweep` { path: segmentKey|loopKey, profile: [...] } // path: segment = one curve; loop = all segments used in order - `sweptBlend` { path: segmentKey|loopKey, bottomProfile: [...], topProfile: [...] } // path: only the FIRST resolved curve is used - `opening` { host: formKey, profile: [...] } // boolean cut through host (single loop) - `adaptiveLoftForm` { profiles: [[adaptivePointKey, ...], ...] } // each inner array = one cross-section; adaptive template Set `isSolid: false` to make any form a VOID that cuts overlapping solids. A perforated opening is best done as a void `extrusion` overlapping the solid (see Limitations on `opening`). ### Curve — visible line elements (not solids) - `modelCurve` { curve: segmentKey, meta?: { lineStyle?: string } } // 3D model line; curve MUST be a segment (line/arc/ellipseArc/spline), not a loop/shape - `symbolicCurve` { curve: segmentKey, meta?: { lineStyle?: string } } // 2D symbolic line; same constraint as modelCurve ### Array — duplicate a node (count/spacing are BAKED, see Limitations) - `linearArray` { source: nodeKey, count(integer), offset: Point2D } - `radialArray` { source: nodeKey, count(integer), center: Point2D, totalAngle(rad) } ### Transform — reposition a node `copy` and `mirror` preserve the source and add a duplicate. `move`, `rotate`, `scale` replace the source in place (Revit mutates the element; no extra copy). - `mirror` { source: nodeKey, axis: "x" | "y" } - `copy` { source: nodeKey, offset: Point3D } - `move` { source: nodeKey, offset: Point3D } - `rotate` { source: nodeKey, axis: "x" | "y" | "z", angle(rad) } - `scale` { source: nodeKey, factor(Coord)=1, origin?: Point3D } ### Datum - `referencePlane` { name: string, axis: "x" | "y", offset(len)=0 } ### Material - `material` { name: string /*Revit material*/, color?: [r, g, b] /*0-255*/, transparency?: number /*0-100*/ } ### Text - `modelText` { text: string, position: Point2D, depth(len), meta?: { label? } } ### Annotation — 2D, view-only - `image` { path: string /*absolute path on Revit machine*/, position: Point2D } - `textNote` { text: string, position: Point2D } - `filledRegion` { profile: shapeKey[] } // hatched region; each key is any closed shape (loop/rectangle/circle/ellipse/polygon/slot); supports outer + inner loops - `detailCurve` { curve: segmentKey, meta?: { lineStyle?: string } } // 2D detail line; curve must be a segment (line/arc/ellipseArc/spline), not a loop/shape - `control` { shape: "horizontal" | "vertical" | "doubleHorizontal" | "doubleVertical", position: Point2D } ### MEP - `connector` { kind: "electrical" | "pipe" | "duct" | "conduit" | "cableTray", host: formKey } ### Family - `nestedFamily` { path: string /*absolute path to .rfa on Revit machine*/, position: Point2D, symbol?: string, rotation?(rad), parameterLinks?: { "": " | -" } } // parameterLinks values are Coord-style: "Width" or "-Width" to negate (mirror) ### Adaptive (adaptive template only) - `adaptivePoint` { position: Point3D } // a placement point - `curveByPoints` { points: [adaptivePointKey, ...] } // reference curve through points; minimum 2 points - `adaptiveLoftForm` — listed under Form ## Modifiers (on a form node) ```json "modifiers": [ { "nodeType": "applyMaterial", "args": { "material": "" } } ] ``` `applyMaterial.args.material` is the KEY of a `material` node. Material nodes are resolved in a pre-pass so they can appear anywhere in the `nodes` array. This is the only modifier. ## Form meta — subcategory & visibility ```json "meta": { "label": "Door Panel", "subcategory": "Panel", // Revit subcategory (graphics override target) "visibility": { "coarse": false, "medium": true, "fine": true } // per detail level; default all true } ``` Both are applied by the runner: `subcategory` assigns the form to a family subcategory, `visibility` controls in which detail levels the form shows. ## How parametric driving works (what flexes) A Coord that references a parameter is wired to a Revit constraint so the family flexes: - Form scalars — extrusion `depth`/`startOffset`, blend `depth`/`bottomOffset`, revolution `startAngle`/`endAngle` — associate to the form's end/angle parameter. - Profile sketch points — each unique (axis, paramKey) gets a reference plane + a labelled dimension, so the sketch point moves when the parameter changes. - `referencePlane.offset` is labelled with its parameter. To get a flexing family, drive sizes through parameters and reference them with `P` (a key), not hard numbers. ## Known limitations (author around these) The runner emits a notice and bakes the value when a feature has no Revit API hook. Do not promise a flex you can't deliver — instead model it differently: - `linearArray.count` / `offset` and `radialArray.count` / `totalAngle` are BAKED at generation (Revit has no built-in parameter for array count/spacing). If the count must flex, model the repeats explicitly (e.g. parameters + copies) or accept a fixed count. - `ellipse` radii and `ellipseArc` radii cannot be dimensioned by Revit — a notice is emitted and the radius is baked at generation size. The containing form's depth still flexes. Use `rectangle`/`circle`/`loop` when you need flexible profile dimensions. - `opening` and `blend` caps take a SINGLE loop — the runner resolves only the first resolvable key; extra loops (holes) are dropped. `extrusion`/`revolution`/`sweep`/ `sweptBlend` pass all loops (outer + holes) to the Revit API. For a perforated cut use a void `extrusion` with the outer loop first and hole loops after. - String-type parameters (text/url/multilineText/material) take a literal, not a formula. - MEP `connector` is created but not auto-joined into a duct/pipe system. Everything else — multi-loop solid extrusions with holes, multi-type catalogs with formula-driven dimensions, nested families with `parameterLinks`, subcategory/visibility graphics, materials, revolutions/sweeps, adaptive lofts, voids — is fully supported. ## Worked example 1 — door panel with knob hardware Multi-extrusion, derived parameters, negative `startOffset`, subcategory + per-detail visibility, and a material. (Width/Height/Thickness come from the door template's base parameters.) ```json { "meta": { "template": "metric-door", "name": "Door with Knob" }, "parameters": [ { "key": "Width", "type": "length", "isInstance": true, "group": "dimensions", "args": { "input": "900" } }, { "key": "Height", "type": "length", "isInstance": true, "group": "dimensions", "args": { "input": "2100" } }, { "key": "Thickness", "type": "length", "group": "dimensions", "args": { "input": "45" } }, { "key": "KnobRadius", "type": "length", "group": "construction", "args": { "input": "18" } }, { "key": "KnobInset", "type": "length", "group": "construction", "args": { "input": "60" } }, { "key": "KnobHeight", "type": "length", "isInstance": true, "group": "construction", "args": { "input": "1000" } }, { "key": "HalfHeight", "type": "length", "group": "constraints", "args": { "formula": "Height / 2" } }, { "key": "KnobX", "type": "length", "group": "constraints", "args": { "formula": "Width / 2 - KnobInset" } } ], "types": [ { "name": "0800 x 2100mm", "values": { "Width": 800, "Height": 2100, "Thickness": 45 } }, { "name": "0900 x 2100mm", "values": { "Width": 900, "Height": 2100, "Thickness": 45 } } ], "nodes": [ { "nodeType": "material", "key": "Wood", "args": { "name": "Wood - Walnut", "color": [120, 81, 45] } }, { "nodeType": "material", "key": "Chrome", "args": { "name": "Metal - Chrome", "color": [205, 205, 210] } }, { "nodeType": "rectangle", "key": "PanelProfile", "args": { "width": "Width", "height": "Height", "center": [0, "HalfHeight"] } }, { "nodeType": "extrusion", "key": "Panel", "meta": { "label": "Door Panel", "subcategory": "Panel" }, "args": { "profile": ["PanelProfile"], "depth": "Thickness", "isSolid": true }, "modifiers": [ { "nodeType": "applyMaterial", "args": { "material": "Wood" } } ] }, { "nodeType": "circle", "key": "KnobProfile", "args": { "radius": "KnobRadius", "center": ["KnobX", "KnobHeight"] } }, { "nodeType": "extrusion", "key": "Knob", "meta": { "label": "Knob", "subcategory": "Hardware", "visibility": { "coarse": false, "medium": true, "fine": true } }, "args": { "profile": ["KnobProfile"], "startOffset": "Thickness", "depth": 55, "isSolid": true }, "modifiers": [ { "nodeType": "applyMaterial", "args": { "material": "Chrome" } } ] } ] } ``` ## Worked example 2 — frame with a glazed panel and a void hole A rectangular frame (outer + inner hole), a recessed glass blend, and a circular void that punches through the frame. Shows multi-loop profiles, a void, and a derived inner size. ```json { "meta": { "template": "metric-generic-model", "name": "Vent Panel" }, "parameters": [ { "key": "W", "type": "length", "group": "dimensions", "args": { "input": "600" } }, { "key": "H", "type": "length", "group": "dimensions", "args": { "input": "400" } }, { "key": "Frame", "type": "length", "group": "dimensions", "args": { "input": "40" } }, { "key": "Depth", "type": "length", "group": "dimensions", "args": { "input": "30" } }, { "key": "InnerW","type": "length", "group": "constraints", "args": { "formula": "W - 2 * Frame" } }, { "key": "InnerH","type": "length", "group": "constraints", "args": { "formula": "H - 2 * Frame" } }, { "key": "Hole", "type": "length", "group": "dimensions", "args": { "input": "60" } } ], "nodes": [ { "nodeType": "material", "key": "Alu", "args": { "name": "Metal - Aluminium", "color": [180, 182, 186] } }, { "nodeType": "rectangle", "key": "Outer", "args": { "width": "W", "height": "H" } }, { "nodeType": "rectangle", "key": "Inner", "args": { "width": "InnerW", "height": "InnerH" } }, { "nodeType": "extrusion", "key": "FrameSolid", "meta": { "label": "Frame" }, "args": { "profile": ["Outer", "Inner"], "depth": "Depth", "isSolid": true }, "modifiers": [ { "nodeType": "applyMaterial", "args": { "material": "Alu" } } ] }, { "nodeType": "circle", "key": "HoleProfile", "args": { "radius": "Hole", "center": [0, 0] } }, { "nodeType": "extrusion", "key": "VentHole", "meta": { "label": "Vent Hole" }, "args": { "profile": ["HoleProfile"], "depth": "Depth", "isSolid": false } } ] } ``` ## Validation & API (TypeScript, mirrored by the C# runner) ```ts validateFamilyDefinition(json) // { valid, errors: ValidationError[] } — run before submitting parseFamilyDefinition(raw) // FamilyDefinitionJSON — throws ZodError on invalid shape safeParseFamilyDefinition(raw) // { success, data?, error? } — safe parse (no throw) evaluateGraph(json, overrides?) // { parameters, errors, notices } — preview values + warnings resolveCoord(coord, params) // number — how a Coord resolves composeFamily(base, fragments) // merge reusable fragments into one wire payload ``` ValidationError kinds: `invalidMeta` (missing/empty meta.template or meta.name), `duplicateParameterKey`, `duplicateNodeKey`, `unknownParameter`, `unknownNodeReference`, `circularDependency`, `invalidKey`, `invalidFormula`, `typeMismatch`. Authoring rules the validator enforces — keep these in mind while generating: - Every node `key` and parameter `key` is unique within the family. - Keys `Math`, `Number`, `Boolean`, `isNaN`, `parseFloat`, `parseInt` are reserved (formula builtins); `__proto__`, `constructor`, `prototype` are also forbidden — any of these as a parameter or node key produces an `invalidKey` error. - A Coord string or formula must reference an existing parameter key — forward references are allowed, but circular dependencies (A formula → B formula → A) are not. - A `profile` entry should name a closed-shape node (rectangle/circle/ellipse/polygon/slot/loop); a `path` should name a segment or loop node. The validator only checks that the key exists — wrong types are silently skipped at generation time, not rejected. - Lengths are mm, angles are radians; put any arithmetic in a parameter formula. ## Links - /docs — human-readable guide (the editor UI; the V/P toggle is the Coord concept) - /docs/nodes — full node reference: every node, its args, units, and field descriptions - /online — the browser node-graph editor - /docs/revit — the Revit plugin guide (download, install, Generate workflow)