Back to blog

MikroTik / RouterOS  ·  Tutorial  ·  May 2026

RouterOS Scripting
Basics

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.

RouterOS Scripting Scheduler Automation Telegram Functions MTCNA+

:local myVar "hello" ; :if ($myVar = "hello") do={ :log info "matched" } keyword variable name + string value variable reference ($ prefix) log command inside do block RouterOS scripting syntax: keywords start with colon · variables declared with :local · referenced with $ · blocks use { }

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.

Prerequisites

01

Syntax Rules — The Five Things You Must Know

RouterOS scripting has its own syntax that looks like nothing else. Learn these five rules and the rest falls into place.

RouterOS scripting — five fundamental rules
# ── 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

Variables and Data Types

RouterOS has six data types. Knowing which one you're working with prevents 90% of scripting bugs.

TypeExampleNotes
str"hello"Text. Concatenate with the . operator.
num42Integer. Arithmetic with + - * /.
booltrue / falseUsed in conditions. Returned by comparisons.
ip192.168.1.1IP address. Supports subnet operations.
array{1;2;3}Semicolon-separated. Iterate with :foreach.
nothing[:toarray ""]Empty / nil. Check with [:len $var] = 0.
RouterOS — variable operations
# 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.
⚠ Gotcha — Quoting Inside Strings

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

Conditionals and Loops

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

:if — Conditionals

RouterOS — :if conditional patterns
# 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"
}

:foreach — Iterate Arrays

RouterOS — :foreach loop patterns
# 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)

:while — Loop Until Condition

RouterOS — :while loop
# 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

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.

RouterOS — function declaration and usage
# 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"
}
⚠ Gotcha — Global Functions Must Be Declared Before They Are Called

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

The scheduler is what makes scripts operational — it runs them on a time pattern, on startup, or at a specific date and time.

RouterOS — scheduler patterns
# 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 Scheduler + Add
WinBox 4.1 — System › Scheduler Identity Clock Scripts Scheduler + Add New Scheduler Entry — daily-backup Name daily-backup Start Time 02:00:00 Interval 1d 00:00:00 On Event /system script run auto-backup ← call a named script, or paste script body inline here Comment Run auto-backup script nightly Apply System › Scheduler · On Event: call a script by name or paste inline · Interval: 1d = daily, 00:30:00 = every 30 minutes
System › Scheduler → + Add. Set Start Time for first run, Interval for recurrence, On Event for the script to run. You can call a named script (/system script run name) or paste the script body directly into On Event.

06

Practical Script 1 — Telegram Alert System

A reusable alert function that any other script can call. One function, one Telegram message, works from Netwatch, scheduler, or any script.

RouterOS — complete Telegram alert 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

Practical Script 2 — Wake-on-LAN Automation

Wake the NAS at 02:00, verify it came up, send a Telegram confirmation. Three tools from this series working together in one script.

RouterOS — automated WoL with verification and alert
# 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

Practical Script 3 — Auto-Backup via Email

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.

RouterOS — nightly auto-backup to email
# 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"
}
🛡 Operator's View — Scripting for Production Networks

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

  1. All keywords start with colon, all variable references start with $, all blocks use { }. These three rules are the entire syntax skeleton. Everything else is built on them.
  2. Capture command output with [ ]. :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.
  3. Use :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.
  4. Global functions must be redeclared after reboot. Add a startup scheduler entry that runs a script to set all your global functions. This is the production-safe pattern — don't rely on functions surviving across reboots.
  5. The scheduler's 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.
  6. Test in the terminal before scheduling. Run scripts interactively, watch the log, fix issues, then schedule. Scheduling an untested script is how you create a router that reboots itself at 02:00 every night.

Running a hotel, villa, or SMB in Crete?

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 →