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.
Write YAML → iagctl db import → Services available → Workflows call them
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.
Gotchas
clusterIdmust match the IAG cluster config — discover withGET /gateway_manager/v1/gateways/paramsmaps to decorator schema — check withiagctl run service <type> <name> --useinventoryis""(empty string) when not targeting nodes, not[]ornull- OpenTofu services require
action: apply|plan|destroyin the service YAML, with field namesvars/var-files(see the OpenTofu example below for the full field list) runServiceresult is JSON-RPC wrapped — see "Result Shape — JSON-RPC Wrapper" below for the full unwrapping patternstdoutis always a string — even when a Python script prints valid JSON,result.stdoutis a string (e.g.,"{\"hostname\":\"Router1\"}"). You must parse it before referencing fields inside it. Use aparsetask (WorkFlowEngine) ortransformationto convert the JSON string to an object.req-filepath is relative toworking-directory— ifworking-directory: scripts, thenreq-file: requirements.txtlooks forscripts/requirements.txtinside the cloned repo, not the repo root$vardoesn't resolve insidenewVariableobjects when wiringrunServiceoutputs — see AGENTS.md Rule 8; use aquerytask instead--forceskips existing same-name resources unless forced, and overwrites secrets too — see "Adding Secrets" below for the full warning and the fix (keepsecrets:out of the top-level YAML entirely).- Decorators reject unknown params — every
--setkey must exist in the decorator schema - Decorator property names become argparse flags verbatim, including underscores — a property named
inventory_namebecomes--inventory_name, not--inventory-name. If your script's argparse flag uses a hyphen, IAG's--inventory_name valuecall fails with "unrecognized arguments". Name every argparse flag exactly like its decorator property. - Unset decorator properties arrive as an empty string, not the schema
default— IAG always passes every decorator property as a CLI flag, even ones the caller didn't--set. If unset, the value is"", not the schema's declareddefault. Don't usechoices=["true","false"]on boolean-style flags —""isn't in that list and argparse will reject it. Instead accept any string and treat anything but"true"as false. - No way to pin the Python interpreter version — services run under the host's default
python3(often 3.9 on older RHEL/Rocky). A dependency using PEP 604 syntax (str | None) withoutfrom __future__ import annotationswill crash in IAG's venv even if it worked locally. Pin dependency versions inrequirements.txtfor 3.9 compatibility. - Local secret storage requires one-time gateway setup —
iagctl create secret --value/--prompt-valuefails with "you must specify a encryption file" until[secrets].encrypt_key_fileis set ingateway.confand the gateway is restarted. Checkiagctl-client get secret-providersfirst — external providers (Vault, CyberArk, etc.) don't need this, but they can only reference secrets that already exist there, not create new ones. - Run
iagctl-clientas the same OS user the gateway server runs as (often not the SSH login user) when creating local secrets — the client reads the sameencrypt_key_filepath from config to encrypt the value before sending it, and a restrictive (e.g.0400) key file owned by the server's user will deny read access to any other OS user, failing with the same "permission denied" error even though the server itself can read it fine. - Ansible
network_clineedsparamiko+look_for_keys = False— addparamikotoruntime.req-file(requirements.txt), and inansible.cfgadd[paramiko_connection]\nlook_for_keys = False. Withoutlook_for_keys = False, password auth fails with "No existing session". Usecisco.iosxr.iosxr_command(oransible.netcommon.cli_command) for show commands — NOTansible.builtin.raw - OpenTofu CLI syntax differs —
iagctl run service opentofu-plan apply <name> --set key=value(theapply/destroysubcommand goes between the type and service name) - OpenTofu results include
state_file— outputs are instate_file.outputs, notresult.stdoutlike Python/Ansible
How It Works
- Write a YAML service file — defines repos, decorators, secrets, services
iagctl db import— loads into IAGiagctl run service— test from CLIGatewayManager.runService— call from Itential workflows
Always start from a helper template — do not build YAML from scratch. Read the matching example first:
- Python service →
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-python-service.yaml - Ansible service →
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-ansible-service.yaml - OpenTofu service →
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-opentofu-service.yaml - Multi-service chain →
${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-multi-service-chain.yaml - Full schema reference →
${CLAUDE_PLUGIN_ROOT}/helpers/iag/service-file-schema.md
Authentication
| 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
Writing Service Files
YAML Structure
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
Service Types
| 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 |
Python Script Services
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:
- Inputs arrive as
--property_nameCLI args. Decorator schema property names become argparse flags. A property nameddevice_ipbecomes--device_ip. - Credentials arrive as env vars from the
secretsblock. Useos.environ.get('DEVICE_USERNAME'). - Behavior selection via
runtime.env— use this to make one script serve multiple services (see pattern below). - Output: JSON to stdout. Always
print(json.dumps(result)). Even on errors, return JSON with"success": false. - Exit code: 0 for any parseable result (success or handled error). 1 only for fatal setup failures (missing credentials).
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
Define a second service the same way, just with a different name/decorator and OPERATION: delete in runtime.env — same file, same secrets, only the operation value changes.
The script checks env vars first, then falls back to argparse:
operation = os.environ.get('OPERATION') or args.op
Ansible Playbook Services
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/look_for_keys gotcha already covered above):
ansible_network_osmust match the vendor collection (e.g.,cisco.iosxr.iosxr,sros)- Inventory uses
{{ var }}Jinja2 refs matching decorator schema property names runtime.req-filecan be a piprequirements.txtor ansible-galaxyrequirements.yml
OpenTofu Plan Services
Complete 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
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.
Decorators — Input Validation
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)
Adding Secrets
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.
Private Git Repos
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
Import / Export
# 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:
- New resources → added
- Existing (same name) → skipped without
--force, replaced with--force - Resources not in the YAML → untouched (never deleted)
Development Loop
When 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).
Testing Services (CLI)
# 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
Calling IAG from Itential Workflows
Finding the clusterId
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.
GatewayManager 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 |
runService Task Wiring
{
"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 |
Result Shape — JSON-RPC Wrapper
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"
}
}
}
Chaining Services in a Workflow
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.
sendCommand Task Wiring
{
"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"
}
}
}
sendConfig Task Wiring
{
"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"
}
}
}
Testing IAG Services via Workflow
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.statusis"complete"(not"error")data.errorisnull(no task errors)data.variables.serviceOutputcontains the extracted stdout from the IAG service
If the job errors with "Service not found on cluster": the clusterId is wrong. Check GET /gateway_manager/v1/gateways/ for the correct cluster name.
When to Use Which
| 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 vs GatewayManager
| 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 |
Operational Commands (Inspect, Verify, Clean Up)
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.
Organizing Services for Teams
Naming Conventions
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
Repository Layouts
| 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
Environment Promotion
| 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 |
CI/CD Integration
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
Before Handing Off
Service quality:
- Service has a decorator with
additionalProperties: false - Service tested:
iagctl run service <type> <name> --set ... - Output is valid JSON (for Python:
print(json.dumps(result))) - Error cases return JSON with
"success": false, not stderr - Service YAML validates:
iagctl db import file.yaml --validate
Workflow integration:
- Itential workflow tested end-to-end with
runServicetask - Workflow extracts
result.stdoutfrom JSON-RPC envelope viaquerytask - Error transitions on
runServicetask (handles service failures)
Security and ops:
- Secrets created via
iagctl create secret --prompt-value(never in YAML) - No top-level
secrets:section in committed service files - Git references pinned to tags (not branches) for production
- Naming conventions followed