Skip to main content
Single source of truth for the schema returned by every NodeHandlerInterface::configSchema(). The Vue builder’s generic renderer (SchemaConfigRenderer.vueSchemaFields.vue) reads this shape directly — when a node type has no inline-styled override (current overrides: SendMessage / Input / Condition / Branch / Assign / Call — see OVERRIDES in ConfigPanel.vue), the entire right-pane config form is built from this array. (subflow and end are now fully schema-driven via the flow-picker and enum-cards field types.) Keep this file in sync with resources/js/builder/components/editor/config/SchemaFields.vue#FIELD_COMPONENTS. If you add a new field type to the renderer, extend the matching section here in the same PR.

1. Overview

Two equivalent ways to express a schema — the fluent builder from fapost/support (recommended) or the raw array. The renderer reads the same wire shape in both cases. Fluent (preferred):
Each field is its own class with a static make(string $name) factory (TextField::make(), NumberField::make(), …) — there is no Field::string() style facade. Concrete field classes live under the Fields\ sub-namespace; Schema and Section live directly under …\Builder\Schema. Raw array (still supported):
The schema is serialized into the node-handler registry payload, sent to the builder on flow load, and rendered through the Vue components in resources/js/builder/components/editor/config/. There is no separate JSON-schema validator — the renderer is permissive and falls back to TextField for unknown types, the backend validator (FlowDefinitionValidator) is the contract for runtime correctness.

2. Top-level keys

Keys that are not arrays-with-a-type, not required, and not sections are ignored.

2.1 sections

  • Sections are rendered in array order. Field keys not declared in any section are dropped (intentional, so authors see visually where each field lives).
  • If sections is missing entirely, the renderer falls back to a single auto-section labelled “Configuration” containing every declared field.
  • A Meta section with the node id is always appended by the renderer — handlers don’t declare it.

2.2 Section icons

Currently registered Heroicon names (see resources/js/builder/components/editor/config/sectionIcons.ts): globe-alt, arrow-down-tray, cog-6-tooth, book-open, magnifying-glass, adjustments-horizontal, bolt, cube, arrow-right-circle, play, clock, flag, square-3-stack-3d. Unknown names fall back to a neutral square — extend sectionIcons.ts rather than passing arbitrary SVG.

3. Field types

Common props (apply to every type unless noted): see §4. Conditional visibility: see §5. Inline validators: see §6.

3.1 string

Single-line text input. Renders TextField.vue with VariablePicker for {{path}} insertion.

3.2 text

Multi-line textarea. Renders TextareaField.vue with VariablePicker. Same prop set as string plus a default of 4 rows of height. No regex validator at the moment.

3.3 number

Numeric input. Renders TextField.vue with type="number" and no VariablePicker (templates don’t substitute into numeric fields).

3.4 boolean

Checkbox toggle. Renders ToggleField.vue.

3.5 enum

Dropdown select. Renders SelectField.vue.

3.6 array

List of strings. Renders ArrayField.vue — one input per item, + Add item button, per-row delete.

3.7 json

Raw JSON textarea. Renders JsonField.vue with Victor Mono font and inline parse-error display. Value is the parsed object/array, not the raw string.

3.8 state-picker

Flow-state path input. Renders StatePickerField.vue — monospace input with VariablePicker.

3.9 key-value

Record<string, string> editor — paired inputs, + Add button. Renders KeyValueField.vue. Duplicate keys are flagged inline (last-write-wins on commit). Empty keys are dropped on commit.

3.10 object

Nested group of fields under a single config key. Renders ObjectField.vue — recursively delegates to SchemaFields with the sub-schema. State path is config[key].subkey; the parent emits the full sub-object on every change, no deep merge in the store.
visible_when paths inside an object resolve against the root config — use dot-notation (transport_options.retries) to reach nested values from sibling fields.

3.11 object-array

Repeater of structured objects. Renders ObjectArrayField.vue — each item is a collapsible accordion with SchemaFields for item.fields. Authors get + Add item, per-item delete, and (optional) min/max bounds. The item_label template substitutes {key} for top-level item values — handy for the collapsed-row preview.

3.12 flow-picker

Searchable dropdown over the assistant’s flows. Renders FlowPickerField.vue (on top of SearchableSelect.vue). Stores the selected flow.id (UUID, language-agnostic), never the name. The option list is read from the builder runtime store (builderStore.availableFlows), not from the schema — so no options key is declared.
Used by subflow.flow_id. exclude_current prevents a flow from referencing itself.

3.13 enum-cards

Radio-card enum — a richer alternative to enum (§3.5) where each option is a full-width clickable card with an optional icon, hint line and colour accent. Renders EnumCardsField.vue. Stores the selected option’s value. accent tints the selected card. Known accents: sage (green), amber, rose; unknown / omitted falls back to the neutral primary accent.
Used by end.status.

4. Common field props

Available on every field declaration.

5. Conditional visibility — visible_when

A field can declare a condition that’s evaluated against the root config; when it fails, the field is not rendered. The stored value stays in node.config even while hidden — flipping the condition back restores the data without loss. Runtime behaviour for hidden fields is the handler’s responsibility (typically: ignore unless parent toggle is on). Short form (equality, AND of all clauses):
Verbose form (multiple operators):
Supported operators: equals (default), in (value must be an array, actual must be one of), truthy. Dot-paths reach into nested objects: transport_options.retries, headers.Authorization. Paths that don’t resolve evaluate as undefined (so equals only matches if value itself is undefined).

6. Field-level validators

Inline-only — backend FlowDefinitionValidator remains the source of truth for what blocks Publish. Validators do not block saves — they’re a UX cue so authors fix obvious typos before hitting Publish. Anything load-bearing must live in the backend validator.

7. Reserved top-level keys

These keys can’t be used as field names — they’re consumed by the renderer for other purposes:
  • required — top-level array<string> of required field keys.
  • sections — top-level grouping.
A schema test (tests/Unit/Domains/Flow/NodeHandlerSchemaSectionsTest.php) enforces that sections[*].fields only references declared field keys.

8. Migration notes

  • Pre-Phase-1 schemas (flat field list, no sections) still render — the fallback “Configuration” section catches every declared field. No change required unless you want the new visual grouping.
  • switch field type — removed. Use condition (branch handler) with multiple rules instead.
  • save_to (string) on SendMessage / Input — superseded by save_to_variable / variable objects ( Variable shape: { name, type, storage, group }). Legacy save_to still accepted by the runtime; new authors should use the new shape.

9. Fluent builders (fapost/support)

The wire format from §2–§3 is generated by the Fapost\Support\Builder\Schema namespace. It’s a thin layer over the same shape — toArray() is the only contract — giving authors autocomplete, type-safe field-specific methods (only min()/max() on NumberField, only regex() on TextField, etc.) and refactor-safety on a vocabulary that’s growing. Every Core handler is fluent today, but the renderer reads the wire shape, so a raw array (e.g. from a Plugin that doesn’t depend on fapost/support) renders identically. Each field is a concrete class under Fapost\Support\Builder\Schema\Fields\* with a static make(string $name) factory inherited from the abstract Field. Schema and Section live directly under Fapost\Support\Builder\Schema. Common methods on every field: label(), help(), default(), placeholder(), required(bool = true), visibleWhen(array).

10. Where the renderer lives