Berik Ashimov // senior.it.engineer
Download CV
POSTS / MIKROTIK

Scheduler and Event-Driven Automation on RouterOS 7: Scripts That Run Themselves

// series · MikroTik RouterOS 7 Guide part 24 of 35
  1. ... 21 earlier
  2. Containers on RouterOS 7: Useful, Dangerous, and Frequently Misused
  3. RouterOS Scripting That Does Not Bite: Scope, Arrays, and Error Handling
  4. Scheduler and Event-Driven Automation on RouterOS 7: Scripts That Run Themselves
  5. Automating RouterOS 7 with the REST API and Ansible: Desired State, Finally
  6. Monitoring RouterOS 7: SNMPv3, Prometheus, and the Metrics That Actually Matter
  7. ... 9 later
view the full series →

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

Terminal window
/system scheduler add name=nightly-backup interval=1d start-time=03:15 \
on-event="/system script run backup-to-remote" policy=read,write,test,sensitive

The 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:

Terminal window
# 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 false

Wrap 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”:

Terminal window
/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:

Terminal window
/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:

Terminal window
# /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:

Terminal window
: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=no

URL-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

Terminal window
/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 breadcrumbs

run-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.

// share