Cron Schedule
A human-readable visualiser for a 5-field cron expression — a plain-English summary, a per-field breakdown and upcoming run times.
Preview
At 09:00 on Mon, Tue, Wed, Thu, Fri
- Minute
- 0
- :00
- Hour
- 9
- 9 AM
- Day of month
- *
- every day
- Month
- *
- every month
- Day of week
- 1-5
- Mon, Tue, Wed, Thu, Fri
<div class="w-full max-w-md">
<x-ui.cron-schedule
expression="0 9 * * 1-5"
title="Daily standup reminder"
:show-next-runs="3"
reference-date="2026-06-13 08:00:00"
/>
</div>
Installation
php artisan ui:add cron-schedule
Registry contract
php artisan ui:add cron-schedule
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/cron-schedule.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Cron Schedule (`cron-schedule`)
A human-readable visualiser for a 5-field cron expression — a plain-English summary, a per-field breakdown and upcoming run times.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add cron-schedule
```
## Usage
```blade
<div class="w-full max-w-md">
<x-ui.cron-schedule
expression="0 9 * * 1-5"
title="Daily standup reminder"
:show-next-runs="3"
reference-date="2026-06-13 08:00:00"
/>
</div>
```
## Props
- `expression` (string, default `* * * * *`)
- `title` (mixed|null, default `null`)
- `showNextRuns` (int, default `0`)
- `referenceDate` (mixed|null, default `null`)
## Use when
- Use for exact technical detail, code readability, diagnostics, and copyable reference material.
- Explaining what a cron expression actually does, next to the expression itself.
- Confirming a schedule before it is saved, by showing the upcoming run times.
## Avoid when
- Do not surface developer-specific patterns in end-user flows unless the task genuinely requires them.
- Editing a schedule; this is a read-only explainer.
- Parsing a non-standard expression: only the 5-field form is understood.
## Anti-patterns
- Exposing developer tooling in end-user flows without a task need
## Rules
- Use the `<brok:cron-schedule>` tag (or `<x-ui.cron-schedule>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/cron-schedule
- Registry JSON (files, props, contract): https://brokui.dev/r/open/cron-schedule.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| expression | string | * * * * * | |
| title | mixed | null | null | |
| showNextRuns | int | 0 | |
| referenceDate | mixed | null | null |
Slots
default—
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- The expression is summarised in plain English, then broken down field by field.
- showNextRuns lists that many upcoming runs, computed from referenceDate when one is given so the output is deterministic.
- An expression that cannot be parsed is reported as invalid rather than guessed at.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Expose exact technical or diagnostic information.
Use when
- Use for exact technical detail, code readability, diagnostics, and copyable reference material.
- Explaining what a cron expression actually does, next to the expression itself.
- Confirming a schedule before it is saved, by showing the upcoming run times.
Avoid when
- Do not surface developer-specific patterns in end-user flows unless the task genuinely requires them.
- Editing a schedule; this is a read-only explainer.
- Parsing a non-standard expression: only the 5-field form is understood.
Use instead
- Plain-language product UI for nontechnical users
Anti-patterns
- Exposing developer tooling in end-user flows without a task need
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- The plain-English summary is text, so the schedule is readable without decoding the five fields.
- Field labels are associated with their values, and decorative separators are aria-hidden.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'expression' => '* * * * *',
'title' => null,
'showNextRuns' => 0,
'referenceDate' => null,
])
@php
use Illuminate\Support\Carbon;
$showNextRuns = max(0, (int) $showNextRuns);
// ── Parse the five fields into materialised value sets ───────────────────
// Each field becomes a sorted list of allowed integers plus an "every" flag
// (the field was "*"). Supports lists (a,b), ranges (a-b) and steps (*/n,
// a-b/n). Out-of-range or unparseable tokens are ignored.
$parseField = function (string $raw, int $lo, int $hi): array {
$raw = trim($raw);
$isStar = $raw === '*' || $raw === '?';
$set = [];
foreach (explode(',', $raw) as $part) {
$step = 1;
if (str_contains($part, '/')) {
[$part, $stepRaw] = explode('/', $part, 2);
$step = max(1, (int) $stepRaw);
}
if ($part === '*' || $part === '?' || $part === '') {
$from = $lo;
$to = $hi;
} elseif (str_contains($part, '-')) {
[$a, $b] = array_pad(explode('-', $part, 2), 2, $lo);
$from = (int) $a;
$to = (int) $b;
} else {
$from = $to = (int) $part;
}
for ($v = $from; $v <= $to; $v += $step) {
if ($v >= $lo && $v <= $hi) {
$set[$v] = true;
}
}
}
$set = array_keys($set);
sort($set);
return ['set' => $set ?: range($lo, $hi), 'star' => $isStar];
};
$fields = preg_split('/\s+/', trim((string) $expression));
$valid = count($fields) === 5;
if (! $valid) {
$fields = ['*', '*', '*', '*', '*'];
}
$minute = $parseField($fields[0], 0, 59);
$hour = $parseField($fields[1], 0, 23);
$dom = $parseField($fields[2], 1, 31);
$month = $parseField($fields[3], 1, 12);
// Normalise 7 → 0 (both mean Sunday) in the weekday field.
$dowRaw = str_replace('7', '0', $fields[4]);
$dow = $parseField($dowRaw, 0, 6);
$dowNames = [0 => __('Sun'), 1 => __('Mon'), 2 => __('Tue'), 3 => __('Wed'), 4 => __('Thu'), 5 => __('Fri'), 6 => __('Sat')];
$monthNames = [1 => __('Jan'), 2 => __('Feb'), 3 => __('Mar'), 4 => __('Apr'), 5 => __('May'), 6 => __('Jun'), 7 => __('Jul'), 8 => __('Aug'), 9 => __('Sep'), 10 => __('Oct'), 11 => __('Nov'), 12 => __('Dec')];
$hour12 = function (int $h): string {
$suffix = $h < 12 ? 'AM' : 'PM';
$base = $h % 12;
if ($base === 0) {
$base = 12;
}
return $base.' '.$suffix;
};
// ── Per-field human interpretation (the breakdown rows) ──────────────────
$rows = [
['name' => __('Minute'), 'raw' => $fields[0], 'human' => $minute['star'] ? __('every minute') : implode(', ', array_map(fn ($m) => sprintf(':%02d', $m), $minute['set']))],
['name' => __('Hour'), 'raw' => $fields[1], 'human' => $hour['star'] ? __('every hour') : implode(', ', array_map($hour12, $hour['set']))],
['name' => __('Day of month'), 'raw' => $fields[2], 'human' => $dom['star'] ? __('every day') : implode(', ', $dom['set'])],
['name' => __('Month'), 'raw' => $fields[3], 'human' => $month['star'] ? __('every month') : implode(', ', array_map(fn ($mo) => $monthNames[$mo], $month['set']))],
['name' => __('Day of week'), 'raw' => $fields[4], 'human' => $dow['star'] ? __('every weekday') : implode(', ', array_map(fn ($d) => $dowNames[$d], $dow['set']))],
];
// ── Plain-English summary (best-effort for common shapes) ────────────────
if ($minute['star'] && $hour['star']) {
$timePart = __('Every minute');
} elseif (! $minute['star'] && count($minute['set']) === 1 && ! $hour['star'] && count($hour['set']) === 1) {
$timePart = __('At').' '.sprintf('%02d:%02d', $hour['set'][0], $minute['set'][0]);
} elseif (! $minute['star'] && count($minute['set']) === 1 && $hour['star']) {
$timePart = __('At ::minute past every hour', ['minute' => sprintf('%02d', $minute['set'][0])]);
} else {
$timePart = __('At multiple times');
}
if (! $dow['star']) {
$dayPart = __('on').' '.implode(', ', array_map(fn ($d) => $dowNames[$d], $dow['set']));
} elseif (! $dom['star']) {
$dayPart = __('on day').' '.implode(', ', $dom['set']).' '.__('of the month');
} else {
$dayPart = __('every day');
}
$monthPart = $month['star'] ? '' : ' '.__('in').' '.implode(', ', array_map(fn ($mo) => $monthNames[$mo], $month['set']));
$summary = trim($timePart.' '.$dayPart.$monthPart);
// ── Upcoming runs (day-stepping; bounded) ────────────────────────────────
$nextRuns = [];
if ($valid && $showNextRuns > 0) {
$ref = $referenceDate ? Carbon::parse($referenceDate) : Carbon::now();
$dayMatches = function (Carbon $day) use ($dom, $month, $dow): bool {
if (! in_array($day->month, $month['set'], true)) {
return false;
}
$domOk = in_array($day->day, $dom['set'], true);
$dowOk = in_array($day->dayOfWeek, $dow['set'], true);
if (! $dom['star'] && ! $dow['star']) {
return $domOk || $dowOk; // cron "OR" quirk when both are restricted
}
return $domOk && $dowOk;
};
$day = $ref->copy()->startOfDay();
$guard = 0;
while (count($nextRuns) < $showNextRuns && $guard < 1500) {
if ($dayMatches($day)) {
foreach ($hour['set'] as $h) {
foreach ($minute['set'] as $mi) {
$candidate = $day->copy()->setTime($h, $mi);
if ($candidate->gt($ref)) {
$nextRuns[] = $candidate;
if (count($nextRuns) >= $showNextRuns) {
break 2;
}
}
}
}
}
$day->addDay();
$guard++;
}
}
@endphp
<div
data-slot="cron-schedule"
{{ $attributes->merge(['class' => 'overflow-hidden rounded-lg border border-border bg-card text-foreground']) }}
>
<div class="border-b border-border px-4 py-3">
@if ($title !== null)
<div class="mb-1 text-sm font-semibold text-foreground">{{ $title }}</div>
@endif
<div class="flex flex-wrap items-center gap-2 font-mono text-sm" aria-label="{{ __('Cron expression') }}: {{ $expression }}">
@foreach ($fields as $field)
<span class="rounded bg-muted px-2 py-0.5 text-foreground">{{ $field }}</span>
@endforeach
</div>
<p class="mt-2 text-sm text-muted-foreground">{{ $summary }}</p>
</div>
<dl class="divide-y divide-border">
@foreach ($rows as $row)
<div class="grid grid-cols-[8rem_4rem_1fr] items-center gap-2 px-4 py-2 text-sm">
<dt class="text-muted-foreground">{{ $row['name'] }}</dt>
<dd class="font-mono text-foreground">{{ $row['raw'] }}</dd>
<dd class="text-muted-foreground">{{ $row['human'] }}</dd>
</div>
@endforeach
</dl>
@if ($nextRuns !== [])
<div class="border-t border-border px-4 py-3">
<div class="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">{{ __('Next runs') }}</div>
<ul class="space-y-1">
@foreach ($nextRuns as $run)
<li class="flex items-center gap-2 text-sm">
<svg class="size-4 shrink-0 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></svg>
<time datetime="{{ $run->toIso8601String() }}" class="font-mono text-foreground">{{ $run->format('D, M j Y, H:i') }}</time>
</li>
@endforeach
</ul>
</div>
@endif
</div>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.5 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0