MOP (Method of Procedure) - Developer Skills Guide
MOP manages command templates and analytic templates for running CLI commands against network devices with validation rules. Command templates execute show commands and evaluate the output against rules. Analytic templates compare command output before and after a change.
MOP is for read-only validation only -- never use it to push configuration to devices. Use Jinja2 templates and workflow tasks for config changes — see /itential-devices (Template Designer) or the environment's native config-push task (AGENTS.md Rule 25).
Customization
Before using this skill, check custom/org/, custom/team/, and custom/dev/
in this skill's own directory. Read every .md file found, in that order
(any folder may be empty or absent). Apply them in addition to everything
below — where a file overrides a specific rule from this document, prefer
the override; more specific wins (dev over team over org). See
.claude/CUSTOMIZATION.md for the full framework and what belongs in
which layer.
Concepts
- Command template = a set of CLI commands + validation rules, run against one or more devices
- Analytic template = pre/post comparison of command output to detect drift
- Variable syntax =
<!variable_name!>in both commands and rules (NOT{{ var }}or$var) - Pass/fail logic = hierarchical: template-level -> command-level -> rule-level, each with AND/OR control
API Reference
All /mop/* endpoints:
| Method | Endpoint | Description |
|---|---|---|
| POST | /mop/createTemplate |
Create a command template |
| GET | /mop/listTemplates |
List all command templates |
| GET | /mop/listATemplate/{name} |
Get a command template by name |
| POST | /mop/updateTemplate/{mopID} |
Update a command template (full replacement) |
| POST | /mop/deleteTemplate/{id} |
Delete a command template |
| POST | /mop/exportTemplate |
Export template (body: {"_id": "..."} or {"name": "..."}) |
| POST | /mop/importTemplate |
Import a template |
| POST | /mop/RunCommandTemplate |
Run a command template against devices |
| POST | /mop/RunCommand |
Run a single ad-hoc command on one device (workflow task) |
| POST | /mop/RunCommandDevices |
Run a single ad-hoc command on multiple devices |
| POST | /mop/RunCommandTemplateSingleCommand |
Run one command from a template by index |
| POST | /mop/GetBootFlash |
Get boot flash image name from a device |
| POST | /mop/reattempt |
Retry/delay mechanism for workflows |
| POST | /mop/createAnalyticTemplate |
Create an analytic template |
| GET | /mop/listAnalyticTemplates |
List all analytic templates |
| GET | /mop/listAnAnalyticTemplate/{name} |
Get an analytic template by name (path param) |
| POST | /mop/updateAnalyticTemplate/{id} |
Update an analytic template |
| POST | /mop/deleteAnalyticTemplate/{id} |
Delete an analytic template |
| POST | /mop/runAnalyticsTemplate |
Run an analytic template (workflow task) |
Template Structure
Create with POST /mop/createTemplate. The body uses a {"mop": {...}} wrapper.
{
"mop": {
"name": "Port_Turn_Up_Pre_Check",
"description": "Validates interface and VLAN before port turn-up",
"os": "",
"passRule": true,
"ignoreWarnings": false,
"commands": [
{
"command": "show interface <!interface!>",
"passRule": true,
"rules": [
{
"rule": "line protocol is",
"eval": "contains",
"severity": "error"
}
]
},
{
"command": "show vlan brief",
"passRule": true,
"rules": [
{
"rule": "<!vlan_id!>",
"eval": "contains",
"severity": "error",
}
]
}
]
}
}
Field reference:
name-- template name (required, must be unique)description-- human-readable descriptionos-- target OS filter (empty string = any OS)passRule(template-level) --true= ALL commands must pass (AND),false= ONE command must pass (OR)ignoreWarnings-- see ignoreWarnings section belowcommands[]-- array of commands to executecommand-- the CLI command string. Variables use<!variable_name!>syntaxpassRule(command-level) --true= ALL rules must pass (AND),false= ONE rule must pass (OR)rules[]-- validation rules applied to the command outputrule-- the string or pattern to match against. Can contain<!variables!>eval-- evaluation operator (case-sensitive, see Rule Evaluation below)severity--"error","warning", or"info"flags-- optional evaluation flags (see Flags below)
Only "name" is required -- template validation uses AJV with strict=false, so minimal templates are accepted.
passRule Logic
- Template-level
passRule: true= ALL commands must pass (AND logic) - Template-level
passRule: false= at least ONE command must pass (OR logic) - Command-level
passRule: true= ALL rules in this command must pass (AND logic) - Command-level
passRule: false= at least ONE rule must pass (OR logic)
ignoreWarnings
Template-level field, default false. When true: only rules with severity: "error" count as real failures. Rules with severity: "warning" or "info" that fail are treated as passing. When false (default): all severity levels count.
{
"mop": {
"name": "...",
"passRule": true,
"ignoreWarnings": true,
"commands": [...]
}
}
Rule Evaluation
The eval field determines how rule matching works. Eval types are case-sensitive.
| Eval | Purpose | Example Rule |
|---|---|---|
contains |
String exists in output | "line protocol is" |
!contains |
String does NOT exist in output | "ERROR" |
contains1 |
String exists exactly once | "Active" |
RegEx |
Regex matches output (capital R and E!) | "/\\d+\\.\\d+/" |
!RegEx |
Regex does NOT match | "/ERROR/" |
#comparison |
Extract + compare two values | See details below |
Flags
Optional flags object on each rule:
case: true= case-INSENSITIVE matching (confusing name --case: truedoes NOT mean case-sensitive)global: true= global search (RegEx only)multiline: true=^/$match start/end of lines, not just start/end of string (RegEx only)
case is available for all eval types. global and multiline are only meaningful for RegEx and !RegEx.
#comparison Details
Extract two values from command output using regex, then compare numerically.
{
"rule": "/Available: (\\d+)/",
"ruleB": "/Total: (\\d+)/",
"eval": "#comparison",
"evaluator": ">=",
"severity": "error"
}
rule/ruleB-- regex patterns (in/pattern/format) to extract values from the command outputevaluator-- comparison operator:=,!=,<,>,<=,>=,%%operator -- passes ifruleB/rule * 100 <= percentage. Set"percentage": 80to pass if ruleB is at most 80% of rule.
Example with percentage:
{
"rule": "/Total: (\\d+)/",
"ruleB": "/Used: (\\d+)/",
"eval": "#comparison",
"evaluator": "%",
"percentage": 80,
"severity": "error"
}
Variable Substitution
- Syntax:
<!variable_name!>in both commands and rules - Variables are substituted BEFORE execution
- If a variable is missing, the command is SKIPPED (not failed!) and counts as PASSED
- This syntax is different from Jinja2 templates (
{{ var }}) and workflow variable references ($var.job.x)
Example command with variables:
{
"command": "show running-config interface <!interface!>",
"passRule": true,
"rules": [
{
"rule": "switchport access vlan <!vlan_id!>",
"eval": "contains",
"severity": "error",
"evaluation": "pass"
}
]
}
Execution
Standalone (without a workflow)
POST /mop/RunCommandTemplate
{
"template": "Port_Turn_Up_Pre_Check",
"variables": {
"interface": "GigabitEthernet0/1",
"vlan_id": "100"
},
"devices": ["IOS-CAT8KV-1"]
}
template-- template name (string)variables-- object with values for<!variable!>substitutionsdevices-- array of device names (or single device name string)
In a Workflow
Use the MOP.RunCommandTemplate task. See /itential-studio for full workflow task wiring patterns.
{
"incoming": {
"template": "$var.job.templateName",
"variables": "$var.job.templateVariables",
"devices": "$var.job.devices"
},
"outgoing": {
"mop_template_results": null
}
}
template-- name of the command template (string or$varreference)variables-- object with values for<!variable!>substitutionsdevices-- array of device names to run against
See /itential-builder for running the workflow via POST /operations-manager/jobs/start.
Ad-Hoc Commands (without a template)
Run a single command directly without creating a template first:
POST /mop/RunCommand
{
"command": "show version",
"variables": {},
"device": "IOS-CAT8KV-1"
}
Returns: {raw, evaluated, device, response, result} — same shape as one entry in commands_results.
For multiple devices: POST /mop/RunCommandDevices with "devices": ["dev1", "dev2"] (array instead of singular device).
To run a single command from an existing template by index: POST /mop/RunCommandTemplateSingleCommand with {"templateId": "name", "commandIndex": 0, "variables": {}, "devices": ["dev1"]}.
Response Shape
{
"all_pass_flag": true,
"result": true,
"name": "Port_Turn_Up_Pre_Check",
"commands_results": [
{
"raw": "show interface <!interface!>",
"evaluated": "show interface GigabitEthernet0/1",
"all_pass_flag": true,
"device": "IOS-CAT8KV-1",
"response": "...command output...",
"result": true,
"parameters": {"interface": "GigabitEthernet0/1"},
"rules": [
{"rule": "line protocol is", "eval": "contains", "result": true, "severity": "error"}
]
}
]
}
result(top-level) -- overall template pass/fail (boolean)all_pass_flag(top-level) -- the template's passRule settingcommands_results[]-- one entry per command per deviceraw-- original command string (before variable substitution)evaluated-- command with variables substitutedresponse-- raw device outputresult-- whether this command passed (boolean)all_pass_flag-- this command's passRule settingdevice-- the device this command ran againstparameters-- the variables that were substitutedrules[].result--true/falsefor each individual rule
Update
POST /mop/updateTemplate/{mopID}
The mopID is the template name (URL-encoded). Uses the same {"mop": {...}} body wrapper as create. The body is a full replacement -- include ALL fields, not just changed ones.
Response on success:
{
"n": 1,
"ok": 1,
"nModified": 1
}
Analytic Templates
Analytic templates compare command output before and after a change to detect drift or validate results. Endpoints are listed in the API Reference table above.
Create an Analytic Template
POST /mop/createAnalyticTemplate
{
"name": "Interface_Change_Validation",
"os": "cisco-ios",
"passRule": true,
"prepostCommands": [
{
"preRawCommand": "show interface GigabitEthernet0/1",
"postRawCommand": "show interface GigabitEthernet0/1",
"passRule": true,
"rules": [
{
"type": "matches",
"preRegex": "/line protocol is (\\w+)/",
"postRegex": "/line protocol is (\\w+)/",
"evaluator": "="
}
]
}
]
}
Structure
name-- template nameos-- target OSpassRule--true= ALL prepostCommands must pass (AND),false= ONE must pass (OR)prepostCommands[]-- array of pre/post command pairspreRawCommand-- CLI command to run before the changepostRawCommand-- CLI command to run after the changepassRule--true= ALL rules must pass,false= ONE must passrules[]-- comparison rulestype--matches,!matches,regex, ortablepreRegex-- regex to extract value from pre-change outputpostRegex-- regex to extract value from post-change outputevaluator-- comparison operator:=,!=,<,>,<=,>=,%
Rule Types
| Type | Purpose |
|---|---|
matches |
Pre and post extracted values must match per evaluation operator |
!matches |
Pre and post extracted values must NOT match |
regex |
Regex-based extraction and comparison |
table |
Table-based comparison of structured output |
Running an Analytic Template
In a workflow, use the MOP.runAnalyticsTemplate task:
{
"incoming": {
"pre": "$var.preCheckTaskId.mop_template_results",
"post": "$var.postCheckTaskId.mop_template_results",
"analytic_template_name": "Interface_Change_Validation",
"variables": {}
},
"outgoing": {
"analytic_result": null
}
}
Critical: The pre and post inputs must be the full RunCommandTemplate output object (which contains a commands_results property). Do NOT pass just the commands_results array — pass the entire result object.
Gotcha: Pre and post commands must have exactly 1 match each in the collected results. If 0 or >1 match, it produces an error. The matching compares against both the raw and evaluated command strings — if variables were used, the evaluated string (with variables replaced) is what will match.
Gotchas
Missing variable = skip = PASS (not fail) -- if a
<!var!>token has no value, the command is silently skipped and counts as PASSED. Verify variables are passed correctly.case: true= case-INsensitive -- confusing naming."flags": {"case": true}enables case-insensitive matching. It does NOT mean case-sensitive.Empty rules = auto-pass -- a command with no rules (
"rules": []) always passes. Add at least one rule if you want validation.RegEx 5-second timeout -- complex regex patterns run in a sandboxed VM with a 5-second limit. Patterns prone to catastrophic backtracking will timeout.
containsdoes substring matching --"100"matches"1002". For exact matching, useRegExwith multiline flag:{"rule": "^<!vlanId!>\\s+", "eval": "RegEx", "severity": "error", "flags": {"multiline": true}}Eval types are case-sensitive --
"RegEx"not"regex"or"REGEX"."#comparison"not"Comparison".Only "name" is required -- template validation uses AJV with strict=false. Minimal templates are accepted.
Update is full replacement --
POST /mop/updateTemplate/{mopID}replaces the entire template. Include ALL fields when updating, not just changed ones.MOP is read-only -- command templates run show commands and evaluate output. Never use MOP to push configuration changes. Use Jinja2 templates and workflow adapter tasks for config changes.
_idequalsname-- the engine sets_id = nameon create. They are always identical. Use either for lookups.Rule-level missing variable ≠ command-level skip -- if a command has
<!var!>missing, the whole command is skipped (passes). But if a rule has<!var!>missing, it getseval: "missing_parameters"and returns"Invalid Rule: Missing Parameters"withresult: false. The rule fails, not skips.Template name change on update = delete + create -- if you update with a different name, the engine deletes the old template and creates a new one. This is destructive — the old
_idis gone.Import renames on collision --
importTemplatedoes not fail on duplicate names. It appends(N)to the name (e.g.,My_TemplatebecomesMy_Template (1)).Cannot set
namespacedirectly -- providingnamespacein the create body throws an error. Namespaces are managed through project membership.
Helper Templates
Always start from a helper template when creating assets. Read the helper file first, then modify it.
| File | API Call | Purpose |
|---|---|---|
${CLAUDE_PLUGIN_ROOT}/helpers/create/create-command-template.json |
POST /mop/createTemplate |
Command template with rules |
${CLAUDE_PLUGIN_ROOT}/helpers/update/update-command-template.json |
POST /mop/updateTemplate/{mopID} |
Update template (full replacement) |
Developer Scenarios
1. Build a pre-check command template
- Identify the show commands needed (e.g.,
show interface,show vlan brief) - Read
${CLAUDE_PLUGIN_ROOT}/helpers/create/create-command-template.jsonas a starting template - Fill in
name,description, add commands with<!variable!>placeholders - Add rules for each command -- use
containsfor simple checks,RegExfor pattern matching - Set
passRuleat template and command level (AND vs OR logic) - Create with
POST /mop/createTemplate - Test standalone with
POST /mop/RunCommandTemplateproviding variables and devices - Check
result(top-level) andcommands_results[].rules[].resultfor pass/fail details
2. Wire RunCommandTemplate into a workflow
After standalone testing passes:
- Use
/itential-studioto build a workflow - Add a
MOP.RunCommandTemplatetask to the workflow - Wire incoming variables:
template,variables,devicesusing$var.job.*references - Wire outgoing: capture results in a variable like
mop_template_results - Add downstream logic to branch on
$var.taskName.result(true/false) - Use
/itential-builderto run viaPOST /operations-manager/jobs/start
3. Build an analytic template for pre/post comparison
- Identify the commands to run before and after the change
- Create an analytic template with
POST /mop/createAnalyticTemplate - Define
prepostCommandswith pre/post command pairs - Add rules with
preRegex/postRegexto extract values for comparison - Set
evaluationoperator (=to verify values match,!=to verify they changed) - In a workflow: run pre-change commands, execute the change, run post-change commands, compare
- Remember: pre and post commands must have exactly 1 match each in results