MikroTik RouterOS
RouterOS CLI syntax has changed substantially across releases, and plausible-looking
commands are frequently wrong for the version actually running. This skill replaces
recall with lookup: a SQLite database harvested directly from live RouterOS devices
via /console/inspect, so every answer is ground truth for a specific version.
Do not write RouterOS syntax from memory. Look it up, then lint it.
Workflow
1. Establish the version first
Every answer depends on it. If you have device access:
/system/resource/print
Take version (e.g. 7.16.1) and architecture-name. Also useful, because the
command tree is package-dependent:
/system/package/print
If you cannot reach the device, ask the user which RouterOS version they are targeting. Do not guess and do not default to "latest" silently.
2. Pick the query that matches the question
Route on the question, not on habit. Most of these are one command; reaching for the wrong one turns a single lookup into a dozen.
| the question is... | use | not |
|---|---|---|
| "what are the arguments to X on version V" | show |
|
| "what values does argument A take" | arg |
|
| "where does X live in the tree" | find |
|
| "what changed between V1 and V2" — drift, upgrade impact, "will my config still work" | diff |
looping show over two versions |
| "when did X change / which release introduced it" | trace |
looping show over every version |
| "what do we have data for" | versions |
python3 scripts/ros_query.py show /ip/firewall/filter/add --version 7.16.1
python3 scripts/ros_query.py arg /ip/address/add interface --version 7.16.1
python3 scripts/ros_query.py find address-list --version 7.16.1
python3 scripts/ros_query.py diff 7.13.5 7.23.2 --path /routing/isis
python3 scripts/ros_query.py trace /routing/isis/instance/add l1.redistribute
python3 scripts/ros_query.py versions
Add --json for machine-readable output.
Two versions in the question means diff — run it first, before any show.
Any question about upgrading, drift, "what's different", or config that works on
one version and not another is a diff, even when it is phrased as a question
about one command. Comparing show output by hand across versions is the wrong
tool: it misses enum value changes and kind changes entirely (those live inside
the argument, so both versions list the same argument name and look identical),
and it buries the real drift in console-wide plumbing. diff reports value-level
and kind-level changes and suppresses the plumbing; show cannot.
Scope diff with --path when the user named a subsystem. Unscoped is fine too
— it is fast — but the output is long.
trace answers "which release did this land in", which is what an upgrade path
actually needs: diff 7.13.5 7.23.2 says a value is gone, trace says it went
at 7.19.4, so a hop to 7.18 is safe. Never answer "when did this change" by
running show against each version in turn.
3. Lint before delivering
This is not optional. Any config you produce — a full .rsc or a single line —
gets validated before it reaches the user or a device:
python3 scripts/ros_lint.py config.rsc --version 7.16.1
python3 scripts/ros_lint.py -c '/ip/address/add address=10.0.0.1/24 interface=ether1' -v 7.16.1
Exit code is non-zero when there are errors. Fix them and re-run. If the linter reports an error you believe is wrong, say so explicitly to the user rather than quietly ignoring it.
Reading the results
Arguments are classified, and the classification changes how much you can trust the value list:
| kind | meaning | how to treat it |
|---|---|---|
enum |
fixed set of values for this version | authoritative — anything else is wrong |
reference |
must name an object that exists on the device (interface=, in-interface=) |
never invent a value; use one the user's config actually defines |
open |
free-form (addresses, comments, numbers) | not checkable from syntax alone |
Some enums are extensible — a firewall chain= may name a user-defined chain —
so the linter reports unknown values there as warnings, not errors.
Reading drift output
diff separates three kinds of change, and they carry very different weight:
arguments changed— an argument that exists in both versions but whosekindor accepted values moved. This is the dangerous category: the config line still parses, so nothing warns you, but the value you were passing may no longer be accepted. Read this section first.arguments added/paths added— new capability. Only matters when downgrading, or when you want the new feature.arguments removed/paths removed— config that will now fail outright. Loud, and therefore less dangerous than a silent value change.
platform_wide_churn is collapsed to counts because it is console plumbing
(comments, group-by, on-event, proplist, follow-strict) that changed
across hundreds of paths in the same release. It is never the answer to "what
changed in this subsystem". Pass --include-noise only if specifically asked.
Non-monotonic changes are harvest artifacts, not release changes
A value or argument that disappears in one release and returns in the next was
never really removed. /console/inspect cannot distinguish a version-level enum
from a live object name, so a harvest taken on a configured router drops values
that the next clean harvest restores.
trace detects this and prints a !! warning. Never report a flagged change
as real drift. If diff shows a removal that looks surprising, trace that
argument before reporting it — the removal may be an artifact of a single dirty
harvest sitting between two clean ones. Cross-checking a sibling subsystem
(/routing/ospf for a /routing/isis finding) confirms it: if the same values
vanished there in the same release, it is the harvest, not RouterOS.
Version fallback
If the exact version is not in the database, the closest one within the same major version is used and the substitution is reported in the output. Always surface that note to the user rather than presenting the answer as exact.
RouterOS 6 is not covered
This database is RouterOS 7 only. RouterOS 6 lacks /console/inspect
entirely, so it cannot be harvested, and the tools refuse v6 queries rather than
answering from v7 data.
If the user is on RouterOS 6: say so plainly, and do not supply v7 syntax as a
substitute. Routing (/routing bgp, /routing filter, routing tables), wireless
(/interface wireless vs /interface wifi) and parts of /ip differ enough that
a v7 answer will be wrong in precisely the places that matter. Point them at
MikroTik's v6 documentation. references/migration-6-to-7.md describes the
differences, but its v6 claims are hand-written and unverified — treat it as
orientation, not ground truth.
What the database does not cover
Introspection gives syntax, never semantics. For behaviour, ordering, and migration
traps, read the curated references — they hold what /console/inspect cannot tell you:
references/gotchas.md— rule ordering,place-before,[find]vs numeric indices, safe-mode, and other ways syntactically valid config still misbehavesreferences/migration-6-to-7.md— what moved or was rewritten between v6 and v7references/scripting.md— the scripting language and its sharp edges
Rebuilding the database
When a new RouterOS release appears, or to add a version:
python3 harvest/harvest.py --host <router-ip> --batch-size 40
python3 harvest/ingest.py --replace
harvest/chr_lab.py and harvest/chr_proxmox.py automate this against Cloud
Hosted Router images, so any released version can be harvested without hardware.
Harvesting a zero-config router (a freshly booted CHR) produces cleaner data
than a configured one: /console/inspect cannot distinguish a version-level enum
from a live object reference, so a device with lots of config pollutes value lists.
The ingester subtracts known live object names to compensate, but a clean baseline
is better.