Thanks for trying it out. I’m glad you got it working. In your screenshot it looks like you just needed to change the Source Path set in the panel to the location of the actual files on your computer.
The latest version of Script Forge eliminates the need for a read file component. You can plug the path directly into the Source param. And if the .gh file is in the same directory as the scripts, you can simply supply the script names rather than the full path.
Script Forge now installs from a yak package rather than manual installation of a .gha.
I have been considering releasing it in the public package manager but am still getting my feet under me regarding plugin development and publishing. This has been a learning process for sure.
The biggest update in the new version is a switch to a JSON header format. This clarifies and standardizes the syntax. It also introduces the ability to set default values for params.
An updated set of examples including separate Python and C# script files along with icons is attached below.
Script Forge Component Reference
Script Forge Component Reference
A reference for the Script Forge component itself: its four inputs, four outputs, and the tree/branch data model that ties them together. It pairs with the one-line tooltips stamped on each param — read those first for the gist, this doc for the detail. The @component header format (how a source script declares its own name, params, icon, etc.) lives in its own doc, Script Forge Header Reference; here the header only matters where it changes what a param does.
Script Forge takes C# or Python 3 source (as text or a file path) and creates or updates stock Grasshopper script components from it. Everything below is about driving that from the four params — not about writing the source. Stock Rhino 8 Grasshopper, no plugins.
The data model in one picture
Everything is organized around one idea: one Source branch = one script.
Source (tree) Target (tree, optional) Outputs (trees)
───────────── ─────────────────────── ───────────────
{0} script A ─┐ {0} guid, guid, … ─┐ {0} id, id, … (one per target)
{1} script B ─┼──▶ {1} 'name' ─┼──▶ {1} id
{2} script C ─┘ (unwired = create) ┘ {2} id
│ │ │
each branch targets that branch's ComponentId / Objects /
is one script source is forged into Success / Log all share
the Source branch paths
- Source fans out: N branches → N (or more) forged components in one pass.
- Target pairs to Source — per branch — and each branch may name several targets, so one script updates many components at once.
- All four outputs mirror Source’s branch paths, so
ComponentId, Objects, Success, and Log line up branch-for-branch and can be wired on together with no graft/flatten/path juggling.
A single un-treed input (one panel, one guid) is just the one-branch case — you never have to think about trees until you want the fan-out.
Inputs
Source — string, tree access
The scripts to forge, one branch per script. A branch’s content can be:
- Source text — a multiline string, or a list of lines (e.g. straight from a Read File component). The branch’s items are joined with newlines into one script.
- A file path — a single line ending in
.cs or .py. The forge reads the file itself; no Read File needed.
- A list of paths — several path lines (or one multiline panel, one path per line). This expands to one component per path, exactly as if the list were grafted.
Detection is by shape (a path is a lone .cs/.py line; real source is multiline and never all-path-looking), so text branches and path branches can mix freely in one tree — there is no mode switch. Relative paths resolve against the folder of the saved .gh; an unsaved document errors on relative paths. Files are read only while Run is true, so path sources cost nothing at rest (unlike a native Read File, which re-reads every solve).
Editing a file re-forges it immediately. While Run is true the forge watches every file-path source on disk: save the file in your editor and it re-forges a moment later, with no second press and no Read File rig. Write, save, watch the component change. (The watch is on each containing folder, filtered down to the exact paths and debounced, so editors that save via a temp file and a rename are caught too; setting Run false tears every watcher down.)
What the re-forge does depends on the slot: a pinned slot (a wired Target, or a header instanceGuid) updates its component in place, while an untargeted create-new slot forges a new component per distinct source and leaves the previous one on the canvas. Pin a slot when an edit must update the same component. A missing file errors only its own branch (the others forge normally); a branch that is almost all paths — one odd line among good ones — warns about a likely typo’d file name before treating the whole branch as source text.
Access is tree deliberately. The component loops the branches itself in a single solve rather than letting GH iterate, so every output path mirrors the input path exactly ({i} in → {i} out). Feeding it a flat list still works — it’s just a single branch {0}.
Target — object, tree access (optional)
Which component(s) each Source branch should update — or, left unwired, an instruction to create new. Whether Target is wired at all is itself meaningful, so the three states are distinct:
| Target state |
Meaning |
| Unwired |
Create-new: each branch falls back to its header’s instanceGuid, else forges a brand-new component next to the forge. |
| Wired, with items for a branch |
Update those targets with that branch’s source. |
| Wired, but zero items for a branch |
Skip that branch — nothing forged, saved state untouched. |
Each Target item may be:
- A component reference — an instance
Guid, a guid string, or component goo (e.g. from Metahopper). Updates that exact component.
- Null or an empty/whitespace item — the create-new marker; forges a new component in that slot. (One list can mix updates and creates.) If the source’s header carries an
instanceGuid, that pin fills in here instead — see rule 4 below.
- A keyword —
name, nickname, name+create, or nickname+create (case-insensitive, the whole item). The source is forged into every on-canvas stock script component of its own language whose Name (or NickName) matches the source header’s own name. No match → skip, unless +create, which forges one new component. This replaces wiring a guid-lookup rig for the common “update all instances of this script” case.
How a branch finds its targets (first rule that applies wins):
- A Target branch with the identical path → all of its items (the source is forged into each — this is the many-targets fan-out).
- Else, if Target is a single flat list:
- several Source branches → the item at the branch’s position (branch 0 ↔ item 0, …);
- one Source branch → every item targets it;
- a flat list of only keywords and/or null/empty items → broadcasts to every branch (each branch re-resolves the keyword against its own header; a null forges one new component per branch). This is what lets a single
name item update every instance of N different scripts in one pass.
- Target wired but resolving to nothing for a branch (empty matching branch, flat list ran out, no matching branch, keyword matched nothing) → skip.
- The slot names no component —
Target unwired, or the item was the create-new marker → the source header’s instanceGuid, else a new component. A header pin therefore beats “create new”: a pinned source re-forges into its own component rather than dropping a copy. Pinning is sticky on purpose.
“Empty” means zero data items, not a blank panel. A blank/whitespace panel still emits one empty string, which reads as the create-new marker and forges a new component. To force a skip, feed the branch zero items.
The same component may not be targeted twice (across branches or within one branch); the second claim errors that slot only and is skipped. The forge also refuses its own InstanceGuid — it can’t forge itself.
Run — bool, item access
Set true to forge. Works with a Boolean Toggle or a momentary Button. While true (or on each button press) it forges every branch that is out of date — meaning the source text, the target, or the target component’s own script text changed since the last run. An up-to-date branch is a no-op, so holding Run true or mashing the button never duplicates anything.
Out-of-band edits are caught. After each forge the target’s settled script text is hashed and stored; a later run whose target no longer matches that hash re-forges the branch from Source (the Log says edited outside the forge). So editing a forged component directly in the script editor is safe — the next Run restores it from the source of truth.
Because state is idempotent, this is safe across recomputes and across closing and reopening the .gh.
Toggle or button? They are genuinely different tools:
- A toggle holds Run true, which is what arms the file watcher — leave it on and every save of a file-path source re-forges by itself. The cost is that while it is on, every input change forges immediately, so switch it off before re-pointing Source/Target panels or the in-between states forge strays.
- A button forges once per press and sidesteps that by construction. Nothing is watched between presses.
Leave the toggle on for a write-save-look iteration loop; use the button when you are rewiring the rig itself.
Force — bool, item access
While Run is true, re-forge every branch even when nothing changed — bypasses the up-to-date skip for that one solve. Useful to restamp icons after the .gh is first saved (relative icon paths resolve only once the doc has a folder), or to rebuild wholesale without editing sources. Also a toggle or button.
Outputs
All four are trees whose paths mirror the Source branches, and all four line up slot-for-slot: within a branch there is one slot per target, so a branch that fans out to 3 targets produces 3 aligned entries in each output.
ComponentId — Guid, tree
The instance guid of each forged component — one branch per Source branch, one item per target. The value encodes the outcome:
| Outcome |
ComponentId value |
| Created or updated OK |
the component’s guid |
| Failed update |
the target’s guid (so you still know which one failed) |
| Failed create |
an empty guid |
| Skipped branch |
empty (no items) |
The forge’s own ComponentId is the easiest way to get a guid you forged — wire it to a panel and copy it, or feed it back into a downstream tool.
Objects — object, tree
The forged components themselves, mirroring ComponentId slot-for-slot — usable anywhere a component-reference input is expected. A slot reads null when its component is not on the canvas right now: deleted, never forged, or a create still in flight (the settle solve fills it in a moment later). ComponentId still records the guid in that case.
Success — bool, tree
One true/false per ComponentId slot: whether that slot’s last forge pass finished without a forge error. Note the distinction — a target script’s own runtime error does not flip Success false; only the forge’s own failures (param sync, compile, stamping) do. Skipped branches stay empty. Wire ComponentId + Success into Dispatch to split forged guids into succeeded and failed lists.
Log — string, tree
A step-by-step report per Source branch — sectioned per target when a branch has several — covering header parse, param sync, lost/renamed wires, compile result, identity/icon stamping, and any warnings or errors. This is the diagnostic surface; read it when a forge doesn’t do what you expected.
Timing. The Log (and Objects, and the in-flight slots) shows the previous run’s report until the new forge completes. A forge finishes a couple of solutions after the press (schedule → mutate → compile → stamp), normally imperceptible. An in-flight slot reads Success true and settles when the pass completes.
Reading the three id-outputs together — each slot is classified by combining them: ComponentId non-empty + Success true = forged; ComponentId non-empty
Success false = failed update (that’s the target it failed on); empty ComponentId + Success false = failed create; all empty = skipped.
Updating in place
When a slot targets an existing component (a wired Target, a keyword match, or a header instanceGuid), the forge updates it rather than replacing it:
- Wires survive by param name, then by position. Params whose names are unchanged keep their underlying param objects, so their wires stay put. A param renamed in place is recycled positionally — the Log reports
renamed X -> Y (n wire(s) kept) — so index-style renames keep their wires too. Only a param that is genuinely removed loses its connections, and each loss is logged as lost wire: …. (Params end up in the header’s order — see the Header Reference.)
- A hidden
out pin is fine. The standard-output param is recognized by its type, never by name or index, so a component whose out is hidden (right-click → Standard Output/Error Parameter) updates cleanly and stays hidden.
- Languages must match. The forge updates in place, and a C# component and a Python component are different classes, so a source only refreshes a target of its own language. Pushing Python source at a C# component (or vice versa) is refused with an error; to switch languages, delete the component and forge a new one (its wires are not carried over).
State, identity, and reshuffles
The forge remembers what it last applied per (forge instance, branch path, target slot), saved in the document’s value table inside the .gh. That’s why repeated presses and document reopens don’t duplicate. Two consequences worth internalizing:
- State follows the branch path. Reordering the branches of unpinned (create-new) sources re-forges them as new components, because the path→state mapping shifted. Pin a script — via a
Target wire or a header instanceGuid — when its identity must survive branch reshuffles.
- Removing a branch leaves its forged component on the canvas untouched; only the branch’s saved state is pruned from the document.
A wired-but-empty branch skipping (rather than creating) is what makes match-by-name and guid-lookup rigs safe: sources whose component isn’t on the canvas are quietly skipped instead of spawning strays.
A header’s declared default seeds an empty param only. Where a source declares a default value for an input, the forge writes it into that param’s persistent data — the internalized value Grasshopper hands the script when nothing is wired in. It does this only when the param has no persistent data of its own. Persistent data is yours to edit (right-click ▸ Set Boolean, Internalise data), and a forge re-runs on every source edit, so re-stamping it would quietly undo what you typed — several times a minute while iterating. The cost of that rule is the other direction: a header can seed a default but never reset one. Clearing is yours too — right-click ▸ Destroy persistent data, then re-forge to pick the header’s value back up.
Requirements & limits
- Rhino 8. Targets are the modern
C# Script / Python 3 Script components (RhinoCodePluginGH). The forge only updates stock components that carry editable source — pointing it at a compiled plugin component (or a legacy GHPython / old C# script component) errors, because there is no script slot to push into.
- SVG icon rasterization uses macOS
sips; on Windows use a PNG or base64 icon (see the Header Reference). Icon problems only warn — they never block a forge.
- Everything else is cross-platform, stock Grasshopper — no plugins.
The component lives on the Params ▸ Util panel of the ribbon.
Typical rigs
- One script, create then keep updating:
Panel (source or file path) → Source, Button → Run. First press creates; put the returned ComponentId in a panel, or add a header instanceGuid, to keep updating in place.
- Live editing loop:
Panel holding a .cs/.py path → Source, Boolean Toggle set true → Run. Now edit the file in your own editor: each save re-forges the component on the canvas by itself. Give the source a header instanceGuid (or wire Target) so every save updates the same component instead of forging a new one.
- Update every instance of a script:
Source ← the script, Target ← a panel containing name. No lookup rig.
- Forge many scripts at once: Entwine / grafted list / merged Read File outputs →
Source (one branch each). Optionally a single name in Target broadcasts “update all instances” across all of them.
- Update specific existing components: wire a component-reference source (e.g. Metahopper’s
Objects) → Target, matched branch-for-branch to Source.
For anything about the source content — the @component header, param type hints, icons, language detection, headerless behavior — see Script Forge Header Reference.
Script Forge Header Reference
Script Forge Header Reference
How to write the @component header that makes a .cs or .py source forgeable by Script Forge — the Grasshopper component that creates or updates C# and Python 3 script components from source text. This documents the header contract only; canvas wiring, targeting, and change-detection live in the companion doc, Script Forge — Component Reference.
The header is optional. With no header the source is injected into a stock script component as-is (params and metadata untouched). Add a header when you want the forge to build params, type hints, tooltips, name, and icon for you.
The shape
The header is the first comment block of the source. It opens with @component and its body is one JSON object — the object’s own closing brace ends the header, so there is no terminator keyword.
C# — a block comment:
/* @component
{
"name": "Curve Frames",
"nickname": "Frames",
"icon": "icons/curve-frames.svg",
"description": "Divides a curve into equal-parameter segments and returns a frame at each division point.",
"inputs": [
{ "name": "Path", "type": "Curve", "access": "item",
"description": "The curve to divide." },
{ "name": "Count", "type": "int", "access": "item", "default": 10,
"description": "Number of segments." }
],
"outputs": [
{ "name": "Planes", "type": "Plane", "access": "list",
"description": "One frame per division point." }
]
}
*/
Python — a module docstring, or hash comments. Same body; the parser strips the comment prefix per line, which is safe because a JSON string can never span a line break.
"""@component
{
"name": "Point Grid PY",
"description": "Builds a grid of points.",
"inputs": [ { "name": "Count", "type": "int", "access": "item",
"description": "Points per side." } ],
"outputs": [ { "name": "Pts", "type": "Point3d", "access": "list",
"description": "The grid points." } ]
}
"""
# @component
# {
# "name": "Curve Divide PY",
# "description": "Divides a curve."
# }
It is strict JSON. No comments, no trailing commas, no single quotes — a stock JSON parser refuses all three. A syntax error is reported with its line and column, and the forge stops on that branch and forges the others.
Anything after the closing brace is ignored, which is what lets the */ or """ sit there. Unrecognized keys are ignored — extra keys never break a forge (see Keys the forge ignores).
Component keys
| Key |
Required |
Meaning |
name |
yes |
Component Name (menus, tooltips). Also what the name target keyword matches. |
description |
yes |
The hover tooltip. No double quotes (see Warnings). |
nickname |
no |
Canvas nickname. Defaults to name. Matched by the nickname target keyword. |
icon |
no |
Path to an SVG/PNG, or an embedded PNG. See Icons. |
language |
no |
csharp or python. Usually auto-detected (see Language detection). |
instanceGuid |
no |
Pins the source to one component: with no Target wire, the forge updates (or recreates) that exact instance. |
inputs / outputs |
no |
Arrays of param objects. See below. |
instanceGuid — pinning a source to one component
instanceGuid is the InstanceGuid of a component on the canvas: the one this source owns. It is what makes an edit-and-re-forge loop land on the same component every time instead of dropping a new one beside it. Forge the source once with no header guid, read the ComponentId the forge returns, and paste it into the header.
It is the fallback, not an override. A Target item naming an actual component wins — so a pinned source can still be pushed somewhere else deliberately. The header guid fills in whenever the slot names no component: Target unwired, or a Target item that is the create-new marker (null or empty). That second case surprises people. A pinned source asked to “create new” doesn’t give you a second copy; it re-pins to its own guid, and the Log says using header instanceGuid as target. Pinning is sticky on purpose — it is what stops an iteration loop from littering the canvas.
It recreates, too. If the pinned component isn’t on the canvas — you deleted it, or you opened a fresh document — the forge creates one and gives it that guid, so the pin keeps working. Delete a forged component, re-forge, and it comes back with the same identity (its wires do not).
There is no need to invent a guid by hand, and no reason to change one: it is the component’s identity, and rewriting it orphans whatever the old value pointed at.
guid is not a valid key. An older header grammar used a bare guid:, and it meant two different things depending on which tool read it. The forge rejects it by name rather than ignoring it — being ignored would silently stop the header pinning anything — with a message naming the replacement. Say instanceGuid.
Keys the forge ignores
category, subcategory, exposure, componentGuid, upgradeFrom and markers are read by build tooling that compiles a source into a permanent plugin component; a forged component’s ribbon placement and identity are not the header’s to set. The forge parses straight past them, so a source written for such a build forges unchanged — and so does one carrying any other extra key. Unrecognized keys are ignored by design. guid, above, is the one exception.
Param objects
Each entry of inputs / outputs is a JSON object:
{ "name": "Keys", "variableName": "OutKeys", "type": "string", "access": "list",
"optional": false, "default": null,
"description": "Entry names." }
| Key |
Required |
Meaning |
name |
yes |
The param’s PrettyName — the human label, and the tooltip title. |
variableName |
no |
The identifier your code receives. Defaults to name. On a forged component this is the pin label. |
nickname |
no |
A pin label for build tooling that compiles the source into a plugin. Never reaches a forged script component. |
type |
yes |
The type-hint converter. See Type hints. |
access |
yes |
item, list, or tree. |
description |
no |
The param tooltip. Defaults to empty — but write one; a param with no tooltip is a defect. |
optional |
no |
Whether the param may be left unwired. Defaults to true. Inputs only. |
default |
no |
A declared default value. Inputs only. See Defaults. |
Params end up in exactly the header’s order. Index-based body code and downstream tooling stay valid — the header is the index order.
Two labels, and which is which
This is the part worth reading twice, because Rhino’s two label slots on a script param do not mean what their names suggest. The pin label is the variable name — the identifier your RunScript receives is exactly the text Grasshopper draws on the pin. The other slot is a free-form human label that shows as the title of the param’s tooltip.
| header key |
where it lands on a forged component |
variableName |
the pin label, and the C# / Python identifier in your code |
name |
the tooltip title |
variableName defaults to name, so a param carrying only name collapses both: write {"name": "Radius", …} and the pin reads Radius and your code takes a Radius argument. That is what you want almost always. (The build-only nickname defaults from name too, and independently — never via variableName, which would let a terse pin label become your variable name.)
Reach for variableName in the two cases where one string can’t do both jobs:
- the label you want isn’t a legal identifier —
{"name": "Max. Radius", "variableName": "MaxRadius"}; the tooltip reads Max. Radius, your code takes MaxRadius;
- an input and an output want the same label — they cannot share one variable, so
{"name": "Keys", "variableName": "OutKeys"} gives you an OutKeys variable, an OutKeys pin, and a tooltip titled Keys (OutKeys).
Rules the forge warns about:
variableName must be an identifier (^[A-Za-z_][A-Za-z0-9_]*$), and on C# sources must not be a C# keyword. Rhino validates nothing here — a variable name of "Not An Identifier" is stored without complaint and then fails to compile — so this warning is the only guard.
variableName must be unique across inputs and outputs combined: every one of them becomes a variable in the same scope.
name and nickname must each be unique per side. Sharing a label between an input and an output is the feature; sharing one within a side gives you two identically-labelled pins.
access
| access |
C# signature |
Meaning |
item |
T |
a single value |
list |
List<T> |
one branch |
tree |
DataTree<T> |
multi-branch |
An item- or list-access component under Grasshopper’s implicit iteration appends the iteration index to its output paths ({i} in ⇒ {i;0} out). If a component’s outputs must mirror its input tree, take tree and loop the branches yourself.
Type hints
type names the param’s Converter. Matching is case-insensitive; the same vocabulary works for C# and Python.
| Hint |
Notes |
bool, int, double, string |
primitives |
object / none / blank |
No Type Hint — raw goo, no conversion |
Guid, DateTime, Complex, Color |
.NET / GH types |
Point3d (point), Vector3d (vector), Plane (plane) |
frames & vectors |
Interval, UVInterval |
Domain and Domain² |
Line, Circle, Arc, Polyline, Rectangle3d |
primitive curves |
Curve (curve) |
any curve |
Box, Transform, Point3dList |
|
Mesh (mesh), Surface, Extrusion, SubD, Brep (brep) |
surfaces & solids |
PointCloud, GeometryBase |
GeometryBase accepts any geometry |
Hatch, TextDot, TextEntity, Leader |
annotations |
An unknown hint falls back to No Type Hint and the forge logs a HINT WARNING — hint typos never fail silently.
Spell the C# name. Grasshopper names two converters differently per language: the C# double converter is float on Python, and string is str. The forge resolves either spelling onto a live param, so a Python header saying "type": "float" works — but double and string are the canonical spellings, and they are what the default vocabulary keys on. A "default" on a float param is rejected, with a message telling you to spell it double.
Python outputs are never hinted. On Python components an output hint is an active converter that breaks list outputs. The forge leaves Python outputs on No Type Hint (matching stock components); an output’s type serves as documentation there. C# outputs are write-only, so their hints are applied but harmless.
optional and default
Two input-side keys. Between them they let a script tell “the user wired false” from “nothing is wired at all” — which a bare bool input cannot.
optional
Defaults to true, so omitting it changes nothing. "optional": false makes Grasshopper require a value: an unwired param goes orange with Input parameter X failed to collect data and the component does not solve at all.
That last part is the trade. A non-optional unwired input stops SolveInstance from running, which is exactly what you asked for — but it also means no code on that component runs to explain itself.
default
A declared value the param starts with. It becomes the param’s persistent data — the internalized value Grasshopper hands the script when nothing is wired in. An unwired {"type": "bool", "default": true} input therefore reads true, which a plain unwired bool never could.
The vocabulary is bool, int, double, string, and nothing else — the types JSON has a scalar for. There is no JSON literal that spells a Point3d, so there is no way to declare one. Internalize such a value on the param itself instead — the forge leaves existing persistent data alone (see below), so it survives every re-forge.
The value’s JSON kind must agree with type. A whole number is accepted on a double param and arrives as 2.0, not as an int; everything else that disagrees is rejected, out loud, on both surfaces:
| header |
verdict |
"type": "bool", "default": "yes" |
default 'yes' is not true or false (bool param) |
"type": "int", "default": 2.5 |
default 2.5 is not a whole number (int param) |
"type": "int", "default": true |
default True is not a whole number (int param) |
"type": "Point3d", "default": 0 |
default is only supported on bool/int/double/string, not 'Point3d' |
"type": "double", "default": [0,0,1] |
default [0, 0, 1] is not a number (double param) |
a default on an output |
an output has no default — nothing is ever collected into it |
optional on an output |
'optional' is an input-side idea — RegisterOutputParams has no Optional pass to mirror it |
A rejected default is never applied; the param is left alone.
Re-forging does not clobber a user’s value
Persistent data is user-visible and user-editable (right-click ▸ Internalise data). The forge applies a declared default only when the param has no persistent data, and leaves an existing value alone. So a value someone internalised survives every subsequent re-forge — at the cost that the header cannot reset one. Clear it on the component if you want the header’s value back.
Multi-line tooltips
JSON’s own escapes, so \n inside a description string is a real newline by the time the parser hands it over:
{ "name": "Target", "type": "object", "access": "tree",
"description": "Which component each branch updates:\n• a guid\n• the keyword name\n• null to create new" }
There is nothing header-specific to know here, which is the point: write the escape JSON already defines, and the tooltip on the canvas breaks where you said it does.
Icons
icon accepts three forms:
- SVG path — rasterized to a 24×24 PNG automatically via macOS
sips (reused until the SVG is newer);
- PNG path — used directly (make it 24×24; GH draws at native pixel size);
- embedded PNG —
base64:<payload> or a full data:image/png;base64, URI, so the icon travels inside the source.
Path resolution: absolute paths are used as-is; relative paths resolve against the folder of the saved .gh — not the folder of the source file. The forge is handed source text and has no way to know where that text came from, so the document is the only anchor there is. Keep icons next to the .gh (e.g. in an icons/ subfolder beside it), or give an absolute path. In an unsaved document, relative icons are skipped with a warning; save the document and re-forge with Force to pick them up.
Icons are strictly best-effort. A missing file, bad extension, corrupt image, or SVG rasterization off macOS produces an icon warning: in the Log and the forge carries on — params, source, and other metadata are unaffected. On Windows, use a PNG path or a base64 icon (SVG-only icons are skipped there).
Language detection
The forge picks the language in this order:
- header
language key (csharp / python);
- a
#! python shebang in the first lines;
- the header comment style —
/* @component ⇒ C#, """@component or # @component ⇒ Python;
- for headerless updates, the target component’s own type;
- headerless heuristics —
Script_Instance or using X; ⇒ C#; a leading docstring or import ⇒ Python.
If none decide, the forge errors and asks for a language key. Add language whenever the language would otherwise be ambiguous.
Headerless sources
A source with no header is injected as-is: the forge creates a stock component (default x, y inputs / out, a outputs) or pushes into the target, touching no params, metadata, or icon.
A headerless create leans entirely on the heuristics above, so a marker-less body (e.g. a bare a = x + y) can’t be classified and errors — add a recognizable marker (import, a docstring, a using) or give it a header. A headerless update never hits this: the target’s own type settles the language.
Warnings
Header problems short of a JSON syntax error are reported in the Log output and never block the forge — the component is built, and the warning tells you what it could not honour. Read the Log when a forge doesn’t do what you expected; it is the whole diagnostic surface.
| Log line |
Cause |
DRIFT WARNING |
a variableName is declared in the header but missing from the RunScript signature, or vice versa — the header and your code disagree about the param list. |
HINT WARNING |
a type wasn’t recognized; the param fell back to No Type Hint. |
NAME WARNING |
a variableName is not an identifier, is a C# keyword, or is claimed by both an input and an output. |
LABEL WARNING |
two params on the same side share a name or nickname. |
DEFAULT WARNING |
a default the param can’t take — the table under default — or a param with no persistent-data slot to put one in. |
OPTIONAL WARNING |
optional was declared on an output. |
QUOTE WARNING |
a description or a label contains a ". Harmless on the canvas; flagged because it is awkward for tooling that reads the source back. |
icon warning: … |
see Icons — always best-effort. |
lost wire: … |
a removed param dropped its connections. A renamed param keeps them and logs renamed X -> Y (n wire(s) kept) instead. |
DRIFT WARNING is the one worth wiring your attention to: the header is what builds the pins, and your RunScript signature is what receives them, so a disagreement means a param exists on exactly one of the two.
Complete example
/* @component
{
"name": "Remap Number",
"nickname": "Remap",
"description": "Remaps a number from one interval into another. Values outside the source interval are extrapolated, not clamped.",
"icon": "icons/remap-number.svg",
"instanceGuid": "3f2a9c10-8b41-4e6d-9a2f-1c7e5b0d4a88",
"inputs": [
{ "name": "Value", "type": "double", "access": "item",
"description": "The number to remap." },
{ "name": "Source", "type": "Interval", "access": "item", "optional": false,
"description": "The interval the value currently lives in." },
{ "name": "Target", "type": "Interval", "access": "item",
"description": "The interval to map into." },
{ "name": "Clamp", "type": "bool", "access": "item", "default": false,
"description": "Hold the result inside Target instead of extrapolating." }
],
"outputs": [
{ "name": "Mapped", "type": "double", "access": "item",
"description": "The remapped number." }
]
}
*/
using System;
using Grasshopper.Kernel;
public class Script_Instance : GH_ScriptInstance
{
private void RunScript(double Value, Rhino.Geometry.Interval Source,
Rhino.Geometry.Interval Target, bool Clamp, out object Mapped)
{
double t = Source.NormalizedParameterAt(Value);
if (Clamp) t = Math.Max(0.0, Math.Min(1.0, t));
Mapped = Target.ParameterAt(t);
}
}