Zum Inhalt

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:

Sekunde Minute Stunde Tag(des Monats) Monat Wochentag
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. */5 in 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.

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 auf error, 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