Cron Jobs
Overview
Plugins can add scheduled tasks using cron. Unraid uses the standard cron daemon with files in /etc/cron.d/. Additionally, Unraid provides a built-in Dynamix Scheduler that offers a GUI-based interface for managing scheduled tasks.
Methods for Scheduling Tasks
There are three primary ways to schedule tasks in Unraid:
| Method | Best For | Persistence |
|---|---|---|
/etc/cron.d/ files |
System-level tasks | Recreate on boot |
| Dynamix Scheduler | User-configurable tasks | Automatic via GUI |
| User Scripts plugin | User-defined scripts | Via plugin settings |
Adding a Cron Job via /etc/cron.d/
Cron files should be installed to /etc/cron.d/ since this directory is in RAM and recreated on boot.
Via PLG File
Install cron jobs directly from your PLG file using a <FILE> element. Note the XML entity encoding: > for > and & for &. The job runs as root and redirects output to prevent cron email spam.
<FILE Name="/etc/cron.d/yourplugin">
<INLINE>
# Run every hour
0 * * * * root /usr/local/emhttp/plugins/yourplugin/scripts/hourly.sh >/dev/null 2>&1
</INLINE>
</FILE>
Via rc.d Script
For dynamic schedules or conditional cron jobs, create/remove the cron file from an rc.d script. This allows enabling/disabling scheduled tasks based on plugin settings without modifying the PLG file.
#!/bin/bash
# /etc/rc.d/rc.yourplugin
case "$1" in
'start')
# Create cron job
echo "*/5 * * * * root /usr/local/emhttp/plugins/yourplugin/check.sh" > /etc/cron.d/yourplugin
;;
'stop')
# Remove cron job
rm -f /etc/cron.d/yourplugin
;;
esac
Dynamix Scheduler Integration
The Dynamix Scheduler provides a GUI for scheduling tasks at Settings → Scheduler. Plugins can integrate with this system to let users configure schedules without editing cron files manually.
Scheduler Configuration File
The Dynamix Scheduler reads from /boot/config/plugins/dynamix/dynamix.cfg:
# Example scheduler entries in dynamix.cfg
parity="0|0|*|*|*" # Parity check schedule
mover="0|3|*|*|*" # Mover schedule
Adding Plugin Tasks to the Scheduler
To integrate with the Dynamix Scheduler, your plugin can add entries to the scheduler page:
<?
// In your settings page, provide scheduler controls
$cfg = parse_plugin_cfg("yourplugin");
// Default schedule: disabled
$schedule = $cfg['schedule'] ?? '';
// Parse cron schedule into components
if ($schedule) {
list($min, $hour, $dom, $mon, $dow) = explode(' ', $schedule);
} else {
$min = $hour = $dom = $mon = $dow = '*';
}
?>
Scheduler UI Controls
Use the standard Dynamix scheduler dropdown pattern:
<dl>
<dt>Schedule:</dt>
<dd>
<select name="schedule_type" onchange="toggleSchedule(this.value)">
<option value="disabled" <?=($cfg['schedule_type']=='disabled')?'selected':''?>>Disabled</option>
<option value="hourly" <?=($cfg['schedule_type']=='hourly')?'selected':''?>>Hourly</option>
<option value="daily" <?=($cfg['schedule_type']=='daily')?'selected':''?>>Daily</option>
<option value="weekly" <?=($cfg['schedule_type']=='weekly')?'selected':''?>>Weekly</option>
<option value="monthly" <?=($cfg['schedule_type']=='monthly')?'selected':''?>>Monthly</option>
<option value="custom" <?=($cfg['schedule_type']=='custom')?'selected':''?>>Custom</option>
</select>
</dd>
</dl>
<div id="schedule_time" style="display:none">
<dl>
<dt>Time:</dt>
<dd>
<select name="schedule_hour">
<? for ($h = 0; $h < 24; $h++): ?>
<option value="<?=$h?>" <?=($cfg['schedule_hour']==$h)?'selected':''?>>
<?=sprintf("%02d:00", $h)?>
</option>
<? endfor; ?>
</select>
</dd>
</dl>
</div>
<script>
function toggleSchedule(type) {
var timeDiv = document.getElementById('schedule_time');
timeDiv.style.display = (type !== 'disabled') ? 'block' : 'none';
}
// Initialize on load
toggleSchedule(document.querySelector('[name="schedule_type"]').value);
</script>
Writing the Cron Entry from Settings
When the user saves settings, write the cron file:
<?
// In your update.php or settings handler
function updateCronSchedule($cfg) {
$cronFile = "/etc/cron.d/yourplugin";
if ($cfg['schedule_type'] === 'disabled') {
// Remove cron job
@unlink($cronFile);
return;
}
// Build cron schedule based on type
switch ($cfg['schedule_type']) {
case 'hourly':
$schedule = "0 * * * *";
break;
case 'daily':
$hour = intval($cfg['schedule_hour']);
$schedule = "0 $hour * * *";
break;
case 'weekly':
$hour = intval($cfg['schedule_hour']);
$dow = intval($cfg['schedule_dow']);
$schedule = "0 $hour * * $dow";
break;
case 'monthly':
$hour = intval($cfg['schedule_hour']);
$dom = intval($cfg['schedule_dom']);
$schedule = "0 $hour $dom * *";
break;
case 'custom':
$schedule = $cfg['schedule_custom'];
break;
}
// Write cron file
$cronEntry = "$schedule root /usr/local/emhttp/plugins/yourplugin/scripts/scheduled_task.sh >/dev/null 2>&1\n";
file_put_contents($cronFile, $cronEntry);
}
?>
Cron Syntax Reference
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday = 0)
│ │ │ │ │
* * * * * user command
Common Patterns
| Pattern | Description |
|---|---|
0 * * * * |
Every hour (on the hour) |
*/5 * * * * |
Every 5 minutes |
0 0 * * * |
Daily at midnight |
0 3 * * * |
Daily at 3:00 AM |
0 3 * * 0 |
Weekly on Sunday at 3 AM |
0 0 1 * * |
Monthly on the 1st at midnight |
0 4 * * 1-5 |
Weekdays at 4:00 AM |
*/15 9-17 * * * |
Every 15 min during 9 AM - 5 PM |
User Context
Always specify the user (typically root) in /etc/cron.d/ files:
* * * * * root /path/to/script.sh
Unlike user crontabs,
/etc/cron.d/files require the username field. Omitting it will cause your job to fail silently.
Logging Output
# Discard output (silent)
0 * * * * root /script.sh >/dev/null 2>&1
# Log to file
0 * * * * root /script.sh >> /var/log/yourplugin.log 2>&1
# Log to syslog
0 * * * * root /script.sh 2>&1 | logger -t yourplugin
# Log with timestamp to file
0 * * * * root /script.sh >> /var/log/yourplugin.log 2>&1 && echo "$(date): Task completed" >> /var/log/yourplugin.log
Persistence
Remember that /etc/cron.d/ is in RAM. Your cron jobs must be recreated:
- On boot (via PLG FILE element or rc.d script)
- After array start if they depend on array
Ensuring Cron Jobs Persist
Option 1: PLG FILE element (recommended for static schedules)
<FILE Name="/etc/cron.d/yourplugin">
<INLINE>
0 * * * * root /usr/local/emhttp/plugins/yourplugin/scripts/task.sh >/dev/null 2>&1
</INLINE>
</FILE>
Option 2: rc.d script (recommended for configurable schedules)
#!/bin/bash
# /etc/rc.d/rc.yourplugin
PLUGIN="yourplugin"
CRONFILE="/etc/cron.d/$PLUGIN"
CONFIG="/boot/config/plugins/$PLUGIN/$PLUGIN.cfg"
case "$1" in
'start')
# Read schedule from config and write cron entry
if [ -f "$CONFIG" ]; then
source "$CONFIG"
if [ -n "$SCHEDULE" ] && [ "$SCHEDULE" != "disabled" ]; then
echo "$SCHEDULE root /usr/local/emhttp/plugins/$PLUGIN/scripts/task.sh >/dev/null 2>&1" > "$CRONFILE"
fi
fi
;;
'stop')
rm -f "$CRONFILE"
;;
'restart')
$0 stop
$0 start
;;
esac
Option 3: Event script (for array-dependent tasks)
#!/bin/bash
# /usr/local/emhttp/plugins/yourplugin/event/started_array
echo "0 * * * * root /usr/local/emhttp/plugins/yourplugin/scripts/array_task.sh >/dev/null 2>&1" > /etc/cron.d/yourplugin_array
Running Scripts at Specific Array States
Sometimes you want cron jobs only when the array is running:
#!/bin/bash
# /usr/local/emhttp/plugins/yourplugin/scripts/conditional_task.sh
# Check if array is started
if [ ! -d /mnt/user ]; then
logger -t "yourplugin" "Array not started, skipping task"
exit 0
fi
# Your task here
logger -t "yourplugin" "Running scheduled task"
Debugging Cron Jobs
Check if cron is running
ps aux | grep cron
View cron execution in syslog
tail -f /var/log/syslog | grep CRON
Test your script manually
/usr/local/emhttp/plugins/yourplugin/scripts/task.sh
echo $? # Check exit code
Verify cron file syntax
cat /etc/cron.d/yourplugin
# Ensure: schedule user command format
# Ensure: file has newline at end
Best Practices
- Always escape XML entities in PLG files (
>becomes>,&becomes&) - Redirect output to avoid email spam from cron
- Add logging for debugging scheduled tasks
- Check array state before accessing array paths
- Use locking to prevent overlapping runs of long tasks:
#!/bin/bash
LOCKFILE="/var/run/yourplugin_task.lock"
# Check for existing lock
if [ -f "$LOCKFILE" ]; then
# Check if process is actually running
PID=$(cat "$LOCKFILE")
if kill -0 "$PID" 2>/dev/null; then
logger -t "yourplugin" "Task already running (PID $PID)"
exit 0
fi
fi
# Create lock
echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT
# Your task here
sleep 60
logger -t "yourplugin" "Task completed"