CronJobs API
Die CronJobs API steuert geplante Automatisierung. Ein separater job-worker-Container führt die Jobs aus (Cron/Interval, Distributed Locking, Multi-Instance) — von einfachen Automatisierungen (Ticket erstellen/aktualisieren, Webhook, Zuweisung) bis zu den System-Monitoren (SLA-Monitor, Eskalations- und Cleanup-Jobs). Es gibt 28 Action-Typen und 23 Built-in-Templates.
Architektur
Backend API (/api/cronjobs) CRUD, RBAC, Audit, Config │ persistiert CronJob/JobExecution in Postgres ▼
job-worker (separater Container)
• Scheduler — Cron/Interval • Queue (BullMQ) — Ausführungs-Warteschlange • 28 Action-Typen • Distributed Lock (Redis): cronjob:lock:{jobId}
• Heartbeat (15s) — Erkennung mehrerer Instanzen / Worker-Status • Eingeschränkter DB-Zugriff: nur CronJob/JobExecution │
▼ Mutationen über Backend-Internal-API, Notifications über notification-worker
Endpoints
Job-Verwaltung /api/cronjobs
| Method | Endpoint | Permission |
|---|---|---|
GET | / | cronjobs.view |
GET | /:id | cronjobs.view |
POST | / | cronjobs.create |
PUT | /:id | cronjobs.edit |
DELETE | /:id | cronjobs.delete (kritisch) |
POST | /:id/restore | cronjobs.restore + cronjobs.viewDeleted |
GET | /stats/summary | cronjobs.view |
GET | /templates/list | cronjobs.view |
GET | /activity | cronjobs.view |
GET / und GET /:id akzeptieren zusätzlich ?includeDeleted=true, um soft-gelöschte Jobs einzuschließen — das erfordert die eigene Permission cronjobs.viewDeleted (sonst 403).
Wiederherstellen verlangt zwei Rechte: cronjobs.restore für die Aktion und cronjobs.viewDeleted für den Zugriff auf den Papierkorb — wer den Papierkorb nicht sehen darf, holt auch nichts daraus zurück. Auf einem gelöschten Job wirkt sonst keine Mutation: Aktualisieren, Aktivieren/Deaktivieren, manuelles Ausführen, Dry-Run, Bulk-Aktionen und das Wiederholen einer Ausführung antworten mit 404, solange er im Papierkorb liegt. Die Ausführungs-Historie bleibt beim Löschen erhalten — sichtbar, aber nicht wiederholbar.
Ausführung & History
| Method | Endpoint | Permission |
|---|---|---|
PATCH | /:id/toggle | cronjobs.enableDisable |
POST | /:id/execute | cronjobs.executeManually |
POST | /:id/dry-run | cronjobs.dryRun |
GET | /executions/list | cronjobs.view |
GET | /executions/:id | cronjobs.view |
POST | /executions/:id/retry | cronjobs.retry (kritisch) |
POST | /bulk/enable · /bulk/disable | cronjobs.enableDisable |
POST | /bulk/delete | cronjobs.delete |
Worker & Config
| Method | Endpoint | Permission |
|---|---|---|
GET | /workers/status | cronjobs.view |
POST | /workers/pause-all · /workers/resume-all | cronjobs.pauseWorkers (kritisch) |
POST | /workers/:instanceId/pause · /resume | cronjobs.pauseWorkers |
POST | /workers/:instanceId/hide · /unhide | cronjobs.hideWorkers |
GET / PUT | /api/cronjobs/config | cronjobs.view / cronjobs.edit |
Datenmodell
CronJob {
id, name (unique),
category: ESCALATION | NOTIFICATION | REPORTING | MAINTENANCE | MONITORING | WORKFLOW | CUSTOM,
status: ENABLED | DISABLED | RUNNING | ERROR,
trigger: Json, // { type, schedule }
actions: Json, // [{ type, parameters }]
filters: Json, // entity scope (e.g. ticketStatuses)
runConditions: Json, // additional conditions
dependencies: String[], // job IDs that must succeed first
timeoutMinutes, maxRetries, runOnStartup,
lastExecutedAt, nextExecutionAt, executionCount, failureCount, avgDurationMs,
createdById, deletedAt // Soft-Delete
}
JobExecution {
id, cronJobId,
status: PENDING | RUNNING | COMPLETED | FAILED | CANCELLED,
triggeredBy: SCHEDULE | MANUAL | EVENT | CONDITION,
startedAt, completedAt, durationMs, retryCount,
results: Json, // [{ action, status, metadata }]
errorMessage?, workerId
}
Trigger
| type | Beschreibung |
|---|---|
cron | schedule.cronExpression (z.B. "0 8 * * 1-5") |
interval | schedule.intervalMinutes oder intervalDays |
condition | bedingungsbasiert (runConditions, s.u.) |
// Cron
{ "trigger": { "type": "cron", "schedule": { "cronExpression": "*/30 * * * *" } } }
// Interval
{ "trigger": { "type": "interval", "schedule": { "intervalMinutes": 5 } } }
Action-Typen (28)
Automatisierung
| type | Beschreibung |
|---|---|
create_ticket | Ticket erstellen (z.B. wiederkehrende Wartung) |
update_ticket | Tickets nach Filter aktualisieren (Status/Priority) |
assign_agent | Agent zuweisen (Strategie „specific": ein fest benannter Agent) |
assign_group | Gruppe zuweisen |
webhook | HTTP-Request an externe URL (SSRF-Schutz, Circuit Breaker, Retry) |
Monitore & Eskalation
| type | Beschreibung |
|---|---|
sla_monitor | SLA-Deadlines prüfen, Warnungen/Breach/Eskalation |
lifecycle_stale_entity_reminder | Inaktivitäts-Reminder für TICKET/PROBLEM/INCIDENT (Assignee→Lead→Manager, nur an Empfänger, die den Vorgang sehen dürfen) — ändert NIE Status oder SLA |
ticket_hold_reminder_check | On-Hold-Tickets mit fälliger Wiedervorlage reaktivieren |
stale_cascading_reminder | Hängende Resolution-Ketten (Incident/Problem resolved, Kind offen) |
major_incident_update_reminder | Major Incidents mit überfälligem nextUpdateETA |
data_breach_deadline_check | DSGVO Art. 33: 72h-Frist (Reminder 48h, Eskalation 72h) |
inventory_due_monitor | Inventur-Fristen: Vorwarnung 3 und 1 Tag vorher, danach überfällig (je Meilenstein einmal) |
expiry_monitor | Ablauf von Assets/Lizenzen/Verträgen (Meilensteine 30/7/3/0 Tage) |
handover_return_reminder | Asset-Rückgabe-Erinnerungen (24h/1h/overdue) |
pending_assignment_retry | Auto-Zuweisung erneut versuchen (Gruppe ohne Agent, z.B. Kapazität voll) |
workload_sync | Agent-Workload-Zähler neu berechnen (für Assignment-Strategien) |
lifecycle_auto_close | Fällige RESOLVED-Tickets zeitgesteuert schließen (mit Vorwarnung) — opt-in je Entität |
lifecycle_wc_auto_resolve | Unbeantwortete WAITING_CUSTOMER-Tickets nach Frist auf RESOLVED (Vor-Stufe zu Auto-Close) — nur Ticket. Ein Elternticket mit offenen Sub-Tickets bleibt stehen und wird im Lauf als übersprungen gezählt (siehe Tickets-API). |
lifecycle_reopen_escalation | Eskalation bei zu häufigem Reopen (reopenCount ≥ Schwelle), nur an Empfänger, die den Vorgang sehen dürfen |
Wartung / Cleanup
| type | Beschreibung |
|---|---|
holiday_autoimport | Deutsche Feiertage (aktuelles + nächstes Jahr) für alle genutzten Business-Hours-Konfigurationen anlegen — berechnet inkl. beweglicher Feiertage, idempotent (keine Dubletten) |
asset_model_clustering | Ähnliche Hersteller/Modell-Schreibweisen clustern (Admin-Review) |
attachment_cleanup | Hängende Scans, Datei-Aufbewahrung, verwaiste und infizierte Dateien (Dateisystem und Virenquarantäne) |
retention_purge | DSGVO-Aufbewahrung gebündelt: setzt alle zeitbasierten Aufbewahrungsfristen der Datenbank in EINEM Lauf durch, von Audit-Events (zweistufig) bis zur Auto-Anonymisierung archivierter User. Ziele und Fristen: siehe Privacy & DSGVO. |
audit_chain_verify | Nächtliche Voll-Verifikation der Audit-Hash-Ketten, die Manipulationen erkennbar machen (Ketten-Kontinuität je Org, Purge-Anker, Purge-Plausibilität); geprüft wird abschnittsweise, Parameter windowSize (Events je Abschnitt, Standard 100.000, erlaubt 1.000–250.000); bei jedem Fund CRITICAL-Alert an alle audit.enterpriseView-Träger |
digest_dispatch | Versendet fällige E-Mail-Digests (Zustellmodus gebündelt — stündlich/täglich/wöchentlich, je Empfänger EINE Sammelmail in dessen Zeitzone) und räumt verwaiste Bulk-Batch-Items ab; No-op ohne Digest-Opt-ins (siehe Notifications-Seite) |
report_schedule_check | Startet fällige Report-Zeitpläne (je Exportformat eine Ausführung) und schließt fertige Läufe mit der Abschluss-Mail ab — ohne diesen Job laufen geplante Reports NICHT |
push_retry | Fehlgeschlagene WebPush erneut senden |
entra_id_sync | Benutzer der Entra-ID-Basisgruppe abgleichen: anlegen, aktualisieren, Rollen zuordnen, Konten sperren, die die Gruppe verlassen haben oder in Entra ID deaktiviert sind; ohne konfigurierte und aktive Entra-ID-Integration ohne Wirkung |
Grenzregel: Die zeitbasierte Aufbewahrung von Datenbank-Einträgen läuft über retention_purge — eine Stelle für alle Fristen; alles, was Dateien oder Virenscans anfasst, bleibt bei attachment_cleanup. Eskalation läuft über sla_monitor und lifecycle_stale_entity_reminder, Kapazität über workload_sync, Rückgabe-Erinnerungen über handover_return_reminder.
Built-in Templates (23)
GET /api/cronjobs/templates/list — liefert vorkonfigurierte Vorlagen (Felder: id, name, description, category, isBuiltIn, tags, template). Zu jeder Vorlage legt der Backend-Start den passenden Built-in-Job an, falls er fehlt — ENABLED, mit Ausnahme von Asset Model Deduplication, die DISABLED startet, weil sie vom genutzten Funktionsumfang abhängt. Die Lifecycle-Jobs wirken erst, wenn der Admin sie je Entität in der Lifecycle-Config aktiviert; der Digest-Job wirkt erst, wenn Benutzer den Digest gewählt haben; der Entra-ID-Sync wirkt erst, wenn die Entra-ID-Integration konfiguriert und aktiv ist. Schedules:
| Template | Kategorie | action | Schedule |
|---|---|---|---|
| SLA Monitor | ESCALATION | sla_monitor | alle 2 Min, runOnStartup |
| Stale Entity Reminder | ESCALATION | lifecycle_stale_entity_reminder | 01:30 |
| Ticket Hold Reminder (Wiedervorlage) | MONITORING | ticket_hold_reminder_check | alle 5 Min |
| Major Incident Update Reminder | MONITORING | major_incident_update_reminder | alle 5 Min |
| Stale Cascading Resolution Reminder | MONITORING | stale_cascading_reminder | 08:00 |
| DSGVO Data Breach Deadline Monitor | MONITORING | data_breach_deadline_check | alle 30 Min |
| Inventory Due Monitoring | ESCALATION | inventory_due_monitor | stündlich |
| Expiry Monitor | MONITORING | expiry_monitor | 07:00 |
| Asset Return Reminders | MONITORING | handover_return_reminder | alle 30 Min |
| Pending Assignment Retry | MAINTENANCE | pending_assignment_retry | alle 5 Min |
| Agent Workload Sync | MAINTENANCE | workload_sync | alle 5 Min |
| WebPush Retry | MAINTENANCE | push_retry | alle 5 Min |
| Notification Digest Dispatch | NOTIFICATION | digest_dispatch | alle 15 Min |
| Report Schedule Check | MAINTENANCE | report_schedule_check | jede Minute |
| Retention Purge (DSGVO) | MAINTENANCE | retention_purge | 03:00 |
| Audit Chain Verify | MONITORING | audit_chain_verify | 04:15 (nach retention_purge) |
| Attachment Cleanup | MAINTENANCE | attachment_cleanup | 03:00 |
| Asset Model Deduplication | MAINTENANCE | asset_model_clustering | So 02:00 · DISABLED |
| Holiday Auto-Import | MAINTENANCE | holiday_autoimport | jährlich 01.11. 04:00, runOnStartup |
| Entra ID User Sync | MAINTENANCE | entra_id_sync | 01:00 |
| Lifecycle Auto-Close | MAINTENANCE | lifecycle_auto_close | 02:30 |
| Lifecycle WC-Auto-Resolve | MAINTENANCE | lifecycle_wc_auto_resolve | 02:00 |
| Reopen Escalation | ESCALATION | lifecycle_reopen_escalation | 03:00 |
Built-in-Jobs: automatische Anlage beim Start
Die Built-in-Jobs werden bei jedem Backend-Start abgeglichen: Fehlende Built-in-Jobs werden anhand ihrer Vorlage angelegt (ENABLED oder DISABLED je Vorgabe, siehe oben), bestehende Jobs werden NIE verändert. Von Admins geänderte Zeitpläne, Parameter und der Aktiv-Status bleiben also erhalten. Ein Update bringt neue Standard-Jobs ohne manuellen Schritt mit.
Job erstellen
POST /api/cronjobs
{
"name": "SLA Monitor - All Entities",
"category": "ESCALATION",
"description": "Check SLA deadlines and trigger escalations",
"trigger": { "type": "interval", "schedule": { "intervalMinutes": 2 } },
"actions": [ { "type": "sla_monitor", "parameters": { "entityType": null, "batchSize": 500 } } ],
"timeoutMinutes": 5,
"maxRetries": 3,
"runOnStartup": true
}
batchSize ist die SEITENGRÖSSE, nicht die Obergrenze eines Laufs: der Monitor liest den fälligen Bestand seitenweise durch, bis nichts mehr folgt. Bricht er dennoch ab (Schutz gegen Endlosläufe), steht das im Job-Ergebnis und im Log.
// Response 201
{
"id": "clx...",
"name": "SLA Monitor - All Entities",
"category": "ESCALATION",
"status": "DISABLED",
"trigger": { "type": "interval", "schedule": { "intervalMinutes": 2 } },
"nextExecutionAt": null,
"createdAt": "2026-01-27T23:00:00.000Z"
}
Neue Jobs starten als DISABLED — Aktivieren über PATCH /:id/toggle (cronjobs.enableDisable).
Ausführung, Dry-Run & Retry
# Run manually (cronjobs.executeManually)
POST /api/cronjobs/:id/execute { "reason": "Testing config" } # -> 202 { executionId }
# Dry-Run: shows affected entities, makes NO changes
POST /api/cronjobs/:id/dry-run
# Retry a failed execution (only status FAILED)
POST /api/cronjobs/executions/:id/retry { "reason": "Network issue resolved" }
# History (filter jobId/status, pagination)
GET /api/cronjobs/executions/list?status=FAILED&limit=20
Status-Werte
| Job (status) | Execution (status) |
|---|---|
ENABLED — aktiv, läuft nach Schedule | PENDING — in Queue |
DISABLED — deaktiviert | RUNNING |
RUNNING — läuft gerade | COMPLETED |
ERROR — letzte Ausführung fehlgeschlagen | FAILED — retry-bar |
CANCELLED — verworfen/terminiert (z.B. verwaiste PENDING-Execution) |
Selbstheilung
Jobs sind (teil-)selbstheilend — ein Job bleibt nicht dauerhaft hängen:
- ERROR ist nicht terminal: Status ERROR bedeutet nur „letzter Lauf fehlgeschlagen". Der Job bleibt aktiv, wird weiter nach Schedule eingeplant (nextRunAt wird neu berechnet) und heilt beim nächsten erfolgreichen Lauf von selbst zurück.
- Kein Hängenbleiben in RUNNING: Bei Fehler/Timeout wird der Distributed-Lock freigegeben; ein Job bleibt nicht fälschlich in RUNNING hängen.
- Catch-up beim (Neu-)Start: Beim Start des job-workers werden überfällige Jobs (nextRunAt in der Vergangenheit) erkannt, als PENDING-Execution nachgeholt und neu terminiert — sie hängen nicht „Overdue für immer". Ein Distributed-Lock stellt sicher, dass nur EINE Instanz den Catch-up macht.
- runOnStartup: Jobs mit diesem Flag (z.B. SLA Monitor) erhalten bei jedem Worker-Start eine Ausführung.
- Orphan-Cleanup: In der Queue (BullMQ) vorhandene, in der DB aber gelöschte Jobs werden beim Start entfernt.
„Teil-selbstheilend": Die zugrunde liegende Fehlerursache wird nicht automatisch behoben — der Job versucht es lediglich beim nächsten Schedule (bzw. innerhalb eines Laufs bis maxRetries) erneut und verlässt den ERROR-Zustand bei Erfolg. Dauerhafte Fehler sollten über die Execution-History/Activity geprüft werden.
Worker-Management
Mehrere job-worker-Instanzen melden sich per Heartbeat (instanceId). GET /workers/status liefert pro Instanz health (null bei offline) und executions; die Queue-Zähler stehen als eigenes Feld queue unter health — waiting, active, completed, failed, delayed — und sind null, solange die Queue der Instanz noch nicht initialisiert ist. Für Wartung lassen sich Worker pausieren (global oder pro Instanz). Pause/Resume ist eine kritische, auditierte Aktion und verlangt einen Grund (reason, min. 10 Zeichen).
POST /api/cronjobs/workers/pause-all { "reason": "Database maintenance window" }
# -> { "pausedWorkers": ["job-worker-abc123"], "failedWorkers": [] }
Filter & Run-Conditions
filters schränken die Ziel-Entities ein; runConditions sind zusätzliche Voraussetzungen. Ein runCondition trägt genau type + duration (Mindestalter in Minuten) — type ist ticket_age oder change_pending_approval.
{
"filters": { "ticketStatuses": ["OPEN", "IN_PROGRESS"], "ticketPriorities": ["LOW", "MEDIUM"] },
"runConditions": [ { "type": "ticket_age", "duration": 4320 } ]
}
In ticketStatuses und ticketPriorities sind nur gültige Ticket-Status bzw. -Prioritäten erlaubt; eine leere Liste wirkt wie kein Filter, ein unbekannter Wert lässt die Bedingungsprüfung fehlschlagen — der Job wird dann nicht ausgelöst. ticket_age zählt Tickets, die älter als duration sind und den Filtern entsprechen; change_pending_approval zählt Changes im Status PENDING_APPROVAL und wertet die Filter nicht aus. Beide Bedingungen sind erfüllt, sobald mindestens ein Datensatz zutrifft.
Webhook-Sicherheit
- SSRF: blockiert localhost, private IPs (10/172.16-31/192.168), Link-Local (169.254), Cloud-Metadata (169.254.169.254)
- Circuit Breaker + Rate-Limit + Retry (Exponential Backoff) + konfigurierbares Timeout
Fehlercodes
| Error | HTTP |
|---|---|
CRONJOB_NOT_FOUND | 404 |
JOB_NAME_EXISTS | 409 |
JOB_CURRENTLY_RUNNING | 409 |
CANNOT_DELETE_RUNNING_JOB | 400 |
CAN_ONLY_RETRY_FAILED_EXECUTIONS | 400 |
JOB_EXECUTION_NOT_FOUND | 404 |
GLOBAL_PAUSE_ACTIVE | 409 |
GLOBAL_PAUSE_ACTIVE: Solange eine globale Worker-Pause aktiv ist, lässt sich keine einzelne Instanz fortsetzen — die globale Pause muss zuerst als Ganzes aufgehoben werden (resume-all).
- ✓ Separater job-worker, Distributed Lock
- ✓ 28 Actions, 23 Built-in-Templates
- ✓ SLA-Monitor als Job, alle 2 Minuten
- ✓ Built-in-Jobs entstehen beim Start automatisch, Admin-Änderungen bleiben erhalten
cronjobs.view/create/edit/delete/restore/viewDeletedcronjobs.enableDisable/executeManually/dryRun/retrycronjobs.pauseWorkers/hideWorkers
Auth-/Rollenmodell: Permissions & RBAC
- SLA Management API – der sla_monitor läuft als Job
- Notification-System – Reminder/Eskalations-Notifications
- Workflows API – event-/form-getriggerte Automatisierung
- Reports & Custom Reports API – Aufbewahrung der Report-Ausführungen (retention_purge)
- Privacy & DSGVO – retention_purge (Aufbewahrungsfristen & Audit-Purge)
- Reopen- & Lifecycle-Governance – lifecycle_auto_close / lifecycle_reopen_escalation
- Tickets API – Sub-Tickets: warum lifecycle_wc_auto_resolve Elterntickets mit offenen Sub-Tickets überspringt