Skip to content

Scripting Language Reference

This page covers the RouterOS scripting language in depth — type system, variable scope, control flow, arrays, string functions, and practical patterns. For script management (creating, running, scheduling), see Scripting.

RouterOS scripting is dynamically typed. The language automatically determines a value’s type based on context, and values can be coerced between types by operators.

TypeDescriptionExample literal
numInteger42, -5
strString"hello"
boolBooleantrue, false
timeDuration/timestamp1h30m, 00:30:00
ipIPv4 address192.168.1.1
ip-prefixIPv4 prefix192.168.0.0/24
ip6IPv6 address2001:db8::1
ip6-prefixIPv6 prefix2001:db8::/32
arrayOrdered list or key-value map{"a";"b";"c"}
nilNo value / undefined
idInternal router object ID

Use :typeof to inspect the type of any value:

:local x 42
:put [:typeof $x] # num
:local s "hello"
:put [:typeof $s] # str
:local u
:put [:typeof $u] # nil

Declared with :local. A local variable is visible only within the current script block. It cannot be read from another script or from a nested scope that was not directly given the value.

:local count 0
:local iface "ether1"
:local enabled true

Declared with :global. Global variables persist across scripts and terminal sessions for the lifetime of the router session. Another script can read or modify them after they are declared.

# Script A — set a global
:global maintenanceMode true
# Script B — read the global
:global maintenanceMode
:if ($maintenanceMode = true) do={
:log info "In maintenance mode"
}

A global variable must be declared with :global in each script that uses it. The declaration does not reset the value — it makes the existing global visible in scope.

Assign or update a value with :set:

:local x 10
:set x ($x + 5) # x = 15
:set x ($x * 2) # x = 30

To unset a variable (make it nil), call :set with no value:

:set x # x is now nil

Always check for nil before using a variable that might be unset:

:local val
:if ([:typeof $val] = "nil") do={
:set val "default"
}
:put $val # "default"
:local load [/system resource get cpu-load]
:if ($load > 80) do={
:log warning ("High CPU: " . $load . "%")
}

The :else block runs when the condition is false:

:local status [/interface get ether1 running]
:if ($status = true) do={
:log info "ether1 is up"
} else={
:log error "ether1 is down"
}
OperatorMeaning
=Equal
!=Not equal
<Less than
>Greater than
<=Less than or equal
>=Greater than or equal

Combine conditions with && (AND) and || (OR). Negate with !:

:local running [/interface get ether1 running]
:local disabled [/interface get ether1 disabled]
# Both must be true
:if ($running = true && $disabled = false) do={
:log info "ether1 is active"
}
# Either condition triggers action
:if ($running = false || $disabled = true) do={
:log warning "ether1 is not forwarding"
}
# Negate a boolean
:if (!$running) do={
:log error "Link is down"
}
:local load [/system resource get cpu-load]
:local mem [/system resource get free-memory]
:if ($load > 90) do={
:if ($mem < 10000000) do={
:log error "Critical: high CPU and low memory"
} else={
:log warning "High CPU load"
}
} else={
:log info "System resources OK"
}

Repeats a block while the condition holds:

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

Always ensure the loop variable changes each iteration to avoid infinite loops.

Iterates over a numeric range. The step parameter is optional (defaults to 1):

# Count from 1 to 5
:for i from=1 to=5 do={
:put ("i = " . $i)
}
# Count by twos
:for i from=0 to=10 step=2 do={
:put ("even: " . $i)
}
# Count down
:for i from=5 to=1 step=-1 do={
:put ("countdown: " . $i)
}

Iterates over arrays and lists:

:local servers {"10.0.0.1";"10.0.0.2";"10.0.0.3"}
:foreach srv in=$servers do={
:put ("Checking: " . $srv)
}

Iterate over command output (returns a list of internal IDs):

:foreach iface in=[/interface find where type="ether"] do={
:local name [/interface get $iface name]
:local up [/interface get $iface running]
:log info ($name . " running=" . $up)
}

RouterOS does not have break or continue. Use :if conditions to skip iterations or a :while with a flag to exit early:

# Skip using :if
:foreach i in={1;2;3;4;5} do={
:if ($i != 3) do={
:put ("Processing: " . $i)
}
}
# Early exit pattern with a flag
:local found false
:local items {"a";"b";"target";"c"}
:foreach item in=$items do={
:if ($found = false) do={
:if ($item = "target") do={
:set found true
:log info "Found target"
}
}
}

Arrays are enclosed in braces with elements separated by semicolons:

:local servers {"10.0.0.1";"10.0.0.2";"10.0.0.3"}
:local ports {80;443;8080}
:local mixed {"eth1";1;true}

Use the -> operator with a zero-based index:

:local dns {"8.8.8.8";"8.8.4.4";"1.1.1.1"}
:put ($dns->0) # "8.8.8.8"
:put ($dns->1) # "8.8.4.4"
:put ($dns->2) # "1.1.1.1"

Get the length with :len:

:put [:len $dns] # 3

Create associative arrays (maps) using key=value pairs:

:local router {"name"="core-sw";"site"="dc1";"ip"="10.0.0.1"}
:put ($router->"name") # "core-sw"
:put ($router->"site") # "dc1"
:put ($router->"ip") # "10.0.0.1"

Accessing a missing key returns nil. Always check with :typeof before using:

:local val ($router->"missing")
:if ([:typeof $val] = "nil") do={
:put "Key not found"
}
:local nameservers {"8.8.8.8";"8.8.4.4";"1.1.1.1"}
:foreach ns in=$nameservers do={
:put ("NS: " . $ns)
}

Convert command output to an iterable array with :toarray:

:local ifaceIds [/interface find where type="ether"]
:foreach id in=$ifaceIds do={
:local name [/interface get $id name]
:put $name
}
:local items {"a";"b"}
:if ([:len $items] > 0) do={
:put ($items->0)
}

Use . to join strings:

:local a "Router"
:local b "OS"
:put ($a . $b) # "RouterOS"
:put ("Version: " . $b) # "Version: OS"

Variables and expressions can be mixed inline:

:local host [/system identity get name]
:local load [/system resource get cpu-load]
:log info ($host . " CPU=" . $load . "%")
:local s "MikroTik"
:put [:len $s] # 8

:pick extracts a substring. Start index is inclusive; end is exclusive:

:local s "RouterOS"
:put [:pick $s 0 6] # "Router" (chars 0-5)
:put [:pick $s 6] # "OS" (from index 6 to end)

:find returns the position of the first match, or nothing (nil) if not found:

:local s "RouterOS-7.12"
:put [:find $s "-"] # 8
# With start offset
:put [:find $s "O" 4] # 7
# Check if found
:local pos [:find $s "X"]
:if ([:typeof $pos] = "nil") do={
:put "Not found"
}
CommandDescriptionExample
:tostrConvert to string[:tostr 42]"42"
:tonumConvert to number[:tonum "42"]42
:toboolConvert to boolean[:tobool "true"]true
:toipConvert to IP type[:toip "192.168.1.1"]
:totimeConvert to time[:totime "1h30m"]
:toarrayConvert to array[:toarray "a,b,c"]
:typeofReturn type name[:typeof $x]"num"
/system script run my-script

Pass positional arguments when running a script:

/system script run testargs 10 20

Inside the script, access them as $1, $2, etc.:

# Script: testargs
:put ("arg1=" . $1 . " arg2=" . $2)

Named arguments can also be passed and accessed as variables:

/system script run notify host=10.0.0.1 msg="link-down"
# Script: notify
:put ("host=" . $host . " msg=" . $msg)

Create a scheduler entry that calls an existing script:

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

Or embed the script code directly in on-event:

/system scheduler add \
name=log-load \
interval=5m \
on-event=":log info ([/system resource get cpu-load] . \"% CPU\")" \
policy=read,test

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

Use :execute to run a script or command in a background job, non-blocking:

:execute "/system script run backup-upload"

The caller does not wait for completion and does not receive output.

Wrap commands in a :do block to catch failures:

:do {
/system backup save name=auto-backup
:log info "Backup saved"
} on-error={
:log error "Backup failed"
}

:onerror runs a handler block and captures the error message:

:onerror e in={
/ip address add address=192.0.2.10/24 interface=ether1
} do={
:log error ("Address add failed: " . $e)
}

Retry a failing command with a delay and maximum attempt count:

:onerror e in={
:retry command={
/tool fetch url="https://example.com/config.rsc" dst-path="config.rsc"
} delay=5s max=3
} do={
:log error ("Fetch failed after retries: " . $e)
}
:local iface "ether1"
:local up [/interface get $iface running]
:if (!$up) do={
:log warning ($iface . " is DOWN")
/tool e-mail send \
subject=("Link down: " . $iface) \
body=("Interface " . $iface . " lost link")
}
:foreach id in=[/interface find] do={
:local name [/interface get $id name]
:local state [/interface get $id running]
:log info ($name . ": running=" . $state)
}
:local ok false
:local attempts 0
:while (!$ok && $attempts < 10) do={
:set attempts ($attempts + 1)
:do {
/tool ping 8.8.8.8 count=1
:set ok true
} on-error={}
:if (!$ok) do={ :delay 5s }
}
:if ($ok) do={
:log info "Connectivity restored after " . $attempts . " attempts"
} else={
:log error "No connectivity after " . $attempts . " attempts"
}
# Script: set-maintenance
:global maintenance true
# Script: check-maintenance
:global maintenance
:if ([:typeof $maintenance] = "nil") do={ :set maintenance false }
:if ($maintenance) do={
:log info "Skipping — maintenance mode"
} else={
# ... do work
}
:local id [/system identity get name]
:local ver [/system package get routeros version]
:local uptime [/system resource get uptime]
:local cpu [/system resource get cpu-load]
:local freemem [/system resource get free-memory]
:local summary ($id . " v" . $ver . " up=" . $uptime . " cpu=" . $cpu . "% free=" . $freemem)
:log info $summary
  • Scripting — Script management, policies, and scheduler integration
  • Scheduler — Timed and startup script execution
  • Logging — View and configure log output
  • Fetch — Download and upload files from scripts