Programmatically create Python components

With AI getting better at coding I’ve found myself looking less for custom grasshopper plugins and more working with AI to write one-off custom components that solve specific problems in the workflow, and the biggest bottle neck now is manually setting input name and expected type on the canvas.

It would be really cool if in the next iteration of grasshopper these graphical elements can also be automated programmatically. It might be hard to infer from the code itself but maybe let us plugin for example an XML or some other well-defined format that can be parsed, so we can use AI to generate that part of the component as well and get to work faster?

Or we can go the Kangaroo route and have python component having just one input, we plug everything into it, and have the script editor register each input with its name that can be referenced inside the script?

Thank you!

this is already possible, have a look at the fileformat *.ghx which is the XML version of the *.gh file.

Here is a MWE that computes a = x + y with a Python component. The script is contained as YSA9IHggKyB5 on line 303 as Base64. Decoding it yields the original python code.

a_eq_x_plus_y.ghx (33.5 KB)

is this creating inside canvas or our-right generate a full definition? I understand we can do the later but I’m more interested in the former so I can collaborate and manually control the definition more

not entirely sure, what you mean. maybe rephrase.

theoretically you can create components with python scripts and given payloads with inputs and outputs with this to the complexity your LLM of choice can handle.

or do you want to hook up python code to git for versioning and execute it in the canvas? this is also possible, i can explain.

I mean I want to be working within a normal manual definition, and drop in a python script component, and allow LLM to generate scripts (with or without MCP) and have the script to programmatically setup the input output format instead of me doing it by hand after copying in the script. Is that how you did it in your example (I need to wait till after the holiday to check your example file)?

Alternatively I imagine you can ask LLM to generate the ghx file directly that includes a python script component, which is helpful but (I assume) it’s only one-off and not interactive, meaning harder to work side by side with an LLM as I create some parts of the canvas, and let LLM insert a component when I need it to.

Something like this?

https://www.food4rhino.com/en/app/raven-ai?lang=en

No, no LLM was involved in this.

I am not sure, if a *.ghx can be changed in the background while it is open.

I used your method and was able to get a component in the generated .ghx file with the correct type hint, thank you!

If would still be much better if I can manually create a Python script component on a regular canvas in the XML format so I can use LLM to automate both the input type hint, names as well as the scripts in one go without manual modification, instead of having to create new ghx files.
Not really asking for Raven, just asking for a way to use script to control the input/output name and type of a python component. Maybe a way to drop a segment of XML in the current open definition to generate a list of connected components?

I’m only asking for it because for work environment it’s going to be a while before we can really use MCP so just trying to make the manual vibe-coding process smoother, thank you!

i am not sure, what changed, but type hints used to be automatic on connecting a wire to the component. at some point this behavior has changed, but that is a question for the mcneel team.

i also just edited the *.ghx file in an editor and hit CTRL+S in the canvas. apparently it has the following behavior:

  • Keep both: creates an incremented version with the new payload

  • replace: resets the edits in the editor to the original state.


not having looked further in it, by “Keep Both” you can manipulate *.ghx defintions and also get versioning, but it does not look very nice to use.

@Jun_Wang, your question about a programmatic way to flesh out script components got me wondering if that approach could also streamline the process when using MCPs. I’ve been exploring Claude with Cordyceps and rhinomcp recently and together they can set type hints and other aspects of script components beyond just the source code. But the process can be slow and clumsy at times.

So I worked with Claude to create a component that can “forge” script components based on raw source code input that includes a header of information like parameter type hints, name, icon, and such. No MCP required. The plugin and a GH definition of examples is attached. Note that it is experimental and likely buggy.

In this incarnation, the header isn’t a standard format like XML, but it is pretty straightforward to read and for an LLM to generate as part of the script.

The Script Forge plugin was compiled on macOS, so I’m not 100% sure it will run on Windows. If anyone tries it, please let me how it runs for you.

Script Forge Documentation

Script Forge

Script Forge is a Grasshopper component that builds other script components from plain text: feed it C# or Python 3 source and press a button, and it creates the component on the canvas — params, type hints, tooltips, name, and icon included. All that metadata comes from an optional @component comment header at the top of the source; without one, the code is simply injected into a stock script component. Point it at the same component again (via the Target input or the header’s guid: field) and it updates in place — wires stay on matching params, and nothing is ever duplicated when the source has not changed. Feed Source a tree — one branch per script — and a single forge fans out to one component per branch, each branch tracked, updated, and reported independently.

/* @component
name:        Remap Number
description: Remaps a number from one interval into another.

@in  Value  | double   | item | The number to remap.
@in  Source | Interval | item | The interval the value lives in.
@in  Target | Interval | item | The interval to map into.
@out Mapped | double   | item | The remapped number.
@end
*/

Fully standalone — stock Grasshopper + Rhino 8, no plugins.

Canvas usage

Param Type Access Meaning
Source (in) string tree The source text, one branch per script — each branch a list of lines (Read File output) or one multiline string. A plain list or single string is simply the one-branch case.
Target (in) any tree Optional component(s) to update: Guids, guid strings, or component references, paired with Source branches (see Forging several scripts at once). Falls back to each header’s guid: field; if both are absent a new component is created next to the forge.
Run (in) bool item Works with a toggle or a momentary button. While true (or on each press), forges every branch whose source text or target changed since the last run.
ComponentId (out) Guid tree Instance guid of the created/updated component, one branch per Source branch.
Log (out) string tree Step-by-step report per Source branch: parse, sync, lost wires, compile result, stamping, warnings.

Typical rig: Panel (file path)Read FileSource, Button (or Boolean Toggle) → Run, and optionally a Panel (guid)Target. The source can just as well come from a multiline panel with the whole script pasted in — anything that produces text works.

Getting a component’s guid (to update it in place):

  • easiest: the forge’s own ComponentId output is the guid of whatever it just forged — wire it to a panel and copy from there;
  • put a guid: field in the header instead — then the source itself pins its component and no Target wire is ever needed;
  • or use any component-info tool (e.g. Metahopper’s Object Info) — the Target input also accepts component reference goo directly, not just guid text.

Semantics worth knowing

  • Wires survive updates by param name first, then by position. Params whose names are unchanged keep their IGH_Param objects, so their wires stay. A param that was 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 params that are truly removed lose wires, and each loss is reported.
  • Params end up in exactly the header’s order, so script bodies (or downstream tooling) that address params by index stay valid — the header is the index order.
  • Idempotent, button-friendly. The forge records what it last applied (in the document’s value table, saved inside the .gh), so recomputes, reopening the file, recompiling the forge, and repeated button presses do not create duplicates. It re-forges only when the source text or target changed, or when the previously forged component was deleted. To force a rebuild: edit the source, or delete the forged component.
  • While a toggle holds Run true, every input change forges immediately. Toggle Run off before re-pointing the source/target panels, or the transient in-between states will forge stray components. (A button avoids this by construction.)
  • A hidden out pin is fine. The stdout param is recognized by its type, never by name or index, so components whose standard output param is hidden (right-click → Standard Output/Error Parameter) update cleanly and stay hidden.
  • The languages must match on update: pushing a Python source at a C# Script component (or vice versa) is refused with an error.

Forging several scripts at once

Source is a tree: each branch is one script. Two panels through Entwine, a grafted list of multiline strings, several Read File outputs merged into branches — any tree shape works, and the single-script rigs above are just the one-branch case. All branches forge in one pass; ComponentId and Log come back as trees with matching paths.

Pairing targets. Each Source branch finds its Target as follows:

  1. a Target branch with the identical path → its first item;
  2. otherwise, if Target is a single flat list, the item at the branch’s position (first branch ↔ first item, and so on);
  3. otherwise (no match, or a null/empty item): the header’s guid: field, or a new component.

A null or empty entry in a Target list therefore means “forge a new component for this one” — so one list can mix updates and creations. The same component may not be targeted by two branches; the second claim errors (that branch only) and is skipped.

Per-branch state. The applied-source key and result guid are tracked per (forge instance, branch path), so re-running with one branch edited re-forges only that branch. The create/update contract, stated plainly:

  • no target → a new component per new (branch, source);
  • same source and target as last run → no-op;
  • target set (input or header guid:) → update in place.

One consequence: state follows the branch path, so reordering the branches of unpinned sources re-forges them as new components. Pin scripts via a Target or a header guid: when identity must survive reshuffles. Removing a branch leaves its forged component on canvas untouched; the branch’s saved state is pruned from the document.

The @component header

The header is the first comment block of the source. It is optional — see Headerless sources below.

/* @component
name:        Curve Frames
nickname:    Frames
category:    Curve
subcategory: Division
icon:        icons/curve-frames.svg
description: Divides a curve into equal-parameter segments and returns a frame
  at each division point. Long values wrap onto indented follow-on lines.

@in  Path       | Curve  | item | The curve to divide.
@in  Count      | int    | item | Number of segments.
@out Planes     | Plane  | list | One frame per division point.
@out Parameters | double | list | The curve parameter at each division point.
@end
*/

In Python use a module docstring or hash comments — same grammar, the parser strips the comment prefix per line:

"""@component
name: Point Grid PY
...
@end
"""

# @component
# name: Curve Divide PY
# ...
# @end

Fields

One per line, key: value. Unrecognized keys are ignored (forward-compatible).

Key Required Meaning
name yes Component Name (shown in tooltips and menus).
description yes The hover tooltip. No double quotes — see Warnings.
nickname no Canvas nickname. Defaults to name.
category / subcategory no Ribbon tab / panel. Informational for inline components.
icon no Path to an SVG or PNG icon, relative to the folder of the saved .gh. See Icons.
language no csharp or python. Usually unnecessary — see Language detection.
guid no Pins the target component instance. With a pinned guid the forge always updates that component (or recreates it with that exact guid), without needing the Target input.
marker no A literal tag string preserved verbatim in the source (for components discovered by source-text scans). May repeat.

Continuation lines

Any non-blank line that is not itself a key: value, @in/@out, or @end line continues the preceding value, joined with a single space. Indent continuations for readability. A blank line ends the continuation. Caveat: a continuation line that looks like a field (Options: a, b) will be parsed as a new field — rephrase, or keep the colon away from the line start.

Param lines

@in  Name | typehint | access | description
@out Name | typehint | access | description

  • Four fields split on the first three | only — the description may freely contain |, :, commas, and non-ASCII.
  • Name must match the RunScript argument name (C#). PascalCase by convention.
  • access is item (single value), list (one branch), or tree (multi-branch). In the C# signature: T ⇒ item, List<T> ⇒ list, DataTree<T> ⇒ tree.
  • Output hints are decorative in GH (outputs are typed by what you assign), but recording them keeps the header a complete spec.

Type hints

The typehint field selects the param’s Converter (right-click → Type Hint on a live param). Matching is case-insensitive; the same vocabulary works for C# and Python components. The full set on Rhino 8:

Hint Notes
bool, int, double, string primitives (double selects the converter Python names float; both spellings accepted)
object (or none, or 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 GH 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 reports a HINT WARNING in the Log — hint typos never fail silently.

Python outputs are never hinted. On Python components an output type hint is an active converter applied to the assigned object wholesale, which breaks list outputs (PyObject to Point3d conversion errors). The forge leaves Python outputs on No Type Hint — matching stock components — and the header’s @out hints serve as documentation. (C# outputs are write-only object sinks, so their hints are applied but harmless either way.) The source push (set_Text) has a side effect of clearing a Python component’s Marshal Inputs / Outputs / Guids toggles, which would leave a returned list wrapped as one opaque PyObject — so the forge captures the three toggles before every push and restores them right after: a fresh component keeps the stock defaults (all on, lists unwrap into proper GH lists), an update keeps whatever the user has chosen.

Icons

icon: accepts three forms:

  • an SVG path — rasterized to a 24×24 PNG automatically (via macOS sips) next to the SVG; the PNG is reused until the SVG is newer;

  • a PNG path — used directly (make it 24×24; GH draws stamped icons at native pixel size);

  • an embedded base64 PNGbase64:<payload> (or a full data:image/png;base64, URI), so the icon travels inside the source with no file on disk. Long payloads may wrap across indented continuation lines; the joining whitespace is ignored by the decoder:

    icon: base64:iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNS
      R0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAA...
    
    

Path resolution: an absolute path is used as-is. A relative path is resolved against the folder of the saved .gh document — because the forge receives plain text, it cannot know where the source file lives (or whether it ever was a file), so the document’s own folder is the only stable anchor. Keep icons next to the .gh (e.g. an icons/ subfolder), or use an absolute path or a base64 payload when that doesn’t suit. If the document has never been saved, relative icons are skipped with a warning saying so. Keep icons square and legible at 24 px.

The icon step is strictly best-effort on every platform: a missing file, an unsupported extension, a corrupt image, an undecodable payload, or SVG rasterization off macOS each produce a clear icon warning: … line in the Log and the forge carries on — params, source, and the rest of the metadata are never affected. On Windows, use a PNG path or a base64 icon (or ship a pre-rasterized <name>.png next to the SVG); SVG-only icons are skipped there with a warning saying exactly that.

Language detection

Priority order:

  1. header language: field (csharp / python);
  2. a #! python shebang in the first lines;
  3. the header comment style — /* @component ⇒ C#, """@component or # @component ⇒ Python;
  4. for headerless updates, the target component’s own type;
  5. headerless heuristics — Script_Instance or using X; ⇒ C#; a leading docstring or import ⇒ Python.

If none of these decide, the forge errors and asks for a language: field.

Headerless sources

A source with no @component header is injected as-is: the forge creates a stock script component (default x, y inputs and out, a outputs) or pushes into the existing target, touching no params, metadata, or icon — exactly like dropping a default component and pasting the code in. The stock default C# and Python scripts work unchanged.

Warnings

The forge validates at forge time and reports problems in the Log (they never block the forge):

  • DRIFT WARNING — a header @in/@out name is missing from the C# RunScript signature, or vice versa. Drift is otherwise invisible: the canvas hints silently win over the signature at solve time.
  • QUOTE WARNING — a description contains a double quote. Fine on canvas, but it breaks the Rhino ScriptEditor plugin builder if the component is ever published (descriptions are embedded as unescaped C# string literals).
  • HINT WARNING — a type hint name was not recognized; the param got No Type Hint.
  • lost wire: … — a renamed/removed param dropped its connections.

Requirements and limits

  • Rhino 8 — targets the modern C# Script / Python 3 Script components (RhinoCodePluginGH). Legacy GHPython and the old C# script component are not supported as targets.
  • SVG icon rasterization uses macOS sips. On Windows, point icon: at a 24×24 PNG, embed it as base64:<payload>, or ship a pre-rasterized <name>.png next to the SVG. Icon problems of any kind only warn and never block the forge. Everything else is cross-platform.
  • One forge handles any number of scripts via Source branches, each with its own state (keyed by instance guid + branch path). Extra forge components also coexist cleanly — and yes, the forge can forge more forges.
  • The Log shows the previous run’s report until a new forge completes — the forge finishes a couple of solutions after the press (schedule → mutate → compile → stamp), which is normally imperceptible.

ScriptForge.gha (189 KB)

Script Forge examples.gh (32.4 KB)