Skip to main content
config.yaml is the settings form. Canvas renders it, someone fills it in, and your calls read the saved values as {{ config.<field> }}. It is the file you touch most. Every new option a node grows lands here and nowhere else.
config.yaml
configSchema is a JSON Schema, so any keyword you already know works. The ui: keys are the platform’s, and they decide how a field is drawn.

Writing the labels

Three fields decide whether the form makes sense to the person filling it in. Say what the setting does, not what it is called. β€œMaximum number of tokens to generate” tells a reader nothing they could not get from the label. β€œThe model stops when it hits this, mid-sentence and without an error” tells them why they might change it. Keep it to a line or two. Detail belongs in your node’s own documentation, not under a form field.

Field types

Text

A choice

enum is the values, enumNames is what a person sees. They are positional, so they must be the same length.

A number

minimum and maximum are enforced, so a bad value is caught in the form rather than by the service.

A switch

Structured data

Making a field wirable

ui:field: template is what lets a field take data from an upstream node instead of a typed value. Without it, the field is whatever someone typed. The syntax is decided by the field’s type, always, on every node. A string field takes a Handlebars template.
Five roots are available: There is no input.* root. A path that matches nothing resolves to empty and says nothing about it, so check the id against the edge you actually drew. Array elements and object keys are dot segments, never brackets: records.0.Name, not records[0].Name. Helpers work anywhere a template does: eq, contains, filter and toJSON. So a conditional lives in the field itself.
An object or array field takes a return expression instead.
A nested object of templates, like { topic: "{{...}}" }, is not valid and never resolves.

The running workflow

The five roots above are your node’s own surroundings. A config field can also reach the run it is part of.
workflow.variables is the one to reach for when several nodes need the same value. Set it once on the workflow rather than wiring it into each node. These are resolved before your node runs, when the platform prepares its settings. So they are available in config.yaml fields, and your calls read the result as {{ config.<field> }}.

What a return expression may do

It is not JavaScript. It is a data-shaping expression that reshapes what is already in front of it, and nothing else. Allowed: reading the context, indexing, object and array literals, spread, template strings, operators, ternaries, and arrow callbacks like items.map(x => x.name). Methods: the non-mutating array ones (map, filter, slice, find, reduce, flat, at, toSorted), the string ones (split, replace, trim, padStart, match), and toFixed. Rejected: process, require, fetch, globalThis, eval, Function, new, assignment, statement blocks, constructor and __proto__. A rejected expression is logged and never runs. Nothing mutates. Use .at(-1) rather than .pop(), and .toSorted() rather than .sort(). sort reorders the array in place, and that array is a live upstream output, so sorting it would reorder it for every other node reading the same value.

Dates

Half the APIs a node calls take a date range, and every one of them wants YYYY-MM-DD or a full ISO string. Date.iso is the formatter.
Date.now() is milliseconds, Date.iso(ms) gives the full ISO string, and .split('T')[0] gives the date part. One function rather than a family, and there is no new Date(...) because that is a construction the sandbox refuses. Security here is by absence. The interpreter never implements those globals, so there is nothing for an expression to escape to.

Showing a field only when it matters

ui:dependencies hides a field until another field has the right value. A scalar means it must match exactly, an array means it must be one of several, and multiple keys are all required at once.
Lint checks that every key names a real sibling field, so a rename cannot leave a field permanently hidden.

The two fields every node has

Two of them are not yours to choose. They are identical in every node, and lint checks they are there:
These belong to the person building the workflow, not to you. The same node type faces staff on one canvas and customers on another, and only they know which. Your own floor goes in node.yaml, and the stricter of the two wins. Both default to off, because a node that gated by default would break every workflow already using it. Who Can Run It covers the whole model.

The rest of the vocabulary

When it goes wrong


Next: Service Connectors