# Writing custom nodes

> Declare a node's interface with comment directives, implement its run function, and use the supported runtime rather than backend internals.

Source: https://kaitoi.io/docs/core/custom-nodes/
Section: Kaitoi Core / Custom nodes and plugins
Written for: Developers authoring or adapting user nodes.
Prerequisites: An account with the required node-authoring capability
Last reviewed: 2026-10-24

---

A user node is Python with comment-based schema directives and a top-level
`run()` function. The directives declare the pins and presentation that Studio
discovers. They are not Python decorators.

## Design the boundary around the computation


A node accepts declared inputs and controls, computes its result and returns declared outputs. Keep names, types and output shape consistent with the implementation. Validate the definition before execution and handle missing or invalid input explicitly. A visual preview and a saved schema are different evidence from a tested computation.

The same value, factor and result names are highlighted in declarations, function arguments and the return map. The example inputs 3 and 2 multiply to the result 6 at the node’s interface.

*Figure: Declared inputs, controls and outputs make a custom node usable.*

## A small deterministic node

```python
# @node title="Scale a Number" category="Math" icon="calculator" preview=true
# @task category="Analyze" subcategory="Data" filter="Math"
# @help Multiply a number by a factor.
# @style minHeight=260 widgetSpacing=60
# @input float value label="Value" default=1.0
# @input float factor label="Factor" default=2.0
# @output float result label="Result"

def run(value, factor):
    return {"result": value * factor}
```

Save it through the supported user-node editor or authoring tool, place it in a
graph, and run a known input. A value of 3 and a factor of 2 should return 6.
This example needs no provider key or additional package.

## Preserve the contract

Use the declared pin names in the function and output dictionary. Every normal
return path must map output names to values. For an actionable failure, raise
an appropriate exception or return a useful `@error`; do not fabricate a
successful result when required work failed.

Node discovery requires valid metadata. Keep matching category/filter metadata,
help, style, and correct pin declarations. Test after changing the interface;
existing graphs can still refer to its earlier pin names.

## Use the public runtime

User nodes have a restricted runtime. Built-in source can use privileges and
imports that user code does not have, so copying a built-in implementation may
need adaptation. Use supported injectables such as `files`, `output`, `package`,
`log`, and `progress` for the operations they provide. Do not depend on backend
modules, direct filesystem access, or internal library paths.

Request helpers by their exact supported parameter names. An undeclared helper
name is not injected automatically and can make the function fail to run.

Declare external packages with supported `@require` metadata. Package approval
and runtime policy still apply. Web previews and custom widgets also need the
documented node-preview integration rather than unrestricted access to Studio.

## Test before publishing

Test valid, empty, and invalid inputs and any failure path. For media, inspect a
real produced file; an empty path or placeholder file is not a usable output.
Keep keys and private data out of source, diagnostics, and bundled assets.

## Separate schema, runtime, and behavior checks

**Declare:** use single-line comment directives, top-level run(), and output
names matching the return mapping. **Runtime:** use exact supported helper
names and managed file representations; dependencies still face policy.
**Test:** verify real values/files and error paths, not just a successful save.

## Checks and next steps

Keep each metadata directive on its own line and preserve exact pin names
when editing an existing interface. `@require` declares a dependency; it does
not bypass the installation's package allowlist or guarantee that a package
can run in the selected environment.

The outer return dictionary maps output names to values. A file-backed
`object` output needs the managed JSON-file representation; returning an
arbitrary Python dictionary as that pin's value is not the same contract.
Use the supported files helper and current data-type reference for the value.
Likewise, allocating an output path does not create valid media by itself.

Review generated-source diagnostics and implementation warnings, then test
a small known example and failure cases. Static acceptance does not prove
provider availability, media quality, complete implementation, or safe reuse
with different inputs. See [Node and data reference](/docs/reference/nodes-and-types/)
and [Creating nodes with Penny](/docs/studio/penny-custom-nodes/).
