@smartkomm/plexus-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +863 -2
  2. package/dist/cli.mjs +5432 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +224 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/agents/use-shared-tools.md +52 -0
  7. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  8. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  9. package/dist/skill/references/audit/answer-access-request.md +115 -0
  10. package/dist/skill/references/config/attach-a-template.md +117 -0
  11. package/dist/skill/references/config/init-tenant.md +121 -0
  12. package/dist/skill/references/config/onboard-tenant.md +71 -0
  13. package/dist/skill/references/config/rename-program-stage.md +99 -0
  14. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  15. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  16. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  17. package/dist/skill/references/config/update-tenant-config.md +174 -0
  18. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  19. package/dist/skill/references/export/create-named-export.md +47 -0
  20. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  21. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  22. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  23. package/dist/skill/references/finance/record-submission-date.md +33 -0
  24. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  25. package/dist/skill/references/marketing/demo-video-links.md +101 -0
  26. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  27. package/dist/skill/references/notifications/mail-level.md +90 -0
  28. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  29. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  30. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  31. package/dist/skill/references/offer/addresses.md +129 -0
  32. package/dist/skill/references/offer/appointment-task.md +79 -0
  33. package/dist/skill/references/offer/assignment-rule.md +195 -0
  34. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  35. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  36. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  37. package/dist/skill/references/offer/author-smartform.md +102 -0
  38. package/dist/skill/references/offer/bind-node-input.md +76 -0
  39. package/dist/skill/references/offer/completeness-check.md +137 -0
  40. package/dist/skill/references/offer/countersign-node.md +91 -0
  41. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  42. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  43. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  44. package/dist/skill/references/offer/edit-smartform.md +121 -0
  45. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  46. package/dist/skill/references/offer/engage-push-node.md +147 -0
  47. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  48. package/dist/skill/references/offer/node-tasks.md +162 -0
  49. package/dist/skill/references/offer/optional-assessment.md +65 -0
  50. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  51. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  52. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  53. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  54. package/dist/skill/references/offer/task-family-request.md +69 -0
  55. package/dist/skill/references/offer/task-result-files.md +52 -0
  56. package/dist/skill/references/offer/template-document-node.md +113 -0
  57. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  58. package/dist/skill/references/resolver/manage-contract-resolver.md +161 -0
  59. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  60. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  61. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  62. package/dist/skill/references/signing/README.md +113 -0
  63. package/dist/skill/references/signing/connect-a-school.md +90 -0
  64. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  65. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  66. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  67. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  68. package/dist/skill/references/test-actions/drive-a-test-application.md +69 -0
  69. package/dist/skill/references/users/add-platform-admin.md +59 -0
  70. package/dist/skill/references/users/add-platform-user.md +59 -0
  71. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  72. package/dist/skill/references/users/change-login-email.md +55 -0
  73. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  74. package/dist/skill/references/users/delete-permission-group.md +76 -0
  75. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  76. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  77. package/dist/skill/references/users/set-password.md +57 -0
  78. package/dist/skill/references/users/use-test-account.md +79 -0
  79. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  80. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  81. package/dist/spec.snapshot.json +156821 -0
  82. package/package.json +53 -4
@@ -0,0 +1,117 @@
1
+ # Release an offer bundle — and check that it actually went live
2
+
3
+ **Use it to** take an edited bundle live on a tenant, stage it for testing first,
4
+ or find out why a change you applied is not visible to applicants.
5
+
6
+ ## The model in one paragraph
7
+
8
+ An offer family is one ascending version stream. Exactly one generation is
9
+ **editable** (the draft, always the newest revision); at most one is **live**
10
+ (what real applicants run); at most one sits between them as the **internal
11
+ candidate** (testable, not public). Applying a bundle writes into the *draft*.
12
+ Publishing freezes the draft and spawns a fresh one. Applications already running
13
+ keep the generation they started on — forever, unless migrated explicitly.
14
+
15
+ So there are two different questions, and they have different answers:
16
+
17
+ | Question | Answered by |
18
+ |---|---|
19
+ | What would `offer apply` write into the draft? | `offer plan` / `offer diff` per-target rows |
20
+ | Is anything waiting to go live? | the `publish` row of `plan`/`diff`, and `offer status` |
21
+
22
+ ## Check before you publish
23
+
24
+ ```bash
25
+ plexus-cli --env staging offer status --tenant <slug> --slug <offer-key>
26
+ ```
27
+
28
+ ```
29
+ field value
30
+ ───────── ───────────────────────────────
31
+ manifest published
32
+ workflow bound
33
+ pedagogy Grundschule Beispielstadt
34
+ draft v12
35
+ internal -
36
+ live v11
37
+ publish PENDING (draft is ahead of live)
38
+ ```
39
+
40
+ `publish: PENDING` means the draft says something the live generation does not —
41
+ regardless of whether *your* bundle changed anything. That is the state you are
42
+ in after an `offer apply` without `--publish`, or after someone saved in the
43
+ Workflow IDE.
44
+
45
+ ## Publish
46
+
47
+ ```bash
48
+ # Go live (default).
49
+ plexus-cli --env staging offer apply --tenant <slug> --dir ./bundle --publish
50
+
51
+ # Or stage as the testable candidate only — real applicants keep the live
52
+ # generation; a Testmodus application can be started against this one.
53
+ plexus-cli --env staging offer apply --tenant <slug> --dir ./bundle --publish --stage internal
54
+ ```
55
+
56
+ The final step of the report names what happened:
57
+
58
+ ```
59
+ step action detail
60
+ ──────────────────── ────────── ──────
61
+ manifest unchanged
62
+ smartform:anmeldung unchanged
63
+ workflow created
64
+ offer published
65
+ ```
66
+
67
+ - `offer: published` — a new generation went live. With the detail *"promoted the
68
+ tested candidate"* it released the generation you staged with `--stage internal`
69
+ rather than cutting a fresh copy of it, so what goes live is what you tested.
70
+ - `offer: already-live` — the draft already equalled the live generation. Nothing
71
+ was flipped, and that is correct.
72
+ - `offer:internal: …` — the same two outcomes for `--stage internal`.
73
+
74
+ ## Verify
75
+
76
+ ```bash
77
+ plexus-cli --env staging offer status --tenant <slug> --slug <offer-key>
78
+ ```
79
+
80
+ `live` must have moved to the draft's old revision and `publish` must read
81
+ `up to date`. In the app, the Workflow IDE's version picker shows the new
82
+ generation as **Aktuell**, and the one below it as **Vorgänger**.
83
+
84
+ ## Gotchas
85
+
86
+ - **`--publish` publishes on the distance to the LIVE head, not to your file.**
87
+ Applying a bundle that matches the draft byte-for-byte still publishes when the
88
+ draft is ahead of live. This is deliberate: the older behaviour compared the
89
+ bundle against the draft, so a second run with `--publish` reported `unchanged`
90
+ everywhere and silently released nothing.
91
+ - **The form and workflow publish gates run only when something is released**,
92
+ and always before the flip — so a draft edited in the IDE is validated and
93
+ finalized before it is frozen, and an apply that releases nothing validates
94
+ nothing. A validation error aborts before any generation changes.
95
+ - **`--stage internal` discards an existing internal candidate** (it is archived).
96
+ That candidate never went live, so nothing an applicant sees changes.
97
+ - **Stage then release is two commands, not two generations.** `--stage internal`
98
+ followed by a plain `--publish` of the same bundle promotes the staged
99
+ generation in place. It does not archive it and release a copy.
100
+ - **Running applications are not migrated.** After a release, older applications
101
+ keep their generation and appear in the board's amber *Veraltete
102
+ Workflow-Version* section when their board layout differs.
103
+ - **`offer plan` is read-only and now carries a `publish` row too** — use it to
104
+ see a pending release without applying anything.
105
+ - **`offer plan` shows program stages as `~stage` / `+stage` / `-stage`** —
106
+ `apply` reconciles `program.stages` by bundle `key`, not by name (see
107
+ `../config/rename-program-stage.md`). Run `offer plan` before the first apply
108
+ on an env: a `-stage` row archives that level, and apply refuses it with
109
+ `[stage/hasActiveApplications]` while an active application sits on it.
110
+ **Runs** are still matched by display name and only ever added: a renamed run
111
+ becomes a second run.
112
+
113
+ ## Related
114
+
115
+ - [Author a SmartForm as code](./author-smartform.md), [Add a workflow node](./add-workflow-node.md)
116
+ - [Manage a contract resolver](../resolver/manage-contract-resolver.md) — the
117
+ apply → assign → publish order
@@ -0,0 +1,78 @@
1
+ # Set an offer's seat capacities in its bundle
2
+
3
+ **When to use:** an offer's Schuljahr × Klassenstufe grid (seats per cohort, open or
4
+ closed) should travel with the bundle — a fresh environment gets the whole grid from one
5
+ `offer apply` instead of one `offer capacity set` per cell.
6
+
7
+ ## Prerequisites
8
+ - An offer bundle on disk (`offer export …`) whose `bundle.yaml` has a `program` block.
9
+ - A token with the tenant's offer-configuration rights (a tenant admin has them) for `offer plan` / `offer apply` on the target env.
10
+ - The stage and run keys of the offer (`program.stages[].key`, `program.runs[].key`) —
11
+ cells sit under a run (by its **bare** key) and are keyed by the **bare** stage key,
12
+ never by a name (names are `{ de, en }` display text).
13
+
14
+ ## Steps
15
+
16
+ Each run lists its cells under `capacities`, keyed by stage key:
17
+
18
+ ```yaml
19
+ program:
20
+ unitKey: primary
21
+ stages:
22
+ - { key: grade-1, name: { de: Klassenstufe 1, en: Grade 1 }, gradeLevel: 1 }
23
+ - { key: grade-2, name: { de: Klassenstufe 2, en: Grade 2 }, gradeLevel: 2 }
24
+ runs:
25
+ - key: "2026-27"
26
+ name: { de: Schuljahr 2026/27, en: School year 2026/27 }
27
+ startDate: '2026-08-01'
28
+ endDate: '2027-07-31'
29
+ capacities:
30
+ grade-1: { isOpen: true, capacity: 24 } # 24 seats, accepting
31
+ grade-2: { isOpen: true, capacity: unlimited } # no cap
32
+ # grade-3: { isOpen: false, capacity: null } # null = unconfigured (clears a stored one)
33
+ ```
34
+
35
+ ```bash
36
+ plexus-cli offer validate --dir <bundle> # offline: shape + stage keys
37
+ plexus-cli --env <env> offer plan --dir <bundle> # one `~cap run×stage from → to` row per changed cell
38
+ plexus-cli --env <env> offer apply --dir <bundle> # writes exactly those cells
39
+ ```
40
+
41
+ The fastest way to get the block right is to let the live offer write it:
42
+ `plexus-cli --env <env> offer export --slug <offer> --out <bundle>` emits every run with
43
+ all its cells.
44
+
45
+ ## Verify
46
+ ```bash
47
+ plexus-cli --env <env> offer plan --dir <bundle> # no `cap:` rows left = the grid matches
48
+ plexus-cli --env <env> offer show --slug <offer> # the live grid
49
+ ```
50
+
51
+ ## Idempotency / gotchas
52
+ - **A listed cell is authoritative** — both `isOpen` and `capacity`, every apply;
53
+ `capacity: null` clears a stored capacity. A cell the bundle does **not** list is left
54
+ alone. A re-apply of an unchanged bundle writes nothing.
55
+ - `capacity` is a whole number ≥ 0, `unlimited`, or `null`. **Unconfigured is not
56
+ unlimited:** an application to an open cell with `null` fails loud.
57
+ - A run with `capacities` must not also carry a run-level `isOpen` (validation error):
58
+ the open state lives on each cell. A NEW run with `capacities` is created closed and
59
+ then opened cell by cell. A run **without** `capacities` keeps the old behaviour — its
60
+ `isOpen` (default open) seeds every cell of a newly created run, and existing cells
61
+ are untouched.
62
+ - **Closing an existing Schuljahr = `isOpen: false` on each of its cells.** A run-level
63
+ `isOpen: false` on an existing run changes nothing and plans no row — a Schuljahr
64
+ "closed" that way stays open; the real close shows one `~cap … open → closed` row per
65
+ cell in the plan.
66
+ - For a real tenant the bundle on disk is a fresh `--env prod` export in a scratch dir,
67
+ never a stale or version-controlled copy — see
68
+ [edit-live-tenant-offer](./edit-live-tenant-offer.md).
69
+ - A cell naming a stage key that neither `program.stages` nor the live program has is
70
+ refused before anything is written (`[capacity/unknownStage]`). A stage the same bundle
71
+ adds plans as `(new)` and is written after the stage exists.
72
+ - `offer run add --capacity` / `offer capacity set` still exist for one-off edits; the
73
+ next `offer apply` of a bundle that lists the cell sets it back. `offer capacity set
74
+ --capacity unlimited` is accepted.
75
+
76
+ ## Related
77
+ - [Rename / remove a program stage](../config/rename-program-stage.md) — the stage keys cells use.
78
+ - [Release an offer bundle](./publish-offer-bundle.md).
@@ -0,0 +1,112 @@
1
+ # Branch on a staff input („Eingabe" + `if` node)
2
+
3
+ **When to use:** the process should take a different path depending on something only
4
+ staff know when they start the application — e.g. a staff member's own child
5
+ („Mitarbeiterbewerbung") skips the review and goes straight to the fee. Two
6
+ pieces: a **staff input** declared on the workflow (`graph.staffInputs`, a checkbox
7
+ staff tick in „+ Bewerbung"), and an **`if` node** whose condition reads it as
8
+ `$env.staffInput.<key>` and routes `true` / `false`. A family never sets a staff input:
9
+ a self-service start records every declared input as `false`.
10
+
11
+ In the workflow IDE the same declaration is the header button „Eingaben" (add an
12
+ input with its key, relabel, remove; the key is fixed once set), and the `if` node is a
13
+ diamond reading „if <pill>": the flow enters on the left, `true` leaves at the top,
14
+ `false` at the bottom.
15
+
16
+ ## Prerequisites
17
+ - A bundle on disk (`offer export`, a live tenant's only into a private scratch dir —
18
+ [Edit a live tenant's offer](./edit-live-tenant-offer.md)). Authoring is offline.
19
+ - The node ids around the branch point: `plexus-cli offer workflow node list --dir <dir>`
20
+ (here: `form` routes `standard` → `review`, and the fee node is `fee`).
21
+ - A key: one identifier segment, lower-case first letter, letters and digits only
22
+ (`staffApplication`, not `staff-application`) — it is read as `$env.staffInput.<key>`.
23
+
24
+ ## Rollout order and checks (a live tenant)
25
+ - **Deploy first, publish second.** `if` is a node type of the release: a
26
+ bundle using it publishes only on a deployment that registers it
27
+ (`[graph/unknownNodeType]` otherwise; the deploy needs a platform operator).
28
+ - **Running applications stay on their version** — the if node changes the
29
+ topology, so nobody is re-pinned; an application already past the form keeps
30
+ its fee step (the waiver „Zahlung überspringen" is the way out for a staff
31
+ child among them).
32
+ - **The Engage push may read the fee step** (`payment: $node.<fee>.payment`): it
33
+ passes the gate (`readSkipsAbsentProducer`) and skips the paid fields for a
34
+ staff child („Zahlungsschritt nicht durchlaufen" in the Übertragungsprotokoll).
35
+ - **Check the assignment rule**: a „+ Bewerbung" start is channel `staff`; a rule
36
+ row on `$env.applicant.channel == 'staff'` (creator responsible) now applies
37
+ to every staff child — decide whether that is wanted before publishing.
38
+ - Edit a live tenant's bundle only through [Edit a live tenant's offer](./edit-live-tenant-offer.md)
39
+ (export into a dated scratch dir, plan, `apply --publish --allow-prod`).
40
+
41
+ ## Steps
42
+ ```bash
43
+ # 1. Declare the staff input (type boolean = a checkbox; both label languages required).
44
+ plexus-cli offer workflow staff-input set staffApplication --dir <dir> \
45
+ --label "Mitarbeiterbewerbung" --label-en "Staff application"
46
+ plexus-cli offer workflow staff-input list --dir <dir>
47
+
48
+ # 2. Put an if node on the form's edge. `true` (a staff child) goes straight to
49
+ # the review, `false` to the fee — name both to be explicit.
50
+ plexus-cli offer workflow node add --dir <dir> --type if \
51
+ --id staffCheck --label "Mitarbeiterbewerbung?" --label-en "Staff application?" \
52
+ --on-edge form:standard \
53
+ --condition '$env.staffInput.staffApplication' \
54
+ --outcomes true:review,false:fee
55
+
56
+ # Later: change the condition or re-route an outcome in place.
57
+ plexus-cli offer workflow node set --dir <dir> --id staffCheck --condition '!$env.staffInput.staffApplication'
58
+ plexus-cli offer workflow node set --dir <dir> --id staffCheck --outcomes true:review,false:fee
59
+
60
+ # 3. Validate, plan, publish.
61
+ plexus-cli offer validate --dir <dir>
62
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir>
63
+ plexus-cli --env staging --tenant <t> offer apply --publish --dir <dir>
64
+ ```
65
+
66
+ ## Verify
67
+ ```bash
68
+ plexus-cli offer workflow node list --dir <dir>
69
+ # form form … standard→staffCheck
70
+ # staffCheck if true, false true→review, false→fee
71
+ jq '.graph.staffInputs, (.graph.nodes[] | select(.type=="if"))' <dir>/workflow.json
72
+
73
+ # After publish: start one test application per answer and see where each stands.
74
+ E="--env staging --tenant <t>"
75
+ plexus-cli $E applications start-test --offer <offer> --run <run> --stage <stage> \
76
+ --applicant test@example.com --staff-input staffApplication=true
77
+ plexus-cli $E applications show <id> # after the form: review (true) / fee (false)
78
+ plexus-cli $E applications store <id> # $env.staffInput.staffApplication = true
79
+ ```
80
+ In the cockpit, „+ Bewerbung" on this offer now shows the checkbox „Mitarbeiterbewerbung".
81
+
82
+ ## Idempotency / gotchas
83
+ - **Every staff start sets every declared input.** The cockpit („+ Bewerbung", with or
84
+ without Stellvertretung), a lead conversion (Interessenten → „In Bewerbung umwandeln")
85
+ and `applications start-test --staff-input key=true|false` (once per input) are refused
86
+ with `[staffInput/missingValue]` when one is left out, `[staffInput/unknownKey]` for a key
87
+ the offer does not declare. A self-service start records each input `false` — there is
88
+ no applicant-callable way to set one.
89
+ - **Read only by a condition (v1).** A SmartForm, a document binding, a task condition or
90
+ an assignment rule cannot read `$env.staffInput.*` — `[address/staffInputConditionOnly]`
91
+ (a form/rule refuses it as an unknown key). Readers: the `if` node's condition, the
92
+ process-data view, the IDE's „Umgebungsvariablen".
93
+ - **`staff-input set` is idempotent by key:** a second call with the same key relabels in
94
+ place (`--label-en` alone re-translates; `--label` alone is refused while it would leave a
95
+ stale English). `staff-input remove <key>` fails loud on an unknown key, and is refused by
96
+ the gate (`[if/condition]`) while a condition still reads the input.
97
+ - **Publish rules:** `[staffInput/keyFormat]`, `[staffInput/duplicateKey]`,
98
+ `[staffInput/unknownType]` (only `boolean`), `[i18n/missingTranslation]` (label without a
99
+ language), `[if/condition]` (no / unparsable condition, or one naming an undeclared input
100
+ or an unknown `$env` leaf), `[if/unroutedOutcome]` (`true` or `false` goes nowhere),
101
+ `[address/envCohortBound]` (a condition on stage/run is refused).
102
+ - **The `if` node never parks** and brings no board column; it decides once, on entry.
103
+ `--condition` is required and never defaulted; it is refused on any other node type.
104
+ - **Running applications keep their version:** the inputs and the branch apply to
105
+ applications started on the newly published version; already-started ones have the
106
+ inputs of the version they pinned.
107
+
108
+ ## Related
109
+ - [Add a workflow node](./add-workflow-node.md) · [Insert a node on one edge and rewire it](./insert-and-rewire-node.md)
110
+ - [Edit a workflow node](./edit-workflow-node.md) (`node set --outcomes`)
111
+ - [Start a test application](../applications/start-a-test-application.md) (`--staff-input`)
112
+ - [Name what a node reads](./addresses.md) (`$env` vocabulary)
@@ -0,0 +1,69 @@
1
+ # Make a task close with an appointment („Termin vereinbaren")
2
+
3
+ **Use it to** turn a workflow-step task such as „Termin für das Kennenlerngespräch
4
+ vereinbaren" into one that is completed by entering an appointment: date,
5
+ time, duration, place and host. Completing it books the appointment at the
6
+ application, mails the family (portal owner) date/time/place with a `termin.ics`
7
+ calendar file, frees the task it
8
+ `blocks`, and lists the appointment under „Anstehende Termine" on the staff dashboard.
9
+
10
+ ## The declaration
11
+
12
+ On the task (a composed `kind: "task"` in the node's `tasks`):
13
+
14
+ ```json
15
+ {
16
+ "key": "termin",
17
+ "kind": "task",
18
+ "title": { "de": "Termin für das Kennenlerngespräch vereinbaren", "en": "…" },
19
+ "assign": { "group": "schulverwaltung" },
20
+ "blocks": { "templateKey": "gespraech" },
21
+ "result": {
22
+ "kind": "appointment",
23
+ "title": { "de": "Kennenlerngespräch", "en": "Introductory talk" },
24
+ "durationMinutes": 45
25
+ }
26
+ }
27
+ ```
28
+
29
+ - `result.title` is the **neutral** title the family's letter and every calendar file
30
+ carry — never a family name.
31
+ - `durationMinutes` (5–480) prefills the dialog; staff may change it per appointment.
32
+ - Publish refuses: an unknown `kind` (`[taskResult/unknownKind]`), a result on an
33
+ approval (`[taskResult/onApproval]`), a title missing `de` or `en`
34
+ (`[taskResult/appointmentMissingTitle]`), a bad duration
35
+ (`[taskResult/appointmentInvalidDuration]`), a stray field
36
+ (`[taskResult/unknownField]`). `offer validate` catches all of them offline.
37
+
38
+ ## With the CLI
39
+
40
+ One spelling, the same verb as every other task result — no `:`-separated
41
+ free text, so a title may contain a colon:
42
+
43
+ ```bash
44
+ plexus-cli offer workflow node task add --dir $WORK/$S --node kennenlernen --key termin \
45
+ --title 'Termin für das Kennenlerngespräch vereinbaren' --team schulverwaltung --due-in-days 3 \
46
+ --blocks gespraech \
47
+ --result appointment --duration 45 \
48
+ --result-title 'Kennenlerngespräch' --result-title-en 'Introductory talk'
49
+ # change later: node task set … --duration 60 / --result-title '…'
50
+ plexus-cli offer workflow node list --dir $WORK/$S
51
+ ```
52
+
53
+ Then the usual live-tenant path ([Edit a live tenant's offer](./edit-live-tenant-offer.md)):
54
+ `offer validate` → `--env prod offer plan` → `--env prod offer apply --allow-prod --publish` → plan
55
+ again until every row is `same`.
56
+
57
+ ## Gotchas
58
+
59
+ - **Running applications keep their pinned graph.** An application already sitting on
60
+ the node with an open `termin` row keeps its old declaration until it is re-pinned;
61
+ new entries into the node get „Termin vereinbaren" in the action bar.
62
+ - **Staff books it from the action bar** („Termin vereinbaren"), like every task; the
63
+ dialog names every missing field, refuses a slot more than two years ahead, and
64
+ closes with a toast. In the Testmodus the shortcut „<Aufgabe> (Test): Termin morgen
65
+ 10:00" books through the same verb.
66
+ - **No portal owner, no mail.** An application staff created has no portal account; the
67
+ appointment is saved and the toast warns „Keine Mail an die Familie …". The same
68
+ happens when the school's outgoing mail is off.
69
+ - The zone is the tenant's (`Europe/Berlin` for every DACH tenant today).
@@ -0,0 +1,69 @@
1
+ # Let a task ask the family and wait for its answer („Nachforderung")
2
+
3
+ **Use it to** turn „Nachforderung: auf Unterlagen der Familie warten" from a task the
4
+ team closes by hand into one that asks the family itself and completes itself the
5
+ moment the family sends. The family gets the field in its task „Offene Felder beantworten" in the portal
6
+ (the task letter `applicant.tasksOpen`, the task's deadline, a reminder before it); the
7
+ team's task waits and holds the decision until then. Nobody ticks it off.
8
+
9
+ ## The declaration
10
+
11
+ On a task that a `spawnTasks` optional action references (and only there; at most
12
+ one such task per action):
13
+
14
+ ```json
15
+ {
16
+ "key": "unterlagenAnfordern",
17
+ "kind": "task",
18
+ "title": { "de": "Nachforderung: auf Unterlagen der Familie warten", "en": "Follow-up: waiting for the family's documents" },
19
+ "assign": { "group": "bewerbungsteam" },
20
+ "params": { "dueInDays": 7 },
21
+ "familyRequest": {
22
+ "fields": [{ "scope": "node", "owner": "form", "key": "admission", "path": "kind.weitereUnterlagen" }]
23
+ }
24
+ }
25
+ ```
26
+
27
+ - **What is asked is chosen at the click.** Pressing the action opens a dialog: the
28
+ presser ticks the fields and writes the text the family reads. The text becomes the
29
+ note at each asked field — in the portal and in the task letter.
30
+ - Exactly one of `fields` (the fields offered, each a full address
31
+ `$node.<formNode>.admission.<field>` — one form) or `from` (the form instance
32
+ `$node.<formNode>.admission`: every file field (`docRef`) of that form is offered). There
33
+ is no default form: an empty `familyRequest` is refused. A **free upload** is a `docRef` field
34
+ with `docRefConstraints: { multiple: true }` („Weitere Unterlagen") — the field must
35
+ exist in the application's form version.
36
+ - `params.dueInDays` is the family's deadline and the task's.
37
+ - Put the action into the decision's `requiresOneOf` if the decision should wait for it.
38
+
39
+ ## CLI
40
+
41
+ ```bash
42
+ plexus-cli offer workflow node task set --dir <dir> --node decision --key unterlagenAnfordern \
43
+ --due-in-days 7 \
44
+ --family-request-fields '$node.form.admission.kind.weitereUnterlagen'
45
+ # or --family-request-from '$node.form.admission': every file field of that form
46
+ plexus-cli offer validate --dir <dir>
47
+ ```
48
+
49
+ Remove with `--clear-family-request`. The task must not carry `result`, `when`,
50
+ `locksEvidence` or `blocks` (and nothing may block it) — `offer validate` names each.
51
+
52
+ ## Verify
53
+
54
+ 1. Cockpit: press the action → the dialog offers the fields; tick one, write the text,
55
+ „Anfordern" → the task shows the clock (system-closed), the decision buttons read
56
+ „Wartet auf: …".
57
+ 2. Portal `/e`: „Offene Felder beantworten" (field under „Nachzureichen"), the text at the field, „bis <Datum>".
58
+ 3. Upload and send as the family → the task is `done`, the decision buttons are free.
59
+
60
+ ## Gotchas
61
+
62
+ - A field already under an open request, hidden, or printed into a held document is not
63
+ offered; the trigger refuses it with the reason, and nothing is spawned.
64
+ - An empty or unchanged answer answers nothing (the same file again is no new document):
65
+ the field stays asked.
66
+ - Withdrawing the request („Zurückziehen") cancels the task; the decision stays held by
67
+ `requiresOneOf` until another action completes.
68
+ - Rows of applications on an OLDER offer version keep the old task; only applications
69
+ pinned to the published version get the family request.
@@ -0,0 +1,52 @@
1
+ # Let a task's result carry files
2
+
3
+ **Use it to** let the person who completes a task attach files to their answer. Example: the
4
+ teacher's trial-day feedback with an English and a maths placement sheet, as PDF or photo.
5
+
6
+ ## The model
7
+
8
+ - **One switch on the task:** `result.files: true`. It is allowed only on prose results:
9
+ `freetext`, `recommendation`, or a `choice` without `writes`. Publish refuses it anywhere
10
+ else (`[taskResult/filesOnKind]`).
11
+ - **Text and files are ONE message** in the chat the result names (`result.chat`). The row
12
+ keeps no text, and the chat keeps, shows and erases the files (GDPR erasure included).
13
+ - **Up to 20 files per result**.
14
+ A normal chat message takes 5. Each file may be PDF, JPEG, PNG or WebP of up to 25 MB;
15
+ an iPhone HEIC photo is converted to JPEG in the browser.
16
+ - **Where the files show:** in the result's message — cockpit → Verlauf/Aktivitäten (the
17
+ chat's tab); a click opens the file in the viewer. The task itself shows at most how
18
+ many files its result carries. Never in the „Dokumente" rail:
19
+ that lists the application's documents, not task attachments. Nothing is
20
+ copied, so deleting the message removes its files.
21
+ - **Who sees them:** whoever reads the feedback text — the SAME chat rule. In a
22
+ `team:<group>` chat that means the members of that group plus platform admins. A
23
+ `tenant_admin` who is in no such group sees neither the text nor the files. Before
24
+ choosing `team:principals`, check that the readers really hold that group (`plexus-cli
25
+ users show <email>`).
26
+
27
+ ## CLI
28
+
29
+ Work in a scratch dir for a real tenant
30
+ ([edit-live-tenant-offer.md](./edit-live-tenant-offer.md)).
31
+
32
+ ```bash
33
+ plexus-cli --env prod --tenant <slug> offer export --slug <offer> --out <dir>
34
+ plexus-cli offer workflow node task set --dir <dir> --node decision --key tasterFeedback --files
35
+ plexus-cli offer validate --dir <dir>
36
+ plexus-cli --env prod --tenant <slug> offer plan --dir <dir> # workflow: change
37
+ # apply only with the school's go (the process owner):
38
+ plexus-cli --env prod --tenant <slug> offer apply --dir <dir> --publish --allow-prod
39
+ ```
40
+
41
+ A new task: `node task add … --result freetext --chat <chat> --files`.
42
+
43
+ ## Verify
44
+
45
+ 1. Open the task in the cockpit. The dialog shows „Dateien (optional)" with the paperclip,
46
+ which accepts several files at once.
47
+ 2. Complete it with two files. Verlauf, tab of the result's chat: one message with the
48
+ text and both files.
49
+ 3. „Dokumente": neither file is listed there.
50
+ 4. As staff outside the chat's team: neither the text nor the files show in Verlauf.
51
+
52
+ Test tenant: Grundschule → „Probetag auswerten" (Schulleitung) takes files.
@@ -0,0 +1,113 @@
1
+ # Generate a letter from a Word template (governedDocument `production: 'template'`)
2
+
3
+ **Use it to** let a workflow document node (Platzangebot, Bestätigung, Bescheinigung) fill a tenant Word
4
+ template and render the PDF automatically — no resolver, no deploy.
5
+
6
+ ## Prerequisites
7
+
8
+ - The Word template is uploaded in the tenant's template module (Vorlagen). Its placeholders use
9
+ `<< input.<feld> >>` (sections `<<#input.<feld>>> … <</input.<feld>>>`).
10
+ - **Exactly one language per letter (required):** every template node declares a
11
+ required enum field (options `de`, `en`), names it in `template.localeField`, binds it to
12
+ `$env.applicant.language` (the applicant's language — the default) and wraps each language's
13
+ text in a section `<<#input.<feld>.de>> … <</input.<feld>.de>>` /
14
+ `<<#input.<feld>.en>> … <</input.<feld>.en>>`. Exactly one section renders, and `input.*` prints
15
+ the way that language writes it (numbers, dates, Ja/Yes, prices, names). A node without
16
+ `localeField` is refused at publish (`[governedDocument/localeFieldMissing]`); a missing section
17
+ blocks publish and file replacement. The English twin `en.input.*` of the old fixed bilingual
18
+ letters is gone — a file naming it is `[governedDocument/unknownPlaceholder]`. A German-only
19
+ school declares one option `de` and binds the field to the fixed value `de`.
20
+ - **Switching a LIVE template to language sections — upload it under a NEW key.** A file
21
+ replacement is checked against EVERY published node naming the key, archived versions included
22
+ (`publishedNodesFillingTemplate` — they still carry running applications), and a node published
23
+ without `localeField` accepts no file at all. So the old key stays with the old versions for
24
+ good; the sectioned file goes in as a new template:
25
+ 1. upload the sectioned Word file as a new template and give it a new key (§ 1, e.g.
26
+ `platzangebot-v2`);
27
+ 2. in the exported bundle point `template.key` at it, add the field `sprache` to
28
+ `template.fields`, set `template.localeField: 'sprache'` and bind it:
29
+ `plexus-cli offer workflow node set-input --dir <bundle> --node <id> --input sprache --address '$env.applicant.language'`
30
+ ([Bind a node input](./bind-node-input.md));
31
+ 3. `offer validate` → `plan` → `apply --publish`.
32
+ Applications pinned to the old version produce no letter any more (the mask names
33
+ `localeFieldMissing`); a re-pin onto the new version is refused, because the new binding changes
34
+ the process topology — restart such a (test) application on the new version.
35
+ - The deployment already runs a release that knows template nodes (validators first, then
36
+ publish); if publish refuses the node type, that needs a platform operator.
37
+
38
+ ## 1. Give the template its stable key
39
+
40
+ There is no UI field yet — set it through the public mutation (the same `updateMetadata` the UI calls,
41
+ so name / role / file-name pattern must be passed as they are):
42
+
43
+ ```bash
44
+ plexus-cli call templates:list --env staging client=<slug> --json # id, name, roleId
45
+ plexus-cli call templates:updateMetadata --kind mutation --env staging client=<slug> \
46
+ templateId=<id> name='Platzangebot' roleId=<roleId> configKey=platzangebot --json
47
+ ```
48
+
49
+ The key is unique per tenant and cannot move while a published process names it.
50
+
51
+ ## 2. Author the node in the bundle (JSON)
52
+
53
+ `plexus-cli offer workflow node add` cannot create a governedDocument node; edit `workflow.json`
54
+ (export → edit → plan → apply, see [Edit a live tenant's offer](./edit-live-tenant-offer.md)):
55
+
56
+ ```jsonc
57
+ {
58
+ "id": "platzangebot", "type": "governedDocument", "label": "Platzangebot",
59
+ "production": "template", "approval": "release", "responseStrategy": "confirm",
60
+ "responseDueInDays": 14,
61
+ "template": {
62
+ "key": "platzangebot",
63
+ "fields": [
64
+ { "key": "kind_vorname", "label": "Vorname des Kindes", "type": "string", "required": true },
65
+ { "key": "datum", "label": "Datum", "type": "date", "processSource": "generatedAt" },
66
+ { "key": "antwortfrist", "label": "Antwortfrist", "type": "number", "processSource": "responseDueInDays" },
67
+ { "key": "sprache", "label": { "de": "Sprache des Schreibens", "en": "Language of the letter" },
68
+ "type": "enum", "required": true,
69
+ "options": [
70
+ { "value": "de", "label": { "de": "Deutsch", "en": "German" } },
71
+ { "value": "en", "label": { "de": "Englisch", "en": "English" } }
72
+ ] }
73
+ ],
74
+ "cockpitFields": [],
75
+ "localeField": "sprache"
76
+ },
77
+ "inputs": {
78
+ "kind_vorname": {
79
+ "kind": "binding",
80
+ "address": { "scope": "node", "owner": "form", "key": "admission", "path": "kind.vorname" }
81
+ },
82
+ "sprache": { "kind": "binding", "address": { "scope": "env", "key": "applicant.language" } }
83
+ }
84
+ }
85
+ ```
86
+
87
+ A bound form field must be exported by the form (`export: true`) and is named by its full address
88
+ (`$node.form.admission.kind.vorname` — bind it with
89
+ `plexus-cli offer workflow node set-input --node platzangebot --input kind_vorname --from form --field kind.vorname`);
90
+ a form that does not run before the node is `[address/notVisible]`, a field it does not export
91
+ `[address/pathNotInTarget]`. An old bundle (`wires`, a binding with `path`) is refused
92
+ — export it again. The deadline alone can be changed
93
+ with `plexus-cli offer workflow node set --id platzangebot --response-due-days 14` (`""` removes it).
94
+
95
+ ## 3. Validate, plan, publish
96
+
97
+ `plexus-cli offer validate <dir>` checks the node shape offline (including the language field,
98
+ `[governedDocument/localeFieldMissing]`). **Publish** additionally refuses a
99
+ key no template carries (`[governedDocument/unknownTemplate]`) and any placeholder the fields cannot
100
+ fill (`[governedDocument/unknownPlaceholder]`).
101
+
102
+ ## Verify
103
+
104
+ An application entering the node gets a run in `contract_generation_runs` (producer: the template)
105
+ and a PDF version on the node's slot within seconds; the cockpit's „Überarbeiten" opens the mask.
106
+
107
+ ## Gotchas
108
+
109
+ - Generation is automatic only while the producer row is open and unblocked; after „Freigabe
110
+ anfordern" nothing regenerates. A changed template file is caught at submit („nicht mehr aktuell").
111
+ - Replacing the template file is refused while a published node could not fill its placeholders.
112
+ - **CLI gap:** the `template` pin (key, fields, cockpitFields) has no `offer workflow node` verb yet —
113
+ JSON only.