Zeitgesteuerte Skripte (job)¶
Ein Scheduler-Job führt ein KCForms-JavaScript nach einem Zeitplan aus — zum Aufräumen, für Reports, zum Einlesen von Verzeichnissen oder für ausgehende Aufrufe. Eingerichtet werden Jobs unter System-Administration → Betrieb & Überwachung → Zeitplaner (siehe Zeitplaner); diese Seite beschreibt, wie das Skript eines Jobs aussieht.
Kein Formular-Kontext
Ein Job läuft ohne Formular und ohne angemeldeten Benutzer
(ctx.user.name ist "scheduler"). ctx.form ist leer, Dialoge (view)
erreichen niemanden, und alles, was ein Formular braucht (pdf.render(),
ctx.attachments, mail-Anhänge per addOutput), steht nicht zur
Verfügung. Verfügbar sind die serverseitigen Werkzeuge —
db, forms,
mail, fs,
csv, http,
ws, ldap
(inkl. ldap.sync), log, kc, ctx.system — und das job-Objekt.
Vorzeitig beenden
Wie in Formular-Hooks beendet return; das Job-Skript an beliebiger Stelle
(der Lauf gilt als erfolgreich). Zum Beenden mit Fehler job.fail(...).
Das job-Objekt¶
| Ausdruck | Typ | Bedeutung |
|---|---|---|
job.name |
String | Name des Jobs (wie im Admin gepflegt). |
job.description |
String | Beschreibung des Jobs ("", wenn leer). |
job.trigger |
String | Auslöser dieses Laufs: cron, interval, once oder manual. |
job.log(msg) |
Funktion | Schreibt eine Zeile in die Lauf-Historie (und ins Server-Log). |
job.fail(msg) |
Funktion | Bricht das Skript sofort ab und markiert den Lauf als Fehler. |
job.log(...) ist der bevorzugte Weg, Ergebnisse festzuhalten: Die Ausgabe
landet pro Lauf in der Historie (scheduled_job_run.output) und ist im Admin
direkt sichtbar — man muss nichts im Server-Log suchen.
Hallo Welt¶
Der bei einer frischen Installation vorbereitete Job (deaktiviert) sieht so aus:
job.log("Hallo Welt vom Zeitplaner! Ausgelöst durch: " + job.trigger);
job.log("Zeit: " + (new Date()).format("yyyy-MM-dd HH:mm:ss"));
// Etwas aus der DB lesen (db.scalar liefert einen Einzelwert):
var anzahl = db.scalar("SELECT COUNT(*) FROM form_definition");
job.log("Formulare in der Datenbank: " + anzahl);
job.log("Fertig.");
Beispiel 1 — Aufräumen (Encoder-Tabelle)¶
Abgelaufene Ad-hoc-Link-Einträge (id_encoder) regelmäßig löschen. Passt gut zu
einem nächtlichen Cron (0 30 3 * * * = täglich 03:30).
// Alles löschen, dessen Ablaufdatum in der Vergangenheit liegt.
var geloescht = db.update(
"DELETE FROM id_encoder WHERE expires_at IS NOT NULL AND expires_at < CURRENT_TIMESTAMP");
job.log("Abgelaufene id_encoder-Einträge gelöscht: " + geloescht);
Beispiel 2 — Report per Mail¶
Eine Kennzahl ermitteln und per Mail verschicken. Der Versand läuft durch
dieselbe Domain-Whitelist wie jeder mail-Aufruf.
var offen = db.scalar(
"SELECT COUNT(*) FROM form_urlaubsantrag");
mail.send({
to: "team@example.de",
subject: "Offene Urlaubsanträge: " + offen,
body: "Stand " + (new Date()).format("dd.MM.yyyy HH:mm")
+ "\nOffene Anträge: " + offen
});
job.log("Report verschickt (" + offen + " offen).");
Beispiel 3 — Verzeichnis einlesen → Datensätze anlegen¶
Der klassische Import-Job: ein Eingangsverzeichnis der
fs-Sandbox durchgehen, jede CSV mit
csv.parse lesen, Zeilen per
db.update (oder forms.create für
Formular-Datensätze) anlegen und die Datei anschließend ins Archiv
verschieben.
fs["/import/inbox"].mkdir(true); // im Beispiel: Ordner sicherstellen
var dateien = fs["/import/inbox"].list(function (f) {
return f.name.toLowerCase().endsWith(".csv");
});
job.log("Gefundene CSV-Dateien: " + dateien.length);
dateien.forEach(function (f) {
var zeilen = csv.parse(f, { header: true, separator: ";" });
zeilen.forEach(function (row) {
db.update("INSERT INTO kunde (name, ort, kundennr) VALUES (${name}, ${ort}, ${nr})",
{ name: row.Name, ort: row.Ort, nr: row.Nr });
});
// Verschieben — fs kennt rename/copy, kein move:
f.rename("/import/archiv/" + f.name);
job.log(f.name + ": " + zeilen.length + " Datensätze importiert.");
});
fs kennt kein move()
Zum Verschieben/Umbenennen nutzt man f.rename(zielPfad), zum Kopieren
f.copy(zielPfad). Ein move() gibt es nicht.
Beispiel 4 — Externe Datenquelle abfragen (db.on)¶
Ein Job kann über db.on("quelle") eine externe
Datenquelle (Oracle, PostgreSQL …)
ansprechen und die Ergebnisse in die KCForms-Datenbank spiegeln.
var oracle = db.on("erp-oracle");
var posten = oracle.query(
"SELECT belegnr, betrag FROM offene_posten WHERE mandant = ?", 100);
db.tx(function (t) { // Transaktion auf der KCForms-DB
t.update("DELETE FROM offene_posten_cache");
t.batch("INSERT INTO offene_posten_cache (belegnr, betrag) VALUES (${belegnr}, ${betrag})",
posten.map(function (p) { // Spaltennamen kommen klein geschrieben an
return { belegnr: p.belegnr, betrag: p.betrag };
}));
});
job.log("Offene Posten gespiegelt: " + posten.length);
Beispiel 5 — Ausgehender HTTP-Abruf¶
Kurse, Stammdaten o. Ä. von einem Web-Dienst holen. Ausgehende Aufrufe laufen
durch die SSRF-Allowlist (siehe http).
var r = http.get("https://api.example.com/rates/latest", { timeout: 20000, retries: 3 });
if (!r.ok) job.fail("Kurs-API antwortete mit HTTP " + r.status); // beendet den Lauf
var kurs = r.json().eur;
db.update("UPDATE einstellung SET wert = ? WHERE schluessel = 'eur_usd'", kurs);
job.log("Kurs aktualisiert: " + kurs);
Fehler & Abbruch¶
job.fail(msg) beendet das Skript sofort und markiert den Lauf in der
Historie als Fehler (Status error, msg als Meldung). Eine nicht
abgefangene Exception tut dasselbe. Erfolgreiche Läufe enden mit Status ok.
var quelle = fs["/import/inbox"];
if (!quelle.exists) job.fail("Eingangsverzeichnis fehlt: /import/inbox");
Gute Praxis¶
- Idempotent bauen. Ein Job kann durch „verpasste Läufe nachholen" oder manuelles Auslösen mehrfach laufen. Schreibe so, dass ein Doppellauf keinen Schaden anrichtet (z. B. vor dem Anlegen prüfen, ob es den Satz schon gibt; Verschieben statt Kopieren; Statusfelder prüfen).
job.log(...)großzügig — es ist die einzige Sicht auf das, was passiert ist. Zähle verarbeitete/übersprungene Sätze.- Laufzeit im Blick. Jobs haben ein eigenes Zeitlimit (Standard 5 Minuten,
je Job einstellbar, global
kcforms.scheduler.default-timeout-ms) — deutlich mehr als die 6 Sekunden der Formular-Hooks, aber kein Freibrief für Endlosschleifen. - Testen mit „Jetzt ausführen". Auch ein deaktivierter Job lässt sich im
Zeitplaner von Hand starten (
job.triggerist dann"manual"); vorher speichern. Die Historie zeigt Ausgabe und Dauer.
Siehe auch¶
- Zeitplaner (Admin) — Jobs anlegen, Trigger (Cron/Intervall/einmalig), Historie, Sicherheit.
db-Binding — Abfragen,db.insert,db.tx,db.on.fs-Sandbox ·csv·mail·http.- Lifecycle-Hooks — dieselben Bindings, aber im Formular-Kontext.