forma-dsl docs
Home Editor GitHub

Command line#

forma-dsl-cli solves a document and writes a binary STL, with no browser and no build step. It is what puts a .forma file in a Makefile, a CI job or a release pipeline.

npm install -g forma-dsl-cli
forma render riser.forma -p height=32 -o build/riser.stl

Or, without installing it, npx forma-dsl-cli render riser.forma -o riser.stl.

It knows nothing about the language itself. Every answer comes from forma-dsl - the same package the editor and the MCP server import - so a block added to the language is one it renders without being told, and an error it reports is the evaluator's own, with the evaluator's own position.


Commands#

Command What it does
forma render <file> solve a document and write a binary STL
forma params <file> list the parameters the document declares
forma models <file> list the models the document declares

A <file> of - reads the document from stdin. params and models take --json.

A document, and an -o/--out that names one, must resolve inside the directory the command is run in: a path that leaves it - by .., an absolute path, or a symlink pointing out of the tree - is refused with exit code 2.

render#

Option Default What it does
-o, --out <file> <file>.stl where to write. - is stdout
-m, --model <name> the first declared which model to build
--part <name> see below one part by name
-p, --param <name=val> each param's own default a value for one declared param. Repeatable
--params <file> - a JSON object of values by name. -p wins over it
--segments <n> the kernel's own a fixed segment count for every curve
--min-angle <deg> the kernel's own smallest angle between adjacent segments of a curve
--min-edge <n> the kernel's own smallest length a curve segment may be shortened to
--max-nodes <n> no limit refuse a document that would build more blocks than this
-q, --quiet off do not print the summary

The three resolution options are setQuality under another name, and are global to the kernel exactly as they are there. --max-nodes is render's own budget.

Without --part, a model with one part exports that part, and a model with several exports them unioned. STL has no notion of a part, and a slicer reading two overlapping shells makes a mess of them.


Passing parameters#

Three ways, and they compose.

One at a time, on the command line. A value is JSON where it parses as JSON, and the string as written otherwise - so there is no second layer of quoting to get wrong:

forma render part.forma -p count=6 -p center=true -p size=[10,20] -p label=M6
Written Arrives as
-p count=6 the number 6
-p center=true the boolean true
-p size=[10,20] the list [10, 20]
-p label=M6 the string "M6"

From a file. --params takes a JSON object of values by name - the thing you commit and diff, rather than assemble in a shell loop:

{
  "height": 36,
  "label": "M6",
  "center": true
}
forma render riser.forma --params params.json -o riser.stl

It has to be a JSON object. An array or a bare number parses but names nothing, and is refused with exit code 2 rather than quietly contributing nothing. Like every other path, it must resolve inside the directory the command runs in.

Both. -p wins over --params, which is what a build matrix wants: the values every variant shares live in the file, and the one that varies goes on the line.

for h in 8 20 40; do
  forma render riser.forma --params shared.json -p "height=$h" -o "build/riser-$h.stl"
done

And the document will tell you what it takes, so the first params.json need not be written by hand:

forma params riser.forma --json | jq 'map({(.name): .default}) | add' > params.json
{
  "height": 20,
  "label": "A",
  "center": false
}

In a pipeline#

Only what was asked for goes to stdout - the STL under -o -, and the listings. The summary and every diagnostic go to stderr, so a pipe stays clean:

forma render riser.forma --segments 64 -o - > riser.stl

Exit codes are the three a script needs to tell apart:

Code What happened
0 it worked
1 the document did not build - a syntax error, an unknown name, a degenerate solid
2 the invocation was wrong - an unknown flag, a missing file, a part that does not exist

The summary names the cache hits, because that is the number explaining why the second render of the same document in a loop is faster, and it is otherwise invisible:

build/riser.stl - 252 triangles · model "riser", part "body" · 6 nodes, 6 solved, 0 cached · 9 ms

Failures#

A failure is reported where an editor and a CI annotation can both find it: the position as a file:line:column: prefix, then the lines around it with a caret under the column.

riser.forma:7:7: rounded_rect: unknown attribute "corner_radius" - rounded_rect reads "center", "radius", "segments", "size"
5 |     extrude {
6 |       height = var.height
7 |       rounded_rect { size = [40, 20]  corner_radius = 4 }
  |       ^
8 |     }
9 |   }

The position comes from the error itself - see Errors for what each stage raises and what it means.


What a document declares#

params --json is the document describing itself, which is what lets a script build every variant without being told what the variants are:

forma params riser.forma --json
[
  {
    "name": "height",
    "type": "number",
    "default": 20,
    "required": false,
    "min": 8,
    "max": 40
  }
]

required is true for a param declared with no default, which is one that has to be supplied before the document will build at all.


Units#

Lengths are unitless in forma, and STL has no unit either. Every consumer treats them as millimetres.