Zeitplaner (Scheduler)¶
Der Zeitplaner führt KCForms-JavaScript-Jobs zeitgesteuert aus — ohne
dass jemand ein Formular öffnet. Typische Aufgaben: nächtliches Aufräumen
von Tabellen, zeitgesteuerter Mailversand (Wochenreport, Erinnerungen), oder
ein Verzeichnis auslesen und Datensätze importieren. Ein Job ist reines
JavaScript mit denselben Werkzeugen wie ein Formular-Skript (db, forms,
mail, fs, http, ws, ldap …) — nur ohne Formular. Wie ein Job-Skript
aussieht, beschreibt Zeitgesteuerte Skripte.
Alles wird in der System-Administration eingerichtet — keine Konfigurationsdateien, kein Blick ins Server-Log: Auslöser, Skript und die Lauf-Historie mit Ausgabe liegen in der Oberfläche.
Kurzreferenz¶
Auslöser-Typen
| Typ | Bedeutung | Feld |
|---|---|---|
cron |
6-Feld-Spring-Cron (Sek Min Std Tag Monat Wochentag) |
Cron-Ausdruck (+ optional Zeitzone) |
interval |
fester Abstand nach Abschluss des letzten Laufs | Sekunden |
once |
genau einmal zu einem Zeitpunkt | YYYY-MM-DDTHH:mm:ss |
Das job-Objekt im Skript
| Aufruf | Zweck |
|---|---|
job.name |
Name des Jobs (String) |
job.description |
Beschreibung (String) |
job.trigger |
"cron" | "interval" | "once" | "manual" |
job.log(msg) |
eine Zeile in die Lauf-Historie (+ Server-Log) schreiben |
job.fail(msg) |
den Lauf als Fehler markieren und abbrechen |
Weitere Werkzeuge im Job: db (inkl. db.on("quelle")), forms, mail,
fs, csv, http, ws, ldap, log, ctx.system. Kein
Formular-Kontext — ctx.form ist leer.
Vorzeitig beenden
return; beendet ein Job-Skript an beliebiger Stelle (der Lauf gilt dann als
erfolgreich). Zum Beenden mit Fehler job.fail(...) verwenden.
Wo konfigurieren¶
System-Administration → Betrieb & Überwachung → Zeitplaner. Links die Liste Jobs (mit Auslöser, Zustand und Ergebnis des letzten Laufs), rechts der Editor. + Neuer Job legt einen Job an, Speichern legt ihn ab, Jetzt ausführen testet einen gespeicherten Job sofort (auch deaktiviert). Unter dem Editor liegt die Lauf-Historie des ausgewählten Jobs.
Neben eigenen Jobs stehen dort:
- Hallo Welt — ein vorbereiteter, deaktivierter Beispiel-Job.
- LDAP-Export: <Konnektor> — legt die LDAP-Konnektor-Verwaltung an, wenn dort ein regelmäßiger Export eingerichtet wird.
Felder eines Jobs¶
| Feld | Bedeutung |
|---|---|
| Name | Anzeige- und Protokollname. Erscheint in der Liste und in der Lauf-Historie. |
| Beschreibung | Freitext, was der Job tut. Im Skript als job.description lesbar. |
| Trigger | Cron (wiederkehrend), Intervall (alle N Sekunden) oder Einmalig (Zeitpunkt) (siehe unten). |
| Zeitzone | Optional (z. B. Europe/Berlin), gilt für Cron und Einmalig. Leer = Zeitzone des Servers. |
| Timeout (ms) | Optional. Bricht einen zu langen Lauf ab. Leer = Standard (5 Minuten, siehe Timeout). |
| aktiviert | Nur aktivierte Jobs werden eingeplant. Ein deaktivierter Job lässt sich weiterhin mit Jetzt ausführen testen. |
| verpasste Läufe nachholen | Nur für Intervall und Einmalig: Läufe, die während eines Ausfalls fällig gewesen wären, werden nach dem Neustart nachgeholt, statt sie zu überspringen. |
| Skript | Der auszuführende JavaScript-Code. |
Auslöser-Typen¶
cron — Spring-Cron (6 Felder)¶
Ein Cron-Ausdruck mit sechs Feldern, durch Leerzeichen getrennt:
| Ausdruck | Bedeutung |
|---|---|
0 */5 * * * * |
alle 5 Minuten |
0 0 2 * * * |
täglich um 02:00 Uhr |
0 0 8 * * MON-FRI |
werktags (Mo–Fr) um 08:00 Uhr |
0 0 * * * * |
stündlich (zur vollen Stunde) |
*/30 * * * * * |
alle 30 Sekunden |
0 0 0 1 * * |
monatlich am 1. um 00:00 Uhr |
Syntax der Felder:
*— jeder Wert (jede Sekunde, jede Minute …).*/n— alle n (Schrittweite), z. B.*/5in der Minuten-Spalte = alle 5 Min.?— „egal" für Tag oder Wochentag (man legt nur eines der beiden fest).- Bereiche —
MON-FRI,9-17. - Listen —
1,15(am 1. und 15.),MON,WED,FRI.
Die Zeitzone steuerst du über das Zeitzonen-Feld (z. B. Europe/Berlin).
Bleibt es leer, gilt die Zeitzone des Servers.
Cron-Ausdruck prüfen
Cron prüfen neben dem Eingabefeld prüft den Ausdruck und zeigt die nächsten fünf Ausführungszeiten (in der eingestellten Zeitzone). Das Skript testest du mit Jetzt ausführen, am besten solange der Job noch deaktiviert ist.
interval — alle N Sekunden¶
Der Job läuft in einem festen Abstand, gemessen nach dem Abschluss des
vorigen Laufs (nicht ab dessen Start). Ein Job mit Intervall 300 und einer
Laufzeit von 20 s startet also alle ~320 s — so kann er sich nie selbst
überholen. Ideal für „regelmäßig, aber nicht uhrzeitgebunden" (Polling eines
Eingangsverzeichnisses, periodischer Abgleich).
once — einmalig zu einem Zeitpunkt¶
Genau ein Lauf zu einem festen Zeitpunkt im Format
YYYY-MM-DDTHH:mm:ss, z. B. 2026-07-10T22:00:00. Praktisch für eine geplante
Einmal-Migration oder einen terminierten Versand. Nach dem Lauf bleibt der Job
mit seiner Historie stehen, feuert aber nicht erneut.
Betrieb & Ausführung¶
Lauf-Historie¶
Jeder Lauf erscheint in der Lauf-Historie des Jobs mit:
| Spalte | Bedeutung |
|---|---|
| Start | Startzeitpunkt des Laufs. |
| Dauer | Laufzeit. |
| Status | ok, error oder skipped (übersprungen, weil der vorige Lauf noch lief). |
| Message | Kurztext — bei Fehler die Fehlermeldung, sonst z. B. der job.fail-Grund. |
| Ausgabe | alle job.log(...)-Zeilen des Laufs. |
Damit ersetzt die Historie das früher übliche Server-Log-Grepping: was ein Job getan (oder warum er gescheitert ist) steht direkt am Job.
Jetzt ausführen¶
Der Jetzt ausführen-Knopf startet den Job sofort — unabhängig vom
Auslöser und auch bei deaktivierten Jobs. Der manuelle Lauf trägt
job.trigger === "manual" und landet wie jeder andere in der Historie. So
testest du ein Skript, ohne auf die nächste Cron-Zeit zu warten.
Überlappungsschutz¶
Ein Job läuft nie doppelt gleichzeitig. Feuert ein Auslöser, während
derselbe Job noch läuft, wird der neue Lauf mit Status skipped vermerkt und
verworfen. Das gilt auch fürs manuelle „Jetzt ausführen" gegen einen bereits
laufenden Job.
Timeout¶
Jeder Job hat ein Timeout. Läuft er länger, wird er abgebrochen und der Lauf
als error markiert. Der Default sind 5 Minuten
(kcforms.scheduler.default-timeout-ms); pro Job überschreibbar über das
Timeout (ms)-Feld. Lange Läufe (großer Import) also mit höherem Timeout
konfigurieren.
Einplanung beim Serverstart¶
Beim Hochfahren des Backends werden alle aktiven Jobs automatisch eingeplant. Deaktivierte Jobs werden nicht eingeplant (bleiben aber per „Jetzt ausführen" testbar). Änderungen an einem Job (Auslöser, Aktiv-Status) werden nach dem Speichern übernommen.
Beispiele¶
Der Unterschied zu Formular-Skripten: Es gibt kein ctx.form, dafür das
job-Objekt. Tabellen- und Spaltennamen der Beispiele (antrag, kunde,
lieferant …) sind Platzhalter für deine eigenen.
1 — Hallo Welt (der vorbereitete Job)¶
Auf einer frischen Installation ist ein deaktivierter Beispiel-Job „Hallo Welt" angelegt. Er schreibt eine Zeile in die Historie und zählt die Formulardefinitionen — ideal, um den Zeitplaner einmal per Jetzt ausführen zu testen, ohne etwas zu verändern:
job.log("Hallo Welt vom Zeitplaner! Ausgelöst durch: " + job.trigger);
job.log("Zeit: " + (new Date()).format("yyyy-MM-dd HH:mm:ss"));
var anzahl = db.scalar("SELECT COUNT(*) FROM form_definition");
job.log("Formulare in der Datenbank: " + anzahl);
job.log("Fertig.");
Klicke im Editor auf Jetzt ausführen und sieh dir die Zeilen unter Ausgabe in der Lauf-Historie an.
2 — Abgelaufene Web-Links aufräumen (täglich)¶
Abgelaufene Web-Links und Ad-hoc-Links aus Mails
funktionieren nicht mehr, bleiben aber in der Tabelle id_encoder. Auslöser
cron, Ausdruck 0 0 3 * * * (täglich 03:00):
var n = db.update(
"DELETE FROM id_encoder WHERE expires_at IS NOT NULL AND expires_at < CURRENT_TIMESTAMP"
);
job.log("Gelöschte abgelaufene Links: " + n);
Nicht nach Alter löschen
Einträge ohne Ablaufdatum sind dauerhafte Web-Links (etwa auf einer
Webseite). Wer stattdessen „alles älter als 90 Tage" löscht, macht diese
Links unbrauchbar. Deshalb nur nach expires_at löschen.
db.update(...) gibt die Anzahl betroffener Zeilen zurück — mit job.log
steht sie am nächsten Morgen in der Historie.
3 — Zeitgesteuerter Mailversand (Wochenreport)¶
Ein wöchentlicher Report: offene Vorgänge zählen und per Mail verschicken.
Auslöser cron, Ausdruck 0 0 7 * * MON (montags 07:00):
var offen = db.scalar(
"SELECT COUNT(*) FROM form_urlaubsantrag"
);
mail.send({
to: "team@example.de",
subject: "Wochenreport — offene Vorgänge",
body: "Guten Morgen,\n\naktuell sind " + offen +
" Vorgänge offen.\n\nAutomatischer Report des KCForms-Zeitplaners."
});
job.log("Wochenreport eingereiht — offene Vorgänge: " + offen);
Mail-Domain-Whitelist beachten
Der Versand läuft durch dieselbe Empfänger-Whitelist wie
mail aus Formular-Skripten. Steht
die Adresse nicht darauf, schlägt mail.send fehl und der Lauf landet als
error in der Historie. Zugestellt wird über den
E-Mail-Ausgang — dort siehst du, ob die Mail angekommen
ist.
4 — Verzeichnis auslesen → Datensätze importieren¶
Ein Eingangsverzeichnis der fs-Sandbox
durchgehen, jede CSV-Datei einlesen, die Zeilen importieren und die Datei
anschließend ins Archiv verschieben. Je Datei eine Transaktion — entweder
alle Zeilen oder keine. Als Auslöser bietet sich interval (z. B. alle 300 s)
an:
var dateien = fs["/inbox"].list(function (f) { // nur CSV-Dateien
return /\.csv$/i.test(f.name);
});
job.log("Gefundene Dateien: " + dateien.length);
var importiert = 0;
dateien.forEach(function (f) {
var zeilen = csv.parse(f, { header: true, separator: ";" });
db.tx(function (t) { // t = Verbindung dieser Transaktion
t.batch("INSERT INTO kunde (nr, name, ort) VALUES (${nr}, ${name}, ${ort})",
zeilen.map(function (r) { return { nr: r.nr, name: r.name, ort: r.ort }; }));
});
f.rename("/archiv/" + f.name); // verschieben statt löschen
importiert += zeilen.length;
job.log("Importiert aus " + f.name + ": " + zeilen.length + " Zeilen");
});
job.log("Import fertig — Datensätze gesamt: " + importiert);
In der Transaktion t verwenden, nicht db
Innerhalb von db.tx(function (t) { … }) gehören alle Anweisungen an t.
Ein db.update(...) darin liefe auf einer eigenen Verbindung und würde
beim Rollback nicht zurückgenommen.
Weiterführend
- CSV parsen/schreiben:
csv-Service. - Verzeichnisse, Lesen/Schreiben, Verschieben:
fs-Sandbox. - Für JSON-Dateien statt CSV:
JSON.parse(fs["/inbox/" + name].readText()).
5 — Externe Datenquelle (nächtlicher Sync)¶
Ein Job kann auch gegen eine externe Datenquelle laufen — db.on("name")
öffnet die Verbindung genau wie in einem Formular-Skript. Ein nächtlicher
Abgleich einer ERP-Tabelle, Auslöser cron 0 0 1 * * *:
// Lesen aus der externen Quelle …
var zeilen = db.on("sap-erp").query(
"SELECT nr, name, ort FROM lieferanten WHERE geaendert_am > ${seit}",
{ seit: "2000-01-01" }
);
// … und in die KCForms-Datenbank spiegeln (erst leeren, dann neu füllen)
db.tx(function (t) {
t.update("DELETE FROM lieferant");
t.batch("INSERT INTO lieferant (nr, name, ort) VALUES (${nr}, ${name}, ${ort})",
zeilen.map(function (l) { return { nr: l.nr, name: l.name, ort: l.ort }; }));
});
job.log("Lieferanten synchronisiert: " + zeilen.length);
Spaltennamen kommen im Ergebnis klein geschrieben an (l.nr), auch wenn
die Datenbank sie groß schreibt. DELETE + INSERT in einer Transaktion
funktioniert auf allen Datenbanken; herstellerspezifische „Upserts" (etwa
ON DUPLICATE KEY UPDATE bei MySQL) gehen natürlich auch.
Details zu db.on(...) und Dialekt-Handling: db-Binding
und Externe Datenquellen.
6 — Fehlerbehandlung¶
Zwei Wege, einen Lauf als fehlgeschlagen zu markieren:
job.fail("Grund")— bewusster, kontrollierter Abbruch. Der Lauf steht auferror, die Message enthält deinen Grund, das Skript endet.- Ein unbehandelter Fehler (geworfene Exception) markiert den Lauf
automatisch als
error; die Fehlermeldung landet in der Historie.
try {
var d = fs["/inbox"];
if (d.list().length === 0) {
job.log("Nichts zu tun — /inbox ist leer."); // Lauf endet erfolgreich
} else {
// … Verarbeitung …
}
} catch (e) {
job.log("Verarbeitung abgebrochen: " + e.message);
job.fail("Import fehlgeschlagen: " + e.message);
}
Fehler sind sichtbar — kein Log-Grepping nötig
Egal ob job.fail oder eine durchgereichte Exception: der Fehlschlag steht
am Job in der Lauf-Historie (Status error + Message). Du musst nicht
ins Server-Log schauen, um zu sehen, dass und warum ein Lauf gescheitert ist.
Wichtige Hinweise¶
Jobs laufen mit voller Serverberechtigung
Ein Job hat DB-Schreibzugriff, kann Mails versenden, auf die Dateisandbox zugreifen und ausgehende HTTP-Aufrufe machen — ohne Formular- oder Benutzer-Kontext, der ihn eingrenzt. Lege Jobs daher nur als vertrauenswürdiger Admin an und prüfe jedes Skript wie produktiven Backend-Code.
Jobs idempotent schreiben
Ein Lauf kann verpasst und nachgeholt (catchUp), manuell angestoßen oder
(bei Fehlkonfiguration) doppelt eingeplant werden. Schreibe Jobs so, dass ein
erneuter Lauf keinen Schaden anrichtet — z. B. vor dem Anlegen prüfen,
ob es den Satz schon gibt, oder Dateien nach dem Import verschieben.
Zeitzone bei Cron
Ein Job ohne Zeitzone nutzt die Zeitzone des Servers. Steht der Server auf UTC, feuert 0 0 8 * * * um 08:00 UTC —
nicht 08:00 Ortszeit. Für lokale Uhrzeiten immer die Zeitzone explizit setzen
(z. B. Europe/Berlin).
Lange Läufe: Timeout hochsetzen
Der Default-Timeout von 5 Minuten passt für Aufräum- und Report-Jobs. Ein großer Import kann länger dauern — dann das Timeout (ms)-Feld erhöhen. Der Überlappungsschutz sorgt derweil dafür, dass der nächste geplante Lauf nicht in den noch laufenden hineinstartet.
Cluster-Betrieb: für Single-Instance ausgelegt
Der Scheduler läuft pro Backend-Instanz im Speicher. Bei mehreren parallelen Backend-Instanzen würde jeder Job auf jeder Instanz feuern (Mehrfachausführung). Der Zeitplaner ist für Single-Instance-Betrieb ausgelegt; für einen echten Cluster bräuchte es eine externe Koordination.
Verwandte Themen¶
- Zeitgesteuerte Skripte — das
job-Objekt und weitere Beispiele. - Lifecycle-Hooks — dieselben Werkzeuge im Formular-Kontext.
db-Binding — Abfragen, Transaktionen (db.tx) und externe Quellen (db.on(...)).mail— Mailversand inkl. Empfänger-Whitelist.fs-Sandbox — Verzeichnisse lesen, Dateien verschieben/löschen.csv-Service — CSV parsen und schreiben.http-Client — ausgehende HTTP-Aufrufe aus einem Job.- Externe Datenquellen — die Quellen hinter
db.on(...).