# New workflow module: form flow end to end How to add a form-based workflow (web form → run → result), learned from building intake, Bauvorhaben, and Mitarbeiter-Einstellen. Three layers: component (Langflow palette), flow (canvas), backend parsing (automatic). ## 1. Form component (`backend/components/kirche/_form.py`) Copy `mitarbeiter_form.py` (small) or `bauvorhaben_form.py` (branching). Rename the class with a **`CustomInput*` prefix** (e.g. `CustomInputMitarbeiter`) — the backend parser matches that prefix; a plain `CustomComponent` name additionally needs a display name ending in `"Form"`. Rules, all load-bearing: - **Self-contained**: stdlib + `lfx` only. NEVER import from sibling form modules — LFX executes the embedded module source without our package, and Langflow round-trips the code. Duplicated helpers are structural, not laziness. - **Inputs**: one `lfx.io` entry per question (`name` = stable JSON key, `display_name` = label, `info` = help, `options`/`value`/`required` as needed) plus the hidden `form_values` plumbing input (reserved name — never use it for a field). - **Branching**: `[visible_if feld=Wert]` markers in `info` (`&` for AND, quotes for spaces). Any depth; trigger types: Dropdown/Text `const`, Bool, Multiselect `contains`, Int/Float exact. Stale markers, cycles, and unknown triggers fail open (always visible). - **Required sentinels**: required fields with empty values carry `" "` (`[" "]` for Multiselect — a plain string crashes LFX at build, `[]` fails validation). The parser treats them as "no default". - **Outputs**: exactly `Output(name="form_data", method="collect")` returning `Data`. Do not touch `collect()` — it is fully dynamic (asked + visible defaults, submitted values win). - Export in `backend/components/kirche/__init__.py`. Then: `docker compose restart langflow` (components load at startup), check the palette via `GET /api/v1/all` (or sidebar search, hard-refresh the browser tab if the entry is missing — the API is source of truth). ## 2. Flow JSON (`backend/flows/.json`) Never hand-edit: add `build_()` in `backend/scripts/make_flow_fixtures.py` and run it (from `backend/`). - Form node via `kirche_node(node_id, component_cls=..., code_override=...)`. - Logic nodes via `custom_node(CODE, ...)` (keep return annotations like `-> Message`: edge handles derive output types from them). - Always wire `ChatInput → form_values` (run envelope), then `form_data → … → ChatOutput`. - Reorder template keys to question order (lfx emits alphabetical; RJSF renders insertion order, and `ui:order` follows it). - The script writes fixtures + canonical files; `git checkout` the churn on unrelated flows, keep canonical files CRLF like the repo. - Custom logic code (e.g. salary math) lives as a `*_CODE` constant in the script — one owner for template and code. ## 3. Backend parsing (automatic, know the contract) `app/flows/input_schema.py` turns the node template into `{json_schema, ui_schema}` per version: `name` → key (verbatim; empty skipped, collisions suffixed), `display_name`/`info`/`value`/`options` → title/description/default/enum, `show: false` skipped, unknown types degrade to text. Markers become `allOf`/`if`/`then` branches; branch-`required` applies only while visible. Runs carry an envelope (`payload`, `fields` = asked names from the cached schema, run context). `collect()` emits defaults only for asked AND visible fields; submitted values always win; hidden values are kept but never inventoried. Raw dicts and the Langflow playground use all visible declared defaults. ## 4. Langflow round-trip (where flows go to die, if careless) - Node **type names must be distinct per component** — Langflow normalizes same-type nodes to the installed class on import and silently discards variant templates. - Upload via `POST /api/v1/flows/upload/`, then fetch the flow back and assert node type, template fields, and code are intact. - Restart re-seeds `backend/flows/` into Langflow; the sync button pulls Langflow → files **matched by flow name, overwriting silently**. Commit before syncing, review the `pulled` report, never rename or repurpose flows casually. - Experiments that must stay out: list their stem in `backend/flows/.flowsignore` (skipped by sync, deploy, validation, frontend, and future pulls). ## 5. Verify before reporting done - `uv run pytest` (full suite incl. real-LFX deploy+run e2e) green. - `uv run ruff check .` clean, `uv run ruff format --check .` clean. - `uv run lfx validate --level 4 tests/fixtures/flows/` all pass. - Live: `GET /api/flows/{id}/input-schema` shows `fallback: null` with expected properties (cached schemas refresh only on new versions — unchanged files keep their cached schema). - Browser click-through in Workflows, including a branched path and an empty submit (branch-required errors). New behavior ships with tests; schema changes ship as Alembic revisions (never edit applied migrations); commit on `feat/`, never directly on `main`.