SOFiSTiK Dynamo Script Builder
Build Dynamo .dyn graphs that target the Revit Dynamo Player workflow: input nodes feed parameters at the top, the graph runs, output (Watch) nodes display the result. Simple data flow lives in Dynamo nodes; non-trivial logic lives in CPython3 Python Script nodes inside the graph.
Core principles
- Plan first, then build. Never start emitting JSON before the user has approved a plan. Dynamo graphs cannot be partially fixed in chat — a wrong shape means the file won't open in Revit.
- One graph = one job. If the user describes two unrelated jobs (e.g. "select walls AND export reactions"), suggest splitting them.
- Logic in Python, plumbing in nodes. Use Zero-Touch nodes for selection, package calls (SOFiSTiK), parameter reads/writes, and Watch outputs. Use a Python Script node any time logic exceeds a single function call (filtering, transforming, transactional Revit modifications, regex, joins beyond
Utils.TableLeftJoin, anything iterative).
- Player-friendly. Inputs that the user is meant to set in the Dynamo Player must be marked
IsSetAsInput: true — but only when the user explicitly asks for that parameter to be a Player input. By default, leave IsSetAsInput: false. Watch nodes that should display in Player are marked IsSetAsOutput: true.
- Always unwrap. Inside Python nodes, every Revit element coming through
IN[i] must be passed through UnwrapElement(...) before .Id, .get_Parameter, etc.
- Revit 2024+
ElementId. Always use el.Id.Value (Int64), never el.Id.IntegerValue — the latter was deprecated in Revit 2024 and throws on modern projects. The builder's add_python rejects code containing .IntegerValue. See references/python_node.md → "Revit 2024+ API breaking changes" for the full migration table.
- Convert units at the SOFiSTiK ↔ Revit boundary. Revit stores lengths in feet, forces in non-standard internal units, etc. SOFiSTiK
.cdb results come in metres and kilonewtons. Any Python node that writes a SOFiSTiK value to a Revit element must wrap it in UnitUtils.ConvertToInternalUnits(value, UnitTypeId.Meters/Kilonewtons/...); any node that reads a Revit value to compare with SOFiSTiK must use ConvertFromInternalUnits. The builder's add_python warns when a script touches Revit element data without any conversion call. See references/python_node.md → "Units — converting between SOFiSTiK and Revit" for the unit table and patterns.
Workflow — follow these steps in order
Step 1: Capture intent
Ask only what's necessary. The minimum you need before drafting a plan:
- What is the script's job? One sentence. Read / modify / export?
- What's the input? Selection in Revit, all elements of a category, a path to a
.cdb or .xlsx, a parameter name, etc.
- What's the output? A modified model, a Watch that shows a list, an Excel file, a created Revit element?
- Which inputs should appear in the Dynamo Player? Don't assume — ask. The user told you their convention:
IsSetAsInput is set only on request, per parameter.
- Does it touch SOFiSTiK results? If yes, where does the
.cdb come from — currently active view, a fixed path, or a user-supplied path?
If the user's prompt already contains all of this, skip to Step 2.
Step 2: Propose a plan and wait for approval
Write a plan in this exact format and stop. Do not write any .dyn JSON yet.
## Plan: <script name>
**Job:** <one sentence>
**Input group** (left side of graph)
- <node 1 — e.g. "String input 'Parameter name' (Player input: yes)">
- <node 2 — e.g. "File path to .cdb (Player input: yes)">
**Processing group** (middle)
- <node — e.g. "All Elements of Category: OST_AnalyticalMember">
- <Python node: <what it does in one line>>
- <SOFiSTiK call: Model.CreateByActiveView → Results.GetSupportForces>
**Output group** (right)
- <Watch node "Filtered elements" (Player output)>
**Files this skill will produce:** `<name>.dyn` (and a separate `<name>.py` reference copy of any Python code longer than ~30 lines, for easier review)
**Open questions:** <only if any>
Then ask: "Does this plan look right? Any changes before I build the .dyn?"
For complex graphs (more than ~10 nodes, multiple Python nodes, or anything the user calls out as tricky), instead of building everything at once, present a TODO list in the plan and offer to walk through it step-by-step. Example:
This is a larger graph. I suggest building it in steps:
- [ ] Step 1: Set up inputs (paths, parameter names) and the SOFiSTiK Model node
- [ ] Step 2: Pull support forces and join with element GUIDs
- [ ] Step 3: Python filter for max reactions per support type
- [ ] Step 4: Watch outputs and Player input flags
Which step should we tackle first, or shall I draft step 1?
Step 3: Build the .dyn
After approval, use the build helper rather than hand-writing JSON. See scripts/build_dyn.py and the canonical schema in references/dyn_schema.md.
Workflow:
- Read
references/dyn_schema.md once if it isn't already in context — it is the source of truth for the Dynamo 3.x JSON shape.
- For any SOFiSTiK call, look up the exact
FunctionSignature and port names in references/sofistik_api.md.
- For Python nodes, start from the boilerplate in
references/python_node.md (Revit imports, DocumentManager, transaction pattern, UnwrapElement).
- Pick the closest template in
assets/templates/, copy it to /home/claude/<workspace>/build_<n>.py, and adapt. Builder API:
g = DynGraph(name=..., description=...) — start a graph.
g.add_string_input(value, label, x, y, player_input=False) — string input. Set player_input=True only when the user explicitly asked for that parameter to appear in the Player.
g.add_filename(value, hint_path, label, x, y, player_input=False, external_label="my.xlsx") — file path input. external_label adds it to NodeLibraryDependencies as External.
g.add_boolean(...), g.add_integer_slider(...) — other Player inputs.
g.add_zero_touch(function_signature, in_ports=[(name, desc), ...], out_ports=[...], x, y, sofistik=True/False) — any .NET function call. Set sofistik=True for any SOFiSTiK.* signature.
g.add_zero_touch_varargs(...) — for List.Join and similar variadic functions.
g.add_python(code, n_inputs, x, y, label) — CPython3 Python node. Pass code with normal \n line breaks; the builder normalizes.
g.add_watch(label, x, y, player_output=False) — Watch. player_output=True makes it a Player output.
g.add_categories(selected="OST_..."), g.add_elements_of_category(), g.add_select_model_elements(category=...) — common Revit selectors.
g.connect(src_node, "src_port", dst_node, "dst_port") — wire two ports. For single-port nodes (StringInput, Watch) pass "".
g.group("Title", [nodes...], color="input"|"processing"|"output"|"sofistik") — group annotation with the convention from dyn_schema.md.
g.mark_player_input(node, label), g.mark_player_output(node, label) — toggle Player flags after the fact.
g.validate() returns a list of structural problems; should be [] before saving.
g.save(path) writes the .dyn.
- Run the build script. Always assert
g.validate() == [] before g.save(...) — catches unwired ports, unknown port refs, missing display names.
- Save to
/mnt/user-data/outputs/<n>.dyn. If any embedded Python is longer than ~30 lines, also save a standalone <n>.py next to it so the user can review/edit Python without opening the JSON.
- Present the file(s) with
present_files.
Step 4: Hand off
End with a short summary of:
- What the script does
- Which nodes are Player inputs/outputs (be explicit if none)
- Any manual step the user must do in Revit (e.g. "open the script in Revit, click 'Run' once to verify, then it will show in Dynamo Player")
- Known limitations (e.g. "the SOFiSTiK package version is pinned to 2026.5.0 in NodeLibraryDependencies — adjust if your install differs")
Reference files
Read these as needed; they are the source of truth.
references/dyn_schema.md — Dynamo 3.x .dyn JSON schema: top-level keys, node ConcreteTypes, port/connector wiring, NodeView geometry, group annotations, the IsSetAsInput/IsSetAsOutput mechanism, the Inputs/Outputs declarations.
references/sofistik_api.md — Quick reference for the SOFiSTiK Analysis + Design Zero-Touch nodes: Analyze, Model, Results, Reinforcement, Design, Utils, Export. Lists exact FunctionSignature strings and return-tuple column indices.
references/python_node.md — CPython3 boilerplate for Python Script nodes: Revit API imports, DocumentManager pattern, transaction handling, UnwrapElement, common pitfalls (level changes, list-vs-single inputs, returning OUT).
scripts/build_dyn.py — Python builder. Use this rather than hand-writing JSON. Has docstrings for every method.
assets/templates/ — Four runnable starter scripts that exercise every feature of the builder. Read the one closest to the user's task, copy it, then adapt:
template_minimal.py — one Player input → Python → Watch (the "hello world").
template_read_results.py — Model.CreateByActiveView → Results.GetSupportForces → 3 Watch outputs (the canonical SOFiSTiK results-reading pattern, no unit conversion needed because data stays inside SOFiSTiK).
template_modify_revit.py — bulk parameter set with a transactional Python node (the canonical write-to-Revit pattern, including UnwrapElement and TransactionManager; sets a string parameter so no unit conversion is needed — see the inline note for how to adapt to numeric values).
template_sofistik_to_revit_units.py — read column normal force from SOFiSTiK and write it onto the matching Revit column with the correct UnitUtils.ConvertToInternalUnits boundary conversion. This is the canonical SOFiSTiK-results → Revit pattern; copy this whenever a Python node needs to push a numeric SOFiSTiK value back into Revit.
Triggering anti-patterns
Don't use this skill for:
- Pure Revit API questions with no Dynamo involved (just answer them directly).
- Reading existing
.dyn files to explain them — you can do that with view/bash directly without the builder.
Do use it for:
- Anything that ends with "...and save it as a .dyn".
- Iterating on an existing
.dyn the user uploaded — read it, propose a diff plan, then rebuild affected sections with the builder.
1---2name: sofistik-dynamo3description: Build Revit Dynamo scripts (.dyn) for Revit 2026 / Dynamo 3.6 that evaluate or modify Revit models and read SOFiSTiK FEA results via the SOFiSTiK Analysis + Design Dynamo package. Use this skill whenever the user asks to create, scaffold, edit, or troubleshoot a Dynamo graph, especially when they mention SOFiSTiK results, the .cdb database, analytical/physical members, the Dynamo Player, Zero-Touch nodes, or want to combine Revit element queries with Python. Trigger this even for partial requests like "I need a Dynamo script that exports support reactions" or "give me the python for unwrapping these analytical elements" — anything that ends up in a .dyn file or its embedded Python.4---56# SOFiSTiK Dynamo Script Builder78Build Dynamo `.dyn` graphs that target the **Revit Dynamo Player** workflow: input nodes feed parameters at the top, the graph runs, output (Watch) nodes display the result. Simple data flow lives in Dynamo nodes; non-trivial logic lives in CPython3 Python Script nodes inside the graph.910## Core principles11121. **Plan first, then build.** Never start emitting JSON before the user has approved a plan. Dynamo graphs cannot be partially fixed in chat — a wrong shape means the file won't open in Revit.132. **One graph = one job.** If the user describes two unrelated jobs (e.g. "select walls AND export reactions"), suggest splitting them.143. **Logic in Python, plumbing in nodes.** Use Zero-Touch nodes for selection, package calls (SOFiSTiK), parameter reads/writes, and Watch outputs. Use a Python Script node any time logic exceeds a single function call (filtering, transforming, transactional Revit modifications, regex, joins beyond `Utils.TableLeftJoin`, anything iterative).154. **Player-friendly.** Inputs that the user is meant to set in the Dynamo Player must be marked `IsSetAsInput: true` — but **only** when the user explicitly asks for that parameter to be a Player input. By default, leave `IsSetAsInput: false`. Watch nodes that should display in Player are marked `IsSetAsOutput: true`.165. **Always unwrap.** Inside Python nodes, every Revit element coming through `IN[i]` must be passed through `UnwrapElement(...)` before `.Id`, `.get_Parameter`, etc.176. **Revit 2024+ `ElementId`.** Always use `el.Id.Value` (Int64), never `el.Id.IntegerValue` — the latter was deprecated in Revit 2024 and throws on modern projects. The builder's `add_python` rejects code containing `.IntegerValue`. See `references/python_node.md` → "Revit 2024+ API breaking changes" for the full migration table.187. **Convert units at the SOFiSTiK ↔ Revit boundary.** Revit stores lengths in feet, forces in non-standard internal units, etc. SOFiSTiK `.cdb` results come in metres and kilonewtons. Any Python node that *writes* a SOFiSTiK value to a Revit element must wrap it in `UnitUtils.ConvertToInternalUnits(value, UnitTypeId.Meters/Kilonewtons/...)`; any node that *reads* a Revit value to compare with SOFiSTiK must use `ConvertFromInternalUnits`. The builder's `add_python` warns when a script touches Revit element data without any conversion call. See `references/python_node.md` → "Units — converting between SOFiSTiK and Revit" for the unit table and patterns.1920## Workflow — follow these steps in order2122### Step 1: Capture intent2324Ask only what's necessary. The minimum you need before drafting a plan:2526- **What is the script's job?** One sentence. Read / modify / export?27- **What's the input?** Selection in Revit, all elements of a category, a path to a `.cdb` or `.xlsx`, a parameter name, etc.28- **What's the output?** A modified model, a Watch that shows a list, an Excel file, a created Revit element?29- **Which inputs should appear in the Dynamo Player?** Don't assume — ask. The user told you their convention: `IsSetAsInput` is set **only on request**, per parameter.30- **Does it touch SOFiSTiK results?** If yes, where does the `.cdb` come from — currently active view, a fixed path, or a user-supplied path?3132If the user's prompt already contains all of this, skip to Step 2.3334### Step 2: Propose a plan and wait for approval3536Write a plan in this exact format and stop. Do not write any `.dyn` JSON yet.3738```39## Plan: <script name>4041**Job:** <one sentence>4243**Input group** (left side of graph)44- <node 1 — e.g. "String input 'Parameter name' (Player input: yes)">45- <node 2 — e.g. "File path to .cdb (Player input: yes)">4647**Processing group** (middle)48- <node — e.g. "All Elements of Category: OST_AnalyticalMember">49- <Python node: <what it does in one line>>50- <SOFiSTiK call: Model.CreateByActiveView → Results.GetSupportForces>5152**Output group** (right)53- <Watch node "Filtered elements" (Player output)>5455**Files this skill will produce:** `<name>.dyn` (and a separate `<name>.py` reference copy of any Python code longer than ~30 lines, for easier review)5657**Open questions:** <only if any>58```5960Then ask: *"Does this plan look right? Any changes before I build the .dyn?"*6162For complex graphs (more than ~10 nodes, multiple Python nodes, or anything the user calls out as tricky), instead of building everything at once, present a **TODO list** in the plan and offer to walk through it step-by-step. Example:6364```65This is a larger graph. I suggest building it in steps:66- [ ] Step 1: Set up inputs (paths, parameter names) and the SOFiSTiK Model node67- [ ] Step 2: Pull support forces and join with element GUIDs68- [ ] Step 3: Python filter for max reactions per support type69- [ ] Step 4: Watch outputs and Player input flags7071Which step should we tackle first, or shall I draft step 1?72```7374### Step 3: Build the `.dyn`7576After approval, use the build helper rather than hand-writing JSON. See `scripts/build_dyn.py` and the canonical schema in `references/dyn_schema.md`.7778Workflow:79801. Read `references/dyn_schema.md` once if it isn't already in context — it is the source of truth for the Dynamo 3.x JSON shape.812. For any SOFiSTiK call, look up the exact `FunctionSignature` and port names in `references/sofistik_api.md`.823. For Python nodes, start from the boilerplate in `references/python_node.md` (Revit imports, DocumentManager, transaction pattern, `UnwrapElement`).834. Pick the closest template in `assets/templates/`, copy it to `/home/claude/<workspace>/build_<n>.py`, and adapt. Builder API:84 - `g = DynGraph(name=..., description=...)` — start a graph.85 - `g.add_string_input(value, label, x, y, player_input=False)` — string input. Set `player_input=True` only when the user explicitly asked for that parameter to appear in the Player.86 - `g.add_filename(value, hint_path, label, x, y, player_input=False, external_label="my.xlsx")` — file path input. `external_label` adds it to `NodeLibraryDependencies` as External.87 - `g.add_boolean(...)`, `g.add_integer_slider(...)` — other Player inputs.88 - `g.add_zero_touch(function_signature, in_ports=[(name, desc), ...], out_ports=[...], x, y, sofistik=True/False)` — any .NET function call. Set `sofistik=True` for any `SOFiSTiK.*` signature.89 - `g.add_zero_touch_varargs(...)` — for `List.Join` and similar variadic functions.90 - `g.add_python(code, n_inputs, x, y, label)` — CPython3 Python node. Pass `code` with normal `\n` line breaks; the builder normalizes.91 - `g.add_watch(label, x, y, player_output=False)` — Watch. `player_output=True` makes it a Player output.92 - `g.add_categories(selected="OST_...")`, `g.add_elements_of_category()`, `g.add_select_model_elements(category=...)` — common Revit selectors.93 - `g.connect(src_node, "src_port", dst_node, "dst_port")` — wire two ports. For single-port nodes (StringInput, Watch) pass `""`.94 - `g.group("Title", [nodes...], color="input"|"processing"|"output"|"sofistik")` — group annotation with the convention from `dyn_schema.md`.95 - `g.mark_player_input(node, label)`, `g.mark_player_output(node, label)` — toggle Player flags after the fact.96 - `g.validate()` returns a list of structural problems; should be `[]` before saving.97 - `g.save(path)` writes the `.dyn`.985. Run the build script. Always assert `g.validate() == []` before `g.save(...)` — catches unwired ports, unknown port refs, missing display names.996. Save to `/mnt/user-data/outputs/<n>.dyn`. If any embedded Python is longer than ~30 lines, also save a standalone `<n>.py` next to it so the user can review/edit Python without opening the JSON.1007. Present the file(s) with `present_files`.101102### Step 4: Hand off103104End with a short summary of:105- What the script does106- Which nodes are Player inputs/outputs (be explicit if none)107- Any manual step the user must do in Revit (e.g. "open the script in Revit, click 'Run' once to verify, then it will show in Dynamo Player")108- Known limitations (e.g. "the SOFiSTiK package version is pinned to 2026.5.0 in NodeLibraryDependencies — adjust if your install differs")109110## Reference files111112Read these as needed; they are the source of truth.113114- `references/dyn_schema.md` — Dynamo 3.x `.dyn` JSON schema: top-level keys, node `ConcreteType`s, port/connector wiring, NodeView geometry, group annotations, the `IsSetAsInput`/`IsSetAsOutput` mechanism, the Inputs/Outputs declarations.115- `references/sofistik_api.md` — Quick reference for the SOFiSTiK Analysis + Design Zero-Touch nodes: `Analyze`, `Model`, `Results`, `Reinforcement`, `Design`, `Utils`, `Export`. Lists exact `FunctionSignature` strings and return-tuple column indices.116- `references/python_node.md` — CPython3 boilerplate for Python Script nodes: Revit API imports, DocumentManager pattern, transaction handling, `UnwrapElement`, common pitfalls (level changes, list-vs-single inputs, returning `OUT`).117- `scripts/build_dyn.py` — Python builder. Use this rather than hand-writing JSON. Has docstrings for every method.118- `assets/templates/` — Four runnable starter scripts that exercise every feature of the builder. Read the one closest to the user's task, copy it, then adapt:119 - `template_minimal.py` — one Player input → Python → Watch (the "hello world").120 - `template_read_results.py` — `Model.CreateByActiveView` → `Results.GetSupportForces` → 3 Watch outputs (the canonical SOFiSTiK results-reading pattern, no unit conversion needed because data stays inside SOFiSTiK).121 - `template_modify_revit.py` — bulk parameter set with a transactional Python node (the canonical write-to-Revit pattern, including `UnwrapElement` and `TransactionManager`; sets a string parameter so no unit conversion is needed — see the inline note for how to adapt to numeric values).122 - `template_sofistik_to_revit_units.py` — read column normal force from SOFiSTiK and write it onto the matching Revit column with the **correct `UnitUtils.ConvertToInternalUnits` boundary conversion**. This is the canonical SOFiSTiK-results → Revit pattern; copy this whenever a Python node needs to push a numeric SOFiSTiK value back into Revit.123124## Triggering anti-patterns125126Don't use this skill for:127- Pure Revit API questions with no Dynamo involved (just answer them directly).128- Reading existing `.dyn` files to explain them — you can do that with `view`/`bash` directly without the builder.129130Do use it for:131- Anything that ends with "...and save it as a .dyn".132- Iterating on an existing `.dyn` the user uploaded — read it, propose a diff plan, then rebuild affected sections with the builder.