Scripting
Scripting
Section titled “Scripting”RouterOS includes a built-in scripting language for automating tasks, making configuration changes, and responding to system events. Scripts are stored in /system script and can be run manually, triggered by the scheduler, called from other scripts, or bound to system events.
Overview
Section titled “Overview”The RouterOS scripting language is an interpreted, line-by-line language with support for:
- Variables — local and global scope
- Conditionals — branching logic based on conditions
- Loops — iteration over ranges, lists, or conditions
- Functions — reusable blocks stored in global variables
- Built-in commands — output, logging, execution, and type conversion
Scripts interact directly with the router’s CLI command tree. Any command valid in the RouterOS terminal is valid inside a script.
Sub-menu
Section titled “Sub-menu”/system scriptScript Properties
Section titled “Script Properties”| Property | Type | Description |
|---|---|---|
name | string | Unique script name |
source | string | The script body (RouterOS code) |
policy | flag list | Permissions the script requires: read, write, policy, test, password, sniff, sensitive, ftp, romon, dude, api |
comment | string | Administrative description |
dont-require-permissions | yes/no | Bypass policy intersection check when called from scheduler or other scripts |
Read-only Properties
Section titled “Read-only Properties”| Property | Description |
|---|---|
run-count | How many times this script has been executed |
last-started | Timestamp of most recent execution |
Script Management
Section titled “Script Management”Viewing Scripts
Section titled “Viewing Scripts”/system script print/system script print detailAdding a Script
Section titled “Adding a Script”/system script add name=my-script \ policy=read,write,test \ comment="Example script" \ source="/log info \"Script started\""For multi-line scripts, edit via Winbox or paste the source block using the CLI editor.
Running a Script
Section titled “Running a Script”/system script run my-scriptRemoving a Script
Section titled “Removing a Script”/system script remove my-scriptScript Syntax
Section titled “Script Syntax”Variables
Section titled “Variables”RouterOS scripts use two scopes:
:local— visible only within the current script or block:global— visible across all scripts and the terminal session
# Declare and assign a local variable:local iface "ether1":local count 0
# Declare a global variable:global lastBackup
# Assign to a variable:set count ($count + 1)
# Use variable in an expression:put ("Interface is: " . $iface)Variable names are case-sensitive. Reference them with $ prefix. Concatenate strings with the . operator.
Environment Variables
Section titled “Environment Variables”Use :environment print to list all currently defined global variables and their values:
:environment printTo retrieve system identity (router name) inside a script:
:local id [/system identity get name]:put ("Router: " . $id)To get the current date and time:
:local d [/system clock get date]:local t [/system clock get time]:put ("Date: " . $d . " Time: " . $t)These values are useful for constructing timestamped filenames or log messages.
Conditionals
Section titled “Conditionals”Use :if for branching. The else block is optional.
:local load [/system resource get cpu-load]
:if ($load > 80) do={ :log warning ("High CPU load: " . $load . "%")} else={ :log info ("CPU OK: " . $load . "%")}Compare values with =, !=, >, <, >=, <=. Combine conditions with && (and) and || (or):
:local status [/interface get ether1 running]:local admin [/interface get ether1 disabled]
:if ($status = true && $admin = false) do={ :log info "ether1 is up and enabled"}For Loop
Section titled “For Loop”Iterate over a numeric range:
:for i from=1 to=5 do={ :put ("Iteration: " . $i)}Foreach Loop
Section titled “Foreach Loop”Iterate over a list or array:
:local ports {"ether1";"ether2";"ether3"}:foreach p in=$ports do={ :put ("Checking: " . $p) :if ([/interface get $p running] = false) do={ :log warning ($p . " is down") }}While Loop
Section titled “While Loop”Repeat while a condition is true:
:local retries 0:while ($retries < 3) do={ :set retries ($retries + 1) :delay 2s :log info ("Retry " . $retries)}Functions
Section titled “Functions”RouterOS does not have a function keyword. Instead, reusable logic is stored in global variables with do={} blocks:
# Define a function:global sendAlert do={ :local msg $1 :log warning ("ALERT: " . $msg)}
# Call it$sendAlert "Disk space low"Parameters are positional ($1, $2, …) or named via the do= parameter list. Functions are visible across scripts once defined in the global scope.
Built-in Commands
Section titled “Built-in Commands”Prints output to the terminal. Useful for debugging.
:put "Hello from script":put ("CPU load: " . [/system resource get cpu-load])Writes to the RouterOS system log at a specified severity level.
:log debug "Debug-level detail":log info "Task completed":log warning "Threshold exceeded":log error "Critical failure"Logged messages appear in /log print and are forwarded to any configured remote syslog targets.
:execute
Section titled “:execute”Runs a command or script asynchronously in a background job:
:execute "/system script run backup-upload"Use this to fire off long-running tasks without blocking the current script. Output is not directly returned to the caller.
:delay
Section titled “:delay”Pauses execution for a specified duration:
:delay 5s:delay 1m30s:error
Section titled “:error”Stops script execution and raises an error condition. Useful inside :do/on-error error handling:
:do { /system backup save name=auto} on-error={ :log error "Backup failed" :error "backup-failed"}String Operations
Section titled “String Operations”| Operation | Example | Result |
|---|---|---|
| Concatenate | "hello" . " " . "world" | "hello world" |
| Length | [:len "MikroTik"] | 8 |
| Substring | [:pick "MikroTik" 0 5] | "Mikro" |
| Find | [:find "MikroTik" "Tik"] | 5 |
| To string | [:tostr 42] | "42" |
| To number | [:tonum "42"] | 42 |
| To IP | [:toip "192.168.1.1"] | IP type |
Arrays
Section titled “Arrays”Arrays are semicolon-separated lists enclosed in braces. Access elements with -> and index (zero-based):
:local dns {"8.8.8.8";"8.8.4.4";"1.1.1.1"}:put ($dns->0) # "8.8.8.8":put [:len $dns] # 3
:foreach ns in=$dns do={ :put ("Nameserver: " . $ns)}Key-value maps use string keys:
:local info {"name"="r1";"site"="dc1"}:put ($info->"name") # "r1"Scheduling Scripts
Section titled “Scheduling Scripts”Scripts can be triggered on a timed basis using /system scheduler. Scheduler entries specify when to run and which script (or inline code) to execute.
Run a Named Script on a Schedule
Section titled “Run a Named Script on a Schedule”/system scheduler add \ name=hourly-check \ start-time=startup \ interval=1h \ on-event=check-wan \ policy=read,write,test,passwordThe scheduler’s policy must be a superset of the script’s required permissions.
Embed Code Directly in a Scheduler Entry
Section titled “Embed Code Directly in a Scheduler Entry”For short one-liners, skip the named script entirely:
/system scheduler add \ name=log-cpu \ interval=5m \ on-event=":log info ([/system resource get cpu-load] . \"% CPU\")" \ policy=read,testRun at Startup
Section titled “Run at Startup”Set start-time=startup and interval=0 to execute exactly once after boot:
/system scheduler add \ name=boot-tasks \ start-time=startup \ interval=0 \ on-event=boot-init \ policy=read,write,testEvent-Driven Scripts
Section titled “Event-Driven Scripts”Beyond the scheduler, RouterOS can call scripts automatically in response to system events. Each subsystem has its own hook mechanism and set of environment variables injected into the script at call time.
Netwatch (Host Reachability Events)
Section titled “Netwatch (Host Reachability Events)”Netwatch probes hosts using ICMP, TCP, HTTP, or DNS, and fires scripts when the state changes:
/tool netwatch add \ host=8.8.8.8 \ interval=30s \ type=icmp \ up-script=wan-up \ down-script=wan-downThe up-script runs when the host becomes reachable; down-script runs when it becomes unreachable. Scripts must have at least read and test policy. For configuration changes on failover, add write.
Example wan-down script that switches to a backup route:
/system script add name=wan-down policy=read,write,test source={ :log warning "Primary WAN down — activating backup route" /ip route set [find comment="primary-wan"] disabled=yes /ip route set [find comment="backup-wan"] disabled=no}DHCP Server (Lease Events)
Section titled “DHCP Server (Lease Events)”The DHCP server calls a script on lease binding, release, and expiry. Set lease-script on the DHCP server:
/ip dhcp-server set [find name=dhcp1] lease-script=dhcp-eventRouterOS injects these variables into the lease script at call time:
| Variable | Description |
|---|---|
$leaseActIP | IP address being bound or released |
$leaseActMAC | MAC address of the client |
$leaseServerName | DHCP server name |
$leaseBound | 1 when binding, 0 when releasing |
Example lease-script that logs every binding:
/system script add name=dhcp-event policy=read,write,test source={ :if ($leaseBound = 1) do={ :log info ("DHCP bound: " . $leaseActMAC . " -> " . $leaseActIP) } else={ :log info ("DHCP released: " . $leaseActMAC . " " . $leaseActIP) }}PPP Profiles (Session Connect/Disconnect Events)
Section titled “PPP Profiles (Session Connect/Disconnect Events)”PPP profiles support on-up and on-down hooks that fire when a session is established or torn down. These work for PPPoE, L2TP, PPTP, and SSTP clients and servers.
/ppp profile set [find name=default] \ on-up="/log info (\"PPP up: \" . \$user . \" \" . \$remote-address)" \ on-down="/log warning (\"PPP down: \" . \$user)"Variables injected by RouterOS at call time:
| Variable | Description |
|---|---|
$user | PPP username |
$local-address | Local tunnel endpoint IP |
$remote-address | Remote tunnel endpoint IP |
$interface | PPP interface name |
$caller-id | Caller identifier (phone number or MAC) |
Script Permissions in Event Context
Section titled “Script Permissions in Event Context”Scripts invoked by events (Netwatch, DHCP, PPP) run under the permissions of the script itself — not the triggering subsystem. Ensure the script’s policy covers all commands it uses:
| Action needed | Policy flag required |
|---|---|
| Read configuration | read |
| Change configuration | write |
| Run tests (ping, trace) | test |
| Access passwords | password |
| FTP upload | ftp |
| Read sensitive values | sensitive |
Examples
Section titled “Examples”Log System Information on Boot
Section titled “Log System Information on Boot”/system script add name=boot-info \ policy=read,test \ source={ :local id [/system identity get name] :local ver [/system package get routeros version] :local up [/system resource get uptime] :log info ("Boot: " . $id . " v" . $ver . " uptime=" . $up) }
/system scheduler add name=boot-log start-time=startup interval=0 \ on-event=boot-info policy=read,testCheck Interface State and Alert
Section titled “Check Interface State and Alert”/system script add name=check-wan \ policy=read,test,password \ source={ :local iface "ether1" :local state [/interface get $iface running] :if ($state = false) do={ :log warning ($iface . " is DOWN") subject=("Link down: " . $iface) \ body=("Interface " . $iface . " has lost link") } }
/system scheduler add name=wan-monitor start-time=startup interval=5m \ on-event=check-wan policy=read,test,passwordUse Global State Across Scripts
Section titled “Use Global State Across Scripts”# Script 1: set global flag:global maintenanceMode true
# Script 2: check before acting:global maintenanceMode:if ($maintenanceMode = true) do={ :log info "Skipping task — maintenance mode active"} else={ :log info "Running task normally"}Automated Daily Configuration Backup
Section titled “Automated Daily Configuration Backup”Saves a backup and uploads it to an FTP server, using the router name and date in the filename:
/system script add name=daily-backup \ policy=read,write,ftp,sensitive \ source={ :local id [/system identity get name] :local date [/system clock get date] :local name ($id . "-" . $date)
/system backup save name=$name :delay 2s
/tool fetch \ address=backup.example.com \ src-path=($name . ".backup") \ dst-path=("/backups/" . $name . ".backup") \ mode=ftp \ user=ftpuser \ password=secret \ upload=yes
:log info ("Backup uploaded: " . $name) }
/system scheduler add \ name=run-backup \ start-time=02:00:00 \ interval=1d \ on-event=daily-backup \ policy=read,write,ftp,sensitiveWAN Failover with Netwatch
Section titled “WAN Failover with Netwatch”Monitor the primary gateway and automatically switch routing when it becomes unreachable. Mark routes with comment for easy selection:
# Setup: tag your routes/ip route set [find gateway=203.0.113.1] comment="primary-wan"/ip route set [find gateway=198.51.100.1] comment="backup-wan"/ip route set [find comment="backup-wan"] disabled=yes
# Failover scripts/system script add name=wan-failover-down \ policy=read,write,test \ source={ :log warning "Primary WAN unreachable — switching to backup" /ip route set [find comment="primary-wan"] disabled=yes /ip route set [find comment="backup-wan"] disabled=no }
/system script add name=wan-failover-up \ policy=read,write,test \ source={ :log info "Primary WAN restored — reverting to primary" /ip route set [find comment="backup-wan"] disabled=yes /ip route set [find comment="primary-wan"] disabled=no }
# Netwatch monitors the primary gateway/tool netwatch add \ host=203.0.113.1 \ interval=10s \ type=icmp \ down-script=wan-failover-down \ up-script=wan-failover-upReusable Alert Function
Section titled “Reusable Alert Function”Define a function once and call it from any script or scheduler entry:
# Define in a startup script (runs once at boot)/system script add name=define-functions \ policy=read,write,test,password \ source={ :global alert do={ :local msg $1 :log warning ("ALERT: " . $msg) /tool e-mail send \ subject=("[" . [/system identity get name] . "] " . $msg) \ body=$msg } }
/system scheduler add name=load-functions start-time=startup interval=0 \ on-event=define-functions policy=read,write,test,password
# Use the function from any other script:global alert$alert "ether1 link down"$alert "Disk space below 10%"Troubleshooting
Section titled “Troubleshooting”Script Fails with “permission denied”
Section titled “Script Fails with “permission denied””Check that the script’s policy includes all permissions the commands it runs require. For configuration changes, include write. For backup/FTP operations, include ftp, sensitive, and password.
Script Runs but Has No Effect
Section titled “Script Runs but Has No Effect”Enable debug logging, run the script manually, and watch for errors:
/system script run my-script/log print where topics~"script"Variable Is Nil / Undefined
Section titled “Variable Is Nil / Undefined”Ensure the variable is declared with :local before use. Global variables are nil until assigned. Check that calling scripts pass arguments correctly.
Script Works in Terminal but Fails from Scheduler
Section titled “Script Works in Terminal but Fails from Scheduler”When called from the scheduler, the script runs in a restricted environment. Ensure:
- The scheduler entry’s
policyis a superset of the script’spolicy - Required flags like
write,policy,sensitive,ftpare present on the scheduler entry - If needed, set
dont-require-permissions=yeson the script
Hardware Verification
Section titled “Hardware Verification”Tested on staging-router-02 running RouterOS 7.15.3 (stable), 2026-03-23. (staging-router-01 was offline at time of testing; staging-router-02 is identical hardware.)
Script Management
Section titled “Script Management”[admin@staging-router-02] > /system script add name=my-script policy=read,write,test \ comment="Example script" source="/log info \"Script started\""[admin@staging-router-02] > /system script run my-script[admin@staging-router-02] > /system script printFlags: I - invalid 0 name="my-script" policy=read,write,test run-count=1 last-started=2026-03-23 12:46:37Variables and Environment
Section titled “Variables and Environment”[admin@staging-router-02] > :local iface "ether1"; :local count 0; :set count ($count + 1); :put ("Interface is: " . $iface)Interface is: ether1
[admin@staging-router-02] > :local id [/system identity get name]; :put ("Router: " . $id)Router: staging-router-02
[admin@staging-router-02] > :local d [/system clock get date]; :local t [/system clock get time]; :put ("Date: " . $d . " Time: " . $t)Date: 2026-03-23 Time: 12:46:37Conditionals and Loops
Section titled “Conditionals and Loops”[admin@staging-router-02] > :local load [/system resource get cpu-load]; :if ($load > 80) do={ :put ("High CPU: " . $load . "%") } else={ :put ("CPU OK: " . $load . "%") }CPU OK: 1%
[admin@staging-router-02] > :for i from=1 to=5 do={ :put ("Iteration: " . $i) }Iteration: 1Iteration: 2Iteration: 3Iteration: 4Iteration: 5
[admin@staging-router-02] > :local ports {"ether1";"ether2";"ether3"}; :foreach p in=$ports do={ :put ("Checking: " . $p) }Checking: ether1Checking: ether2Checking: ether3String Operations and Arrays
Section titled “String Operations and Arrays”[admin@staging-router-02] > :put [:len "MikroTik"]8[admin@staging-router-02] > :put [:pick "MikroTik" 0 5]Mikro[admin@staging-router-02] > :put [:find "MikroTik" "Tik"]5[admin@staging-router-02] > :put [:tostr 42]42[admin@staging-router-02] > :local dns {"8.8.8.8";"8.8.4.4";"1.1.1.1"}; :put ($dns->0); :put [:len $dns]8.8.8.83Functions with Parameters
Section titled “Functions with Parameters”[admin@staging-router-02] > :global alert do={ :local msg $1; :log warning ("ALERT: " . $msg); :put ("ALERT: " . $msg) }[admin@staging-router-02] > :global alert; $alert "Disk space low"ALERT: Disk space low[admin@staging-router-02] > :global alert; $alert "ether1 link down"ALERT: ether1 link downScheduler
Section titled “Scheduler”[admin@staging-router-02] > /system scheduler add name=log-cpu interval=5m \ on-event=":log info ([/system resource get cpu-load] . \"% CPU\")" policy=read,test[admin@staging-router-02] > /system scheduler print detail where name=log-cpu 0 name="log-cpu" start-date=2026-03-23 start-time=12:49:10 interval=5m on-event=:log info ([/system resource get cpu-load] . "% CPU")Boot-Info Script (Real Output)
Section titled “Boot-Info Script (Real Output)”[admin@staging-router-02] > :local id [/system identity get name]; :local ver [/system resource get version]; :local up [/system resource get uptime]; :put ("Boot: " . $id . " v" . $ver . " uptime=" . $up)Boot: staging-router-02 v7.15.3 (stable) uptime=3w4d23:03:35Netwatch Entry
Section titled “Netwatch Entry”[admin@staging-router-02] > /tool netwatch add host=8.8.8.8 interval=30s type=icmp down-script=wan-failover-down up-script=wan-failover-up[admin@staging-router-02] > /tool netwatch printColumns: TYPE, HOST, INTERVAL, STATUS# TYPE HOST INTERVAL STATUS0 icmp 8.8.8.8 30s reachablePPP Profile Event Hooks
Section titled “PPP Profile Event Hooks”[admin@staging-router-02] > /ppp profile set [find name=default] \ on-up="/log info (\"PPP up: \" . \$user)" \ on-down="/log warning (\"PPP down: \" . \$user)"[admin@staging-router-02] > /ppp profile print detail where name=default on-up=/log info ("PPP up: " . $user) on-down=/log warning ("PPP down: " . $user)See Also
Section titled “See Also”- Scripting Language Reference — Type system, advanced control flow, arrays, error handling
- Scheduler — Run scripts on a timed or startup basis
- Netwatch — Probe hosts and trigger scripts on state changes
- Logging — View and configure log output
- E-mail — Send alerts from scripts
- Fetch — Upload/download files from scripts