6.2 KiB
Authoring web forms (CustomInputForm)
Flows that should render a web form start with a CustomInputForm node
(this package — the kirche category in the Langflow palette). The API
derives a JSON Schema + uiSchema from the node's template
(app/flows/input_schema.py) and the webapp renders it with RJSF.
Adding a new form field
- Open the Langflow authoring UI (http://localhost:7860) and load the
flow from
LANGFLOW_LOAD_FLOWS_PATH(canonical files are preloaded). - Edit the
CustomInputFormnode in its code editor: every entry in itsinputslist is one web-form field. Add anlfx.ioinput, e.g.
Supported input types → widgets:from lfx.io import DropdownInput DropdownInput( name="room", # JSON-Schema property key (freely renamable) display_name="Room", # form label (title) info="Where should it take place", # help text (description) options=["Chapel", "Hall"], # dropdown choices (enum) value="Chapel", # prefill (default) required=True, # marks the field required )StrInput/MessageTextInput→ text,MultilineInput→ textarea,SecretStrInput→ password,IntInput→ integer,FloatInput→ number,BoolInput→ checkbox,DropdownInput→ select,MultiselectInput→ multi-select,SliderInput→ slider (needsrange_spec),FileInput→ text stub (real upload later). - Nothing else to change in code:
collect()is dynamic — it emits every input except the plumbing ones, so renames, additions, and removals just work. (The inspector panel can tweakdisplay_name,info,value, andoptionsof existing fields, butnameand new fields need the code editor.) - Wire the envelope: keep the
ChatInput → form_valuesedge (hidden input,show=False). Submitted web-form values arrive there as{"payload": {...}}and are merged over the defaults — including keys the node code does not declare (forward-compatible with renames). Never make form fields required with an incoming edge — that's the envelope's job. - Export the flow (JSON) into
backend/flows/<name>.jsonand commit. The API watcher syncs + deploys it automatically; the form schema is regenerated per version.
Branched questionnaires (visible_if)
A field can appear only when other fields have certain values — nested
to any depth. Declare it in the code editor with a marker in info:
(
DropdownInput(
name="gebaeude",
display_name="Welches Gebäude soll verändert werden?",
options=["Kirche", "Anderes Gebäude"],
value="Kirche",
required=True,
),
)
(
MultiselectInput(
name="bestandteil",
display_name="Welcher Bestandteil der Kirche?",
options=["Glocke", "Orgel", "Innenraum", "Fenster", "Fassade"],
value=[],
info="Mehrfachauswahl möglich [visible_if gebaeude=Kirche]",
required=True,
),
)
(
DropdownInput(
name="glockenmassnahme",
display_name="Welche Maßnahme an der Glocke?",
options=["Reinigung", "Reparatur", "Neuguss"],
value="Reinigung",
info="[visible_if bestandteil=Glocke]",
),
)
Syntax: [visible_if <feld>=<wert>], & for AND
([visible_if a=X & b=Y]), double quotes for values with spaces
([visible_if gebaeude="Anderes Gebäude"]). Trigger matching by input
type: Dropdown/Text const, Bool const (true/false), Multiselect
contains, Int/Float exact number. Nesting is automatic: a field whose
trigger is itself conditional renders one level deeper, without limit.
What the parser does with markers:
- The marker is stripped from the shown help text; the field leaves the
top-level schema and lives only in its
allOf/if/thenbranch, so RJSF hides it until the condition holds.ui:orderkeeps template order, so new questions appear directly below their trigger. requiredon a branched field applies only while visible.- Values are always kept: hidden fields still validate and are submitted (stale values can keep deeper branches satisfied until changed).
- Safety is fail-open: unknown triggers (renamed?), unsupported trigger types, self-references, and trigger cycles keep the field always visible instead of hiding data. Renaming a trigger means updating its markers.
See backend/flows/bauvorhaben.json for a full 4-level example
(Gebäude → Bestandteil → Maßnahme → Gussjahr, plus sibling branch and
bool trigger).
Output type: Data (JSON), not Message
collect() returns Data(data={...}) — a real JSON object. Consequences:
- A terminal
ChatOutputrenders it as a fenced ```json block; the run result text is that block, not raw JSON (unwrap beforejson.loads). - Components consuming
form_datashould read.data(orget_text()), not.textlike withMessage.
Conventions the pipeline relies on
required=Trueon a template field:lfx validate --level 4demands a value or an edge, so the canonical-flow generator (backend/scripts/make_flow_fixtures.py) writes a single-space sentinel value for such fields. The form parser treats whitespace-only values as "no default" — don't ship real whitespace defaults.- Field key =
name(stable, freely renamable but must stay unique and non-empty; collisions get a_2/_3suffix in the form schema); label =display_name; help =info. Renames propagate to downstream components, which read the JSON keys as-is. - Unknown future input types degrade to a plain text field in the web form (parser fallback), so adding exotic lfx inputs never breaks runs.
Regenerating the bundled fixtures
After editing the component source (form_inputs.py), refresh the
canonical + test-fixture exports:
docker compose exec api /opt/venv/bin/python scripts/make_flow_fixtures.py
docker compose exec -T api uv run pytest tests/test_lfx_validate.py
Notes: the generator writes UTF-8/LF explicitly (Windows-safe), and
backend/flows/*.json may carry Langflow round-trip state (key order)
the generator does not reproduce — if a regen rewrites canonical files
beyond the component's own node, revert them and splice only the changed
node fields (code, description, output types, edge handles) instead.