$npx -y skills add tikoci/routeros-skills --skill routeros-scriptingRouterOS scripting language and CLI configuration idioms for .rsc files and interactive commands. Use when: writing or reviewing RouterOS scripts, scheduler/netwatch/on-event snippets, idempotent CLI config, :local/:global/:foreach/:do syntax, [find] selectors, print as-value han
| 1 | # RouterOS Scripting & CLI Config |
| 2 | |
| 3 | RouterOS scripts (`.rsc`, `/system/script`, scheduler `on-event`, Netwatch hooks, and |
| 4 | interactive CLI snippets) use RouterOS's own language. It is **not shell, Lua, Python, |
| 5 | or Tcl**. For the syntax baseline, see [references/syntax.md](./references/syntax.md). |
| 6 | This skill focuses on the traps that LLMs are likely to confidently get wrong. |
| 7 | |
| 8 | ## Do Not Use Interactive Row Numbers in Scripts |
| 9 | |
| 10 | Interactive `print` output shows temporary row numbers (`0`, `1`, `2`...) for the |
| 11 | current console buffer. They are **not stable object IDs** and are not usable inside |
| 12 | scripts. |
| 13 | |
| 14 | ```routeros |
| 15 | # WRONG in scripts — "1" is only an interactive print-buffer row number |
| 16 | /ip/route/set 1 gateway=3.3.3.3 |
| 17 | |
| 18 | # Better — internal IDs have a * prefix, but can change if objects are removed/re-added |
| 19 | /ip/route/set *1 gateway=3.3.3.3 |
| 20 | |
| 21 | # Best — resolve the target at runtime |
| 22 | /ip/route/set [find dst-address="0.0.0.0/0"] gateway=3.3.3.3 |
| 23 | ``` |
| 24 | |
| 25 | For generated config, use stable selectors such as `comment=`, `name=`, or a unique |
| 26 | address/list value. Internal IDs are visible in `print as-value` as `.id=*HEX`. |
| 27 | |
| 28 | ## Treat `[find]` Results as ID Arrays |
| 29 | |
| 30 | `[find ...]` returns zero, one, or many internal IDs. Many commands accept that |
| 31 | array directly (`set`, `remove`, `disable`), but code that needs exactly one object |
| 32 | should check the count before `get`. |
| 33 | |
| 34 | ```routeros |
| 35 | :local ids [/interface/find where name="ether1"] |
| 36 | :if ([:len $ids] = 1) do={ |
| 37 | :put [/interface/get ($ids->0) name] |
| 38 | } else={ |
| 39 | :error ("expected exactly one interface, got " . [:len $ids]) |
| 40 | } |
| 41 | ``` |
| 42 | |
| 43 | Use `[find comment="my-tool-tag"]` for idempotent cleanup, not broad selectors such |
| 44 | as `[find dynamic=no]` that can delete unrelated user configuration. |
| 45 | |
| 46 | ## Quote IP Prefixes in `find` / `where` |
| 47 | |
| 48 | RouterOS does aggressive type conversion, but not always in the direction you expect. |
| 49 | For `/ip/address`, the `address` property is stored as a string including the prefix |
| 50 | length. An unquoted literal like `111.111.1.1/24` is an IP-prefix value and may match |
| 51 | nothing. |
| 52 | |
| 53 | ```routeros |
| 54 | # WRONG — can silently return nothing |
| 55 | /ip/address/print where address=111.111.1.1/24 |
| 56 | |
| 57 | # Correct |
| 58 | /ip/address/print where address="111.111.1.1/24" |
| 59 | |
| 60 | # Correct when value came from a variable |
| 61 | :local prefix 111.111.1.1/24 |
| 62 | /ip/address/print where address=[:tostr $prefix] |
| 63 | ``` |
| 64 | |
| 65 | When a query unexpectedly returns nothing, inspect the stored type: |
| 66 | |
| 67 | ```routeros |
| 68 | :put [:typeof ([:pick [/ip/address/print as-value] 0]->"address")] |
| 69 | ``` |
| 70 | |
| 71 | ## `print as-value` Is Usually an Array of Maps |
| 72 | |
| 73 | `print as-value where ...` returns `{{...}; {...}}`, even when only one row matches. |
| 74 | Pick a row before accessing a property. |
| 75 | |
| 76 | ```routeros |
| 77 | # WRONG — tries to read "gateway" from the outer array |
| 78 | :put ([/ip/route/print as-value where gateway="ether1"]->"gateway") |
| 79 | |
| 80 | # Correct — pick the first map, then read the property |
| 81 | :put ([:pick [/ip/route/print as-value where gateway="ether1"] 0]->"gateway") |
| 82 | ``` |
| 83 | |
| 84 | For commands without `get`, `as-value` is often the only script-friendly output: |
| 85 | |
| 86 | ```routeros |
| 87 | :put ([/tool/fetch url="https://example.com/file.txt" output=user as-value]->"data") |
| 88 | ``` |
| 89 | |
| 90 | ## Monitor Commands Need `once do={...}` |
| 91 | |
| 92 | Commands such as `monitor-traffic` are interactive loops unless told to run once. |
| 93 | Use `once do={}` to capture their temporary values in a script. Hyphenated field |
| 94 | names use `$"name-with-hyphens"`. |
| 95 | |
| 96 | ```routeros |
| 97 | /interface/monitor-traffic ether1 once do={ |
| 98 | :put ("rx=" . $"rx-bits-per-second") |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | Use `:log`, not only `:put`, for scheduler or hook scripts where no terminal is |
| 103 | attached. |
| 104 | |
| 105 | ## Globals Must Be Re-Declared Where Read |
| 106 | |
| 107 | Global variables and global functions are not automatically visible inside another |
| 108 | script or function body. Declare access with `:global name;` before reading or |
| 109 | calling them. |
| 110 | |
| 111 | ```routeros |
| 112 | :global state "ready" |
| 113 | :global showState do={ |
| 114 | :global state |
| 115 | :put ("state=" . $state) |
| 116 | } |
| 117 | |
| 118 | :global addOne do={ :return ($1 + 1) } |
| 119 | :global caller do={ |
| 120 | :global addOne |
| 121 | :return [$addOne 5] |
| 122 | } |
| 123 | ``` |
| 124 | |
| 125 | Use unique variable names. Avoid names that collide with RouterOS properties such as |
| 126 | `dst-address`; if you must access such a variable, use quoted variable syntax |
| 127 | (`$"dst-address"`). |
| 128 | |
| 129 | ## Arrays Are Not JavaScript Arrays |
| 130 | |
| 131 | ```routeros |
| 132 | # Empty array: {} is a syntax error |
| 133 | :local arr [:toarray ""] |
| 134 | |
| 135 | # Indexing and assignment use -> |
| 136 | :set ($arr->"name") "router1" |
| 137 | :put ($arr->"name") |
| 138 | ``` |
| 139 | |
| 140 | String concatenation with `.` distributes over arrays unless you convert first: |
| 141 | |
| 142 | ```routeros |
| 143 | :local arr {"a"; "b"} |
| 144 | :put ("value=" . $arr) # two outputs: value=a and value=b |
| 145 | :put ("value=" . [:tostr $arr]) # o |