Scheduled Tasks
Scheduled Tasks
Cbox Init includes a built-in cron-like scheduler for running periodic tasks without requiring a separate cron daemon.
Overview
The scheduler provides:
- ✅ Standard cron format: Familiar 5-field syntax
- ✅ Per-task execution history: Bounded ring buffer of past runs with exit codes and durations
- ✅ Per-task statistics: Run/success/failure counts, success rate, average duration
- ✅ Overlap protection: A job never runs concurrently with itself by default
- ✅ Pause / resume / manual trigger: Over the management API
- ✅ Graceful shutdown: Running tasks are cancelled cleanly on shutdown
- ✅ No cron daemon: Self-contained scheduling in Go
Not implemented: outbound heartbeat pings to services such as healthchecks.io, and Prometheus metrics for scheduled tasks. Query the schedule status API instead. See Heartbeat Monitoring for the current status.
Basic Configuration
processes:
backup-job:
enabled: true
command: ["php", "artisan", "backup:run"]
schedule: "0 2 * * *" # Daily at 2 AM
restart: never # Important for scheduled tasks
Cron Schedule Format
5-Field Syntax
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
Special Characters
| Character | Meaning | Example |
|---|---|---|
* |
Any value | * * * * * = every minute |
, |
Value list | 1,15,30 = minutes 1, 15, 30 |
- |
Range | 1-5 = Monday through Friday |
/ |
Step | */15 = every 15 minutes |
Common Patterns
schedule: "* * * * *" # Every minute
schedule: "*/5 * * * *" # Every 5 minutes
schedule: "30 * * * *" # Every hour at :30
schedule: "0 2 * * *" # Daily at 2 AM
schedule: "0 9 * * 1-5" # Every weekday at 9 AM
schedule: "0 0 1 * *" # First day of month
schedule: "0 6,18 * * *" # Twice daily (6 AM and 6 PM)
schedule: "0 9-17 * * 1-5" # Business hours (9-5, Mon-Fri)
Schedule Fields
| Field | Type | Default | Description |
|---|---|---|---|
schedule |
string | — | 5-field cron expression |
schedule_timezone |
string | UTC |
UTC, Local, or an IANA name (America/New_York) |
schedule_timeout |
duration | none | Kill the run if it exceeds this (30s, 5m, 1h) |
schedule_max_concurrent |
int | 0 |
0/1 = no overlap; >1 = allow N parallel runs |
The retained history size per job is set globally with schedule_history_size
(default 100).
Task Execution
Execution Lifecycle
[Cron Trigger]
↓
[Skip if already running] (overlap protection)
↓
[Start task process]
↓
[Wait for completion or timeout]
↓
[Record execution in history]
↓
[Wait for next trigger]
A job's state is one of idle, executing, or paused.
Environment Variables
Each scheduled task process receives these environment variables in addition to
its own env:
CBOX_INIT_PROCESS=backup-job # The process name
CBOX_INIT_SCHEDULED=true # Marks this as a scheduled run
Use in scripts:
#!/bin/bash
echo "Scheduled task: $CBOX_INIT_PROCESS"
Concurrency Control
schedule_max_concurrent
Prevents task overlap by limiting concurrent executions:
processes:
database-sync:
command: ["php", "artisan", "sync:database"]
schedule: "*/5 * * * *" # Every 5 minutes
schedule_max_concurrent: 1 # Skip if previous run still active
Values:
0or1- No overlap (skip trigger if the previous run is still active)N(>1) - Allow up to N concurrent executions
schedule_timeout
Kills tasks that exceed a maximum execution time:
processes:
backup:
command: ["php", "artisan", "backup:run"]
schedule: "0 2 * * *"
schedule_timeout: "30m" # Cancel if it runs longer than 30 minutes
Duration formats: 30s, 5m, 1h, 1h30m
Best practice: Set the timeout below the schedule interval to prevent overlap.
Combined Example
processes:
long-task:
command: ["php", "artisan", "process:large-dataset"]
schedule: "0 * * * *" # Every hour
schedule_timeout: "55m" # Cancel if it exceeds 55 minutes
schedule_max_concurrent: 1 # No overlap
restart: never
Schedule Status API
Cbox Init records real per-task execution history and statistics, exposed over
the management API (enable it with api_enabled: true).
Status
curl http://localhost:9180/api/v1/processes/backup-job/schedule
{
"process": "backup-job",
"schedule": {
"name": "backup-job",
"schedule": "0 2 * * *",
"timezone": "UTC",
"state": "idle",
"last_run": "2026-08-26T02:00:00Z",
"next_run": "2026-08-27T02:00:00Z",
"stats": {
"total_executions": 30,
"success_count": 29,
"failure_count": 1,
"running_count": 0,
"success_rate": 96.67,
"average_duration": 45200000000,
"last_execution_time": "2026-08-26T02:00:00Z",
"last_success_time": "2026-08-26T02:00:45Z",
"last_failure_time": "2026-08-20T02:00:12Z"
}
}
}
average_duration is a Go duration in nanoseconds (here ≈ 45.2s).
History
curl "http://localhost:9180/api/v1/processes/backup-job/schedule/history?limit=5"
{
"process": "backup-job",
"limit": 5,
"count": 1,
"history": [
{
"id": 42,
"start_time": "2026-08-26T02:00:00Z",
"end_time": "2026-08-26T02:00:45Z",
"exit_code": 0,
"success": true,
"error": "",
"triggered": "schedule"
}
]
}
triggered is schedule for cron-driven runs and manual for API triggers.
Pause, Resume, Trigger
# Pause (skip scheduled triggers until resumed)
curl -X POST http://localhost:9180/api/v1/processes/backup-job/schedule/pause
# Resume
curl -X POST http://localhost:9180/api/v1/processes/backup-job/schedule/resume
# Trigger now (async; returns immediately)
curl -X POST http://localhost:9180/api/v1/processes/backup-job/schedule/trigger
# Trigger now and wait for the exit code (synchronous)
curl -X POST "http://localhost:9180/api/v1/processes/backup-job/schedule/trigger?sync=true"
Complete Example
version: "1.0"
global:
api_enabled: true
api_port: 9180
processes:
# Database backup - Daily at 2 AM
database-backup:
enabled: true
command: ["php", "artisan", "backup:database"]
schedule: "0 2 * * *"
schedule_timeout: "10m"
restart: never
env:
BACKUP_PATH: /backups
RETENTION_DAYS: "30"
# Cache warming - Every 15 minutes
cache-warmer:
enabled: true
command: ["php", "artisan", "cache:warm"]
schedule: "*/15 * * * *"
schedule_max_concurrent: 1
restart: never
# Reports - Hourly during business hours
hourly-reports:
enabled: true
command: ["php", "artisan", "reports:generate"]
schedule: "0 9-17 * * 1-5" # 9 AM - 5 PM, Mon-Fri
restart: never
# Weekly maintenance - Sunday at 3 AM
weekly-maintenance:
enabled: true
command: ["/usr/local/bin/maintenance.sh"]
schedule: "0 3 * * 0" # Sunday
restart: never
env:
OPTIMIZE_DATABASE: "true"
Laravel Scheduler Integration
Option 1: Cbox Init Native Scheduling
processes:
backup-daily:
command: ["php", "artisan", "backup:run"]
schedule: "0 2 * * *"
restart: never
emails-hourly:
command: ["php", "artisan", "emails:send"]
schedule: "0 * * * *"
restart: never
Pros: individual task monitoring via the schedule API, per-task timeouts, direct control over each schedule.
Option 2: Laravel Scheduler
processes:
laravel-scheduler:
enabled: true
command: ["php", "artisan", "schedule:run"]
schedule: "* * * * *" # Let Laravel decide which tasks run
restart: never
app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
$schedule->command('backup:run')->daily();
$schedule->command('emails:send')->hourly();
$schedule->command('cache:prune')->daily();
}
Trade-off: centralised task definition in code, but all tasks share one process and one execution-history entry per minute.
Best Practices
✅ Do
Always use restart: never so a completed task is not restarted:
scheduled-task:
schedule: "0 2 * * *"
restart: never
Bound long tasks with schedule_timeout and keep it below the interval:
hourly-task:
schedule: "0 * * * *" # Every hour
schedule_timeout: "55m" # Below the 60-minute interval
Make tasks idempotent so retries and manual triggers are safe.
❌ Don't
Don't use restart: always — the task would rerun immediately after finishing:
# ❌ Bad
task:
schedule: "0 2 * * *"
restart: always
# ✅ Good
task:
schedule: "0 2 * * *"
restart: never
Don't run daemons on a schedule — scheduled commands must exit:
# ❌ Bad — never exits
task:
schedule: "* * * * *"
command: ["./background-daemon"]
# ✅ Good — runs and exits
task:
schedule: "* * * * *"
command: ["./process-batch-then-exit"]
Troubleshooting
Task Not Running
- Validate the cron expression (e.g. with crontab.guru).
- Confirm the process is
enabled: true. - Check the logs for the scheduler registering the job.
Task Runs Multiple Times
Set restart: never — a scheduled task with restart: always reruns immediately after completing.
Task Overlaps Itself
Set schedule_max_concurrent: 1 (the default already skips overlapping runs) and/or a schedule_timeout below the interval.
See Also
- Process Configuration - Schedule configuration
- Management API - Runtime task inspection and control
- Examples - Practical examples