# 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 1. Open the Langflow authoring UI (http://localhost:7860) and load the flow from `LANGFLOW_LOAD_FLOWS_PATH` (canonical files are preloaded). 2. Edit the `CustomInputForm` node **in its code editor**: every entry in its `inputs` list is one web-form field. Add an `lfx.io` input, e.g. ```python 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 ) ``` Supported input types → widgets: `StrInput`/`MessageTextInput` → text, `MultilineInput` → textarea, `SecretStrInput` → password, `IntInput` → integer, `FloatInput` → number, `BoolInput` → checkbox, `DropdownInput` → select, `MultiselectInput` → multi-select, `SliderInput` → slider (needs `range_spec`), `FileInput` → text stub (real upload later). 3. 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 tweak `display_name`, `info`, `value`, and `options` of existing fields, but `name` and new fields need the code editor.) 4. **Wire the envelope**: keep the `ChatInput → form_values` edge (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. 5. Export the flow (JSON) into `backend/flows/.json` and 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`: ```python ( 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 =]`, `&` 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`/`then` branch, so RJSF hides it until the condition holds. `ui:order` keeps template order, so new questions appear directly below their trigger. - `required` on 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 `ChatOutput` renders it as a fenced ```json block; the run result text is that block, not raw JSON (unwrap before `json.loads`). - Components consuming `form_data` should read `.data` (or `get_text()`), not `.text` like with `Message`. ## Conventions the pipeline relies on - `required=True` on a template field: `lfx validate --level 4` demands 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`/`_3` suffix 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.