# Cron Tick Hooks

https://dev.wisecp.com/es/cron-tick-hooks

The hooks over work nobody is watching: run starts, task registration, the queue, periodic rounds and worker management.

## Overview

Scheduled work runs in a **tick**: the system wakes each minute, takes jobs off the queue, opens **helper workers** where needed, and finishes.

One thing is true of every hook here: **nobody is watching**. An error you throw is shown to no one and output you print reaches no one; the only trace is in the records.

## Reference

### Registering a scheduled task of your own

registercronjobs

`CronJobQueue` registration is a side effect

Runs while the task registry is gathered. This is where you **register** your own.

Parameters 0

—It takes no parameters. You register by calling `CronJobQueue::register()`, not by returning.

Return 1

voidThe return is ignored; registration happens as a **side effect**.

Listener PHP

```php
Hook::add('register:cronjobs', 10, function () {
    // You register with a call, not with a return.
    CronJobQueue::register('acme.sync', AcmeSyncHandler::class);
});
```

### Stopping a tick from running

gatecron.worker.run

`cron.php` the run environment

Runs before a tick starts. Stopping it means **no job is processed** in that tick.

Parameters 2

$isCliboolWhether it runs from the command line. False means it was triggered over a request.

$sapistringThe name of the interface running it. Read this where you want it to run in one environment only.

Return 1

stringA non-empty string **blocks** the tick; the text comes back as the reason. No error is thrown and the tick is quietly skipped.

Listener PHP

```php
Hook::add('gate:cron.worker.run', 10, function ($isCli, $sapi) {
    // No task runs inside a maintenance window.
    if (Acme::maintenanceWindow()) return 'maintenance window';

    return null;
});
```

### Following a tick starting

actioncron.tick.started

`cron.php` it can be a helper

Runs where a tick starts.

Parameters 3

$workerIdstringThis tick's unique worker id. Several ticks can run at once; keep your records apart by this id.

$isChildboolWhether this is a **helper worker**. True means it is not the main tick but a helper opened under load; both run this hook.

$startTsintThe time the tick started.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:cron.tick.started', 10, function ($workerId, $isChild, $startTs) {
    // Helper workers run this hook too: tell the two apart.
    if ($isChild) return;

    Ops::heartbeat($workerId, $startTs);
});
```

### Following a tick finishing

actioncron.tick.completed

`cron.php` it can be partial

Runs after a tick finished.

Parameters 2

$payloadarrayThe tick summary: `status` (`ok` or `partial`), the worker id, how many jobs ran, the duration, the errors, plus scheduler and helper details. `partial` means the tick **did not finish** its work.

$isChildboolWhether this was a helper worker.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:cron.tick.completed', 10, function ($payload, $isChild) {
    // partial means the tick ran out of room; a run of them is a capacity problem.
    if (($payload['status'] ?? '') === 'partial')
        Ops::warn('cron-partial', (int) ($payload['processed'] ?? 0));
});
```

### Following a processed job

actioncron.job.processed

`CronJobQueue` once per job

Runs after a queued job was processed.

Parameters 2

$jobarrayThe processed queue row: id, type, payload, attempts. An attempt count above one means the job **failed before**.

$workerIdstringThe id of the worker that ran it.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:cron.job.processed', 10, function ($job, $workerId) {
    // An attempt count above one means this job failed before.
    if ((int) ($job['attempts'] ?? 0) > 1)
        Ops::note('cron-retry', $job['type'] ?? '', (int) $job['attempts']);
});
```

### Joining the daily work

actioncron.day.run

`cronjobs/Daily` your return is recorded

Runs in the daily task round. Unlike the other hooks **your return is recorded**.

Parameters 0

—It takes no parameters.

Return 1

bool|arrayYour return is **gathered and shown in the panel** as success, duration and errors. A wrong return breaks nothing; it only makes your entry look bad.

Listener PHP

```php
Hook::add('action:cron.day.run', 10, function () {
    // Your return shows in the panel: true on success, a note otherwise.
    $done = Acme::nightlyReport();

    return $done ? true : ['status' => false, 'message' => 'the report failed'];
});
```

### Joining the hourly work

actioncron.hour.run

`cronjobs/HourlyExecute` your return is recorded

Runs once an hour. Attach hourly work here instead of opening a task file: cache clearing, summaries, outside syncing.

Parameters 0

—It takes no parameters.

Return 1

bool|arrayYour return is **gathered and shown in the panel** as success, duration and errors. A wrong return breaks nothing; it only makes your entry look bad.

Listener PHP

```php
Hook::add('action:cron.hour.run', 10, function () {
    $n = Acme::syncPartnerCatalog();

    return ['status' => true, 'message' => $n . ' records synced'];
});
```

### Joining the per-minute work

actioncron.minute.run

`cronjobs/PerMinuteExecute` your return is recorded

Runs every minute, the tightest of the rounds. It suits queue draining, outside polling and health checks.

Parameters 0

—It takes no parameters.

Return 1

bool|arrayYour return is gathered and shown in the panel. Time spent here is paid again every minute: if your work is long, hand it to the queue instead of finishing it here.

Listener PHP

```php
Hook::add('action:cron.minute.run', 10, function () {
    // Keep it short: this block runs every minute.
    $sent = Acme::drainOutbox(50);

    return ['status' => true, 'message' => $sent . ' messages sent'];
});
```

### Joining the monthly work

actioncron.month.run

`cronjobs/MonthlyExecute` your return is recorded

Runs once a month: month-end summaries, reconciliation, archiving.

Parameters 0

—It takes no parameters.

Return 1

bool|arrayYour return is gathered and shown in the panel. Because it runs once a month, a fault can sit unnoticed for weeks; put a clear note in your return.

Listener PHP

```php
Hook::add('action:cron.month.run', 10, function () {
    $rows = Acme::archiveLastMonth();

    return ['status' => true, 'message' => $rows . ' rows archived'];
});
```

### Changing how many helpers open

filtercron.spawn.count

`cron.php` server load

Runs after it was decided how many helper workers to open under load.

Parameters 1

$decisionarrayrefThe decision: `count` (how many to open), pending jobs, active workers, the threshold, the ceiling, the reason. The field that acts is **`count`**; the rest explain the decision.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:cron.spawn.count', 10, function (&$decision) {
    // Only count acts; open no helpers while the server is under load.
    if (Acme::loadHigh()) $decision['count'] = 0;
});
```

### Changing the chart task list

filtercron.dashboard_chart_tasks

`automation/dashboard` passed by link

Runs while the task picker of the activity chart is being prepared on the automation board. You can add a task type of your own or hide the internal ones.

Parameters 1

$optionsarrayby linkWhat the picker holds: the key is the task name, the value is the title on screen. It is passed by link, so you make your change by writing over the array, **not by returning it**.

Return 1

voidThe return is ignored. Sorting happens before you, so anything you add stays at the end of the list.

Listener PHP

```php
Hook::add('filter:cron.dashboard_chart_tasks', 10, function (&$options) {
    // Add a task of your own to the list.
    $options['acme-sync'] = 'Acme sync';

    // Hide an internal one.
    unset($options['queue-cleanup']);
});
```

### Stopping a run by hand

gatecron.task_run_now

`AdminAutomation` an operator triggers it

Runs before an operator runs a task by hand.

Parameters 2

$taskstringThe key of the task about to run.

$user_idintThe id of the staff member acting.

Return 1

stringA non-empty string **stops** the run; the text is shown to the operator. Triggered from the panel, this hook is one whose message **somebody actually reads**.

Listener PHP

```php
Hook::add('gate:cron.task_run_now', 10, function ($task, $user_id) {
    // Triggered from the panel: a person reads your message.
    if ($task === 'invoice.generate' && Acme::billingFrozen())
        return 'Billing is frozen; this task cannot run now.';

    return null;
});
```

### Following the kill switch

actioncron.kill_switch_toggled

`AdminAutomation` everything stops

Runs where all scheduled work is switched on or off.

Parameters 2

$enabledboolThe new state. Switched off, **no task runs**: no invoices, no suspensions, no renewals.

$user_idintThe staff member who acted.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:cron.kill_switch_toggled', 10, function ($enabled, $user_id) {
    // Switching off stops every automation: forgotten, not even invoices go out.
    if (!$enabled) Ops::alert('cron-disabled', (int) $user_id);
});
```

## Pitfalls

> **An error thrown here reaches nobody**
> 
> On scheduled work hooks **nobody is watching**. An error you throw is shown to neither customer nor operator and only lands in the records. To make a problem known, do it **yourself**: a notification, a record, an alert.

> **Helper workers run the same hooks**
> 
> Under load the system opens helper workers and **each one** runs the tick hooks. A notification on "tick started" sends **dozens** in a busy minute. Read the helper mark in the second parameter.

> **One job can be processed more than once**
> 
> A failed job is **tried again** and the processed hook runs on each attempt. A listener reacting without reading the attempt count produces a run of records or messages for one job. Check the counter **in your first line**.

> **On the daily hook your return is recorded**
> 
> Unlike other event hooks, the periodic ones **gather your return** and show it in the panel as success, duration and errors. Returning nothing leaves your entry blank. Return true on success and **an array with a note** on failure.

## Related Articles

- Scheduled Task Hooks
- [Service Status Hooks](https://dev.wisecp.com/en/service-status-hooks)
- [Invoice Lifecycle Hooks](https://dev.wisecp.com/en/invoice-lifecycle-hooks)
