| .. | ||
| __init__.py | ||
| bauvorhaben_form.py | ||
| form_inputs.py | ||
| mitarbeiter_form.py | ||
| README.md | ||
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).
User-built form components (no kirche base needed)
A plain CustomComponent whose display name ends with "Form" is also
parsed into a web form — same template contract, no base class. To make
submitted values reach it at runtime, add an envelope input and merge it
in the output method (pattern):
from lfx.io import MessageTextInput # add to imports
import json
# add to inputs:
MessageTextInput(name="form_values", display_name="Form values", value="", show=False)
# at the top of the output method:
try:
raw = self.form_values.text if hasattr(self.form_values, "text") else self.form_values
submitted = json.loads(raw or "{}")
except Exception:
submitted = {}
if isinstance(submitted.get("payload"), dict):
submitted = submitted["payload"]
def val(name, default):
return submitted.get(name, default)
Then use val("rolle", self.rolle) etc., and wire a ChatInput node
into form_values on the canvas. Branch markers ([visible_if ...]
in info) work here too — the parser reads the template only.
Asked fields only (run envelope fields)
The API sends the asked field names (from the version's form schema)
in the run envelope's fields list. collect() then emits defaults
only for asked AND currently visible fields (branch conditions are
evaluated against submitted values over code defaults, so hidden
fields stay out exactly as the frontend never submits them): the
template may declare fields unknown to this code (they arrive via
envelope and merge normally), and unasked code defaults stay out of
the output. Without the list — raw dicts, Langflow playground — all
visible declared defaults apply as before.
Variant flows (own component per flow)
A flow whose fields differ from CustomInputForm gets its own
standalone component (see bauvorhaven_form.py → "Bauvorhaben Form"
in the palette): same class pattern, own inputs list. Standalone
means it: no imports between form modules — the LFX runtime executes
the embedded module source without our kirche package, so every form
component must be self-contained (stdlib + lfx only). Copy the file
as the starting point for future forms.
Rule: code-declared inputs must equal template fields
(test_embedded_code_matches_template_fields enforces it; required
Multiselect uses the [" "] sentinel). Generate node + flow with
kirche_node(..., component_cls, code_override) and
build_bauvorhaben() in backend/scripts/make_flow_fixtures.py
rather than hand-editing JSON. Restart Langflow afterwards so the
palette picks up the new component, then build/upload the flow.
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 ([" "]for Multiselect, which rejects a plain string at runtime). 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.