kirche-einfach/docs/new_workflow_module.md

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`.