# Node Configuration & Schema-Driven Forms

> Applies to: ChengOS v0.1.0+ ｜ Last verified: 2026-08-12 ｜ Source: `chengflow-ui/src/features/editor/utils/controlUtils.tsx`, `chengflow-ui/src/features/editor/components/DynamicNodeInspector.tsx`, `chengflow/docs/i18n-node-convention-guide.md`

Select a node and the right-hand property panel shows its configuration. Those forms are
**generated**, not hand-written: the backend publishes each node's inputs as a JSON Schema
carrying `x-*` UI extensions, and the frontend's field control registry turns each field into a
control. Add an input to a node in Rust and its form field appears with no frontend change.

Knowing how that mapping works is what lets you predict the panel instead of being surprised
by it.

## How a field becomes a control

The registry resolves each field through an ordered chain and stops at the first match. The
order is the important part:

1. **Connected state** — highest priority. A field fed by an incoming edge renders as a
   read-only "connected" indicator, not an editable box. This is why a field you wired up
   stops being typeable: the edge is the value now.
2. **File path fields** — render as a path picker.
3. **`max_tokens`** — gets a purpose-built control.
4. **Sensitive fields** — inferred *from the field name*. A name containing `password`,
   `api_key`/`apikey`, `token`/`access_token`, or `secret`/`private_key` becomes a masked
   input automatically. Passwords and secrets mask fully; API keys show the last 4 characters
   and tokens the last 6, so you can tell two keys apart without exposing either.
5. **Explicit `x-control`** — the node author's declared choice (see the table below).
6. **Enum / options** — a field with an enum becomes a select.
7. **Type match** — the fallback: `string` → text, `number` → number, `boolean` → switch.

Step 4 is worth remembering when authoring nodes: naming a field `api_key` gets you masking
for free, and naming a non-secret field `token` gets you masking you did not want.

## Available controls

`x-control` selects from a fixed set of renderers:

| Control | Renders as |
|---|---|
| `text`, `input`, `textarea`, `rich-text` | Single-line or expandable multi-line text |
| `number`, `slider` | Numeric entry or a slider |
| `switch`, `checkbox` | Toggle |
| `select`, `radio`, `multi-select` | Choice from options |
| `code`, `code-editor` | Syntax-highlighted editor (language from `x-language`) |
| `json`, `json-editor` | Structured JSON editor |
| `key-value` | Editable key/value pairs |
| `date`, `time`, `datetime-local` | Date and time pickers |
| `file`, `file-upload` | Path picker or uploader (accepted types from `x-accept`) |
| `color`, `color-picker` | Colour picker |
| `credential-select` | Vault credential dropdown, filtered by `x-credential-providers` |
| `llm-model-select` | Unified model dropdown across all connected providers |
| `workflow-select` | Workflow picker (filtered by `x-workflow-types`) |
| `dynamic-select` | Options fetched at runtime from `x-api-endpoint` |
| `mcp-server-manager`, `mcp-visibility-editor` | MCP server configuration widgets |
| `document-extract-selector` | Document field selection widget |

## Extensions that change behaviour

Beyond picking a control, `x-*` extensions shape the panel itself:

**Visibility and grouping**

| Extension | Effect |
|---|---|
| `x-advanced` | Hidden behind the advanced section |
| `x-conditional` | Shown only when a condition holds |
| `x-depends-on` | Options are recomputed from another field's value |
| `x-collapsed-visible` | Stays visible on a minimized node |
| `x-debug-field` | Diagnostic only |
| `x-deprecated` | Marked as deprecated but still rendered |
| `x-readonly` | Displayed, not editable |

**Options sourcing**

`x-options` supplies a static list. `x-options-source` / `x-options-from` supplies a dynamic
one — notably `ports`, which builds options from the node's own input port names, which is how
"pick one of my inputs" fields work. `x-allow-custom` lets you type a value that is not in the
list.

**Model selection**

`x-model-type`, `x-model-options`, `x-model-name-includes`, `x-model-name-excludes`, and
`x-provider-filter` narrow the model dropdown — so an embedding field offers embedding models
rather than every model the provider exposes.

**Other**

`x-rows` (textarea height), `x-step` (numeric step), `x-dynamic-array` (repeatable groups),
`x-list-modal` / `x-compact-list` / `x-list-max-visible` (list presentation),
`x-auto-populate` (fill from context), `x-fallback-control` (what to use if the preferred
control is unavailable), `x-generic-kv` and `x-provider-presets` (credential forms).

## Referencing other nodes' values

Rather than hardcoding, reference an upstream value with `{{variable_name}}`. The variable
selector — the picker beside supported fields — lists what is actually reachable at that point
in the graph, which is more reliable than typing a name and hoping.

Two ways to get a value into a node, and the distinction matters:

- **An edge** into an input port passes data directly. The field becomes read-only (rule 1).
- **A `{{...}}` reference** inside a text field interpolates into that text. Use this for
  prompts, where you want a sentence around the value.

## Credentials

Fields with `credential-select` list matching vault entries by provider. Select an entry; the
node stores a reference. The secret is fetched server-side at execution time and never sent to
the browser or into a model prompt.

## Translated labels

Labels, descriptions, placeholders, and option text come from i18n bundles keyed by
`x-i18n-key`, loaded from `/api/v1/i18n`. Untranslated entries fall back to `en-US` rather than
rendering blank, so a missing translation degrades instead of breaking.

## Next steps

- [Canvas basics](/docs/editor-canvas-basics) — assembling the graph.
- [Testing and debugging](/docs/editor-testing-debug) — verifying configuration.
- [LLM and model configuration](/docs/config-llm-models) — populating the model dropdown.
