Zum Inhalt

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.

job.log("Job \"" + job.name + "\" gestartet (Trigger: " + job.trigger + ")");

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.trigger ist dann "manual"); vorher speichern. Die Historie zeigt Ausgabe und Dauer.

Siehe auch