Events

Event-driven jobs, webhooks, pseudo-events, and route events
flowchart TD
    subgraph Sources["πŸ“‘ Event Sources"]
        WH["`πŸ”— Webhook<br/>**GET + Token**`"]
        DC["`πŸ“ž Direct Call<br/>**EventQueue::push**`"]
        PE["πŸ“‹ Pseudo-Events<br/>Log table poller"]
        RE["πŸ”€ Route-Events<br/>POST on mutating routes"]
    end

    subgraph Queue["⏳ Queue"]
        CE[("`πŸ“¦ cj_event<br/>**DB Queue**`")]
    end

    subgraph Tick["`⏱ Tick *(every 60 s)*`"]
        MATCH["Match events<br/>coalesced: 1 per name"]
        BUDGET["`⏲ **60 s** wall-clock budget`"]
    end

    subgraph Execution["βš™ Execution"]
        JOB["`Job script<br/>child process via **proc_open**`"]
        ENV["🌐 CRONJOB_EVENT<br/>CRONJOB_EVENT_PAYLOAD"]
    end

    subgraph Storage["πŸ’Ύ Storage"]
        RUN[("cj_run<br/>run history")]
        ER[("cj_event_run<br/>link")]
    end

    WH --> Queue
    DC --> Queue
    PE --> Queue
    RE --> Queue
    CE --> Tick
    MATCH --> BUDGET
    BUDGET --> JOB
    JOB --> ENV
    JOB --> RUN
    JOB --> ER

    classDef sourceStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
    classDef queueStyle fill:#fff3e0,stroke:#E65100,stroke-width:2px
    classDef tickStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
    classDef execStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px
    classDef storageStyle fill:#f5f5f5,stroke:#616161,stroke-width:1px

    class WH,DC,PE,RE sourceStyle
    class CE queueStyle
    class MATCH,BUDGET tickStyle
    class JOB,ENV execStyle
    class RUN,ER storageStyle

Besides time-based schedules, a job can be triggered by an event. webtrees has no event system, so events are plain DB rows (a small cj_event queue) that the tick drains once a minute. An event-triggered job runs for a queued event whose name matches its Event name β€” coalesced: per tick at most the newest event of each name is drained (one run per name per tick), and the name’s older pending duplicates are dropped β€” a burst of identical events never queues a burst of job runs (job scripts are idempotent/incremental by convention).

Naming scheme (analogous to job names). Event names are slugs (a-z 0-9 _ -, max 64 chars total), optionally namespaced <domain>:<slug>:

FormMeaning
plain slug (e.g. index-dirty)your own events β€” webhook or direct EventQueue::push()
cronjob:<slug>built-in pseudo-events (log-table pollers)
_route:<slug>route-triggered events (see below)
<module>:<slug>events announced by a module (see “Announcing events”)

Queuing an event has four sources:

  1. Webhook (external systems). GET the endpoint shown in the admin page (section “Event webhook”): GET /module/_cronjob_/Event?event=<name>&data=<json> with the token in the X-Cronjob-Token header. data is an optional JSON object, stored with the event. The endpoint requires the token, is rate-limited (20/min per site), and only accepts events while the module is enabled. It is a GET, not a POST: webtrees rejects POSTs without a session CSRF token, which an external caller cannot send.
  2. Direct call (your own modules). From PHP code in another module: \Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue::push('index-dirty', ['tree' => 'X']); β€” this requires the cronjob module to be installed; guard it with class_exists('Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue').
  3. Pseudo-events (built-in pollers). webtrees has no event bus, so core actions (logins, failed logins, logouts, record edits, errors, searches) are observed by polling the webtrees log table. Enable the offered cronjob:pseudo-events job and the module reads the new log entries at most every 5 minutes, queueing one event per entry.
  4. Route events (built-in, request-triggered). A curated set of mutating editor routes under /tree/{tree}/ (record-specific routes with {xref} in the path, plus a small tree-level allowlist) queues its event immediately, right inside the request that made the change.

The tick matches each pending event against the enabled jobs that carry an event trigger for that name (a job can have several event triggers, or none at all), runs them, then marks the event handled (consumed once β€” even on failure, so a failing job does not re-run the same event forever). The drain is bounded: it takes the newest event per name (coalescing, see above) and stops after a 60 s wall-clock budget, so a saturated queue cannot starve the rest of the tick β€” the remaining events simply continue with the next tick. Every consuming run is additionally linked to the event in the cj_event_run cross table (an event can trigger several jobs), and the run history shows which event triggered a run. Handled events are purged after 7 days (their cj_event_run links are removed by the FK cascade); events with no matching job are discarded so the queue does not grow.

Event payload in the child process. Event runs hand the event to the job script via the process environment (the argv is reserved for the job’s static, strictly validated arguments):

VariableSet whenValue
CRONJOB_EVENTevery event runthe event name (doubles as the “this is an event run” marker)
CRONJOB_EVENT_PAYLOADevent run with a non-empty payloadthe event payload as JSON (route parameters, log-table entries, webhook data)
$event   = getenv('CRONJOB_EVENT');                       // false on schedule / manual runs
$payload = json_decode((string) getenv('CRONJOB_EVENT_PAYLOAD'), true) ?: [];
$xref    = $payload['xref'] ?? '';                        // e.g. from a _route:* event
  • Variable absent = no payload. The payload is truncated to 4000 bytes when the event is queued; a JSON cut mid-way no longer decodes, in which case CRONJOB_EVENT_PAYLOAD is not set at all (never empty, never broken JSON).
  • Treat the payload as untrusted input β€” above all for webhook data, whose sender is external. Validate values (xrefs, names, …) before using them.
  • The payload is independent of the command type: module CLI scripts and core commands (which simply ignore the variables) both work on event runs.
  • The payload is also visible in the run history (event detail), independent of the child process.

Pseudo-events

log-table polling

Route events

request-triggered

Last modified November 4, 2026: cross-references (60f426f)