16 neue Task-Typen dokumentiert (S+M-Ausbau), README-Index erweitert, SQL-Migrationen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-07-02 08:58:16 +02:00
parent b1fb3efaa1
commit 146adc970a
20 changed files with 668 additions and 0 deletions
+24
View File
@@ -243,16 +243,25 @@ Jeder Task-Typ ist in einer eigenen Datei unter [`tasks/`](tasks/) dokumentiert
Die Struktur-Container `sequence` und `parallel`/`branch` sind oben unter [Struktur-Elemente](#struktur-elemente) beschrieben.
Einzelne Tasks (`signal_wait`, `aktenzeichen_vergabe`) benötigen eine eigene kleine Tabelle — die zugehörigen Migrationen liegen unter [`sql/`](sql/).
## Steuerung, Logik und Zeit
| Task | Was er kann |
|---|---|
| [`aktenzeichen_vergabe`](tasks/aktenzeichen_vergabe.md) — Aktenzeichen vergeben | Kollisionssicher die nächste Nummer aus einem jahresweisen Nummernkreis ziehen und formatieren. |
| [`beleg_abgleich`](tasks/beleg_abgleich.md) — Belege abgleichen | Felder zweier Datensätze mit Toleranzen vergleichen (z. B. Bestellung ↔ Rechnung). |
| [`calc`](tasks/calc.md) — Berechnung | Wertet arithmetische/logische Ausdrücke sicher aus und schreibt die Ergebnisse als Kontextvariablen. |
| [`decision_table`](tasks/decision_table.md) — Entscheidungstabelle | Mehrspaltige Entscheidungstabelle (DMN-light) statt verschachtelter if-Kaskaden. |
| [`escalate`](tasks/escalate.md) — Eskalation bei Fristüberschreitung | Hält den Workflow bis zum Ablauf einer Frist im Wartezustand; solange bleibt der ursprüngliche Bearbeiter zuständig. |
| [`event_race`](tasks/event_race.md) — Ereignis-Wettrennen | Ereignisbasiertes Gateway: die zuerst abgeschlossene Warte-Option gewinnt, der Rest wird storniert. |
| [`external_trigger`](tasks/external_trigger.md) — Auf externen Callback warten | Pausiert den Workflow, bis ein externes System (Signatur-Dienst, Webhook, Pipeline, manueller Klick) eine generierte Callback-URL mit einmaligem Token aufruft. |
| [`foreach_parallel`](tasks/foreach_parallel.md) — Parallele Multi-Instanz | Kind-Tasks je Listenelement unabhängig ausführen, optional mit Abschlussbedingung. |
| [`frist_rechner`](tasks/frist_rechner.md) — Fristen berechnen | Verwaltungsfristen mit Bekanntgabefiktion, Feiertagen und Werktagsregel berechnen. |
| [`if`](tasks/if.md) — Bedingte Verzweigung | Führt abhängig von einer Bedingung die Tasks im `<then>`- oder im optionalen `<else>`-Zweig aus. |
| [`json_transform`](tasks/json_transform.md) — JSON umformen | Werte per Pfadausdruck aus JSON ziehen und als Kontextvariablen ablegen. |
| [`log_step`](tasks/log_step.md) — Audit-Log-Eintrag | Schreibt einen unveränderlichen Eintrag in das Audit-Log des Workflows (mit Task-ID, Aktion, Benutzer, Details und Zeitstempel). |
| [`loop_foreach`](tasks/loop_foreach.md) — Schleife über eine Liste | Iteriert über ein Array aus dem Kontext und führt die direkt enthaltenen Kind-Tasks pro Element aus. |
| [`map_lookup`](tasks/map_lookup.md) — Wertetabelle / Mapping | Bildet einen Eingabewert über eine Inline-Tabelle auf einen Zielwert ab — spart lange `<if>`-Kaskaden für Zuordnungen wie Status → Empfänger oder Typ → Feldname. |
@@ -260,9 +269,12 @@ Die Struktur-Container `sequence` und `parallel`/`branch` sind oben unter [Struk
| [`schedule_resume`](tasks/schedule_resume.md) — Geplante automatische Fortsetzung | Pausiert den Workflow und plant einen Weck-Zeitpunkt, zu dem der Workflow automatisch fortgesetzt wird — im Gegensatz zu `wait_until`, das passiv auf den nächsten Trigger wartet. |
| [`set_var`](tasks/set_var.md) — Kontextvariablen setzen | Setzt eine oder mehrere Kontextvariablen ohne Benutzerinteraktion. |
| [`signal_fire`](tasks/signal_fire.md) — Internes Ereignis auslösen | Feuert ein Ereignis auf dem internen Event-Bus der Anwendung, sodass registrierte Listener reagieren — ohne Umweg über die HTTP-API. |
| [`signal_wait`](tasks/signal_wait.md) — Auf Signal warten | Pausieren, bis ein anderes Ereignis ein passendes Signal (Name + Korrelation) hinterlegt. |
| [`stop`](tasks/stop.md) — Workflow abbrechen | Bricht den gesamten Workflow sofort ab; die Engine setzt den Gesamtstatus auf `CANCELLED`. |
| [`subworkflow`](tasks/subworkflow.md) — Anderen Workflow einbetten | Lädt eine zweite Workflow-XML (Name ohne `.xml` aus dem Workflow-Verzeichnis) und führt sie synchron im laufenden Workflow aus — inklusive Wartezuständen. |
| [`try_catch`](tasks/try_catch.md) — Fehlergrenze mit Retry | Fehler abfangen, mit Backoff wiederholen und im catch-Zweig weiterlaufen statt den Workflow abzubrechen. |
| [`wait_until`](tasks/wait_until.md) — Bis Zeitpunkt warten | Pausiert den Workflow bis zu einem festen oder relativen Zeitpunkt. |
| [`wiedervorlage`](tasks/wiedervorlage.md) — Wiedervorlage | Vorgang bis zu einem Termin parken und dem Bearbeiter mit Notiz wieder vorlegen. |
## Interaktion und Kommunikation
@@ -271,6 +283,7 @@ Die Struktur-Container `sequence` und `parallel`/`branch` sind oben unter [Struk
| Task | Was er kann |
|---|---|
| [`approve_reject`](tasks/approve_reject.md) — Genehmigen/Ablehnen | Zeigt dem Bearbeiter eine JA/NEIN-Entscheidungsseite, optional mit eingebetteter PDF-Vorschau im Vollbild. |
| [`assign_group`](tasks/assign_group.md) — Gruppen-Freigabe | Genehmigungsschritt an eine Gruppe zustellen; wer zuerst entscheidet, übernimmt (claim). |
| [`calendar_event`](tasks/calendar_event.md) — Kalendereintrag anlegen | Legt vollautomatisch (ohne Benutzerinteraktion) einen Ereignis-Eintrag an — Termin, Frist oder Vermerk — und verknüpft ihn mit einem Bezugsobjekt (z. |
| [`cloud_link`](tasks/cloud_link.md) — Öffentlichen Cloud-Freigabelink erzeugen | Gibt eine Datei im angebundenen Cloud-Speicher per öffentlichem Link frei und schreibt die Link-URL in den Kontext. |
| [`email`](tasks/email.md) — E-Mail versenden | Versendet eine E-Mail (ohne Benutzerinteraktion) mit Betreff, Text und optionalen Anhängen. |
@@ -283,6 +296,17 @@ Die Struktur-Container `sequence` und `parallel`/`branch` sind oben unter [Struk
| [`webhook`](tasks/webhook.md) — HTTP-Aufruf an externe URL | Sendet einen HTTP-Request an eine beliebige URL (z. |
## KI und Fachdienste
| Task | Was er kann |
|---|---|
| [`ki_auftrag`](tasks/ki_auftrag.md) — KI-Auftrag | Freitext-Auftrag (klassifizieren/zusammenfassen/entwerfen) an die zentrale KI-Kette, optional mit Datei/Variable als Material. |
| [`ki_dok_extrakt`](tasks/ki_dok_extrakt.md) — KI-Feldextraktion | Schema-basierte Feldextraktion aus PDF/Text per KI mit Pflichtfeld-Validierung. |
| [`paperless_ablage`](tasks/paperless_ablage.md) — Archiv-Ablage | Datei mit Metadaten ins Dokumentenarchiv (Paperless-ngx) übergeben. |
| [`rag_recherche`](tasks/rag_recherche.md) — RAG-Recherche | Belegte Antwort mit Quellenangaben aus dem hausinternen Vektor-RAG. |
| [`route_dienstfahrt`](tasks/route_dienstfahrt.md) — Routing/Kilometer | Distanz/Fahrzeit über das hausinterne Routing berechnen und Kilometerangaben plausibilisieren. |
## PDF- und Dokument-Verarbeitung
> **Konvention:** Signatur-Tasks (`pdf_sign`, `pdf_sign_at_text`) gehören ans **Ende** einer PDF-Bearbeitungskette — Text-/Stempel-Tasks verwerfen beim Re-Import vorhandene Signaturen. Details in den jeweiligen Task-Dokus.
+7
View File
@@ -0,0 +1,7 @@
-- Nummernkreise für aktenzeichen_vergabe (atomar via LAST_INSERT_ID-Trick).
CREATE TABLE IF NOT EXISTS `workflow_counter` (
`kreis` VARCHAR(64) NOT NULL,
`jahr` INT NOT NULL DEFAULT 0,
`wert` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`kreis`, `jahr`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+11
View File
@@ -0,0 +1,11 @@
-- Persistente Signale für signal_fire → signal_wait (Workflow-zu-Workflow).
CREATE TABLE IF NOT EXISTS `workflow_signal` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL,
`korrelation` VARCHAR(128) NOT NULL DEFAULT '',
`payload` TEXT NULL,
`verbraucht` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_signal` (`name`, `korrelation`, `verbraucht`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+33
View File
@@ -0,0 +1,33 @@
# `aktenzeichen_vergabe` — Nummer aus Nummernkreis ziehen
**Zweck:** Zieht atomar (kollisionssicher auch bei parallelen Workflows) die nächste laufende Nummer aus einem benannten Nummernkreis und formatiert daraus ein Aktenzeichen/Geschäftszeichen. Nummernkreise laufen standardmäßig pro Jahr (jahresweise Rücksetzung auf 1).
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `nummernkreis` | **ja** | — | Frei wählbarer Name des Nummernkreises |
| `muster` | nein | `%d` | Formatmuster; `%d`/`%04d` = laufende Nummer, `{{JAHR}}` = aktuelles Jahr, weitere Platzhalter erlaubt. Ohne `%` wird die Nummer angehängt |
| `jahresreset` | nein | `true` | Zähler je Jahr zurücksetzen |
| `output_var` | nein | `az` | Prefix der Ausgabevariablen |
**Eingangswerte:** Platzhalter in allen Parametern, z. B. `{{verfahren_kuerzel}}` im `<muster>`.
**Ausgangswerte:**
- `<prefix>` — formatiertes Aktenzeichen
- `<prefix>_nummer` — rohe laufende Nummer
**XML-Beispiel**
```xml
<task type="aktenzeichen_vergabe" id="az_ziehen">
<config>
<nummernkreis>antraege</nummernkreis>
<muster>AZ-{{JAHR}}/%04d</muster>
<jahresreset>true</jahresreset>
<output_var>az</output_var>
</config>
</task>
```
**Hinweis:** Benötigt eine eigene Zähler-Tabelle, die per mitgelieferter `.sql` anzulegen ist. Ist sie nicht vorhanden, endet der Task mit Fehler. Die Nummernvergabe ist atomar (`INSERT … ON DUPLICATE KEY UPDATE` mit `LAST_INSERT_ID`-Trick).
+43
View File
@@ -0,0 +1,43 @@
# `assign_group` — Genehmigung durch eine Gruppe
**Zweck:** Stellt einen Genehmigungsschritt einer GRUPPE (oder Personenliste) zu, statt einer festen Person: Alle Berechtigten werden benachrichtigt, der Erste, der die Aufgabe übernimmt und entscheidet, gewinnt („claim"). So bleibt ein Vorgang nicht liegen, wenn ein einzelner Zeichnungsberechtigter im Urlaub ist. Optional mit eingebetteter PDF-Vorschau.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `gruppe` | *(eine der beiden)* | — | Gruppennamen (CSV); Berechtigte werden aufgelöst |
| `personen` | *(eine der beiden)* | — | Feste Personenliste (E-Mail oder ID, CSV) |
| `pdf` | nein | — | Pfad-Variable zu einer PDF; wird als Vorschau eingebettet |
| `title` | nein | `Freigabe erforderlich` | Überschrift |
| `subtitle` | nein | *(leer)* | Untertitel |
| `approve_label` | nein | `Genehmigen` | Beschriftung Zustimmen-Button |
| `reject_label` | nein | `Ablehnen` | Beschriftung Ablehnen-Button |
| `require_reason_on_reject` | nein | `true` | Begründung bei Ablehnung erzwingen |
| `output_var` | nein | `entscheidung` | Prefix der Ausgabevariablen |
Mindestens eine der Quellen `<gruppe>` oder `<personen>` muss Berechtigte liefern.
**Eingangswerte:** Platzhalter in allen Parametern. Die aufgelösten Berechtigten werden beim ersten Lauf eingefroren; `assign_to` = CSV aller Berechtigten, damit die Engine die Gruppe benachrichtigt.
**Ausgangswerte:**
- `<prefix>``approved` oder `rejected`
- `<prefix>_reason` — Begründung (ggf. leer)
- `<prefix>_by` — Kennung des bearbeitenden Gruppenmitglieds (claim)
- `<prefix>_at` — ISO-8601-Zeitstempel
**XML-Beispiel**
```xml
<task type="assign_group" id="freigabe_bau">
<config>
<gruppe>GRUPPE_BAU,GRUPPE_LEITUNG</gruppe>
<pdf>{{latest_pdf_path}}</pdf>
<title>Rechnung freigeben</title>
<subtitle>Betrag {{betrag}} EUR</subtitle>
<output_var>freigabe</output_var>
</config>
</task>
```
**Hinweis:** Nur ein Mitglied des Berechtigtenkreises darf abschließend entscheiden; Absenden durch Unbefugte wird abgewiesen. Alternativ zu `<gruppe>` kann eine feste `<personen>`-Liste angegeben werden.
+39
View File
@@ -0,0 +1,39 @@
# `beleg_abgleich` — Felder zweier Datensätze vergleichen
**Zweck:** Automatischer Abgleich z. B. Bestellung ↔ Rechnung („Dunkelverarbeitung"): Stimmen alle Vergleichsfelder innerhalb der Toleranz, kann der Workflow direkt weiter (z. B. Zahlungsfreigabe); Abweichungen landen strukturiert im Kontext für eine manuelle Klärung. Typische Quellen sind die `*_json`-Ausgaben von `ki_dok_extrakt`.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `a` | **ja** | — | JSON-Objekt A |
| `b` | **ja** | — | JSON-Objekt B |
| `vergleich` (`feld`) | **ja** | — | Zu vergleichendes Feld (gleicher Name in A und B); wiederholbar |
| `vergleich` (`feld_a`/`feld_b`) | nein | — | Alternativ unterschiedliche Feldnamen in A und B |
| `vergleich` (`toleranz`) | nein | *(exakt)* | Toleranz absolut (`0.5`) oder relativ (`2%`); ohne Angabe exakter String-Vergleich (case-insensitiv) |
| `output_var` | nein | `abgleich` | Prefix der Ausgabevariablen |
**Eingangswerte:** Die JSON-Objekte unter `<a>` und `<b>` (Platzhalter erlaubt; einzelner `{{var}}` wird roh übernommen).
**Ausgangswerte:**
- `<prefix>_ok``"1"` wenn alle Vergleiche bestanden, sonst `"0"`
- `<prefix>_diff_json` — JSON-Liste der Abweichungen `[{feld, wert_a, wert_b, toleranz}]`
- `<prefix>_diff_text` — lesbare Abweichungsliste (für Mails)
**XML-Beispiel**
```xml
<task type="beleg_abgleich" id="dreiwege">
<config>
<a>{{bestellung_json}}</a>
<b>{{rechnung_json}}</b>
<vergleich feld="betrag_brutto" toleranz="2%"/>
<vergleich feld="positionen" toleranz="0"/>
<vergleich feld="lieferant"/>
<vergleich feld_a="re_betrag" feld_b="betrag" toleranz="1%"/>
<output_var>abgleich</output_var>
</config>
</task>
```
**Hinweis:** `<a>` und `<b>` müssen JSON-Objekte enthalten, sonst endet der Task mit Fehler. Die relative Toleranz bezieht sich auf den Wert aus A.
+54
View File
@@ -0,0 +1,54 @@
# `decision_table` — Entscheidungstabelle (DMN-light)
**Zweck:** Ersetzt verschachtelte `if`-Kaskaden durch eine mehrspaltige Entscheidungstabelle. Mehrere Eingabewerte werden Zeile für Zeile gegen Regeln geprüft; die erste (`hit_policy=first`) bzw. alle (`hit_policy=all`) passenden Regeln setzen ihre `<set>`-Variablen. Passt keine Regel, greift optional der `<default>`-Zweig.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `hit_policy` | nein | `first` | `first` = erste passende Regel gewinnt; `all` = alle passenden Regeln, letzte gewinnt bei gleichnamigen Variablen |
| `inputs`/`in` (`var`) | **ja** | — | Eingabespalten in Reihenfolge; `var` ist der Kontext-Variablenname. Reihenfolge = Reihenfolge der `<when>` je Regel |
| `rules`/`rule` | **ja** | — | Regelzeilen; jede muss genau so viele `<when>` haben wie es `<in>`-Spalten gibt |
| `when` (`op`) | — | `op=eq` | Vergleich je Spalte; siehe Operatoren unten |
| `set` (`name`) | — | — | Zu setzende Kontextvariable der Treffer-Regel |
| `default`/`set` | nein | — | Greift, wenn keine Regel passt |
Operatoren (`op`): `eq`, `ne` (Gleichheit, numerisch wenn beide Seiten numerisch, sonst String case-insensitiv), `gt`, `gte`, `lt`, `lte` (numerisch), `contains` (Teilstring, case-insensitiv), `regex` (PCRE ohne Delimiter), `empty`/`notempty` (Wert leer/nicht leer), `any` (passt immer, Platzhalter-Spalte).
**Eingangswerte:** Die unter `<in var="…"/>` genannten Kontextvariablen; Platzhalter in `<when>`- und `<set>`-Werten.
**Ausgangswerte:**
- alle `<set>`-Variablen der Treffer-Regel(n)
- `<task_id>_matched` — Anzahl passender Regeln (`0` = Default griff bzw. nichts)
**XML-Beispiel**
```xml
<task type="decision_table" id="freigabe_regel">
<config>
<hit_policy>first</hit_policy>
<inputs>
<in var="summe"/>
<in var="tg"/>
</inputs>
<rules>
<rule>
<when op="gt">5000</when>
<when op="eq">TG1</when>
<set name="freigeber">geschaeftsfuehrer</set>
<set name="stufe">2</set>
</rule>
<rule>
<when op="lte">5000</when>
<when op="any"/>
<set name="freigeber">sachgebietsleiter</set>
</rule>
</rules>
<default>
<set name="freigeber">registratur</set>
</default>
</config>
</task>
```
**Hinweis:** Jede Regel muss exakt so viele `<when>`-Spalten haben wie `<inputs>` definiert; sonst endet der Task mit Fehler. Numerische Vergleiche akzeptieren Dezimalkomma (`1234,56`).
+34
View File
@@ -0,0 +1,34 @@
# `event_race` — Ereignisbasiertes Gateway (erstes Ereignis gewinnt)
**Zweck:** Mehrere Warte-Optionen laufen gleichzeitig; die Option, die zuerst komplett abgeschlossen ist, gewinnt — alle übrigen werden storniert. Deckt Muster ab wie „Genehmigung ODER 14-Tage-Frist ODER Widerruf-Signal".
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `option` (`id`) | **ja** (min. 2) | *(md5 des Zweigs)* | Ein Warte-Zweig; `id` benennt die Option |
| `option`/`task` | **ja** | — | Tasks der Option (Sequenz je Zweig) |
**Eingangswerte:** Kontext der enthaltenen Tasks; `assign_to` an einem wartenden Kind-Task wird automatisch übernommen.
**Ausgangswerte:**
- `<task_id>_winner``id` der Gewinner-Option (danach per `if` verzweigen)
**XML-Beispiel**
```xml
<task type="event_race" id="antwort_oder_frist">
<option id="genehmigt">
<task type="approve_reject" id="er_approve"></task>
</option>
<option id="frist_um">
<task type="wait_until" id="er_frist"><config><relative>+14 days</relative></config></task>
</option>
<option id="zurueckgezogen">
<task type="signal_wait" id="er_sig"><config><signal>antrag_zurueckgezogen</signal>
<korrelation>{{vkz}}</korrelation></config></task>
</option>
</task>
```
**Hinweis (Engine-Modell):** Es darf höchstens EINE Option interaktive Tasks (Formular/Genehmigung) enthalten — die übrigen Optionen müssen aus passiven Warte-Tasks bestehen (`wait_until`, `signal_wait`, `schedule_resume`, `external_trigger`, `escalate`), weil die Engine pro Aufruf nur eine Formularseite ausliefern kann. Mindestens zwei Optionen sind nötig.
+37
View File
@@ -0,0 +1,37 @@
# `foreach_parallel` — Parallele Multi-Instanz über eine Liste
**Zweck:** Führt die Kind-Tasks für JEDES Listenelement unabhängig aus — im Gegensatz zu `loop_foreach` blockiert ein wartendes Element nicht die übrigen. Optional mit Abschlussbedingung: Sind z. B. 80 % der Elemente fertig, werden die restlichen storniert.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `items` | **ja** | — | Quelle der Elemente: Kontext-Array, JSON-String oder CSV (wie `loop_foreach`) |
| `as` | nein | `item` | Variablenname des aktuellen Elements |
| `completion_condition` | nein | — | Abbruchbedingung `a op b` (`>=`,`<=`,`>`,`<`,`==`,`!=`); Variablen `{{fertig}}`, `{{gesamt}}` |
| `rest` | nein | `cancel` | Bei erfüllter Bedingung: `cancel` (Rest stornieren) oder `weiter` (Rest zu Ende laufen) |
| Kind-Tasks | **ja** | — | `<task>`-Elemente direkt im Element; je Listenelement eigener Zustand |
**Eingangswerte:** Die Liste unter `<items>`; je Element `{{<as>}}`, `{{<as>_index}}` und bei Objekt-Elementen flache Felder `{{<as>_feld}}`.
**Ausgangswerte:**
- `<task_id>_fertig` — Anzahl komplett abgeschlossener Elemente
- `<task_id>_gesamt` — Gesamtanzahl
- `<task_id>_storniert` — Anzahl stornierter Elemente (bei `rest=cancel`)
- pro fertigem Element: `<task_id>_<index>_<var>` mit dessen Ergebnisvariablen
**XML-Beispiel**
```xml
<task type="foreach_parallel" id="stellungnahmen">
<config>
<items>{{empfaenger_json}}</items>
<as>person</as>
<completion_condition>{{fertig}} >= 3</completion_condition>
<rest>cancel</rest>
</config>
<task type="webhook" id="fp_call"></task>
</task>
```
**Hinweis (Engine-Grenze):** Gedacht für automatische Kind-Tasks (`webhook`, `email`, PDF-Verarbeitung, `signal_wait`, `wait_until` …). Interaktive Formulare pro Element funktionieren nur eingeschränkt, weil die Engine pro Aufruf nur eine Formularseite ausliefern kann — für Mehrpersonen-Eingaben stattdessen `quorum` verwenden.
+39
View File
@@ -0,0 +1,39 @@
# `frist_rechner` — Verwaltungsfristen berechnen
**Zweck:** Berechnet ein Fristende ab einem Startdatum inkl. Bekanntgabefiktion, sächsischer Feiertage und Werktagsregel (§ 31 VwVfG / §§ 187 ff. BGB sinngemäß): Fällt das Fristende auf Samstag/Sonntag/Feiertag, verschiebt es sich auf den nächsten Werktag. Optional wird zusätzlich ein Vorwarn-Datum („Frist minus N Werktage") für Wiedervorlagen/Eskalationen geliefert.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `start` | nein | *(heute)* | Startdatum; Formate `d.m.Y`, `Y-m-d` oder `strtotime` |
| `bekanntgabe_tage` | nein | `0` | Bekanntgabefiktion (z. B. `+3` Tage Post) |
| `dauer` | **ja** | — | Dauer: `1M` Monate, `14T` Kalendertage, `10WT` Werktage, `2W` Wochen, `1J` Jahre |
| `ende_auf_werktag` | nein | `true` | Fristende auf nächsten Werktag verschieben |
| `feiertage` | nein | `SN` | Feiertagsregion (Sachsen); `NONE` = nur Wochenenden |
| `vorwarn_werktage` | nein | — | Vorwarn-Datum N Werktage vor Fristende |
| `output_var` | nein | `frist` | Prefix der Ausgabevariablen |
**Eingangswerte:** Alle Parameter unterstützen Platzhalter, z. B. `{{versanddatum}}` in `<start>`.
**Ausgangswerte:**
- `<prefix>_start` — wirksamer Fristbeginn (nach Bekanntgabe), `d.m.Y`
- `<prefix>_ende` — Fristende, `d.m.Y`
- `<prefix>_ende_iso` — Fristende, `Y-m-d` (für `calendar_event`/`wait_until`)
- `<prefix>_vorwarn` / `<prefix>_vorwarn_iso` — Vorwarn-Datum (nur mit `vorwarn_werktage`)
**XML-Beispiel**
```xml
<task type="frist_rechner" id="widerspruchsfrist">
<config>
<start>{{versanddatum}}</start>
<bekanntgabe_tage>3</bekanntgabe_tage>
<dauer>1M</dauer>
<vorwarn_werktage>5</vorwarn_werktage>
<output_var>frist</output_var>
</config>
</task>
```
**Hinweis:** Feiertage werden ohne die PHP-`calendar`-Extension über die Gauß'sche Osterformel bestimmt; `SN` enthält zusätzlich Reformationstag sowie Buß- und Bettag.
+40
View File
@@ -0,0 +1,40 @@
# `json_transform` — JSON-Werte umformen
**Zweck:** Zieht per Pfadausdruck Werte aus einer JSON-Variable (z. B. Webhook-Antwort, `*_json` aus Auswahl-Tasks) und legt sie als Kontextvariablen ab — statt langer `set_var`/`calc`-Ketten. Pro `<ziel>` entsteht eine Variable; mehrere Treffer lassen sich aggregieren.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `quelle` | **ja** | — | JSON-String (Platzhalter erlaubt); ein einzelner `{{var}}` wird roh übernommen |
| `ziel` (`name`) | **ja** | — | Zielvariable; wiederholbar |
| `ziel` (`pfad`) | **ja** | — | Pfadausdruck (siehe unten) |
| `ziel` (`aggregat`) | nein | `first` | `first`, `last`, `sum`, `count`, `min`, `max`, `join`, `all` |
| `ziel` (`join`) | nein | `,` | Trenner bei `aggregat="join"` |
| `ziel` (`default`) | nein | *(leer)* | Wert, wenn der Pfad nichts liefert |
Pfadsyntax: `feld.unterfeld` (Objektzugriff), `liste[0]` (Index), `liste[*]` (alle Elemente → Array), `liste[?key=wert]` bzw. `liste[?key!=wert]` (Filter über String-Vergleich).
**Eingangswerte:** Die JSON-Variable unter `<quelle>`; Platzhalter in `pfad`.
**Ausgangswerte:**
- je `<ziel>` eine Kontextvariable `<name>`
- bei Array-/Objekt-Ergebnis zusätzlich `<name>_json` (JSON-String); `<name>` enthält dann den Roh-Wert (für `loop_foreach` nutzbar)
**XML-Beispiel**
```xml
<task type="json_transform" id="antwort_zerlegen">
<config>
<quelle>{{webhook_antwort}}</quelle>
<ziel name="buchungsnr" pfad="ergebnis.nummer"/>
<ziel name="summe" pfad="posten[*].betrag" aggregat="sum"/>
<ziel name="erste_id" pfad="posten[0].id"/>
<ziel name="offene" pfad="posten[?status=offen].betrag" aggregat="join" join=", "/>
<ziel name="anzahl" pfad="posten[*]" aggregat="count"/>
<ziel name="fallback" pfad="gibts.nicht" default="-"/>
</config>
</task>
```
**Hinweis:** `<quelle>` muss gültiges JSON enthalten, sonst endet der Task mit Fehler. `aggregat` wirkt nur, wenn der Pfad mehrere Werte liefert.
+38
View File
@@ -0,0 +1,38 @@
# `ki_auftrag` — Freitext-Auftrag an die KI
**Zweck:** Macht die zentrale KI-Kette des Hauses als Workflow-Baustein nutzbar: klassifizieren, zusammenfassen, Text entwerfen. Optional wird der Inhalt einer Datei (PDF → Textextraktion) oder einer Kontextvariable als Material an den Prompt angehängt. Alle KI-Aufrufe laufen über die zentrale KI-Kette — keine eigenen LLM-Calls in Tasks.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `prompt` | **ja** | — | Auftrag an die KI |
| `rolle` | nein | *(leer)* | Rollenbeschreibung, wird dem Prompt vorangestellt |
| `eingabe_datei` | nein | — | PDF-/Textdatei als Material (PDF wird per Textextraktion gelesen) |
| `eingabe` | nein | — | Kontextvariable als Material |
| `max_zeichen` | nein | `20000` | Material auf N Zeichen kürzen (Minimum 500) |
| `format` | nein | `text` | `text`, `einwort` (erstes Wort, kleingeschrieben) oder `json` |
| `output_var` | nein | `ki_antwort` | Prefix der Ausgabevariablen |
**Eingangswerte:** Platzhalter in allen Parametern; Material aus `<eingabe_datei>` (Datei) und/oder `<eingabe>` (Variable).
**Ausgangswerte:**
- `<output_var>` — Antworttext (bei `format=einwort` das erste Wort, kleingeschrieben)
- `<output_var>_data` — nur bei `format=json` und parsebarer Antwort: die Struktur
**XML-Beispiel**
```xml
<task type="ki_auftrag" id="klassifizieren">
<config>
<prompt>Klassifiziere das Schreiben. Antworte mit GENAU einem Wort:
antrag, beschwerde, rechnung oder info.</prompt>
<rolle>Du bist ein erfahrener Verwaltungssachbearbeiter.</rolle>
<eingabe_datei>{{posteingang_pdf}}</eingabe_datei>
<format>einwort</format>
<output_var>dok_klasse</output_var>
</config>
</task>
```
**Hinweis:** Scheitern alle KI-Backends der Kette, endet der Task mit Fehler. Bei `format=json` wird versucht, JSON auch aus Markdown-Zäunen zu extrahieren.
+44
View File
@@ -0,0 +1,44 @@
# `ki_dok_extrakt` — Feldextraktion aus Dokumenten per KI
**Zweck:** Zieht definierte Felder (Schema) aus einer hochgeladenen PDF-/Textdatei — z. B. Rechnungsnummer, Betrag, IBAN aus einer Eingangsrechnung — und legt sie als Kontextvariablen ab. Pflichtfelder werden validiert; fehlt eines, endet der Task NICHT mit Fehler, sondern setzt `<prefix>_ok = 0`, damit der Workflow per `if` in eine manuelle Nacherfassung verzweigen kann. Läuft über die zentrale KI-Kette des Hauses.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `quelle` | **ja** | — | PDF- oder Textdatei |
| `schema`/`feld` (`name`) | **ja** | — | Zu extrahierendes Feld; wiederholbar |
| `feld` (`typ`) | nein | `string` | `string`, `betrag`, `iban` oder `datum` (Normalisierung + Plausibilitätsprüfung) |
| `feld` (`pflicht`) | nein | `nein` | `ja`/`true`/`1` = Pflichtfeld |
| `feld` (`hinweis`) | nein | — | Zusatzhinweis für die KI |
| `max_zeichen` | nein | `20000` | Dokumenttext auf N Zeichen kürzen (Minimum 1000) |
| `output_var` | nein | `extrakt` | Prefix der Ausgabevariablen |
Feld-Typen: `string` = Freitext, `betrag` = `"1.234,56"``"1234.56"`, `iban` = ohne Leerzeichen, Großschreibung + Format-Check, `datum` = normalisiert auf `d.m.Y` (zusätzlich `<feld>_iso` als `Y-m-d`).
**Eingangswerte:** Die Datei unter `<quelle>` (Platzhalter erlaubt).
**Ausgangswerte:**
- `<prefix>_<feldname>` — je Schema-Feld (leer wenn nicht gefunden)
- `<prefix>_json` — alle Felder als JSON
- `<prefix>_ok``"1"` wenn alle Pflichtfelder plausibel gefüllt, sonst `"0"`
- `<prefix>_fehlend` — CSV der fehlenden/unplausiblen Pflichtfelder
**XML-Beispiel**
```xml
<task type="ki_dok_extrakt" id="rechnung_lesen">
<config>
<quelle>{{rechnung_pdf}}</quelle>
<schema>
<feld name="re_nr" typ="string" pflicht="ja" hinweis="Rechnungsnummer"/>
<feld name="betrag_brutto" typ="betrag" pflicht="ja"/>
<feld name="iban" typ="iban"/>
<feld name="datum" typ="datum"/>
</schema>
<output_var>extrakt</output_var>
</config>
</task>
```
**Hinweis:** Liefert die Datei keinen extrahierbaren Text (z. B. Scan ohne Textebene) oder ist die KI-Antwort kein parsebares JSON, endet der Task mit Fehler. Fehlende Pflichtfelder dagegen sind kein Fehler, sondern werden über `<prefix>_ok` signalisiert.
+40
View File
@@ -0,0 +1,40 @@
# `paperless_ablage` — Dokument im Archiv ablegen
**Zweck:** Übergibt eine Datei aus dem Workflow an das Dokumentenarchiv (Paperless-ngx REST-API) inkl. Metadaten (Titel, Korrespondent, Dokumenttyp, Tags). Das Archiv verarbeitet den Upload asynchron; der Task liefert die Task-UUID zurück.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `datei` | **ja** | — | Abzulegende Datei |
| `titel` | nein | — | Dokumenttitel |
| `korrespondent` | nein | — | Korrespondent (Name/ID) |
| `dokumenttyp` | nein | — | Dokumenttyp |
| `tags` | nein | — | Tags (CSV) |
| `base_url` | nein | *(ENV `PAPERLESS_URL`)* | Basis-URL des Archivs |
| `token` | nein | *(ENV `PAPERLESS_TOKEN`)* | API-Token |
| `output_var` | nein | `paperless` | Prefix der Ausgabevariablen |
**Eingangswerte:** Platzhalter in allen Parametern, z. B. `{{latest_pdf_path}}` in `<datei>`.
**Ausgangswerte:**
- `<prefix>_task_uuid` — Task-UUID des Archivs (Verarbeitung läuft asynchron)
- `<prefix>_ok``"1"` bei erfolgreicher Übergabe
**XML-Beispiel**
```xml
<task type="paperless_ablage" id="archivieren">
<config>
<datei>{{latest_pdf_path}}</datei>
<titel>Rechnung {{re_nr}}</titel>
<korrespondent>{{lieferant}}</korrespondent>
<dokumenttyp>Eingangsrechnung</dokumenttyp>
<tags>rechnung,{{verfahren}},freigegeben</tags>
<base_url>{{paperless_url}}</base_url>
<output_var>paperless</output_var>
</config>
</task>
```
**Hinweis:** Zugang bevorzugt über die Umgebungsvariablen `PAPERLESS_URL`/`PAPERLESS_TOKEN` setzen, damit das Token nicht in der Workflow-XML steht. Fehlt der Zugang oder scheitert der Upload, endet der Task mit Fehler — bei nicht-blockierender Archivierung in einen `try_catch` legen.
+34
View File
@@ -0,0 +1,34 @@
# `rag_recherche` — Belegte Antwort aus dem RAG
**Zweck:** Stellt eine Frage an das hausinterne Vektor-RAG: semantische Suche über den eingepflegten Korpus (z. B. Gesetzestexte, Regelwerke, Akten), Reranking und eine Antwort mit Quellenangaben über die zentrale KI-Kette des Hauses. Typischer Einsatz: vor einer Genehmigungsentscheidung die einschlägigen Vorgaben ermitteln und dem Genehmiger als Hinweis mitgeben.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `frage` | **ja** | — | Frage an das RAG |
| `filter` (`key`) | nein | — | Korpus-Filter, wiederholbar; Attribut `key`, Wert im Element |
| `top` | nein | `8` | Anzahl der Quellen-Auszüge (Minimum 1) |
| `rerank` | nein | `true` | Reranking der Treffer aktivieren |
| `output_var` | nein | `rag` | Prefix der Ausgabevariablen |
**Eingangswerte:** Platzhalter in `<frage>` und den `<filter>`-Werten, z. B. `{{massnahme_typ}}`.
**Ausgangswerte:**
- `<prefix>_antwort` — belegte Antwort (zitiert Auszüge mit `[Nr]`)
**XML-Beispiel**
```xml
<task type="rag_recherche" id="rechtscheck">
<config>
<frage>Welche Vorgaben gelten für {{massnahme_typ}}?</frage>
<filter key="typ">gesetz</filter>
<top>5</top>
<rerank>true</rerank>
<output_var>rag</output_var>
</config>
</task>
```
**Hinweis:** Ist das RAG oder die KI-Kette nicht erreichbar, endet der Task mit Fehler.
+39
View File
@@ -0,0 +1,39 @@
# `route_dienstfahrt` — Distanz/Fahrzeit über Routing
**Zweck:** Berechnet die schnellste Kfz-Route zwischen zwei Punkten über das hausinterne Routing und liefert Distanz und Fahrzeit als Kontextvariablen. Optional wird ein angegebener Kilometerwert gegen die berechnete Distanz plausibilisiert (z. B. Reisekostenabrechnung).
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `von` | **ja** | — | Startpunkt als Koordinatenpaar `"x,y"` |
| `nach` | **ja** | — | Zielpunkt als Koordinatenpaar `"x,y"` |
| `epsg` | nein | `25833` | Koordinatensystem; `25833` = „Ost,Nord", `4326` = „lon,lat" (WGS84) |
| `vergleich_km` | nein | — | Angegebener Kilometerwert für die Plausibilisierung |
| `toleranz_prozent` | nein | `15` | Toleranz für die Plausibilisierung (nur mit `vergleich_km`) |
| `output_var` | nein | `route` | Prefix der Ausgabevariablen |
**Eingangswerte:** Platzhalter in allen Parametern, z. B. `{{start_koord}}` in `<von>`.
**Ausgangswerte:**
- `<prefix>_km` — Distanz in km (2 Nachkommastellen)
- `<prefix>_min` — Fahrzeit in Minuten (1 Nachkommastelle)
- `<prefix>_plausibel` — nur mit `vergleich_km`: `"1"` wenn innerhalb der Toleranz, sonst `"0"`
- `<prefix>_abweichung_km` — nur mit `vergleich_km`: Differenz (vergleich berechnete km)
**XML-Beispiel**
```xml
<task type="route_dienstfahrt" id="km_check">
<config>
<von>{{start_koord}}</von>
<nach>{{ziel_koord}}</nach>
<epsg>25833</epsg>
<vergleich_km>{{abgerechnete_km}}</vergleich_km>
<toleranz_prozent>15</toleranz_prozent>
<output_var>route</output_var>
</config>
</task>
```
**Hinweis:** `<von>`/`<nach>` müssen ein Koordinatenpaar `"x,y"` sein; andernfalls oder bei nicht erreichbarem Routing endet der Task mit Fehler.
+6
View File
@@ -7,6 +7,8 @@
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `event` | ja | — | Ereignisname. |
| `korrelation` | nein | *(leer)* | Korrelationsschlüssel (z. B. `{{vkz}}`); persistiert das Signal für `signal_wait`-Empfänger. |
| `payload` | nein | *(leer)* | String/JSON; landet beim Empfänger in dessen `payload_var`. |
| `<arg>` | nein | — | Wiederholbar; skalares Argument (Platzhalter erlaubt). |
| `<load type="massnahme" md5="…"/>` | nein | — | Lädt eine Maßnahme als Argument. |
| `<load type="person" id="…"/>` | nein | — | Lädt eine Person als Argument. |
@@ -22,8 +24,12 @@ Die Argumente werden in XML-Reihenfolge übergeben; nicht ladbare `<load>`-Objek
<task type="signal_fire" id="emit_done">
<config>
<event>massnahme.saved</event>
<korrelation>{{vkz}}</korrelation>
<payload>{{ergebnis_json}}</payload>
<load type="massnahme" md5="{{massnahme_md5}}" />
<arg>{{ICH.id}}</arg>
</config>
</task>
```
**Hinweis:** Sind `<korrelation>` und/oder `<payload>` gesetzt, wird das Signal zusätzlich zum Event-Bus-Feuer persistiert und weckt Workflows, die per `signal_wait` darauf lauschen (über `resume_at` + `cron-resume`). Die Persistenz nutzt eine eigene Signal-Tabelle, die per mitgelieferter `.sql` anzulegen ist; fehlt sie, wird nur gewarnt und das Event-Bus-Feuern läuft normal weiter.
+36
View File
@@ -0,0 +1,36 @@
# `signal_wait` — Auf ein internes Signal warten
**Zweck:** Catch-Gegenstück zu `signal_fire`: Der Workflow pausiert, bis ein anderes Workflow-/Systemereignis ein passendes Signal (Name + Korrelationsschlüssel) hinterlegt hat. Damit sind Workflow-zu-Workflow-Abhängigkeiten möglich („warte, bis der Freigabe-Workflow desselben Verfahrens durch ist"). Optional beendet ein Timeout das Warten mit einem Flag statt ewig zu blockieren.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `signal` | **ja** | — | Signalname, auf den gewartet wird |
| `korrelation` | nein | *(leer)* | Korrelationsschlüssel; leer = jedes Signal dieses Namens |
| `ab_start` | nein | `true` | Nur Signale nach Task-Start zählen; `false` = auch bereits vorhandene |
| `consume` | nein | `true` | Signal verbrauchen (einmalige Zustellung) |
| `timeout_tage` | nein | — | Nach N Tagen ohne Signal mit Flag weiter |
| `timeout_flag_var` | nein | `<task_id>_timeout` | Variablenname des Timeout-Flags |
| `payload_var` | nein | — | Variablenname für die Payload des Signals |
**Eingangswerte:** Platzhalter in allen Parametern, z. B. `{{vkz}}` in `<korrelation>`.
**Ausgangswerte:**
- `<payload_var>` — Payload des empfangenen Signals (String); bei JSON zusätzlich `<payload_var>_data`
- `<timeout_flag_var>``"1"` bei Timeout, sonst `"0"`
**XML-Beispiel**
```xml
<task type="signal_wait" id="warte_auf_freigabe">
<config>
<signal>verfahren_freigegeben</signal>
<korrelation>{{vkz}}</korrelation>
<timeout_tage>14</timeout_tage>
<payload_var>freigabe_daten</payload_var>
</config>
</task>
```
**Hinweis:** Persistenz über eine eigene Signal-Tabelle, die per mitgelieferter `.sql` anzulegen ist. Der Task hinterlegt beim ersten Lauf einen Listener-Marker im Ausführungszustand; `signal_fire` findet ihn und weckt den Workflow über `resume_at` + `cron-resume`. Ist ein Timeout gesetzt, wird zusätzlich ein Weckruf zum Timeout-Zeitpunkt geplant.
+35
View File
@@ -0,0 +1,35 @@
# `try_catch` — Fehlergrenze mit Retry-Policy
**Zweck:** Umschließt beliebige Tasks: Wirft einer davon einen Fehler (`TaskResult error` oder PHP-Exception), bricht nicht mehr der ganze Workflow ab. Stattdessen wird — nach optionalen automatischen Wiederholungen mit Backoff — der `<catch>`-Zweig ausgeführt (z. B. Admin-Mail, Ersatzwert setzen) und der Workflow läuft weiter. Retries nutzen `resume_at` + `cron-resume`.
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `try`/`task` | **ja** | — | Zu überwachende Tasks (wie eine Sequenz) |
| `retry` (`max`) | nein | `0` | Anzahl automatischer Wiederholungen nach einem Fehler |
| `retry` (`backoff`) | nein | `60` | Wartezeiten in Sekunden je Versuch (CSV); letzter Wert gilt für weitere |
| `catch`/`task` | nein | — | Tasks nach erschöpften Retries; ohne `<catch>` wird der Fehler durchgereicht |
**Eingangswerte:** Kontext der umschlossenen Tasks. Im `<catch>`-Zweig zusätzlich verfügbar:
- `<task_id>_fehler` — Fehlermeldung des letzten Versuchs
- `<task_id>_versuche` — Anzahl der Versuche
**Ausgangswerte:** Die Ausgaben der ausgeführten `<try>`- bzw. `<catch>`-Tasks landen im Kontext. Ohne `<catch>` endet der Task nach erschöpften Retries mit Fehler.
**XML-Beispiel**
```xml
<task type="try_catch" id="hkr_sicher">
<try>
<task type="webhook" id="an_hkr"></task>
</try>
<retry max="3" backoff="60,300,3600"/>
<catch>
<task type="email" id="admin_info"></task>
<task type="set_var" id="fallback"><var name="hkr_status">manuell</var></task>
</catch>
</task>
```
**Hinweis:** Bei einem Retry wird nur der fehlgeschlagene Kind-Task zurückgesetzt; der Task geht in `waiting` und plant den Neuversuch über `resume_at`. `waiting`/`stop`/`return` der Kind-Tasks werden unverändert durchgereicht.
+35
View File
@@ -0,0 +1,35 @@
# `wiedervorlage` — Vorgang parken und wieder vorlegen
**Zweck:** Der Klassiker der Registratur: Der Workflow wird bis zu einem Termin schlafen gelegt und dann dem Bearbeiter mit Notiz wieder vorgelegt. Optional geht N Tage vorher eine Erinnerungs-Mail raus. Der Bearbeiter schließt die Wiedervorlage aktiv über einen „Erledigt"-Button ab (mit optionalem Vermerk). Das automatische Wecken nutzt `resume_at` + `cron-resume` (wie `schedule_resume`).
**Parameter**
| Name | Pflicht? | Default | Beschreibung |
|---|---|---|---|
| `termin` | **ja** | — | Termin: `strtotime`-Ausdruck (`+4 weeks`), `d.m.Y` oder `Y-m-d` |
| `an` | **ja** | — | Bearbeiter (E-Mail oder Personen-ID) |
| `notiz` | nein | `Wiedervorlage fällig.` | Freitext-Notiz auf der Vorlageseite |
| `erinnerung_tage_vorher` | nein | — | Info-Mail N Tage vor dem Termin |
| `output_var` | nein | `wv` | Prefix der Ausgabevariablen |
**Eingangswerte:** Platzhalter in allen Parametern, z. B. `{{ICH.mail}}` in `<an>`, `{{az}}` in `<notiz>`. Der Termin wird beim ersten Lauf eingefroren.
**Ausgangswerte** (nach Erledigung durch den Bearbeiter):
- `<prefix>_erledigt_am` — Zeitstempel `d.m.Y H:i`
- `<prefix>_vermerk` — Freitext-Vermerk (ggf. leer)
**XML-Beispiel**
```xml
<task type="wiedervorlage" id="wv_lra">
<config>
<termin>+4 weeks</termin>
<an>{{ICH.mail}}</an>
<notiz>Antwort zu {{az}} prüfen</notiz>
<erinnerung_tage_vorher>2</erinnerung_tage_vorher>
<output_var>wv</output_var>
</config>
</task>
```
**Hinweis:** Der Task pausiert bis zum Termin (`waiting`) und legt den Weckzeitpunkt in `resume_at` ab; bei gesetzter Erinnerung wird zuerst zur Erinnerungszeit, dann zum Termin geweckt. Ist die Testmodus-Mail gesetzt, wird die Erinnerung dorthin umgeleitet.