Scheduling checks
Health::for($owner) scopes every health operation to one owner. Use its fluent builder with preset frequencies and a check class — no magic strings:
use RoundlyConsulting\Alerts\Facades\Health;
$team = Team::first();
Health::for($team)->monitor(DiskUsageCheck::class)
->everyFiveMinutes()
->throttle(maxAttempts: 2, decayMinutes: 60)
->meta(['server_id' => '2d4fcdff-9787-49b1-8b73-3d411f80ae1b'])
->save();
// The same builder through the model trait:
$team->monitorCheck(DiskUsageCheck::class)->everyFiveMinutes()->save();monitor() accepts a Check class name or a registered key — the key of an inline check, or of an instance registered with as(). For an inline check the builder starts from the options declared on define(). A class that exists but does not extend Check throws InvalidHealthCheck. save() persists and returns the HealthCheck row.
Frequencies
| Method | Stored frequency |
|---|---|
everyMinute() | * * * * * (the default) |
everyFiveMinutes() | */5 * * * * |
everyTenMinutes() | */10 * * * * |
everyFifteenMinutes() | */15 * * * * |
everyThirtyMinutes() | */30 * * * * |
hourly() | @hourly |
daily() | @daily |
weekly() | @weekly |
monthly() | @monthly |
frequency(string) | a preset name (hourly, everyFiveMinutes, yearly, …) resolved via Frequency::toCron(), or a cron string |
cron(string) | the expression, verbatim |
$monitors = Health::for($team);
$monitors->monitor(DiskUsageCheck::class)->hourly()->save();
$monitors->monitor(DiskUsageCheck::class)->frequency('hourly')->save(); // preset by name
$monitors->monitor(DiskUsageCheck::class)->frequency('everyFiveMinutes')->save();
$monitors->monitor(DiskUsageCheck::class)->cron('15 3 * * *')->save(); // 03:15 every day
$monitors->monitor(DiskUsageCheck::class)->cron('0 9-17 * * 1-5')->save(); // hourly, office hours
$monitors->monitor(DiskUsageCheck::class)->cron('0 9 * * MON-FRI')->save(); // day and month names work tooDue-ness is evaluated by a small native cron parser: five fields (minute, hour, day of month, month, day of week) with *, lists (1,2,3), ranges (1-5) and steps (*/5, 1-30/2), day and month names (MON-FRI, JAN,JUL — case-insensitive), plus the aliases @yearly, @annually, @monthly, @weekly, @daily, @midnight and @hourly. Both 0 and 7 mean Sunday.
Expressions are validated on save(): one the scheduler could not evaluate throws InvalidCronExpression and nothing is stored, so a user-supplied cron string fails where you save it:
use RoundlyConsulting\Alerts\Exceptions\InvalidCronExpression;
try {
Health::for($team)->monitor(DiskUsageCheck::class)->cron($expression)->save();
} catch (InvalidCronExpression $e) {
// The scheduler could not evaluate it — nothing was stored.
}A row written another way — by a seeder or by hand — is still checked at run time: Health::runDue() skips a row whose cron it cannot evaluate, reports it to your exception handler and keeps dispatching the rows after it.
Every option
The same builder takes flap debounce, recovery confirmation, a per-check timeout, tags, channel routing and an escalation policy:
use RoundlyConsulting\Alerts\Checks\DatabaseCheck;
Health::for($team)->monitor(DatabaseCheck::class)
->everyFiveMinutes()
->failAfter(3) // open only after 3 consecutive failures
->recoverAfter(2) // close only after 2 consecutive OKs
->timeout(5) // mark failed after 5 seconds
->tags(['critical', 'db']) // group + filter
->notifyVia(['database']) // channels for the default notification
->notifyVia(['mail', 'database'], level: 3) // override channels at escalation level 3
->escalate([1 => 'owner', 3 => 'team', 5 => 'oncall'])
->throttle(maxAttempts: 3, decayMinutes: 10)
->save();| Method | Effect | Default |
|---|---|---|
throttle(int $maxAttempts, int $decayMinutes) | At most maxAttempts notifications per decayMinutes, per recipient, for this row. | 1 per 1 minute |
failAfter(int) | Consecutive failures needed before an alert opens (values below 1 clamp to 1). | 1 |
recoverAfter(int) | Consecutive successes needed before an open alert closes. | 1 |
timeout(int $seconds) | Time budget for check(); an overrun becomes a failed result. | none |
tags(array) | Tags for grouping, report filtering and muting (de-duplicated). | [] |
notifyVia(array $channels, ?int $level = null) | Channels for the bundled notification — globally, or for one escalation level. | mail, database |
escalate(array $policy) | Escalation policy: consecutive-failure threshold ⇒ notifiable group. | config escalation |
meta(array) | Your own data, readable in the check as $this->healthCheck->meta. | [] |
save() | Persists and returns the HealthCheck row. | — |
The declarative options are stored in the row’s meta under reserved keys — fail_after, recover_after, timeout, notify_via, notify_via_levels and escalation — alongside your own meta. Avoid those names in meta(). Options left at their defaults are not written.
Scheduling from a DTO
schedule() takes a ScheduleHealthCheckData, and the trait offers the same call as a shortcut:
use RoundlyConsulting\Alerts\DataTransferObjects\ScheduleHealthCheckData;
Health::for($team)->schedule(new ScheduleHealthCheckData(
check: DiskUsageCheck::class, frequency: 'hourly', maxAttempts: 2, decayMinutes: 60,
));
// Trait shortcuts for the same call:
$team->monitor(new ScheduleHealthCheckData(check: DiskUsageCheck::class, frequency: 'hourly'));
$team->createHealthCheck('disk_usage_check', '*/5 * * * *', maxAttempts: 2, decayMinutes: 60, tags: ['db']);new ScheduleHealthCheckData(
string $check, // Check class-string or registered key
string $frequency = '* * * * *', // preset name or cron
int $maxAttempts = 1,
int $decayMinutes = 1,
int $failAfter = 1,
int $recoverAfter = 1,
?int $timeout = null,
array $tags = [],
?array $notifyVia = null,
array $notifyViaLevels = [], // [level => channels]
array $escalation = [], // [threshold => group]
array $meta = [],
);The DTO resolves a class name to its key and a preset name to cron, and validates the result; a valid cron string is stored unchanged. createHealthCheck() is a positional shortcut that builds the same DTO and schedules it through Health::for($this).
Listing and removing schedules
Health::for($team)->monitors(); // Collection<HealthCheck>, oldest first
Health::for($team)->unmonitor(DiskUsageCheck::class); // soft-deletes the team's schedules; returns the count
Health::for($team)->unmonitor($healthCheck); // one row — refused if it belongs to another ownermonitors() lists only scheduled rows — the on-demand rows that run() keeps are left out. unmonitor() accepts a check class name, a registered key, a Check instance or a HealthCheck row. A row scheduled against a different owner throws InvalidHealthCheck — the handle is a security boundary.
The Frequency enum
use RoundlyConsulting\Alerts\Enums\Frequency;
Frequency::toCron('everyFiveMinutes'); // '*/5 * * * *' (names match case-insensitively)
Frequency::toCron('yearly'); // '@yearly'
Frequency::toCron('15 3 * * *'); // anything else is returned unchanged
Frequency::Hourly->value; // '@hourly'
Frequency::names(); // Collection: EveryMinute, EveryFiveMinutes, …Frequency has the same enums-for-laravel helpers as Status. Its values are cron strings, so build pickers from names() or a check’s frequencies() map rather than labels().
Working with the row
$row = $team->healthChecks()->first();
$row->health_check; // 'disk_usage_check'
$row->frequency; // '*/5 * * * *'
$row->consecutive_failures; // flap counter
$row->effectiveTags(); // row tags merged with the check's own tags()
$row->isScheduled(); // false for an on-demand (run-now) row
$row->isDue(); // is the cron due this minute? (never for an on-demand row)
$row->options()->failAfter(); // declarative options read back from meta
$row->dispatchHealthCheckJob(); // queue a run right now, due or not
$row->delete(); // soft delete — the scheduler skips it
$row->restore(); // and picks it up againShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.