Pseudo-events
log-table polling
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>:
| Form | Meaning |
|---|---|
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:
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.\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').cronjob:pseudo-events job and the module reads the new log entries at most every 5 minutes, queueing one event per entry./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):
| Variable | Set when | Value |
|---|---|---|
CRONJOB_EVENT | every event run | the event name (doubles as the “this is an event run” marker) |
CRONJOB_EVENT_PAYLOAD | event run with a non-empty payload | the 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
CRONJOB_EVENT_PAYLOAD is not set at all (never empty, never broken JSON).data, whose sender is external. Validate values (xrefs, names, β¦) before using them.