Variables, conditionals, loops, functions, and the scheduler — with real examples: Wake-on-LAN automation, Telegram alerts, and an auto-backup script you can deploy today.
Every Netwatch alert, every scheduled backup, every WoL trigger in this tutorial series is a script. RouterOS has a complete scripting language built in — variables, conditionals, loops, functions, and a scheduler that runs scripts on any time pattern you define. You don't need to know Python or Bash. You just need to know the syntax, which is unique to RouterOS but consistent and learnable in an afternoon.
This tutorial builds from the ground up: syntax rules, variables, control flow, functions, then three complete real-world scripts you can deploy immediately — a Telegram alert when any host goes down, a Wake-on-LAN automation, and an auto-backup that emails you a config file every night.
01
RouterOS scripting has its own syntax that looks like nothing else. Learn these five rules and the rest falls into place.
# ── Rule 1: Keywords start with colon ──────────────────────────────────── # Every scripting keyword is prefixed with ":" # :local :global :if :foreach :while :do :return :log :put :set :log info "this is a log message" :put "this prints to terminal" # ── Rule 2: Variables declared with :local or :global ──────────────────── # :local = exists only in this script/function # :global = persists across scripts and scheduler runs :local myName "Hotel-01" :global lastBackup # ── Rule 3: Variables referenced with $ prefix ─────────────────────────── :local greeting "Hello" :put $greeting # prints: Hello :put ("prefix-" . $greeting) # . concatenates strings → prefix-Hello # ── Rule 4: Blocks use { } — NOT indentation ───────────────────────────── # if/else, loops, and functions all use curly braces for their bodies :if ($x > 0) do={ :put "positive" } else={ :put "zero or negative" } # ── Rule 5: Strings use double quotes, numbers are bare ────────────────── :local name "router-01" # string — double quotes :local count 5 # number — no quotes :local flag true # boolean — no quotes :local ts [/system clock get time] # capture command output with [ ]
02
RouterOS has six data types. Knowing which one you're working with prevents 90% of scripting bugs.
| Type | Example | Notes |
|---|---|---|
| str | "hello" | Text. Concatenate with the . operator. |
| num | 42 | Integer. Arithmetic with + - * /. |
| bool | true / false | Used in conditions. Returned by comparisons. |
| ip | 192.168.1.1 | IP address. Supports subnet operations. |
| array | {1;2;3} | Semicolon-separated. Iterate with :foreach. |
| nothing | [:toarray ""] | Empty / nil. Check with [:len $var] = 0. |
# String concatenation with . :local hostname [/system identity get name] :local msg ("Alert from: " . $hostname) :put $msg # → Alert from: Hotel-01 # Capture command output into a variable using [ ] :local uptime [/system resource get uptime] :local version [/system resource get version] :put ("Version: " . $version . " Uptime: " . $uptime) # Array declaration and access :local servers {192.168.20.50; 192.168.20.51; 192.168.20.52} :put ($servers->0) # → 192.168.20.50 (zero-indexed) :put [:len $servers] # → 3 (length of array) # Type conversion :local numStr "42" :local num [:tonum $numStr] :put ($num + 1) # → 43 # Global variable — persists between scheduler runs :global failCount :if (![:isset $failCount]) do={ :set failCount 0 } :set failCount ($failCount + 1) # Use globals carefully — they persist until router reboot.
If your string contains double quotes, escape them with a backslash: \". This is the most common syntax error in Netwatch scripts. For example, a log message inside a down-script must be written as: :log warning "host \"192.168.20.50\" is DOWN". Getting this wrong causes the script to fail silently with no error in the log — the scheduler just skips execution.
03
:if checks a condition. :foreach iterates an array. :while loops until a condition is false. These three cover every control flow pattern you'll need.
# Basic if/else :local x 10 :if ($x > 5) do={ :log info "x is greater than 5" } else={ :log info "x is 5 or less" } # Comparison operators # = equal != not equal # > greater than < less than # >= greater/equal <= less/equal # && logical AND || logical OR ! NOT # Check if a DHCP lease exists for a hostname :local leaseCount [/ip dhcp-server lease count-numbers \ where host-name="fileserver"] :if ($leaseCount > 0) do={ :log info "fileserver has a DHCP lease" } else={ :log warning "fileserver not found in DHCP leases" } # Nested condition with AND :local cpu [/system resource get cpu-load] :local mem [/system resource get free-memory] :if ($cpu > 80 && $mem < 10000000) do={ :log warning "High CPU and low memory simultaneously" }
# Iterate a static array :local vlans {10; 20; 30; 40} :foreach vlan in=$vlans do={ :put ("Processing VLAN: " . $vlan) } # Iterate over RouterOS objects — find returns an array of internal IDs :foreach leaseId in=[/ip dhcp-server lease find \ where status=bound] do={ :local ip [/ip dhcp-server lease get $leaseId address] :local host [/ip dhcp-server lease get $leaseId host-name] :put ($host . " → " . $ip) } # Prints every bound DHCP lease as "hostname → IP" # Count iterations with an index variable :local count 0 :foreach i in=[/interface find where running=yes] do={ :set count ($count + 1) } :put ("Running interfaces: " . $count)
# Wait for a host to come up after WoL (ping loop with timeout) :local target "192.168.20.50" :local attempts 0 :local maxAttempts 12 :local isUp false :while ($attempts < $maxAttempts && !$isUp) do={ :delay 5s :local result [/ping address=$target count=1 as-value] :if ($result->"received" > 0) do={ :set isUp true :log info ("Host " . $target . " is UP after WoL") } :set attempts ($attempts + 1) } :if (!$isUp) do={ :log warning ("Host " . $target . " did not respond after 60s") } # Loops up to 12 times, 5s apart = 60s max wait. # Exits early if host responds. Logs warning if timeout.
04
Functions let you write a block of logic once and call it from anywhere — a scheduler, a Netwatch script, or another function.
RouterOS functions are declared as global variables that hold a script block. Call them with [$funcName param1=val1]. They can accept parameters and return values.
# Declare a function as a global variable :global sendTelegram :set sendTelegram do={ :local message $1 :local token "YOUR_BOT_TOKEN" :local chatId "YOUR_CHAT_ID" /tool fetch url=("https://api.telegram.org/bot" . $token . \ "/sendMessage?chat_id=" . $chatId . \ "&text=" . $message) \ keep-result=no } # Call the function — $1 is the first positional argument [$sendTelegram "Test alert from Hotel-01"] # Function with named parameters :global wakeAndVerify :set wakeAndVerify do={ :local iface $iface :local mac $mac :local target $target /tool wol interface=$iface mac=$mac :delay 45s :local result [/ping address=$target count=3 as-value] :return ($result->"received" > 0) } # Call with named parameters :local success [$wakeAndVerify \ iface=vlan20 mac="AA:BB:CC:DD:EE:FF" target="192.168.20.50"] :if ($success) do={ :log info "Server woke successfully" } else={ :log warning "Server did not respond after WoL" }
Global functions set with :global funcName; :set funcName do={...} only persist in memory until the router reboots. When the scheduler runs a script that calls a global function, it must either declare the function inline, or the function must be set up by a startup script that runs before any scheduler entries. The safest pattern for production: put the function declaration at the top of every script that uses it. This is verbose but reliable.
05
The scheduler is what makes scripts operational — it runs them on a time pattern, on startup, or at a specific date and time.
# Run every day at 02:00 [admin@MikroTik] > /system scheduler add \ name="daily-backup" \ start-time=02:00:00 \ interval=1d \ on-event="/system script run auto-backup" \ comment="Run auto-backup script nightly" # Run every 30 minutes [admin@MikroTik] > /system scheduler add \ name="health-check" \ interval=00:30:00 \ on-event="/system script run health-check" \ comment="30-minute health check" # Run on startup (interval=0, no start-time) [admin@MikroTik] > /system scheduler add \ name="startup-init" \ on-event="/system script run startup-init" \ start-time=startup \ comment="Initialise globals on boot" # Run inline script directly (no separate script object needed) [admin@MikroTik] > /system scheduler add \ name="log-uptime" \ interval=1h \ on-event={:log info ("Uptime: " . [/system resource get uptime])}
/system script run name) or paste the script body directly into On Event.06
A reusable alert function that any other script can call. One function, one Telegram message, works from Netwatch, scheduler, or any script.
# Script name: telegram-alert # Place in System › Scripts, call from Netwatch down-scripts or scheduler. # Requires: Telegram bot token and chat ID. # Get them free at: https://t.me/BotFather :local botToken "1234567890:ABCDefGhIJKlmNoPQRsTUVwxyZ" :local chatId "-1001234567890" :local hostname [/system identity get name] :local uptime [/system resource get uptime] # Build the message — encode spaces as + for URL :local msg ("🔴 ALERT — " . $hostname . \ "%0AHost: 192.168.20.50 is DOWN" . \ "%0AUptime: " . $uptime . \ "%0ATime: " . [/system clock get time] . \ " " . [/system clock get date]) # Send via Telegram Bot API :do { /tool fetch \ url=("https://api.telegram.org/bot" . $botToken . \ "/sendMessage?chat_id=" . $chatId . \ "&text=" . $msg) \ keep-result=no \ http-method=get :log info "Telegram alert sent" } on-error={ :log warning "Telegram alert FAILED — check internet connectivity" } # :do { } on-error={ } = try/catch pattern. # If /tool fetch fails (no internet), logs warning instead of crashing. # %0A = URL-encoded newline in Telegram messages.
07
Wake the NAS at 02:00, verify it came up, send a Telegram confirmation. Three tools from this series working together in one script.
# Script name: wake-nas-verify # Schedule: daily at 01:55 (5 min before backup job) # Assumes: NAS at 192.168.20.50, MAC AA:BB:CC:DD:EE:FF :local nasIP "192.168.20.50" :local nasMac "AA:BB:CC:DD:EE:FF" :local nasIface "vlan20" :local botToken "YOUR_BOT_TOKEN" :local chatId "YOUR_CHAT_ID" :local hostname [/system identity get name] # Step 1: Check if NAS is already up :local alreadyUp [/ping address=$nasIP count=1 as-value] :if ($alreadyUp->"received" > 0) do={ :log info "NAS already online — skipping WoL" :error "NAS already up" # :error exits the script cleanly } # Step 2: Send magic packet :log info ("Sending WoL to " . $nasMac . " via " . $nasIface) /tool wol interface=$nasIface mac=$nasMac # Step 3: Wait and verify (up to 60 seconds) :local attempts 0 :local isUp false :while ($attempts < 12 && !$isUp) do={ :delay 5s :local ping [/ping address=$nasIP count=1 as-value] :if ($ping->"received" > 0) do={ :set isUp true } :set attempts ($attempts + 1) } # Step 4: Report result via Telegram :local status :if ($isUp) do={ :set status "✅ NAS is UP" :log info ("WoL success: NAS responded after " . \ ($attempts * 5) . "s") } else={ :set status "❌ NAS did NOT respond after 60s" :log warning "WoL failed: NAS unreachable" } :local msg ($status . "%0AHost: " . $nasIP . \ "%0ARouter: " . $hostname) :do { /tool fetch url=("https://api.telegram.org/bot" . \ $botToken . "/sendMessage?chat_id=" . \ $chatId . "&text=" . $msg) keep-result=no } on-error={ :log warning "Telegram send failed" }
08
Every night at 02:00, export the config, attach it to an email, and send it offsite. If the router is ever bricked or stolen, you have yesterday's config in your inbox.
# First: configure SMTP settings (one-time setup) [admin@MikroTik] > /tool e-mail set \ server=smtp.gmail.com \ port=587 \ start-tls=yes \ from=noctis.alerts@gmail.com \ user=noctis.alerts@gmail.com \ password="YOUR_APP_PASSWORD" # Use a Gmail App Password (not your main password). # Generate at: Google Account → Security → App passwords # Script name: auto-backup # Schedule: daily at 02:00 :local hostname [/system identity get name] :local date [/system clock get date] :local dateStr [:pick $date 7 11] # year :local dateStr ($dateStr . "-" . [:pick $date 0 3]) # month :local dateStr ($dateStr . "-" . [:pick $date 4 6]) # day :local filename ($hostname . "-" . $dateStr . ".rsc") # Step 1: Export config to file /export file=$hostname # Creates /hostname.rsc on router flash # Step 2: Also create binary backup /system backup save name=$hostname # Creates /hostname.backup on router flash # Step 3: Send .rsc export as email attachment :do { /tool e-mail send \ to="sp1r4@noctis.gr" \ subject=("RouterOS Backup: " . $hostname . " " . $date) \ body=("Automated backup from " . $hostname . \ "\nDate: " . $date . \ "\nUptime: " . [/system resource get uptime] . \ "\nVersion: " . [/system resource get version]) \ file=($hostname . ".rsc") :log info ("Backup email sent for " . $hostname) } on-error={ :log warning ("Backup email FAILED for " . $hostname) } # Step 4: Clean up old backup files (keep last 3) :local files [/file find where name~($hostname . ".rsc")] :if ([:len $files] > 3) do={ /file remove ($files->0) :log info "Removed oldest backup file" }
Always wrap external calls in :do {} on-error={}. Any script that calls /tool fetch or /tool e-mail send will fail silently if the internet is down. Without try/catch, the script crashes mid-execution and subsequent steps never run. Wrap every network call and log a warning on failure — then the rest of the script continues.
Test scripts in the terminal before scheduling them. Paste the script body directly at the CLI prompt and run it interactively. Watch the log with /log print follow in a second terminal session. Once it works correctly, move it to System › Scripts and schedule it. Never schedule an untested script on a production router.
Use :log liberally during development, sparingly in production. Every :log info call writes to the router's volatile log buffer. A script that runs every 5 minutes and logs 10 lines will fill the 1000-line log buffer and push out useful entries. In production, only log state changes and errors — not routine confirmation messages.
Takeaways
:local ip [/ip address get 0 address] stores the result of any RouterOS command in a variable. This is how scripts interact with the router's running config.:do {} on-error={} around every external call. /tool fetch, /tool e-mail send, and /ping can all fail. Without error handling, a script crashes silently mid-execution and subsequent steps are skipped with no indication of what happened.start-time=startup replaces autorun scripts. Use it to initialise globals, start containers, and verify interface states on boot. It's more reliable than the deprecated autorun approach.NOCTIS builds and maintains automated monitoring and backup systems for hospitality networks — Telegram alerts, nightly config backups, and scheduled maintenance scripts. If your network has no automation, one missed failure can cost you hours of downtime. Book a call to discuss what your network needs.
Book a Discovery Call →