Recipes¶
Patterns that come up in real forms. Each one is short, complete, and explains the reasoning rather than just the wiring.
Contents — Gate a form behind a button · One form, several outcomes · Dependent dropdowns · A wizard · Progressive disclosure · Cross-field validation · A live total · Preview what a form is about to do · Remember answers between runs · Run a graph unattended · Ship a form as a file · Pick Revit elements · Match Revit's theme · Long forms · Debug a form
Gate a form behind a button¶
The problem. In Automatic run mode the dialog reappears every time anything upstream changes. Even in Manual mode, re-running a graph to fix something downstream re-asks a question that was already answered.
The fix. Wire a boolean into trigger. Exactly false skips the dialog and returns the last
answers.
Flip it to true, run, flip it back to false. Every subsequent run reuses the answers.
trigger doubles as a sequencing input: wire the output of some earlier node into it when the
form must not appear until that work is done.
Manual run mode is still the right setting for any graph built around a form.
triggermakes it pleasant; it does not make Automatic mode a good idea.
One form, several outcomes¶
The problem. "Place", "Place and continue", and "Preview" are three answers to one question, and three separate forms is three times the maintenance.
The fix. Layout.Button closes the form and reports its tag.
Form.Show("Place families", [
Input.DropDown("Family type", items: types, key: "type"),
Input.Integer("Count", defaultValue: 1, key: "count"),
Layout.Row([
Layout.Button("Preview", tag: "preview"),
Layout.Button("Place", tag: "place", primary: true)
])
])
Then branch on Result.ButtonClicked(form) — "preview", "place", or "cancel" if they backed
out.
Footer buttons work too, via Form.Options(extraButtons: [...]), if you would rather they sat
beside Submit than in the body.
Dependent dropdowns¶
The problem. Choosing a category should narrow the family list.
The honest answer. Interlude cannot repopulate a dropdown's items while the form is open — options are fixed when the form is built. A form is a value, and the value does not change while it is on screen.
What to do instead, in order of how well it works:
Show one dropdown per category and hide the rest. Fine for a handful of categories, and the answers stay separate:
Behavior.VisibleIf(
Input.DropDown("Wall type", items: wallTypes, key: "wallType"),
Condition.Equals("category", "Walls"))
Behavior.VisibleIf(
Input.DropDown("Door type", items: doorTypes, key: "doorType"),
Condition.Equals("category", "Doors"))
Only the visible one is validated, and both appear in the results — read whichever matches the chosen category.
Or use two forms. Ask for the category, then build the second form from the answer:
Two dialogs is a real cost, but it scales to any number of categories and the second form is built from live data.
Or show everything with a filter. Input.ListBox has a search box; a single list of every
family, filtered by typing, is often better than two clicks through a hierarchy.
A wizard¶
The problem. Twenty fields is intimidating in one column.
The fix. Tabs. Everything is answered in one dialog, so there is no back-and-forth state to manage, but the user sees one page at a time.
Form.Show("Export", [
Layout.Tabs([
Layout.TabPage("What", [
Input.ListBox("Views", items: views, key: "views")
]),
Layout.TabPage("Where", [
Input.DirectoryPath("Folder", key: "folder"),
Input.TextBox("Prefix", key: "prefix")
]),
Layout.TabPage("How", [
Input.DropDown("Format", items: ["DWG", "IFC", "PDF"], key: "format"),
Input.CheckBox("Overwrite existing", key: "overwrite")
])
])
], width: 620)
A field failing validation on a hidden tab still blocks submission, and the error is on a page the
user cannot see. Keep required fields on the first tab where you can, and lean on helpText.
Progressive disclosure¶
The problem. Advanced options that nine users in ten never need.
The fix. A collapsible section, closed by default.
Layout.Section("Advanced", [
Input.Number("Tolerance", defaultValue: 0.001, key: "tolerance"),
Input.CheckBox("Verbose logging", key: "verbose")
], collapsible: true, expanded: false)
Unlike VisibleIf, a collapsed section's fields are still visible to the form — they are
validated and they can be required. That is the right behaviour here: the user can open the
section and fix the problem.
Use Behavior.VisibleIf on the section instead when the options genuinely do not apply.
Cross-field validation¶
The problem. "End must be after start" involves two fields, and it has to re-check when either changes.
The fix. Rule.CompareTo reads another field by key and declares the dependency, so the
session re-runs it when that field moves.
Behavior.WithValidation(
Input.DatePicker("End", key: "end"),
Rule.CompareTo("start", "GreaterThan", "The end date must be after the start date."))
Works for numbers too — a maximum that must exceed a minimum.
A live total¶
The problem. A quantity and a unit price should show a total that keeps up.
The fix. A computed field. It becomes read-only and recalculates in dependency order.
Behavior.WithComputed(
Input.Number("Subtotal", unit: "£", key: "subtotal"),
Compute.Arithmetic("quantity", "Multiply", "unitPrice"))
Behavior.WithComputed(
Input.Number("Total", unit: "£", key: "total"),
Compute.Arithmetic("subtotal", "Add", "vat"))
Chains are fine and settle in one pass — total sees the new subtotal, not last run's. Declare
them in any order; the dependency graph decides.
A loop (a from b, b from a) is rejected when the form is built, with both keys named.
For a text summary you want back out of the form, Compute.Format into a field:
Behavior.WithComputed(
Input.TextBox("Summary", key: "summary"),
Compute.Format("{quantity} items for {orderedBy}, £{total} including VAT"))
To show one without collecting it, use Layout.Preview instead — see below.
Preview what a form is about to do¶
The problem. A form describes a rename rule, and the user is guessing what it will produce.
The fix. Layout.Preview. It takes a template and shows the result, live, as the fields it
reads are edited:
One node. A preview answers nothing: it has no key, never appears in values, and is never
validated. That is the difference between it and a read-only field carrying a computed value —
reach for that one when you need the result back out of the form, and this one when you just need
the user to see it.
Where the sample comes from. A preview shows one thing, and Interlude knows nothing about the fifty views your graph is holding. So put the sample on the form:
That costs a line and earns it back, because the field is editable: someone can paste the worst name in the model into it and see what the rule does before committing to it.
Choosing between two forms. value accepts any Compute node, not just a template:
Layout.Preview("New name",
Compute.If(Condition.IsChecked("add_number"),
Compute.Format("{prefix}{sample_name} {start_number:000}"),
Compute.Format("{prefix}{sample_name}")))
Numbers. {start_number:000} pads to three digits. Any .NET format specifier works after the
colon — {total:F2}, {due:yyyy-MM-dd} — and without one a number prints the shortest form that
round-trips, so 546.0 reads 546. This bites in previews more than anywhere else, because a
preview is the one place the exact characters are the point.
What a preview cannot do. Templates interpolate; they do not transform. There is no find-and-replace, no substring, no case change. A rename rule of that kind has to be computed in the graph, and the preview can only show what the form itself can work out. If that is your rule, show the parts you can and say so in the help text rather than previewing something that is not quite what will happen.
samples/rename-views.json is this recipe as a working form.
Remember answers between runs¶
On by default. A form re-opens with the answers it was last submitted with; cancelling never overwrites them.
Give the form a stable formId if you want that memory to survive edits to the form:
Without one, the id is derived from the title and field keys — so adding a field starts a fresh
memory rather than half-restoring the old one. That is usually what you want; a formId is for
when it is not.
Form.Forget("acme.export-views") clears it. Form.Forget() clears everything.
Memory lives in the Dynamo process and is gone when Dynamo closes. To persist across sessions,
write the answers yourself with Form.ToJson-style serialisation and feed them back as defaults.
Run a graph unattended¶
The problem. A graph with a form is scheduled, or run from the command line, or through Generative Design. There is nobody to answer it.
The fix. Decide explicitly which you want:
headlessUseDefaults: false (default) → the graph stops, with an explanation
headlessUseDefaults: true → every field's default, wasSubmitted = false
The default is to stop, deliberately. A graph that silently proceeds with defaults having asked nobody anything is a graph that quietly does the wrong thing at 3am.
When you do opt in, check wasSubmitted and treat false as "nobody chose this" — because nobody
did.
Ship a form as a file¶
The problem. The same form is needed in five graphs, and five copies drift apart.
The fix. Build it once, save it, load it.
The file can live in a repository, be reviewed in a pull request, and be edited by hand — the preview harness reloads and reshows it whenever it is saved.
Use Result.HasKey in a graph reading a form it did not build. See form-json.md
for what does and does not survive the round trip — live Revit elements as dropdown options being
the notable exception.
Pick Revit elements¶
Interlude has no Revit dependency, so there are no Revit-aware picker nodes. There do not need to
be: collect elements in the graph and pass them as items.
All Elements of Category ──► items ─┐
Element.Name ──► displayNames ─┴─► Input.DropDown("Wall type", key: "wallType")
Result.ValueByKey(values, "wallType") gives back the element, not its name.
For several:
Input.ListBox("Rooms", items: rooms, displayNames: roomNames, allowMultiple: true, key: "rooms")
Result.GetList(values, "rooms") → the room elements
A form built this way cannot be saved to JSON and reopened in another model — the elements do not exist there. Build the options in the graph each run.
Or keep the form in a file and fill in the one field the model owns. Form.WithOptions puts a
graph's objects onto a choice field of a form that already exists, so the layout, labels, conditions
and validation stay in the checked-in file and only the live data comes from the graph:
read from disk ──► Form.FromJson ──► Form.WithOptions(key: "rooms", items: rooms,
displayNames: roomNames)
──► Form.ShowDefinition(trigger)
The file holds a placeholder for rooms — or nothing at all — and the graph replaces it each run.
What comes back is still the element. Chain the node once per field.
Match Revit's theme¶
The default look is neubrutalist — heavy outlines, hard shadows, loud flat colour — and it follows the Windows light/dark setting, which is usually what Revit follows too. It is a deliberate choice, and it is not the right one for a dialog that has to disappear into a corporate deployment.
For the conventional look, wire a theme:
Theme.Light()
Theme.Dark("#4C8DFF")
Theme.Create(mode: "Dark", accent: "#F0A500", density: "Compact", cornerRadius: 2)
To keep the default and only turn it down a little, start from it:
Theme.Neubrutalism(dark: true, accent: "#4C8DFF")
Theme.Create(borderWidth: 1, shadowOffset: 2, shape: "Square", heavyText: true)
Accent text colour is picked by contrast, so a bright accent still reads.
Interlude never writes to the host application's resources, so nothing you do here can leak into Revit's own UI.
Long forms¶
Layout.Tabsfirst — see a wizard.density: "Compact"andlabelWidth: 0fit noticeably more on screen.maxHeightsets where the form starts scrolling;widthis worth raising past 420 for anything with side-by-side fields.Layout.Rowputs related short fields on one line:
Layout.Row([
Input.Number("Width", unit: "mm", key: "width"),
Input.Number("Height", unit: "mm", key: "height")
])
Layout.Gridwithcolumns: "auto, *"aligns a column of labels against a column of controls more tightly than the default label column does.
Debug a form¶
Form.Check reports what Interlude can see wrong without showing anything: conditions naming
fields that do not exist, duplicate keys, loops between computed values.
The commonest finding is a typo in a condition's key. A condition on a key no field uses always reads as empty, so the field it controls never appears — and nothing else tells you why.
Result.Keys lists what a form actually answered, which settles any question about what a
label slugified to.
Form.ToJson is the fastest way to ask for help: it is the whole form, reproducible by
anyone.
The preview harness renders any form JSON in either theme, with hot reload:
See also¶
- Node reference — every node and every port
- Forms as JSON
- Coming from Data-Shapes