# The Tick

> How the once-a-minute tick schedules and runs jobs

---

The **tick** is the heart of the module: once a minute it looks at every enabled job, decides what is due, and runs it. Neither the webtrees core nor the operating system does any scheduling itself — they only start <a href="https://github.com/bschwede/wt-cronjob/tree/main/cli/tick.php" target="_blank" rel="noopener"><code>tick.php</code></a>
.




<pre class="mermaid">flowchart TD
    subgraph Triggers[&#34;`⏱ Triggers *(once per minute)*`&#34;]
        CRON[&#34;`🕑 OS cron / systemd&lt;br/&gt;*every minute*`&#34;]
        WATCH[&#34;`👁 watch daemon&lt;br/&gt;*(in-module)*`&#34;]
    end

    subgraph Guard[&#34;🛡 Guard&#34;]
        TP[&#34;`📄 cli/tick.php · **cron:tick**&lt;br/&gt;*guard(): 403 over HTTP*`&#34;]
        OFF{&#34;`⚠ site offline?&lt;br/&gt;*data/offline.txt*`&#34;}
        BOOT[&#34;`🚀 boot()&lt;br/&gt;*webtrees &#43; DB*`&#34;]
    end

    subgraph Selection[&#34;🔎 Select work&#34;]
        TIME[&#34;`📅 time-based jobs&lt;br/&gt;**next_run_at due** *(UTC)*`&#34;]
        CE[(&#34;`📦 cj_event&lt;br/&gt;*event queue*`&#34;)]
        DRAIN[&#34;`🧮 drain queue&lt;br/&gt;*coalesced · 60 s budget*`&#34;]
    end

    subgraph Execution[&#34;`⚙ Execution *(isolated child)*`&#34;]
        RUN[&#34;`🧪 JobRunner&lt;br/&gt;**proc_open**`&#34;]
        LIM[&#34;`⏲ timeout 30–3600 s&lt;br/&gt;last **64 KB** · exit code`&#34;]
    end

    subgraph Storage[&#34;💾 Storage&#34;]
        CJRUN[(&#34;`cj_run&lt;br/&gt;*run history*`&#34;)]
        MARK[&#34;`🏷 tick.last&lt;br/&gt;*stale &gt; 300 s → tick_stale*`&#34;]
    end

    CRON --&gt; Guard
    WATCH --&gt; Guard
    TP --&gt; OFF
    OFF --&gt;|&#34;offline&#34;| SKIP[&#34;⏭ skip tick&#34;]
    OFF --&gt;|&#34;online&#34;| BOOT
    BOOT --&gt; TIME
    BOOT --&gt; DRAIN
    CE --&gt; DRAIN
    TIME --&gt; Execution
    DRAIN --&gt; Execution
    RUN --&gt; LIM
    LIM --&gt; CJRUN
    BOOT --&gt; MARK

    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 CRON,WATCH sourceStyle
    class OFF,CE queueStyle
    class TP,TIME,DRAIN tickStyle
    class RUN,LIM execStyle
    class CJRUN,MARK,SKIP storageStyle
</pre>

## What starts a tick

Pick **one** trigger (once):

- **OS cron** — `* * * * *` from the webtrees root (see [Installation](setup/installation)).
- **systemd timer** — the generated unit from the module's admin page.
- **watch daemon** — the in-module resident process that re-launches <a href="https://github.com/bschwede/wt-cronjob/tree/main/cli/tick.php" target="_blank" rel="noopener"><code>tick.php</code></a>
 every 60 s and survives FPM worker recycling ([Installation](setup/installation), which also carries the daemon's lifecycle diagram).

All three run the same command: `php modules_v4/cronjob/cli/tick.php cron:tick`.

## What one tick does

1. **Guard** — `CliBootstrap::guard()` answers `403` if the script is ever reached over HTTP; then `CliBootstrap::boot()` loads webtrees + the database.
2. **Offline check** — if `data/offline.txt` exists the tick is skipped entirely, before any database access — e.g. during a webtrees update, when schema and code are in flux.
3. **Select the work** — two sources, combined in one pass:
   - **time-based jobs** whose `next_run_at` has passed (cron schedules, **UTC**); the due triggers are computed against the last run (or the job's creation, for never-run jobs).
   - **event jobs** — the `cj_event` queue is drained oldest-first, **coalesced to one run per event name**, within a **60-second wall-clock budget**. Event jobs receive the event name and payload via the `CRONJOB_EVENT` / `CRONJOB_EVENT_PAYLOAD` environment. See the [event pipeline](events).
4. **Run each job in isolation** — `JobRunner` validates the command (a `modules_v4/<module>/cli/<script>.php` path, or one of the four allowlisted core commands; at most 10 argument tokens) and launches it as a **child process** with a hard **timeout** (30–3600 s). Only the **last 64 KB** of output are captured; a job that hits its timeout is killed ([`proc_terminate`](https://www.php.net/manual/en/function.proc-terminate.php)) and reported as exit `-9`.
5. **Record** — every run is written to `cj_run` (status, exit code, duration, the trigger that fired it, captured output) — this is the [run history](usage). Event-triggered runs are additionally linked via `cj_event_run`.
6. **Marker** — the tick writes `data/cronjob/tick.last`. If that marker is older than 300 s, the module reports itself as `tick_stale` in the admin status.

## Manual runs

You can also run the tick by hand for debugging — `--dry-run`, `--job=<name>`, `--strict` and `--full-output` are described on the [CLI scripts](cli-scripts) page. Note that `--full-output` may expose personal data, so only use it where the output destination is trusted.
