Back to all articles
Tutorial By MikroRadius Team

MikroTik RouterOS Scripting Deep Dive: Variables, Loops, Functions & Error Handling

RouterOS scripting is more powerful than most admins realize. Beyond simple one-liners, you can write full programs with variables, loops, arrays, functions, error handling, and HTTP fetch. This deep dive covers the scripting language from basics to advanced automation patterns.

Our scheduler guide covered basic scripts and scheduling. This guide goes deeper into the RouterOS scripting language itself – the syntax, data types, control flow, and patterns you need for serious automation.

Scripting Basics

Variables

# Local variable (scope: current script)
:local myVar "Hello"
:local count 42
:local isActive true

# Global variable (persists across scripts)
:global sharedVar "Available everywhere"

# Print
:put $myVar
:log info "Count is $count"

Data Types

:local myString "text"           # String
:local myNumber 100              # Number
:local myBool true               # Boolean
:local myIP 192.168.88.1         # IP address
:local myTime 01:30:00           # Time
:local myArray {1; 2; 3}         # Array
:local nothing                   # Nothing (nil)

String Operations

:local first "Mikro"
:local second "Tik"
:local combined ("$first$second")    # "MikroTik"

# String length
:local len [:len $combined]          # 8

# Substring
:local sub [:pick $combined 0 5]     # "Mikro"

# Find
:local pos [:find $combined "Tik"]   # 5

# Convert
:local numStr [:tostr 42]            # "42"
:local strNum [:tonum "42"]          # 42

Control Flow

If/Else

:local cpu [/system resource get cpu-load]
:if ($cpu > 80) do={
  :log warning "CPU high: $cpu%"
} else={
  :log info "CPU normal: $cpu%"
}

For Loop

:for i from=1 to=10 do={
  :put "Iteration $i"
}

Foreach

:foreach iface in=[/interface find type=ether] do={
  :local name [/interface get $iface name]
  :local running [/interface get $iface running]
  :put "$name - running: $running"
}

While Loop

:local attempts 0
:while ($attempts < 5) do={
  :set attempts ($attempts + 1)
  :if ([/ping 8.8.8.8 count=1] > 0) do={
    :log info "Internet OK on attempt $attempts"
    :set attempts 5
  } else={
    :delay 5s
  }
}

Arrays

# Create array
:local servers {"1.1.1.1"; "8.8.8.8"; "9.9.9.9"}

# Access element (0-indexed)
:put ($servers->0)     # "1.1.1.1"
:put ($servers->2)     # "9.9.9.9"

# Iterate
:foreach srv in=$servers do={
  :if ([/ping $srv count=1] = 0) do={
    :log error "DNS server $srv unreachable"
  }
}

# Key-value array
:local config {"name"="Router1"; "location"="DC1"; "role"="core"}
:put ($config->"name")     # "Router1"

Functions

# Define a function (stored as a global variable)
:global sendAlert do={
  :local msg $1
  :local botToken "YOUR_BOT_TOKEN"
  :local chatId "YOUR_CHAT_ID"
  /tool fetch url="https://api.telegram.org/bot$botToken/sendMessage\?chat_id=$chatId&text=$msg" keep-result=no
  :log info "Alert sent: $msg"
}

# Call the function
$sendAlert "WAN link is down!"

Error Handling

# :do on-error catches errors
:do {
  /ip address add address=192.168.1.1/24 interface=ether99
} on-error={
  :log error "Failed to add IP – interface might not exist"
}

# Check before acting
:if ([:len [/interface find name=ether5]] > 0) do={
  /interface set ether5 disabled=no
} else={
  :log warning "ether5 not found"
}

Practical Scripts

Script 1: WAN Failover Monitor

# Check primary WAN, switch to backup if down
:local primaryGW 203.0.113.1
:local backupGW 198.51.100.1
:local pingResult [/ping $primaryGW count=3]

:if ($pingResult = 0) do={
  :log warning "Primary WAN down – switching to backup"
  /ip route set [find comment="primary-default"] disabled=yes
  /ip route set [find comment="backup-default"] disabled=no
} else={
  /ip route set [find comment="primary-default"] disabled=no
  /ip route set [find comment="backup-default"] disabled=yes
}

Script 2: Auto-Block Heavy Users

# Find PPPoE users exceeding 100GB and add to address list
:foreach session in=[/ppp active find] do={
  :local user [/ppp active get $session name]
  :local bytes [/ppp active get $session bytes]
  :local download [:pick $bytes ([:find $bytes "/"] + 1) [:len $bytes]]
  :if ([:tonum $download] > 107374182400) do={
    :local ip [/ppp active get $session address]
    /ip firewall address-list add list=heavy-users address=$ip timeout=1d comment=$user
    :log warning "Heavy user: $user ($ip) exceeded 100GB"
  }
}

Script 3: Daily Health Report

:local identity [/system identity get name]
:local uptime [/system resource get uptime]
:local cpu [/system resource get cpu-load]
:local memFree [/system resource get free-memory]
:local memTotal [/system resource get total-memory]
:local activeUsers [:len [/ppp active find]]
:local version [/system resource get version]

:local report "\F0\9F\93\8A Daily Report: $identity\n"
:set report "$report Uptime: $uptime\n"
:set report "$report CPU: $cpu%\n"
:set report "$report RAM: $memFree / $memTotal\n"
:set report "$report Active users: $activeUsers\n"
:set report "$report Version: $version"

:log info $report
# Send via Telegram or email

Script 4: Bulk DNS Import

:local domains {"ads.google.com"; "doubleclick.net"; "tracking.example.com"; "analytics.spam.com"}

:foreach domain in=$domains do={
  :do {
    /ip dns static add name=$domain address=0.0.0.0 comment="Blocked"
    :log info "Blocked: $domain"
  } on-error={
    :log warning "Already blocked: $domain"
  }
}

Scheduling Scripts

# Run every 5 minutes
/system scheduler add name=wan-monitor interval=5m on-event="/system script run wan-failover"

# Run daily at 7 AM
/system scheduler add name=daily-report interval=1d start-time=07:00:00 on-event="/system script run health-report"

# Run once at specific date
/system scheduler add name=one-time start-date=2026-12-31 start-time=23:59:00 on-event=":log info \"Happy New Year\"" interval=0

See our scheduler guide for more scheduling patterns.

Environment Variables

# Available inside scheduled scripts:
:local scriptName $0          # Name of the current script
:local schedulerOwner $1      # Owner of the scheduler

# Get system info
:local hostname [/system identity get name]
:local date [/system clock get date]
:local time [/system clock get time]

HTTP Fetch (API Calls)

# GET request
/tool fetch url="https://api.example.com/status" output=user as-value

# POST request
/tool fetch url="https://hooks.slack.com/services/YOUR/WEBHOOK" http-method=post http-data="{\"text\":\"Alert from MikroTik\"}" http-header-field="Content-Type: application/json" output=none

# Download a file
/tool fetch url="https://your-server.com/blocklist.rsc" dst-path="blocklist.rsc"

Debugging Scripts

  • :put $variable – Print to terminal (only in terminal, not in scheduler).
  • :log info $message – Write to system log (visible everywhere).
  • /system script run scriptName – Run interactively and see output.
  • /system script print detail – Check for syntax errors (last-started, run-count).
  • /log print where topics~"script" – Filter script-related log entries.

Best Practices

  • Use :local, not :global – Global variables persist and can cause conflicts between scripts.
  • Add error handling – Wrap risky operations in :do { } on-error={ }.
  • Log extensively – Use :log info/warning/error for debugging production scripts.
  • Test in terminal first – Run scripts manually before scheduling them.
  • Keep scripts short – Break complex logic into multiple scripts that call each other.
  • Comment your code – Use # comments for future maintenance.

Conclusion

RouterOS scripting turns your MikroTik from a static configuration device into a programmable network appliance. Automated failover, health monitoring, bulk operations, webhook alerts – anything you do manually can be scripted and scheduled. Master the basics (variables, loops, error handling), then build up to production automation scripts.

For HTTP-based automation, also see our REST API guide. For managing users programmatically, MikroRadius provides its own API that complements RouterOS scripting.

Was this article helpful?