Skip to main content
One statement per line. # starts a comment — comments are kept (they ride the statement below them, or the line they end, and survive reformatting). Blank lines are ignored. Type declarations may also take the block form: end the head with : and put one parameter per indented line beneath it (any consistent indent). Same model either way — the one-line form stays legal, and the formatter picks. Statement order across the file never matters — references resolve across the whole file. Within a line, slot order is fixed: category → type → [id] → placement (see Line anatomy). A model may span multiple .loft files: every file in a project directory compiles as ONE model with ONE namespace — reference names across files directly, with no import lines, and file layout never changes meaning (folders navigate, never scope).

Start here — build in this order

Everything below this section is reference. This section is method. Most broken models are correctly-spelled statements written in the wrong order and measured from the wrong thing.
  • Datums first. Declare levels and grids before anything measured from them. Order doesn’t matter to the parser, but it matters to you: an element you can’t anchor is a datum you haven’t declared yet
  • Types declare facts once; instances reference a type and add only what it delegates. A door’s size is a fact about the product — state it in a doortype, and every placement follows. One rule, every category
  • Then hosts, then what they host. Walls carry doors and windows (floors and roofs will carry their own). An opening names its host and a station along it, so the host must exist as a decision before the opening can be placed — this is the order the model depends in, whatever order you type
  • Relative by default; absolute is the last resort. Prefer, in this order: a declared datum ((grid.A, grid.1)) → an expression off one (grid.A + 14') → another element’s datum (wall.W1.end, door.D1.jamb.far) → a bare literal. A literal is the right answer only where the number has no reference — an origin, or a free choice
  • Say the measurement, not the coordinate. grid C vertical at grid.B + 30'-0" states a bay width; at 54'-0" states a position and discards why it sits there. Widen the first bay and grid C follows on its own — the coordinate form leaves every later number to be recomputed by hand
  • One travel frame governs everything. A wall’s own from → to direction decides its left from its right — which face justify names, which way layers stack, which side swing hinges on, and where at 0' starts for its openings. Choose the direction, state it in a comment, then write the line
  • When travel and measurement disagree, anchor to a datum. Closing a perimeter often forces a wall to run opposite the direction you’d naturally measure along it. Rather than counting backwards from that wall’s start, place its openings off a grid: at 4' past grid.2
  • Check your work with the plan’s dims button — it cycles stated → measured → off. stated draws only the facts your statements assert: a grid dimensioned from the anchor you chained it off, a carrier’s offset, an opening’s position. measured draws the adjacent-pair chain a paper drawing would show, from resolved positions. If the stated view comes up nearly empty, you dimensioned in coordinates — there are no relationships in the model for it to draw

Line anatomy — how a declaration reads

Every declaration is category → type → [id] → placement, with a slot dropped where the category doesn’t carry it. One of the two leading name slots is always required, so the first name after the keyword is never ambiguous.
  • Read the element form as a noun phrase: wall EXT-1 W1 = “an EXT-1 wall, called W1” — the type says what it’s made of, the id names it, the rest places it
  • Declare an element id only when something will reference it — host an opening, anchor a chain (wall.W1.end), take a carrier offset. Datums always carry their id
  • The v0.1 trailing spelling (wall W1 from … type EXT-1) is retired — the parser recognizes it and answers with the corrected line

Lengths

Always carry units. Bare numbers are never lengths.
  • Word suffixes (12ft, 8in) are accepted on input; canonical output prints the symbol forms
  • Rejected near-misses: 12'6" (the hyphen is mandatory — write 12’-6”) · 1/2' (no fractional feet — write 6”) · 12 (bare numbers are never lengths — write 12’)

Coordinates

(x, y), plan view: positive X is right (east), positive Y is up the page (north). Each slot is a length literal or a position expression over datums:
  • A bare token is never a reference and a bare number is never a length: a reference names its category (grid.A), a length carries its units.

Expressions

Anywhere a position or distance goes, a static expression works: + - * /, parentheses, datum references.
  • Declared vs projected: grids and levels are declared datums — they get their own statements; elements project the rest (wall.W1.end, door.D1.hinge). Both are named datums. An expression like grid.A - 5' is a position, not a datum — nothing can reference it later; where a slot demands a named datum, declare one there: grid A1 vertical at grid.A - 5' label "A.1"
  • A datum reference is a position; two same-axis positions subtract into a distance; positions never add, multiply, or divide
  • Bare numbers only multiply or divide — a bare number is never a length
  • An offset needs an anchor: write grid.A + (grid.B - grid.A)/2, not (grid.B - grid.A)/2
  • Space the minus between names: (grid.B - grid.A)/2, never (grid.B-grid.A)/2 — hyphens are name characters (as in EXT-1)
  • Cycles between datum positions are errors
  • Every reference names its category — grid.2, level.L1, wall.W1.end — so a name is never ambiguous and never guessed

Project & units

Levels

A level has three facts: elev; above — the level above it, worked out from the elevations when you don’t write it, written (above level.L3, or above none) when the next level up by elevation isn’t the one; and height, worked out from above when you don’t write it (the topmost level repeats the story below). Story height is a fact about the building, stated once. A level has above; an element has top; top above is how they meet.
  • above <level> names the level above when the default is wrong — a mezzanine you don’t want to stop at; above none says nothing is above; height beside either overrides the worked-out story. A level has no top (write above); stack is reserved
  • A level is declared once in the unit (any file, any position — datums beside the grids by convention). A bare level L2 line is the file’s level statement: file-scoped content that sets the default base for every element in the file that has one. A default, never a requirement — at most one per file, legal anywhere in it
  • With exactly ONE level declared, it is every element’s default and no file statement is needed. With two or more, an element needing a base must get one — from the file’s statement, or its own base — or the error on the element names both fixes
  • Elements default to their level’s base and top: a wall runs from its level to that level’s top (its elevation plus its story); base level.L2 + 7', height 9', top level.L3, and top above — the file’s next level up — override per instance (see Walls)

Grids

  • vertical = line of constant X
  • horizontal = line of constant Y
  • An id carries no dot — declare grid A5 … label "A.5" and the bubble reads A.5 while every reference says grid.A5

Wall types

  • Layers stack left → right of the wall’s travel (flip mirrors them)
  • core: marks the structural layer

Door & window types

The category requires; the type binds. Every door/window type must satisfy width and height — by fixing a value (stated once, every placement follows) or delegating it (the word instance: each placement supplies it).
  • Rhymes with walltype: keyword, type name, parameter pairs. Type names are project-unique per category
  • Block form (doortype HM-A: + indented lines) and one-line form are the same declaration — the block reads better past two or three parameters, and each parameter line is its own error address. Only doortype/windowtype take a block in v0.4; walltype stays one-line
  • Supplying a fixed parameter on a placement is an error naming the type and its value — duplicate the type to change it. Omitting a delegated width/height is an error too: the placement owes what the type left open
  • u-<name> adds user parameters — job-specific data that rides the format (typed from the literal: length, true/false, or quoted text — a count or a rating is text, since no literal spells a bare number; a delegated one states its type: u-hardware instance text). One name has ONE value type within its category — u- vocabulary belongs to the component, so a windowtype may type the same name differently than a doortype
  • A delegated u- parameter is optional unless the type adds trailing required — then every placement must supply the pair, same error as a missing delegated width
  • swing, reverse, and sill stay on placements — per-placement facts, never product facts (a door’s unstated sill is the level; a window’s is required)

Walls

A wall’s line is stated one of two ways — by its endpoints, or by a carrier it rides plus two extents that cut it. Both are first-class; pick whichever states the facts you already know. Carrier + extents is not a partition trick — an exterior wall running along a grid is the case it fits best. It is how a line-based element is placed — walls and roomlines today, every line element to come.
  • Order is category, type, id, placement — wall EXT-1 W1 from … reads “an EXT-1 wall, called W1” (and rhymes with its walltype EXT-1 declaration)
  • id (W1) optional — required only to host doors/windows
  • justify: centerline (default) | left | right — which face of the wall the drawn line is, in YOUR from → to direction (you typed it; you already know left from right). A side may name its plane: bare left is the finish face; justify left foc holds the structural core face (fos/foc/fom — face of stud/concrete/masonry, one plane, three field spellings), justify left fof says finish face out loud; justify core.centerline holds the core’s own center (differs from centerline on an asymmetric assembly). Hold the core when finishes may change — it’s the plane that doesn’t move
  • Everything is in the travel frame: layers stack left → right, door arcs open right; flip mirrors that content, never the body
  • Draw perimeters clockwise with justify left and the outside faces out
  • Carrier — along <ref> rides the location line ALONG a declared reference plane and justify places the body; at <offset> <side> of <ref> holds it off one instead. A wall carrier always needs both a side and an offset (along wall.W3 is an error)
  • Extents — any two transverse datums: grids, crossing walls, or wall line datums (wall.W5.end)
  • left/right are the frame of the wall THIS statement declares — its own from → to travel — never the carrier’s
  • Derive the frame in a comment before picking a side — # traveling W4→W5 (east), right = south — and the side words stop biting
  • A wall carrier with no plane named measures from the wall’s core face on your side — at 4' right of wall.W3 is 4’ clear of W3’s studs/block, same rule as past wall.W3; name another plane to measure elsewhere (right of wall.W3.fof the finish, right of wall.W3.centerline the center). Extents (from wall.W1 to wall.W3) are joins — the wall runs to the crossing wall’s centerline. The wall must sit clearly on the stated side (zero or straddling offsets are errors)
  • State each fact once: edit the offset and the wall, its openings, and its dimensions all move together
  • Vertical facts: a wall’s base is its level (the file’s level statement, or the unit’s one level) and its top is that level’s top. base <level> [± offset] pins the base per instance (Revit’s Base Constraint + Base Offset) — a wall from L1 to L4 is one element, and base level.L1 top level.L4 says where it lives. height 9' and top <level.L3|wall.W1.top> [± offset] override the top as ever (height defaults to the rest of the base level’s story)

Element datums (anchors & accessors)

Every element projects datums you can reference — <category>.<id>.<datum> (wall.W1.end, door.D1.jamb).
  • Projection table — wall: start · end · centerline · core.centerline · fos/foc/fom · fof · top · base | door: centerline · head · jamb · sill + hinge/strike (swinging leaf) | window: centerline · head · jamb · sill | opening: centerline · head · jamb · sill (the base interface, nothing more)
  • Line datums (wall.W1.end) fill a whole coordinate slot; plane datums (wall.W1.centerline, wall.W1.core.centerline, wall.W1.top) join expressions on their axis; opening datums are stations — reference them in directed measurements (past door.D1.jamb)
  • Wall planes, field notation: fos / foc / fom = the structural core face (face of stud / concrete / masonry unit — ONE plane; spell it for the material), fof = finish face, cl = centerline anywhere it appears (wall.W6.cl, door.D1.cl, to cl), core.centerline = the core’s center. Faces come in pairs, so they resolve where a tape can pick the side: at 4' past wall.W6.foc measures from the core face facing the opening (past wall.W6, with no plane named, means the same); in an expression, use .centerline / .core.centerline. ff is held for far face — finish face is fof
  • jamb.near/jamb.far are framed by the host wall — near toward its start, far toward its end (the origin at measures from). Stable in any direction, so at 2' past door.D1.jamb.far reads as it sounds
  • Bare jamb is whichever jamb the measurement reaches first — the one nearer the reference you measured from. Swap past for short of and it changes ends, which is why bare jamb always needs a direction word
  • Opening datums are stations (lengths from the wall’s start), so they do arithmetic in that wall’s own at: door 3070 D9 in wall.W1 at door.D1.jamb.far + 2'
  • Flip the swing and the jambs don’t move; rehang the door and hinge follows
  • Guardrail: anchor to declared datums (grids, levels) by default, to elements by exception — chains state genuinely relative intent, never a substitute for gridding
  • Driver → driven, one direction: W2 follows W1, W1 never feels W2; cycles are errors

Doors

  • The size lives on the doortype, not the line (see Door & window types) — a parameter pair is legal on a placement only when the type delegates that parameter
  • at measures from the wall’s start to the door center — a plain length is a station from that start (at 4'-0"), and everything below measures the same station off a datum instead
  • past <ref> measures forward from a reference; short of <ref> holds back from one — both along the wall’s from→to travel. A grid line or a datum like door.D1.jamb.far is a POINT: the tape runs from it to this opening’s center. An ELEMENT reference with no datum named (past door.D4) measures clear — from the reference’s facing jamb to this opening’s facing jamb — so at 0' past door.D4 is a flush sidelite, and past door.D4.centerline is the center-to-center spelling. A CROSSING WALL with no plane named (past wall.W6) is its core face facing this opening — face of stud/concrete/masonry, the plane a finish change never moves — the same as past wall.W6.foc; name another plane to measure elsewhere: past wall.W6.centerline (or .cl, the wall’s center), past wall.W6.fof (finish face), past wall.W6.core.centerline. From a wall the landing stays this opening’s center; add to jamb for clear
  • Add to jamb to measure to the opening’s near jamb instead of its center — the jamb the measurement reaches first, coming from the reference: at 2' past grid.1 to jamb. to jamb.far measures across to the far side instead (“the opening ends at 10’”): at 10' to jamb.far. (to edge, the older spelling, still parses; with an element reference, the near jamb is already the default landing.) to centerline overrides the other way — the measurement lands on the opening’s center: at 4' past door.D4 to centerline puts D4’s far jamb 4’ from this door’s centerline
  • swing is the hinge side (wall’s travel frame); doors open away from the wall’s facing — add reverse to open toward it. Hardware handing verbatim: swing left = LH, swing left reverse = LHR
  • Defaults: swing right; height and width come from the type, always

Windows

  • Same binding rule as doors: size on the windowtype, placement facts on the line
  • sill is required on every window and has no default — a sill height is where THIS window sits, not what the product is, so the type can’t supply it
  • It measures up from the level, not from the wall’s base: two windows written sill 3'-0" sit at the same elevation even when one host wall starts lower. sill 0' is a legal answer

Openings

  • The type-less cutter: a hole has no product identity, so instance dimensions are correct here — not a workaround. width and height are both required (no type carries them); an unstated sill is the level itself — a floor-to-head passage
  • sill measures up from the level, exactly as a window’s does — a hole and a window sit on the same plane
  • Same placement grammar as doors/windows (past / short of / to jamb / to centerline), and it projects the base datums (opening.O1.jamb.far)

Rooms

  • A room point marks an enclosure — anchor it to declared datums only (grid expressions fine: at (grid.A + 12', grid.1 + 8')). Element datums (wall.W1.end) are errors here: a wall edit must never drag a room along. The id is the room NUMBER — unique, optional, any id text (210, 210A, A-101, 210-1; a hyphen inside a token is always part of a name, so subtraction is always spaced: grid.2 - grid.1) — and it is what doors and finishes will reference; name is the tag text
  • The boundary is derived from the walls around the point (finish faces; doorways don’t break it) and the area shows on the plan tag and the takeoff. A room whose walls don’t close reads NOT ENCLOSED and the plan rings the open ends — close the wall, extend it, or draw a roomline; two room points in one enclosure each report the other
  • roomline is a zero-width room separation line: no type, no geometry, no 3D — a hairline on the plan. It uses the same placement as a wall (point pair, or from <extent> to <extent> at <offset> left|right of <ref> / along <grid>), may reference grids and walls, and nothing references it — it only closes a boundary. Id optional

Views

A view is a declared query over the model — never a container of elements. Every fact a view has is authorable on the view line itself; a viewtype is an OPTIONAL template — a named bundle of defaults — and a written fact on the view is a visible override. A view owns its file: each view statement lives in its own views/ file (its page), with only project, units, and viewtype declarations allowed beside it.
  • One view per file. A file holding a view holds only that view — a second view, or any element beside one (a wall, a room, even a walltype), is an error teaching the split. viewtype is the exception: a type declaration lives anywhere, so a template may sit beside the one view that applies it, or with the other types. Declaring a view is the moment a project becomes a directory; a single file with no view still draws — the implicit plan of its level, at the 1/8” default
  • Slot order is every statement’s: category → type-or-kind → id → placement. The second word is a viewtype reference or a bare kind word (plan, section, elevation) for an untyped view; kind words can never name a viewtype. The id is what references the view (a token); name "…" is the title that prints and what the picker shows — optional, trailing everything. The placement is the kind’s own: a plan is of a level (neither hosted in nor placed at; of level.L9 with no such level is an unknown-datum error naming the levels you do have), a section or elevation is placed by its plane: at <station> to <station>
  • The kinds are plan, section, elevation; rcp is a reserved word that names its era; a discipline (structural, MEP) is a filter on the query, never a kind
  • Sections and elevations are one machine under two words — the drawing set’s vocabulary. at is the cut plane’s station, to the far clip’s, each an ordinary station expression (grid refs preserved, wall anchors refused — a wall edit must not move the drawing); the view looks from at toward to and sees that far, so swapping them looks the other way, and to equal to at sees nothing. The axis comes from the grids the stations name; name none and the axis word leads: vertical/horizontal before at. Walls the plane crosses draw CUT — paper mass bounded by the cut pen, no poché (cut beside a wall, not on it: a plane down a wall’s own line cuts it at full length); walls beyond draw in the projection pen, nearer covering farther, openings punched from cut walls and projected on beyond faces; the mass’s outline profiles at the cut pen (the drawing is read by its silhouette); every level draws as a datum line with its name and elevation. Optional top/bottom trim the frame at LEVELS (top level.L3 + 2' — a bare length is a plan’s range word, not a trim); untrimmed shows everything, ground to bulkhead
  • The facts — scale, cut, and the range words top/bottom/depth — are each optional on both statements, any authoring order (canonical print order: scale cut top bottom depth). Resolution is per fact: the instance’s word, else its viewtype’s, else the language default — scale 1/8” flat, cut 4’-0”. Scale is a length literal in paper inches per model foot (1/4", 1/8", 3/16", 1"); it sizes the hatches, the text, and the line weights, so zooming the plan magnifies the drawing instead of changing its scale. Cut is a length above the level
  • The view range — top, bottom, depth — are lengths relative to the level (a leading - reads below it). Defaults when unwritten: top = the level’s top, bottom = the level, depth = bottom. What a plan shows is computed against them, never authored: an element crossing the cut plane draws cut; wholly between the cut and the top, dashed (above); wholly below the cut and above the depth, in the projection pen (beyond — depth -10' is how a plan shows the story below); outside the range, not at all. Openings follow their host. The relationships are errors with the fix written out — bottom above the cut, top below it, depth above bottom — checked over the effective values, so an authored word conflicts even with a defaulted one
  • from (x, y) to (x, y) after the query is the crop — the rectangle the view shows (Revit’s crop region), from the lower-left corner, to the upper-right. It is a placement on the view, before the facts; the corners are ordinary coordinates, so a crop to grids follows them when a bay widens, and a corner off an unknown grid is the usual unknown-datum error. Anchor corners to grids or literals, never to a wall (from wall.W1.end is refused like a room’s point). An inside-out pair (to left of or below from) is an error with the swap written out, never silently fixed. Grid lines end on the crop, bubbles sit just past it, dimension strings stack just inside it (paper offsets from the standard, the same at every scale), and elements outside it clip — a grid the crop doesn’t reach, a string to it, a string or room tag not wholly inside it, are hidden, never half-drawn. No crop: the view shows the whole model plus a margin
  • A view takes no indented body — annotations (dimensions, tags, notes) are reserved for a future version. New projects start two files — model.loft and the plan’s own views/ file — no project ships with zero views

Reserved keywords

Every major building category is reserved for future versions — using one is an error (not silently ignored). Highlights:
  • instance is a reserved value word: the delegation marker inside type declarations, an error as a value anywhere else

Words per statement

Every word each statement takes, from the category catalog — the same rows the parser reads. The type slot is type; a word’s options follow it; a default sits in parentheses; the words reserved for a future version come last, with what will read them.
  • project — no words
  • units — no words
  • level — elev · above <level>|none (the next level up by elevation) · height (the story to above; the topmost level borrows the story below) · reserved: stack (a real interleaved-sequence model (a garage’s 8’ levels beside the podium’s 10’ ones)); label (a bubble or tag printing text other than the id — the grid’s bubble is the one that ships)
  • grid — vertical|horizontal · at · label
  • walltype — width · layers · note
  • doortype — width · height · u-<name> · reserved: fire-rating (the duration value type (90 min, 2 hr) — every parameter value carries units; until then the corpus spells it as text, u-fire-rating "90"); hardware-set (the door worksheet — u- today, a built-in candidate then); kind (a second kind of leaf — the door’s and the window’s declarations list what is coming (D0242))
  • windowtype — width · height · u-<name> · reserved: fire-rating (the duration value type (90 min, 2 hr) — every parameter value carries units; until then the corpus spells it as text, u-fire-rating "90"); hardware-set (the door worksheet — u- today, a built-in candidate then); kind (a second kind of leaf — the door’s and the window’s declarations list what is coming (D0242))
  • wall — type · from · to · along · left · right · justify centerline|left|right|core.centerline (centerline) · flip (off) · base (the file’s level) · top (the wall’s level’s top) · height · reserved: function (the wall worksheet’s second pass — decided at decision 65(e), not yet a clause); structural-usage (the structure era); room-bounding (the furred or decorative wall that must not bound a room; bounding is per story since decision 74); label (a bubble or tag printing text other than the id — the grid’s bubble is the one that ships); phase, demolished (the phasing era — phases are an ordered list declared in their own file, and a view looks at the model as of one)
  • door — type · in · at · past · short · width · height · u-<name> · sill (the host level + 0’) · swing left|right (right) · reverse (off) · reserved: label (a bubble or tag printing text other than the id — the grid’s bubble is the one that ships); phase, demolished (the phasing era — phases are an ordered list declared in their own file, and a view looks at the model as of one)
  • window — type · in · at · past · short · width · height · u-<name> · sill · reserved: label (a bubble or tag printing text other than the id — the grid’s bubble is the one that ships); phase, demolished (the phasing era — phases are an ordered list declared in their own file, and a view looks at the model as of one)
  • opening — in · at · past · short · width · height · sill (the host level + 0’) · reserved: label (a bubble or tag printing text other than the id — the grid’s bubble is the one that ships); phase, demolished (the phasing era — phases are an ordered list declared in their own file, and a view looks at the model as of one)
  • room — at · reserved: level (a room that differs from its file’s level); department, occupancy (roomtype, or u- on categories); phase, demolished (the phasing era — phases are an ordered list declared in their own file, and a view looks at the model as of one)
  • roomline — from · to · along · left · right · reserved: phase, demolished (the phasing era — phases are an ordered list declared in their own file, and a view looks at the model as of one)
  • viewtype plan — scale (1/8”) · cut (4’-0”) · top (the level’s top) · bottom (the level + 0’) · depth (the range bottom) · reserved: units (the metric / areas era — a tier on the view ?? viewtype ?? project cascade)
  • viewtype section|elevation — scale (1/8”) · reserved: units (the metric / areas era — a tier on the view ?? viewtype ?? project cascade)
  • view plan — type · of · from · scale (1/8”) · cut (4’-0”) · top (the level’s top) · bottom (the level + 0’) · depth (the range bottom) · name · reserved: phase (the phasing era — the phase filter, discipline’s twin (a filter on the query, never a kind)); discipline (the views/filters era — phase’s twin (a filter on the query, never a kind); object styles stay one standard, the view’s discipline picks the resolver cell the way its scale does)
  • view section|elevation — type · vertical|horizontal · at · to · scale (1/8”) · top · bottom · name · reserved: phase (the phasing era — the phase filter, discipline’s twin (a filter on the query, never a kind)); discipline (the views/filters era — phase’s twin (a filter on the query, never a kind); object styles stay one standard, the view’s discipline picks the resolver cell the way its scale does)