Help us improve
Share bugs, ideas, or general feedback.
From itential-builder
Builds and runs IAG (Itential Automation Gateway) services—Python scripts, Ansible playbooks, OpenTofu plans—using YAML definitions imported with iagctl. Invoke with action or service-name.
npx claudepluginhub itential/builder-skillsHow this command is triggered — by the user, by Claude, or both
Slash command
/itential-builder:SKILL [action or service-name]skills/iag/Files this command reads when invoked
The summary Claude sees in its command listing — used to decide when to auto-load this command
# IAG — Itential Automation Gateway IAG exposes Python scripts, Ansible playbooks, and OpenTofu plans as REST APIs. Everything is defined in YAML, imported with `iagctl db import`. --- ## Gotchas - **`clusterId` must match** the IAG cluster config — discover with `GET /gateway_manager/v1/gateways/` - **`params` maps to decorator schema** — check with `iagctl run service <type> <name> --use` - **`inventory` is `""` (empty string)** when not targeting nodes, not `[]` or `null` - **OpenTofu services require `action: apply|plan|destroy`** in the service YAML — field names are `vars` and `...
/SKILLResolves GitHub issue via isolated worktree, TDD workflow, and auto-closing PR creation.
/SKILLCreates conventional git commit from conversation intent using git-agent and pushes to remote. Accepts optional Claude model name for co-author.
/SKILLSurfaces current session task from state file, evaluates clarity (prompts for clarification if needed), assesses completion, and verifies if fully done.
Share bugs, ideas, or general feedback.
IAG exposes Python scripts, Ansible playbooks, and OpenTofu plans as REST APIs. Everything is defined in YAML, imported with iagctl db import.
Write YAML → iagctl db import → Services available → Workflows call them
clusterId must match the IAG cluster config — discover with GET /gateway_manager/v1/gateways/params maps to decorator schema — check with iagctl run service <type> <name> --useinventory is "" (empty string) when not targeting nodes, not [] or nullaction: apply|plan|destroy in the service YAML — field names are vars and var-files (NOT plan-vars / plan-var-files)runService result is JSON-RPC wrapped — extract with query path result.stdout, not stdoutstdout is always a string — even when a Python script prints valid JSON, result.stdout is a string (e.g., "{\"hostname\":\"Router1\"}"). You must parse it before referencing fields inside it. Use a parse task (WorkFlowEngine) or transformation to convert the JSON string to an object.req-file path is relative to working-directory — if working-directory: scripts, then req-file: requirements.txt looks for scripts/requirements.txt inside the cloned repo, not the repo root$var doesn't resolve inside newVariable objects — use separate query tasks insteadiagctl create secret --prompt-value. Keep secrets: out of services.yaml so --force never overwrites them.--force to overwrite existing services--force overwrites secrets too — placeholder secrets replace real ones--set key must exist in the decorator schemaiagctl db import file.yaml --validate before importingnetwork_cli needs paramiko + look_for_keys = False — add paramiko to runtime.req-file (requirements.txt), and in ansible.cfg add [paramiko_connection]\nlook_for_keys = False. Without look_for_keys = False, password auth fails with "No existing session". Use cisco.iosxr.iosxr_command (or ansible.netcommon.cli_command) for show commands — NOT ansible.builtin.rawiagctl run service opentofu-plan apply <name> --set key=value (the apply/destroy subcommand goes between the type and service name)state_file — outputs are in state_file.outputs, not result.stdout like Python/Ansibleiagctl db import — loads into IAGiagctl run service — test from CLIGatewayManager.runService — call from Itential workflowsAlways start from a helper template. Read the matching example from ${CLAUDE_PLUGIN_ROOT}/helpers/iag/ first, then modify:
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-python-service.yaml${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-ansible-service.yaml${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-opentofu-service.yaml${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-multi-service-chain.yaml${CLAUDE_PLUGIN_ROOT}/helpers/iag/service-file-schema.mdDo NOT build YAML from scratch. Read the helper first.
| Mode | Auth | How |
|---|---|---|
| Local | None needed | iagctl talks to local IAG directly |
| Server/Client | Login required | iagctl login <username> → interactive password prompt |
| Itential workflows | Pre-configured | Platform admin sets up gateway. clusterId references it. |
The agent cannot run iagctl login — it requires an interactive terminal. If the engineer hasn't logged in yet, tell them:
"Run
iagctl login adminin your terminal and enter your password. Once done, I can continue."
Quick check — if this works, you're authenticated:
iagctl get services
A service file has these top-level sections (all optional — include only what you need):
decorators: [] # Input schemas for services
repositories: [] # Git repos with code
services: [] # Python/Ansible/OpenTofu services
registries: [] # Package registries (PyPI, Galaxy)
secrets: [] # Credentials and keys
| Type | Key fields | Runs |
|---|---|---|
python-script | filename, runtime.env, runtime.req-file | Python file from repo |
ansible-playbook | playbooks, runtime.inventory, runtime.env | Ansible playbook(s) from repo |
opentofu-plan | action, vars, var-files, state-file | OpenTofu apply/plan/destroy |
executable | filename, arg-format | Custom executable |
Complete service YAML with all common fields:
decorators:
- name: my-service # should match service name
schema:
$id: my-service # should match service name
$schema: https://json-schema.org/draft/202012/schema
properties:
device_ip:
type: string
description: "Target device IP"
examples: ["10.0.0.1", "172.20.100.63"]
device_type:
type: string
description: "Netmiko device type"
enum: ["cisco_ios", "cisco_xr", "cisco_nxos"]
default: "cisco_ios"
interfaces:
type: string
description: "Comma-separated interface names"
required:
- device_ip
- interfaces
type: object
repositories:
- name: my-repo
url: https://github.com/org/repo.git
reference: main
services:
- name: my-service
type: python-script
description: Connects to device and returns interface health report
filename: main.py
working-directory: scripts # directory containing main.py in repo
repository: my-repo
decorator: my-service # links to decorator above
secrets: # injected as env vars at runtime
- name: device-username
type: env
target: DEVICE_USERNAME # script reads os.environ['DEVICE_USERNAME']
- name: device-password
type: env
target: DEVICE_PASSWORD
runtime:
req-file: requirements.txt # or pyproject.toml — installs dependencies
env: # extra environment variables
NETMIKO_TIMEOUT: "30"
Python script contract — how IAG runs your script:
--property_name CLI args. Decorator schema property names become argparse flags. A property named device_ip becomes --device_ip.secrets block. Use os.environ.get('DEVICE_USERNAME').runtime.env — use this to make one script serve multiple services (see pattern below).print(json.dumps(result)). Even on errors, return JSON with "success": false.Script template:
#!/usr/bin/env python3
import argparse
import json
import os
import sys
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--device_ip", required=True)
parser.add_argument("--device_type", default="cisco_ios")
parser.add_argument("--interfaces", required=True)
args = parser.parse_args()
username = os.environ.get("DEVICE_USERNAME")
password = os.environ.get("DEVICE_PASSWORD")
if not username or not password:
print(json.dumps({"success": False, "error": "DEVICE_USERNAME and DEVICE_PASSWORD env vars required"}))
sys.exit(1)
try:
result = {"success": True, "data": do_work(args, username, password)}
print(json.dumps(result))
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
if __name__ == "__main__":
main()
One-file-multi-service pattern: Same Python file, different services with different runtime.env:
services:
- name: aws-ec2-add
type: python-script
filename: aws-ec2.py # same file
working-directory: aws-operations
repository: my-repo
decorator: aws-ec2-add
runtime:
env:
OPERATION: add # script checks os.environ.get('OPERATION')
OUTPUT_FORMAT: json
secrets:
- name: aws_access_key_id
type: env
target: AWS_ACCESS_KEY_ID
- name: aws-ec2-delete
type: python-script
filename: aws-ec2.py # same file
working-directory: aws-operations
repository: my-repo
decorator: aws-ec2-delete
runtime:
env:
OPERATION: delete # different operation
OUTPUT_FORMAT: json
secrets:
- name: aws_access_key_id
type: env
target: AWS_ACCESS_KEY_ID
The script checks env vars first, then falls back to argparse:
operation = os.environ.get('OPERATION') or args.op
Complete service YAML — runtime block is critical for Ansible:
decorators:
- name: sros-config
schema:
$id: sros-config
$schema: https://json-schema.org/draft/202012/schema
properties:
sros_cli_commands:
type: array
items:
type: string
minItems: 1
description: "CLI commands to execute"
target_hosts:
type: string
description: "Target hosts or inventory groups"
default: "all"
required:
- sros_cli_commands
type: object
repositories:
- name: my-ansible-repo
url: git@github.com:org/ansible-playbooks.git
private-key-name: git-ssh-key
services:
- name: sros-config
type: ansible-playbook
description: Execute CLI commands on Nokia SROS devices
playbooks:
- sros_config.yml # one playbook per service (array but always single)
working-directory: sros_config # directory containing the playbook
repository: my-ansible-repo
decorator: sros-config
runtime:
inventory: # REQUIRED for Ansible — inventory file(s)
- inventory.yaml
config-file: ansible.cfg # optional — custom ansible config
env: # IMPORTANT — controls Ansible behavior
ANSIBLE_HOST_KEY_CHECKING: "false" # disable SSH host key checking
ANSIBLE_STDOUT_CALLBACK: json # JSON output — critical for structured results
Ansible service with secrets (SSH key injection):
services:
- name: linux-patch-check
type: ansible-playbook
playbooks:
- patch_check.yml
working-directory: linux_patch_check
repository: my-ansible-repo
decorator: linux-patch-check
secrets:
- name: SELAB-PEM # secret name in IAG
type: env
target: SELAB-PEM # playbook reads with lookup('env', 'SELAB-PEM')
runtime:
inventory:
- inventory.yaml
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
ANSIBLE_STDOUT_CALLBACK: json
The playbook writes the injected key to a temp file:
- name: Write PEM to temp file
ansible.builtin.copy:
content: "{{ lookup('env', 'SELAB-PEM') }}"
dest: "/tmp/ssh_key.pem"
mode: '0600'
Multiple services sharing a working-directory — different playbooks in the same directory:
services:
- name: linux-patch-check
playbooks: [patch_check.yml]
working-directory: linux_patch_check # same directory
# ...
- name: linux-execute-patch
playbooks: [execute_patch.yml]
working-directory: linux_patch_check # same directory
# ...
- name: linux-mock-patch
playbooks: [mock_patch.yml]
working-directory: linux_patch_check # same directory
# ...
Ansible runtime options (all optional, in the runtime: block):
| Field | Purpose | Example |
|---|---|---|
inventory | Inventory file(s) | ["inventory.yaml"] |
config-file | ansible.cfg path | "ansible.cfg" |
env | Environment variables | {ANSIBLE_HOST_KEY_CHECKING: "false"} |
req-file | pip requirements or ansible-galaxy requirements.yml | "requirements.txt" or "requirements.yml" |
extra-vars | Extra variables | ["env=prod"] |
extra-vars-file | Variable files | ["vars.yml"] |
check | Dry-run mode | false |
diff | Show diffs | true |
forks | Parallel processes | 10 |
tags | Run only these tags | "webservers" |
limit | Limit to hosts | ["host1"] |
Ansible network_cli for network devices (Cisco XR, IOS, NXOS, Nokia SROS):
Use network_cli connection with vendor modules (e.g., cisco.iosxr.iosxr_command, ansible.netcommon.cli_command). This is the recommended approach for network devices.
Required files in the working directory:
requirements.txt — pip dependencies for network_cli:
paramiko
ansible.cfg — must include look_for_keys = False for password auth:
[defaults]
host_key_checking = False
stdout_callback = json
timeout = 30
[persistent_connection]
connect_timeout = 30
command_timeout = 30
[paramiko_connection]
look_for_keys = False
inventory.yaml — use Jinja2 refs to decorator schema properties:
all:
children:
xr_device:
hosts:
xr-router:
ansible_host: "{{ device_ip }}"
ansible_user: "{{ device_username }}"
ansible_password: "{{ device_password }}"
ansible_connection: network_cli
ansible_network_os: cisco.iosxr.iosxr
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
ansible_host_key_checking: false
ansible_paramiko_host_key_checking: false
health_check.yml — playbook using vendor module:
---
- name: Cisco XR Health Check
hosts: xr_device
gather_facts: false
tasks:
- name: Run show commands
cisco.iosxr.iosxr_command:
commands:
- show version
- show platform
- show ip interface brief
register: command_output
- name: Display results
ansible.builtin.debug:
msg:
device_ip: "{{ ansible_host }}"
show_version: "{{ command_output.stdout[0] }}"
show_platform: "{{ command_output.stdout[1] }}"
show_ip_interface_brief: "{{ command_output.stdout[2] }}"
services.yaml — wire it all together with runtime.req-file:
services:
- name: xr-health-check
type: ansible-playbook
playbooks:
- health_check.yml
working-directory: playbooks
repository: xr-health-check-repo
decorator: xr-health-check
runtime:
inventory:
- inventory.yaml
config-file: ansible.cfg
req-file: requirements.txt
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
ANSIBLE_STDOUT_CALLBACK: json
Key points:
paramiko in requirements.txt — IAG installs it in the service venvlook_for_keys = False in ansible.cfg — fixes "No existing session" error with password authansible_network_os must match the vendor collection (e.g., cisco.iosxr.iosxr, sros){{ var }} Jinja2 refs matching decorator schema property namesruntime.req-file can be a pip requirements.txt or ansible-galaxy requirements.ymlComplete service YAML — note the correct field names:
decorators:
- name: azure-landing-zone
schema:
$id: azure-landing-zone
$schema: https://json-schema.org/draft/202012/schema
properties:
resource_group_name:
type: string
description: "Resource group name"
default: "lz-demo-rg"
vnet_address_space:
type: array
items:
type: string
pattern: "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$"
default: ["10.0.0.0/16"]
required:
- resource_group_name
type: object
repositories:
- name: my-tofu-repo
url: git@github.com:org/opentofu.git
private-key-name: git-ssh-key
services:
- name: azure-landing-zone
type: opentofu-plan
description: Deploy Azure landing zone infrastructure
working-directory: infra/modules/landing-zone # directory with .tf files
repository: my-tofu-repo
decorator: azure-landing-zone
action: apply # REQUIRED: apply, plan, or destroy
vars: [] # optional: ["-var flags"] e.g. ["region=us-east-1"]
var-files: [] # optional: ["-var-file flags"] e.g. ["prod.tfvars"]
state-file: null # optional: custom state file path
IMPORTANT — field names: The fields are vars and var-files, NOT plan-vars / plan-var-files. The action field is required.
Secrets for cloud credentials use the TF_VAR_ convention:
services:
- name: deploy-infra
type: opentofu-plan
working-directory: infra
repository: my-tofu-repo
decorator: deploy-infra
action: apply
vars: []
var-files: []
state-file: null
secrets:
- name: aws-access-key
type: env
target: TF_VAR_aws_access_key # OpenTofu reads TF_VAR_* as variables
- name: aws-secret-key
type: env
target: TF_VAR_aws_secret_key
Decorator params pass directly as OpenTofu variables — each property in the decorator schema becomes a variable available to your .tf files. Backend/provider config lives in the .tf files, not the service YAML.
Every service should have a decorator. The $id should match the service name:
decorators:
- name: my-service
schema:
$id: my-service # match service name, not "root"
$schema: https://json-schema.org/draft/202012/schema
properties:
device_ip:
type: string
description: "Target device IP"
format:
type: string
enum: ["json", "table"] # restricted values
default: "json"
commands:
type: array # array with item validation
items:
type: string
minItems: 1
verbose:
type: string
enum: ["true", "false"] # booleans as strings (common pattern)
default: "false"
required:
- device_ip
type: object
additionalProperties: false # reject unknown params (recommended)
Best practice: Never put real secret values in YAML. Define secret references in the service, create actual secrets separately.
# In services.yaml — only references, no values
services:
- name: my-service
type: python-script
filename: main.py
working-directory: scripts
repository: my-repo
secrets: # injected as env vars at runtime
- name: api-token # secret name in IAG
type: env
target: API_TOKEN # script reads os.environ['API_TOKEN']
# Create secrets separately — never in the YAML file
iagctl create secret api-token --prompt-value
WARNING: --force import overwrites secrets too. If your YAML has a top-level secrets: section with placeholder values, --force will replace real secrets with placeholders. Keep the top-level secrets: section out of services.yaml entirely. Only define secret references inside each service's secrets: array.
repositories:
# SSH auth (most common):
- name: private-repo
url: git@github.com:org/private.git
private-key-name: git-ssh-key # name of secret holding SSH key
reference: main
# HTTPS auth:
- name: https-repo
url: https://github.com/org/repo.git
username: myuser
password-name: git-password # name of secret holding password
Create the SSH key secret separately: iagctl create secret git-ssh-key --prompt-value
# Validate only (no changes)
iagctl db import services.yaml --validate
# Dry run with checks
iagctl db import services.yaml --check
# Import (additive — new added, existing skipped)
iagctl db import services.yaml
# Import with overwrite (existing replaced by name)
iagctl db import services.yaml --force
# Export current state
iagctl db export state.yaml
# Import directly from Git repo
iagctl db import --repository https://github.com/org/repo.git --reference main
Import behavior:
--force, replaced with --forceWhen iterating on service code, every change requires pushing to Git and re-importing — IAG pulls code from the repo, not from local files.
Edit code → git commit + push → iagctl db import services.yaml --force → iagctl run service → repeat
Tip: Keep secrets out of services.yaml so --force imports don't clobber them (see Secrets warning above).
# List services
iagctl get services
iagctl get services --type python-script
# See what inputs a service expects
iagctl run service python-script my-service --use
# Run with inputs
iagctl run service python-script my-service \
--set device_ip=10.0.0.1 \
--set device_type=ios
# Ansible
iagctl run service ansible-playbook my-playbook --set target_host=router1
# OpenTofu apply
iagctl run service opentofu-plan apply my-plan --set region=us-east-1
# OpenTofu destroy
iagctl run service opentofu-plan destroy my-plan
# Raw JSON output
iagctl run service python-script my-service --raw
The clusterId is required for all GatewayManager tasks. Discover it via the platform API:
GET /gateway_manager/v1/gateways/
This returns the list of configured gateway clusters. Use the cluster name as the clusterId value in workflow tasks.
| Task | What it does |
|---|---|
runService | Run an IAG service by name |
sendCommand | Send CLI commands to inventory nodes |
sendConfig | Send config text to inventory nodes |
getServices | List available services |
getGateways | List connected gateways |
{
"name": "runService",
"app": "GatewayManager",
"type": "automatic",
"location": "Application",
"displayName": "GatewayManager",
"actor": "Pronghorn",
"variables": {
"incoming": {
"serviceName": "device-info",
"clusterId": "ankitcluster",
"params": {"device_ip": "10.0.0.1", "device_type": "ios"},
"inventory": ""
},
"outgoing": {
"result": "$var.job.iagResult"
}
}
}
Incoming:
| Field | Type | Description |
|---|---|---|
serviceName | string | IAG service name (same name as in YAML/iagctl) |
clusterId | string | Gateway cluster ID — ask the engineer |
params | object | Key/value inputs matching the decorator schema |
inventory | array or "" | Target nodes: [{"inventory": "inv-name", "nodeNames": ["node1"]}] or "" if not needed |
Outgoing:
| Field | Type | Description |
|---|---|---|
result | object | JSON-RPC envelope with service execution result |
runService returns a JSON-RPC envelope, NOT raw stdout:
{
"id": "dc7c4a5d-...",
"jsonrpc": "2.0",
"result": {
"return_code": 0,
"stdout": "{ ... script output ... }",
"stderr": "",
"start_time": "2026-03-03T19:26:37Z",
"end_time": "2026-03-03T19:26:37Z",
"elapsed_time": 0.659
},
"status": "completed"
}
To extract stdout in a workflow: use a query task with path result.stdout:
{
"name": "query",
"app": "WorkFlowEngine",
"type": "operation",
"variables": {
"incoming": {
"pass_on_null": false,
"query": "result.stdout",
"obj": "$var.job.iagResult"
},
"outgoing": {
"return_data": "$var.job.serviceOutput"
}
}
}
Pass output from one service as input to the next:
runService(device-info)
→ query: extract result.stdout → parse JSON
→ runService(config-generator) with params from previous output
→ query: extract result.stdout
→ runService(config-validator)
Each query extracts result.stdout from the JSON-RPC envelope. If the stdout is JSON, parse it before passing as params to the next service.
{
"name": "sendCommand",
"app": "GatewayManager",
"type": "automatic",
"actor": "Pronghorn",
"variables": {
"incoming": {
"clusterId": "ankitcluster",
"commands": ["show version", "show ip interface brief"],
"inventory": [{"inventory": "my-inventory", "nodeNames": ["router1"]}]
},
"outgoing": {
"result": "$var.job.commandResult"
}
}
}
{
"name": "sendConfig",
"app": "GatewayManager",
"type": "automatic",
"actor": "Pronghorn",
"variables": {
"incoming": {
"clusterId": "ankitcluster",
"config": "$var.job.renderedConfig",
"inventory": [{"inventory": "my-inventory", "nodeNames": ["switch1"]}]
},
"outgoing": {
"result": "$var.job.configResult"
}
}
}
After CLI testing passes (iagctl run service), test the full workflow integration:
1. Create the workflow (runService → query to extract stdout):
POST /automation-studio/automations
2. Start a job:
POST /operations-manager/jobs/start
{
"workflow": "My IAG Workflow",
"options": {
"type": "automation",
"variables": {
"device_ip": "172.20.100.63",
"device_type": "cisco_xr",
"interfaces": "GigabitEthernet0/0/0/0",
"clusterId": "ankitcluster"
}
}
}
3. Check the job:
GET /operations-manager/jobs/{jobId}
Verify:
data.status is "complete" (not "error")data.error is null (no task errors)data.variables.serviceOutput contains the extracted stdout from the IAG serviceIf the job errors with "Service not found on cluster": the clusterId is wrong. Check GET /gateway_manager/v1/gateways/ for the correct cluster name.
| Need | Use |
|---|---|
| Run a Python/Ansible/OpenTofu service | GatewayManager.runService |
| Send ad-hoc CLI commands | GatewayManager.sendCommand or AGManager.itential_cli |
| Push config text to device | GatewayManager.sendConfig or AGManager.itential_set_config |
| Run MOP validation checks | MOP.RunCommandTemplate (separate from IAG) |
| AGManager | GatewayManager | |
|---|---|---|
| Tasks | One per script/playbook (e.g., itential_cli) | Generic (runService, sendCommand) |
| Input style | Task-specific variables | serviceName + params object |
| When to use | Built-in IAG capabilities | Custom services built with iagctl |
After importing, use these to verify and manage resources:
# === LIST RESOURCES ===
iagctl get services
iagctl get services --type python-script
iagctl get services --type ansible-playbook
iagctl get services --type opentofu-plan
iagctl get repositories
iagctl get secrets
iagctl get decorators
iagctl get registries
iagctl get clusters # find clusterId for workflows
# === INSPECT A SPECIFIC RESOURCE ===
iagctl describe service <name> # full details: repo, decorator, secrets, runtime
iagctl describe repository <name> # URL, reference, auth method
iagctl describe decorator <name> # JSON schema
iagctl describe secret <name> # secret metadata (value redacted)
# === DELETE ===
iagctl delete service <name>
iagctl delete repository <name>
iagctl delete decorator <name>
iagctl delete secret <name>
# === EXPORT CURRENT STATE ===
iagctl db export current-state.yaml # full dump of everything in IAG
After every import, verify with:
iagctl describe service <name>
This confirms the service was created with the correct repo, decorator, secrets, and working directory.
Services: {team}-{domain}-{action} e.g. netops-device-health-check
Decorators: {service-name} e.g. netops-device-health-check
Repositories: {team}-{purpose} e.g. netops-automation
Secrets: {team}-{system}-{purpose} e.g. netops-git-ssh-key
Tag services: tags: [team:netops, domain:network] — filter with iagctl get services --tag team:netops
| Layout | When | Structure |
|---|---|---|
| Standalone repo | One service per repo | services.yaml at repo root, code in subdirectory |
| Mono-repo | < 20 services, one team | .gateway/services/{name}.yml per service, shared repo |
| Multi-repo | 20+ services, domain ownership | Each team owns a repo with its own services.yaml |
Standalone repo (cleanest for individual services):
cisco-interface-check/
├── services.yaml ← decorators + repos + services in one file
└── scripts/
├── main.py
└── requirements.txt
Mono-repo (shared codebase, per-file service definitions):
automation-services/
├── .gateway/services/ ← one YAML per service
│ ├── device-info.yml
│ └── config-push.yml
├── device-info/main.py
└── config-push/main.py
| Setting | Dev | Staging | Production |
|---|---|---|---|
Git reference | branch | release branch | tagged version (e.g., v1.2.3) |
| Secrets | --prompt-value | vault or --prompt-value | vault only |
| Import mode | --force | --check then import | --validate → --check → import |
| Who imports | developer | CI/CD pipeline | CI/CD with approval |
GitLab CI:
stages: [validate, deploy]
validate:
stage: validate
script: iagctl db import services.yaml --validate
only: [merge_requests]
deploy-dev:
stage: deploy
script:
- iagctl login $IAG_USER
- iagctl db import services.yaml --force
only: [develop]
deploy-prod:
stage: deploy
script:
- iagctl db import services.yaml --check
- iagctl db import services.yaml
only: [main]
when: manual
GitHub Actions:
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: iagctl db import services.yaml --validate
- run: iagctl db import services.yaml --force
Service quality:
additionalProperties: falseiagctl run service <type> <name> --set ...print(json.dumps(result)))"success": false, not stderriagctl db import file.yaml --validateWorkflow integration:
runService taskresult.stdout from JSON-RPC envelope via query taskrunService task (handles service failures)Security and ops:
iagctl create secret --prompt-value (never in YAML)secrets: section in committed service filesAlways start from a helper template. Read the matching example from ${CLAUDE_PLUGIN_ROOT}/helpers/iag/ first, then modify:
| File | Purpose |
|---|---|
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-python-service.yaml | Python script service |
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-ansible-service.yaml | Ansible playbook service |
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-opentofu-service.yaml | OpenTofu plan service |
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-multi-service-chain.yaml | Multi-service orchestration |
${CLAUDE_PLUGIN_ROOT}/helpers/iag/service-file-schema.md | Full YAML schema reference |