101 lines
5 KiB
Markdown
101 lines
5 KiB
Markdown
# 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/<name>_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/<name>.json`)
|
|
|
|
Never hand-edit: add `build_<name>()` 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/<short>`, never
|
|
directly on `main`.
|