Action API
The full contract of HenScriptMacroBase, the class every action extends. For a guided walkthrough
start at Write a Custom Action.
Identity and presentation
Section titled “Identity and presentation”| Method | Returns | Notes |
|---|---|---|
get_id() | StringName | Required, and abstract. Unique and permanent, saved actions reference it. |
get_display_name() | String | Name in the UI. Empty falls back to the file name capitalized. |
get_description() | String | One or two sentences, shown on hover. |
get_icon() | String | Lucide icon name from addons/hengo/assets/new_icons/, no extension. |
get_color() | String | Hex tint, for example '#f97316'. |
Icon and color fall back to the category, which is the first-level folder the file sits in.
| Method | Returns | Notes |
|---|---|---|
get_inputs() | Array[Dictionary] | Value slots, see input keys. Called once at load, the result is cached. |
get_outputs() | Array[Dictionary] | {name, type, id, doc}, optionally branch. Each one needs a get_output_<id>() -> String. |
get_flow_inputs() | Array[Dictionary] | {name, id} where the id is a phase: enter, update, physics, exit. |
get_flow_outputs() | Array[Dictionary] | {name, id, doc, optional}. Each one is a branch on the card. |
Because get_inputs() is cached from the first call, the types of the ports are static per file.
A file cannot show a Vector2 slot on a 2D node and a Vector3 one on a 3D node: that is two files,
gated by get_target_classes().
Input keys
Section titled “Input keys”| Key | Type | Meaning |
|---|---|---|
name | String | Label in the editor. |
type | String | Variant type or class name. Drives the widget and the binding filter. |
id | StringName | The {{placeholder}} name. |
doc | String | One-line tooltip. |
default_value | any | Starting value. |
optional | bool | A required slot left empty skips the action, an optional one is left out of the code. |
raw | bool | Emit the value verbatim instead of quoting it as a literal. |
options | Array[String] | Fixed choices, rendered as a picker. Pair with raw, and make default_value the first option. |
option_labels | Array[String] | What each option reads as on screen, when the emitted value is not what a human should see. |
lvalue | bool | Assignment target. Satisfied only by a binding, never by an expression or an inline action. |
bind_only | bool | Read-only slot that needs a real source (a node, a raycast). Any binding works, node paths included. |
type_from | StringName | Adopt the bound type of another input or output instead of the declared one. |
picker | StringName | Suggest values the project actually has, listed when the slot opens: input_action, audio_bus, scene_path, group. |
node_ref_input(doc, name, id) on the base class builds the usual “which node” slot for you,
already bind_only and defaulting to Self.
Branch keys
Section titled “Branch keys”A flow output takes name, id, doc and optional. An optional branch is a shortcut, not the
reason the action runs: leaving it unwired keeps the plain body instead of failing the action. Ask
is_flow_connected(id) inside the body and emit the if only when somebody wired it.
Bodies
Section titled “Bodies”| Method | Returns | Where it lands |
|---|---|---|
get_flow_enter() | String | The state’s enter(). |
get_flow_update() | String | The state’s update(delta). |
get_flow_physics() | String | The state’s physics(delta). |
get_flow_exit() | String | The state’s exit(). |
get_output_<id>() | String | Right side of the assignment for that output. |
get_unstored_body() | String | What to emit when the action has outputs and nobody stores any. Empty means it is a pure producer and vanishes. |
get_flow_reset() | String | Top of the state’s enter(), whatever phase the action is on. |
get_flow_teardown() | String | The state’s exit(), the mirror of the reset. |
get_script_base() | String | Class-level declarations inside the state class, so a value survives frames. |
get_script_scope() | String | Declarations at script scope, next to the script’s variables. |
get_function_overrides() | Array[Dictionary] | {name, params, body} merged into a virtual method on the node, for example _input. |
Only bodies that reach an emitted action are collected. A skipped action leaves behind no declaration, no connect and no override.
Placeholders
Section titled “Placeholders”| Token | Replaced with |
|---|---|
{{<input_id>}} | The slot’s resolved value: inline action, expression, binding or literal, in that priority. |
{{out:<id>}} | <store> = <get_output_id()> when the output is stored, and the whole line is dropped when it is not. |
{{<flow_output_id>}} | The branch’s steps and transition, or pass when unset. |
{{loop_body}} | The nested actions, one indent deeper. Injected last. |
{{VCNODE_ID}} | This action’s unique id. Use it in every name you emit. |
Substitution order is outputs, inputs, branches, {{VCNODE_ID}}, then {{loop_body}}.
Not every body goes through every pass. get_flow_reset() and get_flow_teardown() get input
substitution and {{VCNODE_ID}}. get_script_base(), get_script_scope() and
get_function_overrides() get only {{VCNODE_ID}}, so read configured values with value_of()
and paste them into the string yourself.
Names available inside a body
Section titled “Names available inside a body”_ref, the node the generated script runs on. Inside a function body this is emitted asselfinstead, since a function has no state class around it.delta, in theupdateandphysicsphases only.- Anything you declared through
get_script_base()(state class) orget_script_scope()(script scope, reached from the state as_ref.<name>).
Behaviour flags
Section titled “Behaviour flags”| Method | Returns | Meaning |
|---|---|---|
get_default_phase() | StringName | Phase a newly added action lands on. Empty picks the first declared flow input. |
get_target_classes() | Array[StringName] | Native classes the action serves. Empty means all of them. |
get_has_body() | bool | The action holds a nested action list. The body must contain {{loop_body}}. |
get_needs_loop() | bool | Only valid inside a loop body, refused at the top level. |
get_needs_function() | bool | Only valid inside a function body, refused in a state. This is what Finish uses. |
get_validation_error() | String | A non-empty reason skips the action with a loud marker. |
gate_validation_error() | String | Ready-made reason for a gating action with nothing nested and no branch wired, which would otherwise emit if/else of two pass. |
Context
Section titled “Context”These are primed on a fresh instance before any body getter runs, so a body can inspect how the user configured the action.
| Member or method | Meaning |
|---|---|
target_class | Base class of the script being generated. |
input_values | input id -> literal, only for slots holding a literal. |
bound_inputs | input id -> true for slots fed by a binding, expression or inline action. |
connected_flows | branch id -> true for branches with somewhere to go. |
nested_action_count | How many actions this one nests, its body and every branch of it. |
action_phase | The phase this body is being emitted at. A nested action runs at its loop’s phase, not the one it was stored with. |
loop_depth | How many loop bodies wrap this action. Above zero means it runs many times in one frame. |
value_of(id, fallback) | The literal a slot holds, or the fallback. |
is_bound(id) | Whether a slot has a bound source. |
is_flow_connected(id) | Whether a branch was wired. |
any_flow_connected() | Whether any branch was. |
targets(class) | Whether target_class inherits from class, for dispatching inside a body. |
output_branch(id) | The branch an output is only produced inside, empty when it is reachable on every path. |
Inlining
Section titled “Inlining”An action can be plugged directly into another action’s input when it is a pure value producer:
it declares outputs, declares no required flow outputs, has no loop body, does not need a loop,
declares no persistent state (no script base, no script scope, no overrides, no reset or teardown),
and every non-empty phase body consists of nothing but {{out:...}} lines.
An optional branch nobody wired still counts as a pure producer, since it emits nothing.
Everything else has to store its result in a variable first.
Seeing what an action emits
Section titled “Seeing what an action emits”Before opening an action’s .gd to work out what code it writes, ask the CLI:
godot --headless --path . -s tools/hengo_cli.gd -- --preview <action_id>It prints the inputs with their defaults and options, the outputs with their real right-hand side,
the branches, the body emitted per phase and per target class, what the action declares
(state_vars, reset, teardown, overrides) and a copyable JSON block for the script CLI.
Two things the preview saves you from: the same action emits different code per target class
(change_color has a CanvasItem variant and a Node3D one), and a field with options can change
the shape of the body, not just the constant pasted into it.
The exported catalog
Section titled “The exported catalog”The plugin can dump the whole pool, the same data that powers the Actions reference:
godot --headless --path . -s tools/hengo_cli.gd -- --export-actionsgodot --headless --path . -s tools/hengo_cli.gd -- --export-actions res://temp/actions.jsongodot --headless --path . -s tools/hengo_cli.gd -- --export-actions out.json --with-codeWith no path it writes into the docs site checked out next to the plugin
(../HengoFront/src/data/actions.json), and fails if that folder is not there. Pass a path to write
anywhere else, and --with-code adds what each action emits. The file carries a categories array
and an actions array with every id, name, category, description, phases, inputs, outputs, branches
and target classes. --list-actions prints a slimmer, per-class view.