The previous article covered the scripting language itself. This one is about making scripts run without you: on a schedule, on link failure, on a DHCP lease, on an event. This is where RouterOS automation earns its keep — and where it bites, because a script that runs unattended at 4 AM has no one watching it misbehave. The difference between automation and a time bomb is the discipline around it: idempotency, overlap guards, and alerts that tell you what happened.
The Scheduler
/system scheduler add name=nightly-backup interval=1d start-time=03:15 \ on-event="/system script run backup-to-remote" policy=read,write,test,sensitiveThe rules I follow after years of scheduled-task archaeology on various platforms:
Schedulers call named scripts; they don’t inline code. on-event accepts raw script text, and stuffing forty lines into it is how config exports become unreadable and diffs become lies. One-liner in the scheduler, logic in /system script where it’s versionable and testable by hand with /system script run.
interval vs start-time semantics matter. interval=1d start-time=03:15 runs daily at 03:15. start-time=startup with an interval runs relative to boot — useful for “every 5 minutes forever”, wrong for “at 3 AM”. And a scheduler with interval=0 fires exactly once at start-time, which is occasionally exactly what you want (delayed one-shot after a maintenance reboot).
Stagger your schedules. Backup at 03:15, cleanup at 03:40, update check at 04:05. Five jobs all at 03:00 compete for CPU and flash I/O on a router that might also be pushing packets.
Policies are least-privilege. A scheduler’s policy caps what the script can do. The alert script that only reads interface state and calls fetch doesn’t need write or sensitive. The scripting article covered why dont-require-permissions deserves suspicion; the same logic applies here.
Overlap: The Classic Silent Failure
The scheduler has no concept of “still running.” A 5-minute-interval script that occasionally takes 6 minutes (a fetch to a slow endpoint, a big export) gets a second instance started on top of the first. Two instances writing the same file or modifying the same address-list interleave into garbage.
The guard is a global lock flag:
# inside the script:global jobRunning:if ($jobRunning = true) do={ :log warning "job: previous run still active, skipping" :error "overlap"}:set jobRunning true:do { # ... actual work ...} on-error={ :log error "job: failed" }:set jobRunning falseWrap the work in :do ... on-error so the flag always clears — otherwise one failure wedges the lock until reboot (globals don’t survive reboot, which here is a feature). For extra safety on long jobs, log start and end; “started, never finished” in the log is diagnostic gold.
Netwatch: Automation on Reachability Events
Netwatch pings a target and runs scripts on state transitions — the primary event hook for “do something when X becomes unreachable”:
/tool netwatch add host=8.8.8.8 interval=10s timeout=2s \ up-script="/system script run wan1-restored" \ down-script="/system script run wan1-failed" \ comment="WAN1 health via 8.8.8.8"Note the quoting: up-script/down-script hold script source, not a script name — calling a named /system script from them keeps the netwatch entry a one-liner and the logic where it belongs.
The multi-WAN article used netwatch for failover; the general pattern is wider. down-script might disable a route, switch a DNS server, or just alert. Rules of engagement:
Probe through the path you’re testing. Netwatch pings from the router; if policy routing can send that probe out another WAN, your “WAN1 health check” is testing WAN2. Pin the probe: newer 7.x netwatch supports options including source/routing controls (check /tool netwatch parameters on your version — this area gained features through 7.x, including probe types beyond ICMP like TCP and DNS checks). Where the version falls short, probe a target that only exists via that path (the provider’s gateway).
Debounce. interval=10s with a flappy target executes your scripts every ten seconds. If the action is heavy (interface disable, config change), make the script itself check how recently it last acted — a timestamp in a global — or lengthen the interval. An aggressive netwatch on a marginal link turns one problem into a config that thrashes.
Both scripts, always. A down-script without an up-script is a one-way valve; manual cleanup will be forgotten.
DHCP Lease Scripts
The DHCP server can run a script on every lease event — assignment and release:
/ip dhcp-server set lan-dhcp lease-script="lease-notify"Inside the script, RouterOS provides variables: leaseBound (1 on assign, 0 on release), leaseActIP, leaseActMAC, leaseServerName. The canonical uses:
# /system script add name=lease-notify source=...:if ($leaseBound = 1) do={ :log info "DHCP: new lease $leaseActIP -> $leaseActMAC" # e.g. add unknown MACs to a watch list :if ([:len [/ip dhcp-server lease find mac-address=$leaseActMAC and comment~"known"]] = 0) do={ /ip firewall address-list add list=unknown-dhcp address=$leaseActIP timeout=1h }}New-device alerting on a home or office network is genuinely useful security signal — combined with the fetch-based notification below, you learn about every unfamiliar MAC within seconds. Keep lease scripts fast: they run inline with DHCP processing, and a slow fetch inside one delays lease handout. Better: have the lease script set a flag/list entry, and let a scheduler do the slow notification out-of-band.
Alerts: Getting Events Off the Router
A log entry nobody reads is not an alert. /tool fetch pushes events anywhere with an HTTP endpoint; Telegram is the path of least resistance:
:local token "123456:ABC-your-bot-token":local chat "-1001234567890":local msg "[$[/system identity get name]] WAN1 down at $[/system clock get time]"/tool fetch url="https://api.telegram.org/bot$token/sendMessage?chat_id=$chat&text=$msg" \ keep-result=noURL-encode gotchas apply — spaces are fine in practice via fetch, but special characters in messages will bite; keep alert text simple. For email, /tool e-mail works once configured with a submission server, but in 2026 an HTTP webhook (Telegram, Slack, ntfy, your own endpoint) is more reliable than SMTP from a router IP that half the world’s mail filters distrust.
Wrap fetch in :do {} on-error {} — the alert path failing must never break the script’s real work. And alert on recovery too; “resolved” messages are what let you sleep through the ones that fix themselves.
Config-as-Script and Its Limits
Since scripts can contain any command, it’s tempting to treat a .rsc file as your config management: keep the golden config as a script, /import it on fresh boxes. This works for initial provisioning — the first-boot article’s hardening steps make a perfect import file — and falls apart for ongoing management, because RouterOS imports are imperative: running the same add twice creates duplicates, there’s no diffing, no convergence, no rollback. Import-based management drifts within weeks.
Idempotency needs find-before-act everywhere (:if ([:len [find ...]] = 0) do={ add ... }), which turns a 20-line config into 60 lines of guards. At that point you’ve reinvented a bad configuration management tool. The honest boundary: scripts and scheduler for reactions (events, health, alerts, backups) and one-shot provisioning; an external system for desired state. That external system is the next article — the REST API and Ansible, where idempotency is the tool’s job instead of yours.
Verification
/system scheduler print detail # next-run times sanity check/system script print # run-count column: is it actually firing?/tool netwatch print # current up/down state and since when/log print where topics~"script|error" # your scripts' own breadcrumbsrun-count is the quiet hero: a scheduler that looks configured but has run-count=0 after a week is disabled, policy-blocked, or start-time-confused. Check it after every change.
Closing Thoughts
RouterOS event automation is a small toolkit — scheduler, netwatch, lease scripts, fetch — that composes into real operational coverage: nightly backups shipped off-box, WAN failure handled and announced, strange MACs flagged before anyone asks “what’s that device?” Keep each unit small, guarded against overlap, wrapped in on-error, and loud about what it did. And know the boundary: when you find yourself writing find-before-act guards around config changes, stop — that’s the API and Ansible’s job, and it’s exactly where this series goes next.