Skip to content

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.

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.

/system script
PropertyTypeDescription
namestringUnique script name
sourcestringThe script body (RouterOS code)
policyflag listPermissions the script requires: read, write, policy, test, password, sniff, sensitive, ftp, romon, dude, api
commentstringAdministrative description
dont-require-permissionsyes/noBypass policy intersection check when called from scheduler or other scripts
PropertyDescription
run-countHow many times this script has been executed
last-startedTimestamp of most recent execution
/system script print
/system script print detail
/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.

/system script run my-script
/system script remove my-script

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.

Use :environment print to list all currently defined global variables and their values:

:environment print

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

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"
}

Iterate over a numeric range:

:for i from=1 to=5 do={
:put ("Iteration: " . $i)
}

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")
}
}

Repeat while a condition is true:

:local retries 0
:while ($retries < 3) do={
:set retries ($retries + 1)
:delay 2s
:log info ("Retry " . $retries)
}

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)
/tool e-mail send to[email protected] subject="Router Alert" body=$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.

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.

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.

Pauses execution for a specified duration:

:delay 5s
:delay 1m30s

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"
}
OperationExampleResult
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 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"

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.

/system scheduler add \
name=hourly-check \
start-time=startup \
interval=1h \
on-event=check-wan \
policy=read,write,test,password

The scheduler’s policy must be a superset of the script’s required permissions.

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,test

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,test

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

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

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

RouterOS injects these variables into the lease script at call time:

VariableDescription
$leaseActIPIP address being bound or released
$leaseActMACMAC address of the client
$leaseServerNameDHCP server name
$leaseBound1 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:

VariableDescription
$userPPP username
$local-addressLocal tunnel endpoint IP
$remote-addressRemote tunnel endpoint IP
$interfacePPP interface name
$caller-idCaller identifier (phone number or MAC)

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 neededPolicy flag required
Read configurationread
Change configurationwrite
Run tests (ping, trace)test
Access passwordspassword
FTP uploadftp
Read sensitive valuessensitive
/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,test
/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")
/tool e-mail send to[email protected] \
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,password
# 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"
}

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,sensitive

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

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%"

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.

Enable debug logging, run the script manually, and watch for errors:

/system script run my-script
/log print where topics~"script"

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:

  1. The scheduler entry’s policy is a superset of the script’s policy
  2. Required flags like write, policy, sensitive, ftp are present on the scheduler entry
  3. If needed, set dont-require-permissions=yes on the script

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

[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 print
Flags: I - invalid
0 name="my-script" policy=read,write,test run-count=1
last-started=2026-03-23 12:46:37
[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:37
[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: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
[admin@staging-router-02] > :local ports {"ether1";"ether2";"ether3"}; :foreach p in=$ports do={ :put ("Checking: " . $p) }
Checking: ether1
Checking: ether2
Checking: ether3
[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.8
3
[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 down
[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")
[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:35
[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 print
Columns: TYPE, HOST, INTERVAL, STATUS
# TYPE HOST INTERVAL STATUS
0 icmp 8.8.8.8 30s reachable
[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)
  • 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