Use this skill when someone has an approved solution design and is ready to build. Trigger it for phrases like "solution design is approved", "go ahead and build", "implement the design", "create the workflows", "build everything per the design", or "the design is locked — implement it". Also trigger it when a build is failing mid-way and needs debugging, or when /qa-agent hands back a failing test case for a fix. This skill implements the approved solution-design.md end-to-end — creating all workflows, templates, projects, and configs, and testing each component individually. If the user has a solution-design.md and wants to turn it into working automation, this is the right skill. Invoke after /solution-arch-agent produces an approved solution-design.md. Hands off to /qa-agent once the build is complete — /qa-agent owns acceptance testing and the as-built record.
Instalación
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Use this skill when someone has an approved solution design and is ready to build. Trigger it for phrases like "solution design is approved", "go ahead and build", "implement the design", "create the workflows", "build everything per the design", or "the design is locked — implement it". Also trigger it when a build is failing mid-way and needs debugging, or when /qa-agent hands back a failing test case for a fix. This skill implements the approved solution-design.md end-to-end — creating all workflows, templates, projects, and configs, and testing each component individually. If the user has a solution-design.md and wants to turn it into working automation, this is the right skill. Invoke after /solution-arch-agent produces an approved solution-design.md. Hands off to /qa-agent once the build is complete — /qa-agent owns acceptance testing and the as-built record.
Approved solution-design.md (all platform data already present in workspace)
Agent does
Builds all components per design, tests each piece individually, reports delivery outcomes
Engineer action
Reviews delivery and resolves open build questions
Deliverable
Deployed assets (workflows, templates, projects)
Customer receives
Delivered project — all workflows, templates, and configs individually tested, packaged, and access granted. Formal acceptance testing and sign-off happen next, in /qa-agent.
Build implements the approved plan. The builder never re-pulls discovery data — it uses what the Solution Architecture Agent left in the workspace. If any required file is missing, stop and surface as an upstream failure.
Testing during Build is component-level, not acceptance-level. "Test each piece" here means confirming a task or workflow runs without error and wires variables correctly — not verifying the delivered solution satisfies the customer's stated acceptance criteria end-to-end. That's /qa-agent's job, running against the completed build with real test data the engineer confirms. Don't skip component testing because "QA will catch it" — a structurally broken workflow wastes a live acceptance-test run.
This skill covers everything needed to build and test Itential automation assets: projects, workflows, templates, and command templates.
Workspace Contract
The builder receives a complete workspace. All discovery data is already present. Solution-design (or setup for explore mode) has already pulled everything.
Required files (must exist before build starts):
{use-case}/
.auth.json ← auth token
.env ← credentials (for re-auth if token expires)
openapi.json ← API reference (pulled by solution-arch-agent or explore)
tasks.json ← task catalog (pulled by solution-arch-agent or explore)
apps.json ← app/adapter type names (pulled by solution-arch-agent or explore)
adapters.json ← adapter instances (pulled by solution-arch-agent or explore)
applications.json ← app health (pulled by solution-arch-agent or explore)
May also exist (spec-contingent):
use-case-memory.md ← living context: IDs, decisions, gotchas, open items — READ THIS FIRST
customer-spec.md ← approved HLD (Requirements)
feasibility.md ← approved feasibility assessment
customer-context.md ← business rules (if provided)
solution-design.md ← approved Solution Design / LLD
devices.json ← device inventory
workflows.json ← existing workflows
device-groups.json ← device groups
task-schemas.json ← fetched on demand during build (append-only, never pre-populated)
test-report.md ← if returning from /qa-agent with a failing test case — has the exact case ID, expected vs actual, and evidence
The builder NEVER re-pulls bootstrap or discovery data. If tasks.json, apps.json, or adapters.json is missing, stop and tell the user — that's an upstream failure, not something to silently fix.
Exception — .auth.json bootstrap: If .auth.json is missing but .env exists with AUTH_METHOD=oauth, CLIENT_ID, and CLIENT_SECRET, the builder MUST authenticate and create .auth.json before proceeding — do NOT stop and report an upstream failure. See the Bootstrap Authentication section below.
The only API calls the builder makes are:
Auth bootstrap — POST /oauth/token when .auth.json is missing (see below)
Create — POST workflows, templates, projects
Update — PUT to edit assets
Test — POST jobs/start, GET job status
Schema fetch — task schemas not yet in task-schemas.json (append to file after fetching)
Re-auth — if token expires, use .env to refresh .auth.json
Bootstrap Authentication
When .auth.json is missing but .env has AUTH_METHOD=oauth with CLIENT_ID and CLIENT_SECRET, authenticate automatically before proceeding.
The correct Itential SaaS/Cloud OAuth endpoint is:
POST {PLATFORM_URL}/oauth/token
Content-Type: application/x-www-form-urlencoded
Content-Type MUST be application/x-www-form-urlencoded — NOT application/json. Sending JSON returns HTTP 415.
The /login endpoint does NOT support OAuth client credentials on SaaS instances — always use /oauth/token.
On success, write .auth.json with the token so all subsequent API calls just work.
Helper script:${CLAUDE_PLUGIN_ROOT}/scripts/oauth_bootstrap.py — reads .env, POSTs to /oauth/token, writes .auth.json. The builder should run this automatically when .auth.json is missing and .env has AUTH_METHOD=oauth.
Build Lifecycle
0. Memory file → create or read use-cases/{name}/use-case-memory.md
1. Decompose → identify parent/child split before writing any code
2. Create project → container for all assets
3. Discover tasks → search tasks.json, fetch schemas
4. Build children first → each child workflow independently testable
5. Build templates → Jinja2 (config gen) or TextFSM (output parsing)
6. Build command templates → MOP pre/post checks with validation rules
7. Build orchestrator last → parent wires tested children via childJob
8. Add assets to project → move/copy into the project
9. Set project membership → resolve spec members, PATCH immediately after import
10. Test each component → jobs/start, check results (component-level, not acceptance-level)
11. Debug → check job.error, filesystem-first
12. Reconcile → diff built vs designed, update artifacts
13. Update memory file → record IDs, decisions, gotchas, test results, open items
14. Update this skill → if you hit a platform behavior not documented here, add it before closing out
15. Hand off to /qa-agent → build is complete; real IDs are in solution-design.md §D
Step 0 — memory file:
At the start of every session, check for use-cases/{use-case}/use-case-memory.md:
Exists → read it before doing anything else. It tells you the platform, project ID, what's already built, decisions made, and open items. Don't re-discover what's already documented.
Missing → create it now from ${CLAUDE_PLUGIN_ROOT}/helpers/use-case-memory.md template. Fill in Platform URL, Stage: build, Status: active immediately.
Step 13 — update memory file after every session:
Before closing out any build session, update use-case-memory.md with:
Any new asset IDs (project ID, workflow UUIDs, transformation IDs, adapter names)
Any architectural decisions made and why
Any gotchas hit and how they were fixed
Test results (date, what was tested, outcome)
Updated open items list
Stage and Status if they changed — mid-build, Stage stays build; only update it to test at the Step 15 handoff
The memory file is what makes it possible to pick up a use-case after weeks without re-discovering everything from scratch.
Step 14 — how to update this skill:
New platform behavior (error shape, field constraint, task gotcha) → add detail to the relevant body section (### query, ### childJob, ### Projects, etc.), then add a one-liner to the Gotchas pre-flight list under the right category.
New pattern or workflow recipe → add to ## Workflow Patterns and, if the pattern is reusable, export the project from the platform and save it to ${CLAUDE_PLUGIN_ROOT}/helpers/assets/. Add a row to the Helper Templates table in this file pointing to it.
Do NOT create a new top-level section for a single finding — put it where a builder would look when working on that topic.
Step 15 — hand off to /qa-agent: Once every component has been individually tested and solution-design.md Section D has real IDs (project ID, workflow IDs) instead of placeholders, the build is complete. Update use-case-memory.md to Stage: test before ending the session. Tell the engineer the build is done and route to /qa-agent for acceptance testing and the as-built record — don't write as-built.md here.
If /qa-agent hands back a failing test case: fix the specific issue it identifies (it gives you the case ID, expected vs. actual, and evidence — a job ID or static-check output). Don't re-examine the whole build; the failure report tells you exactly what broke. Once fixed, tell the engineer so /qa-agent can re-run just that case.
Guides
Guide 1: Build a workflow end-to-end
Follow these steps in order. Do not skip any step.
STOP. Before writing a single line of task JSON — run these commands.
The asset projects in helpers/assets/ are real, production-tested imports. Read them first.
Do not guess task structure from memory. Do not copy from helpers/create/ for task bodies.
helpers/create/ is for API wrappers (project/workflow creation endpoints) only — not task JSON.
# 1. Find which asset project matches your use casels${CLAUDE_PLUGIN_ROOT}/helpers/assets/
# 2. Extract the workflow most similar to what you're building
jq '[.components[] | select(.type=="workflow")] | .[].document.name' \
${CLAUDE_PLUGIN_ROOT}/helpers/assets/vendor-servicenow.json
# 3. Read its full task map — this is your reference
jq '[.components[] | select(.type=="workflow") | select(.document.name | test("WORKFLOW_NAME"; "i"))] | first | .document | {tasks, transitions}' \
${CLAUDE_PLUGIN_ROOT}/helpers/assets/vendor-servicenow.json
# 4. Extract the specific task type you need
jq '[.components[].document.tasks // {} | to_entries[] | select(.value.name == "TASK_NAME")] | first | .value' \
${CLAUDE_PLUGIN_ROOT}/helpers/assets/vendor-servicenow.json
Replace vendor-servicenow.json with whichever asset file best matches your use case:
Before writing any JSON, identify the parent/child split from the solution design. Ask for each phase:
Can this phase be run and tested on its own? → Child workflow
Does it loop over multiple items (devices, records)? → Child workflow with loopType
Is it reusable across other use cases? → Child workflow
Is it a simple sequential step with no independent test value? → Task in orchestrator
Build order is always: children first, orchestrator last. The orchestrator is just childJob calls to tested children — it should not contain raw adapter tasks unless there is no logical way to split.
Read a full workflow from asset projects before building any multi-workflow solution:
Becomes this workflow task (extract a real adapter task from an asset project first — e.g. jq '[.components[].document.tasks // {} | to_entries[] | select(.value.location == "Adapter")] | first | .value' ${CLAUDE_PLUGIN_ROOT}/helpers/assets/vendor-servicenow.json):
{"a1b2":{"name":"createChangeRequest","canvasName":"createChangeRequest","summary":"Create Change Ticket","description":"Creates a ServiceNow change request","location":"Adapter","locationType":"Servicenow","app":"Servicenow","type":"automatic","displayName":"ServiceNow","variables":{"incoming":{"body":"$var.e1a1.merged_object","adapter_id":"$var.job.adapter_id"},"outgoing":{"result":null},"error":"","decorators":[]},"groups":[],"actor":"Pronghorn","scheduled":false,"nodeLocation":{"x":700,"y":600}}}
Mapping rules:
name, canvasName → from tasks.json
app, locationType → from apps.json (NOT tasks.json)
displayName → from tasks.json
location → "Adapter" or "Application" (from tasks.json)
type → from tasks.json directly — do not guess. It is per-task, not per-app. Read it alongside name, app, location, and canvasName: jq '.[] | select(.name == "taskName") | {name, app, type, canvasName, location}' tasks.json
actor → "Pronghorn" for all tasks except childJob (which uses "job")
incoming → each schema key becomes a variable. Wire with $var for top-level values
outgoing → set to null (capture later with $var.taskId.outVar)
Add adapter_id to incoming for adapter tasks (not in schema, always required)
Add error and decorators to variables block
Step 5: Handle object inputs. If a task's incoming variable is type: "object" (like body), you CANNOT put $var references inside it — they won't resolve. Use a merge task before it:
Then wire the adapter task's body to "$var.e1a1.merged_object".
Step 6: Handle opaque schemas. Some task schemas show body: {type: "object"} with no inner field details. The adapter validates internally. To discover required fields:
Try creating with minimal fields — the error message lists what's missing (e.g., "must have required property 'summary'")
Check openapi.json for the adapter's endpoint schema
Call the adapter directly: POST /{adapter_id}/{method} with {} body — read the validation error
Step 7: Wire transitions. Every adapter task needs BOTH success and error transitions:
If both success and error need to reach workflow_end, route error to an intermediate newVariable task first (JSON can't have duplicate keys).
Step 8: Add inputSchema/outputSchema. List all job variables the workflow expects as input and produces as output.
Step 9: Pre-submit checklist.
Task IDs are hex-only ([0-9a-f]{1,4})
app and locationType values come from apps.json .name, NOT tasks.json and NOT the adapter instance name (e.g., EmailOpensource not email)
adapter_id is the adapter instance name (e.g., email), NOT the type name
adapter_id values come from adapters.json.results[].id — NEVER from the spec's adapter identity table. The spec is a design document; adapters.json is the source of truth for the target environment.
canvasName values come from tasks.json canvasName field
Every adapter task has adapter_id in incoming
Every adapter task has an error transition
evaluation tasks have both success AND failure transitions
evaluation operators are from the closed enum (contains, !contains, <, <=, >, >=, ==, !=) — no others exist
evaluationoperand_2 literal values containing regex metacharacters (., (, ), [, ], ?, +, *, |) are properly escaped, OR stored in a newVariable constant-holder task to avoid incomingRefs cache issues after API PUT
No $var.<taskId>.<out> references inside nested forEach bodies — use $var.job.<varName> instead
Incoming variable types match task schema exactly (arrays for to/cc/bcc, numbers for page/pageSize, etc.)
No $var references inside nested objects (use merge/makeData)
merge uses "variable", childJob uses "value"
No {task:"job", variable:"x"} in merge/childJob for workflow-internal variables — {task:"job"} refs add x to inputSchema.required, prompting operators for values that should be internal. Use the producing task ref instead (query→return_data, newVariable→value, makeData→output, merge→merged_object)
If a query downstream of a childJob returns null despite the child succeeding: check whether "obj": "$var.<childJobId>.job_details" is resolving — on some platform versions it is treated as a literal string. Fix: insert a merge task between childJob and query using {"task": "<childJobId>", "variable": "job_details"} in data_to_merge, then point obj to $var.<mergeId>.merged_object (see Guide 4)
childJob has actor: "job", all others have actor: "Pronghorn"
workflow_end transition is empty {}
Canvas layout follows the vertical spacing convention — non-forked sequences on a constant-x spine, fork branches offset to spine±264 and stay in their own column until convergence
No transition lines cross task nodes (the spine column is empty between a fork and its convergence point)
Sequential y-delta ~108px (tight grid)
LCM Create actions only: the instance-write merge task's data_to_merge covers every field in the resource model's schema.required array — missing even one field causes an instance write failure after provisioning (resources are orphaned from LCM). Read the model's schema.required before building the merge task: jq '.schema.required' helpers/assets/lcm/<model>.json
ViewData manual tasks:view is a top-level field; incoming.variables is present (even if {}); displayName: "Tools", no actor field
restCall downstream query: path targets body field directly (e.g., "access_token") — NOT "response.access_token" (restCall has no wrapper, unlike adapter tasks)
childJob loop: if child workflow has inputSchema.required fields beyond what each data_array element contains, use the forEach enrichment pattern (forEach → merge → arrayPush) to add shared fields into each element before the childJob loop; set variables: {} on the childJob
forEach body:incoming contains ONLY data_array (no job_id); loop body tasks have no external error transitions; last body task has an empty {} transition; $var.job.<varName> inside loop body instead of $var.<taskId>.<output>
makeData with childJob-sourced merge: if a merge task references a childJob variable, do NOT wire that merge's merged_object into makeData.incoming.variables — use query to extract individual values first
Complete working example: Read the ServiceNow "Create Change Request" workflow before building — it demonstrates merge → adapter create → query → adapter update with error transitions:
servicenow-prod, email — this goes in incoming, NOT in the task-level app field
incoming vars
From task schema (multipleTaskDetails)
body, changeId
outgoing vars
From task schema, set to null
result
Guide 2: Debug a failed job
Step 1: Get the job:
GET /operations-manager/jobs/{jobId}
Step 2: Check data.status. If "error", read data.error[]:
data.error[].task → failing task ID
data.error[].message.IAPerror.displayString → human-readable error
Step 3: Match the error to a fix:
Error message
Cause
Fix
"Schema validation failed on must have required property 'X'"
Missing field in adapter body
Add the field to merge task
"Method not found"
Wrong task name or app
Check tasks.json and apps.json
"No available transitions"
Missing error transition
Add "state": "error" transition
"Cannot find workflow"
childJob ref broken after project move
Update workflow field with @projectId: prefix
"Referenced job variable: undefined"
merge uses "value" instead of "variable"
Change to "variable" in data_to_merge
Job stuck in "running"
No error transition on failed task
Add error transition
Step 4: Fix locally, PUT to update, re-run. Don't recreate — updating preserves the ID.
Guide 2b: Work with any unfamiliar adapter task
Follow Guide 1 Steps 1-6 for discovery. Quick reference for the lookup commands:
# Step 1 — find the task
jq '.[] | select(.app | test("meraki";"i")) | {name, app, displayName}' {use-case}/tasks.json
# Step 2 — get the correct app name (tasks.json app field is often wrong for adapters)
jq '.[] | select(.name | test("meraki";"i")) | {name, type}' {use-case}/apps.json
# Step 2 — get the adapter instance name
jq '.results[] | select(.package_id | test("meraki";"i")) | {id, state}' {use-case}/adapters.json
# Step 3 — fetch the task schema# POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true# {"inputsArray": [{"location": "Adapter", "pckg": "<app from apps.json>", "method": "<task name>"}]}
You now have three values: app (from apps.json), adapter_id (from adapters.json .id), displayName (from tasks.json). Two things to pay extra attention to beyond Guide 1:
Enforce data types from the schema. When the schema says "type": "array", you MUST pass an array — even for single values:
"to": "user@example.com" → WRONG. Use "to": ["user@example.com"]
"pageSize": "100" → WRONG if schema says number. Use "pageSize": 100
"cc": "" → OK only if schema allows string; if array, use "cc": []
Always check task-schemas.json for the exact type of each incoming field before wiring.
Inspect the actual response before wiring a query path. Adapter responses are transformed — they do not match the native API's structure. After a successful test run:
GET /operations-manager/jobs/{jobId} — find the task in data.tasks by its task ID
Read the task's outgoing variables — that is the real response object
Use jq to explore: jq '.data.tasks["a1b2"]' job.json
Wire the query path from what you see — not from the upstream API docs
End-to-end sequence:
1. tasks.json search → found "getDevice", app "networkAdapter"
2. apps.json lookup → correct app name is "NetworkAdapter" (capital N)
3. adapters.json → adapter_id is "network-prod-1"
4. multipleTaskDetails → incoming: {deviceId: string}, outgoing: {result: object}
5. Build + test → job completes
6. Inspect job → result is {"response": {"hostname": "...", "model": "..."}}
7. Wire query path → "response.hostname" (NOT "result.hostname" or "data.hostname")
Guide 3: Add a task to an existing workflow
Step 1: Extract the task structure from an asset project that uses the same task type:
Step 2: Fill in the fields using the mapping rules from Guide 1 Step 4.
Step 3: Generate a hex task ID (e.g., d4e5) — must be [0-9a-f]{1,4}.
Step 4: Add the task to tasks and add transitions. Remember error transitions on adapter tasks.
Step 5: Update via PUT /automation-studio/automations/{id} with {"update": {...}}.
Guide 4: Build a childJob (parent calls child workflow)
childJob has two modes. Both are tested and verified on a live platform.
Mode A: Single child — pass variables with {"task","value"}
The parent passes specific variables to one child workflow run.
Parent childJob task:
{"a1a1":{"name":"childJob","canvasName":"childJob","summary":"Run Single Child","location":"Application","locationType":null,"app":"WorkFlowEngine","type":"operation","displayName":"WorkFlowEngine","variables":{"incoming":{"task":"","workflow":"My Child Workflow","variables":{"deviceName":{"task":"job","value":"targetDevice"},"action":{"task":"static","value":"validate"}},"data_array":"","transformation":"","loopType":""},"outgoing":{"job_details":null}},"actor":"job"}}
Variable passing rules (uses "value", NOT "variable"):
{"task": "job", "value": "targetDevice"} → passes the parent's targetDevice job variable to the child as deviceName
{"task": "static", "value": "validate"} → passes the literal string "validate"
{"task": "b2c3", "value": "return_data"} → passes a previous task's output (preferred for runtime data)
WARNING — {task:"job"} refs in childJob variables add fields to inputSchema.required — same behavior as merge (see ### merge section). Only use {task:"job", value:"x"} for genuine workflow inputs. For runtime data produced by earlier tasks, use {task:"<taskId>", value:"<outVar>"} to reference the producing task directly.
WRONG for task output refs in childJob:{"task": "b2c3", "variable": "return_data"} — "variable" is for merge/evaluation only.
In childJob, ALL refs (job, static, AND task output) use "value". Using "variable" causes undefined.indexOf() at job start time (P6.4.0+) — the workflow fails before any task runs.
Query uses flat variable names — "taskStatus", NOT "variables.job.taskStatus".
If the query returns null even though the childJob succeeded — the $var form in obj may not resolve on your platform version. Use the merge+taskRef workaround:
If the query returns null (platform-version-specific $var resolution issue), use the same merge+taskRef workaround described above (Mode A) — capture job_details via {"task": "a1a1", "variable": "job_details"} in merge, then query $var.m1m1.merged_object.
Loop element completeness — required fields must be in each element (not in variables).
The platform validates the child workflow's inputSchema.required against each element's keys only. Static variables set on the childJob task are NOT counted toward satisfying required fields. If your loop elements only contain per-iteration fields (e.g., subnet_name, subnet_cidr) but the child also requires shared fields (e.g., subscription_id, region), the validation fails before any iteration runs.
Fix — forEach enrichment pattern: enrich each element with the shared fields before the childJob loop, then set variables: {} on the childJob:
forEach (loop over elements) → merge (add shared fields to current_item) → arrayPush (append enriched element to new array)
↓ (after forEach success)
childJob (data_array: enrichedArray, variables: {})
// forEach outgoing binds current_item to job var{"outgoing":{"current_item":"$var.job.currentElement"}}// merge combines current element + shared fields{"data_to_merge":[{"task":"forEachId","variable":"current_item"},{"key":"subscription_id","value":{"task":"job","variable":"subscription_id"}},{"key":"region","value":{"task":"job","variable":"region"}}]}// → $var.mergeId.merged_object is the enriched element// arrayPush appends to accumulator{"incoming":{"job_variable":"enrichedElements","item_to_push":"$var.mergeId.merged_object"}}// childJob uses the enriched array and no static variables{"data_array":"$var.job.enrichedElements","variables":{},"loopType":"parallel"}
Loop output shape (each element is a flat spread of the child's job variables):
Use "[**].taskStatus" in a query to extract one field from all iterations.
childJob checklist
actor is "job" (NOT "Pronghorn")
task is "" (empty string)
job_details outgoing is null
All incoming fields present — even unused ones: "data_array": "", "transformation": "", "loopType": ""
Variables use {"task","value"} NOT $var (single mode)
variables is {} when using data_array (loop mode)
Child workflow's inputSchema.required matches what you're passing
loopType: "" (single), "parallel" (simultaneous), "sequential" (one at a time)
If a downstream query of a childJob returns null: the "obj": "$var.<childJobId>.job_details" form may not resolve on this platform version — use merge+taskRef workaround (see "Extracting single child output" above)
Building the child workflow
The child workflow must:
Accept inputs via inputSchema that match what the parent passes
Set output variables via newVariable or task outgoing → $var.job.x
Handle errors internally (try-catch pattern) so it always completes:
Preferred: Import a project (atomic — all assets in one call)
Always use import instead of create + add components. Import creates the project with all workflows, templates, and MOP templates inside it in a single atomic call. No intermediate state, no broken childJob refs, no project-locking issues.
POST /automation-studio/projects/import
Build all assets locally first, then import everything at once:
Warning: Both move and copy rename assets with @projectId: prefix but do NOT update internal references (childJob workflow fields, template names). You must fix these manually.
If a name cannot be resolved, ask the engineer for the reference ID — do not guess.
Resolve membership references from spec
MANDATORY: Import sets the OAuth service account as project owner — not the UI user from the spec. The engineer specified in the spec's Project Membership table will be locked out of the project unless you PATCH membership immediately after import. This runs in Phase 3 (Import), not Phase 6 (Deliver).
There is no user/group lookup API on the Itential platform. The only way to resolve a username (e.g., joksan.flores@itential.com) or group name (e.g., solutions-engineers) to a platform reference ID is by scanning existing projects' members.
Step 1: Build a membership lookup table.
The list endpoint (GET /automation-studio/projects?limit=50) does NOT include username/name on member objects — only individual GET /automation-studio/projects/{id} calls do. Scan all projects to build the lookup:
If a username or group cannot be resolved from the lookup table, stop and ask the engineer. Do not guess reference IDs or skip members.
Baseline members (when no spec membership is defined): If there is no Project Membership table in the spec, or when doing a freeform build/import outside the spec lifecycle, ask the engineer:"Which user accounts or groups should have access to this project?" — do not assume or skip. Once you have the names, resolve them via the lookup table above and PATCH immediately. Without this step the engineer will be locked out of the project in the IAP UI. See #63
Project Thumbnail
Operation
Endpoint
Set
PUT /automation-studio/projects/{id}/thumbnail — body: {"imageData": "<data-URI>", "backgroundColor": "<hex>"}
Get
GET /automation-studio/projects/{id}/thumbnail — returns {"data": {"image": "<data-URI>", "backgroundColor": "<hex>"}}
imageData must be a full data URI — not raw base64. Passing raw base64 without the data:image/png;base64, prefix returns HTTP 200 and stores the value, but the UI renders a black/blank image with no error.
Optimal dimensions: 330 × 100 px — matches the project card aspect ratio in Automation Studio
Accepted formats: jpg, jpeg, png — max 1000 KB
backgroundColor (hex, e.g. "#1B2A4A") sets the card background color visible before the image loads
JSON Forms
JSON Forms have their own dedicated skill — itential-json-forms. See that skill for the form structure (struct / schema / uiSchema / bindingSchema), the static-enum vs. REST-bound vs. cascading dropdown (aka field dependency) patterns, the full API reference (including the bulk-only DELETE), and the manual-trigger wiring (legacyWrapper: false).
Helper templates for forms still live under ${CLAUDE_PLUGIN_ROOT}/helpers/:
create-json-form.json — static-enum dropdowns
create-json-form-rest-bound.json — REST-bound or cascading dropdowns
Operations Manager (Automations & Triggers)
Method
Endpoint
Description
POST
/operations-manager/automations
Create an automation
GET
/operations-manager/automations
List automations
POST
/operations-manager/triggers
Create a trigger
PATCH
/operations-manager/triggers/{id}
Update a trigger
GET
/operations-manager/triggers
List triggers
Create a Manual Trigger with JSON Form
This is a two-step process: create the automation, then create a manual trigger that binds to it.
Use the helper template: ${CLAUDE_PLUGIN_ROOT}/helpers/create/create-ops-manager-automation.json
Critical: legacyWrapper must be false. When creating a manual trigger with a JSON form, set legacyWrapper: false. The default is true, which wraps form field values under formData, breaking the mapping to workflow job variables. With legacyWrapper: false, form field values map directly to workflow input variables by name.
{use-case}/tasks.json should already exist — pulled by /solution-arch-agent or /explore during feasibility. Do not re-pull if the file exists. If missing, fetch it:
GET /workflow_builder/tasks/list → save to {use-case}/tasks.json
GET /automation-studio/apps/list → save to {use-case}/apps.json
Before fetching schemas from the API, check if an asset project already has the task wired up. If it does, you get the exact field structure for free — no API call needed.
# Does any asset project use this task? Find it by task name:
grep -rl '"name": "TASK_NAME"'${CLAUDE_PLUGIN_ROOT}/helpers/assets/
# Extract the wired task from the matching project:
jq '[.components[].document.tasks // {} | to_entries[] | select(.value.name == "TASK_NAME")] | first | .value' \
${CLAUDE_PLUGIN_ROOT}/helpers/assets/MATCHING_FILE.json
# See which tasks a specific workflow uses:
jq '[.components[] | select(.type=="workflow") | select(.document.name | test("WORKFLOW"; "i"))] | first | .document.tasks | to_entries[] | {id:.key, name:.value.name, app:.value.app}' \
${CLAUDE_PLUGIN_ROOT}/helpers/assets/MATCHING_FILE.json
IMPORTANT: The pckg value must come from apps.json, NOT tasks.json. The names can differ (e.g., tasks.json says ServiceNow but apps.json says Servicenow).
Before fetching schemas:
Search asset projects (above) — if found, use the wired example directly
Check if {use-case}/task-schemas.json exists — search it next
Only call multipleTaskDetails for tasks not found in either place
After fetching, append to {use-case}/task-schemas.json
nodeLocation Spacing Convention
Workflows are laid out top-to-bottom (vertical) by default — this is the Itential best practice for readability and consistency, and matches the conventions used in the platform's working examples. Use horizontal only when the engineer explicitly asks for it.
Vertical Layout (default)
Rule
Value
Sequential tasks (y-delta)
+108px
Fork branch offset from spine (x-delta)
±264px
Spine x
a constant column (e.g. x=600)
Clean canvas principles:
The spine is a constant x — non-forked sequences (start, single-thread tasks, end, convergence points) sit on it.
Forks split off the spine — at a fork point, both outgoing branches leave the spine column. Place one at spine - 264 and the other at spine + 264. The spine column stays empty between the fork and the convergence point so transition lines don't cross task nodes. Direction (which branch goes left vs. right) is the engineer's call — pick whatever keeps the picture clean.
Branches stay in their own column until they converge.
Convergence tasks (workflow_end, merges, error sinks) return to the spine x.
Tight y-spacing — the canvas grid is dense; ~108px between sequential rows reads well. Don't pad to +250 or +360.
Preserve Studio-arranged positions — if an engineer has arranged a workflow in Automation Studio, treat its nodeLocation values as authoritative. Always read from the live export before reimporting. Never recalculate positions from scratch on a workflow that has already been arranged.
Example — fork with a shared error handler (same pattern as ServiceNow "Create Change Request" in helpers/assets/vendor-servicenow.json):
If the engineer explicitly asks for horizontal, swap x and y throughout: phases advance on x, fork branches offset on y, spine becomes a constant y row. Same magnitudes, opposite axes.
Workflows
Workflow Structure
POST /automation-studio/automations
Body wraps the workflow in {"automation": {...}}:
{"automation":{"name":"My Workflow","description":"Does something useful","type":"automation","canvasVersion":3,"encodingVersion":1,"font_size":12,"tasks":{"workflow_start":{"name":"workflow_start","groups":[],"nodeLocation":{"x":600,"y":200}},"a1b2":{"name":"query","canvasName":"query","summary":"Extract Data","description":"Extracts field from response","location":"Application","locationType":null,"app":"WorkFlowEngine","type":"operation","displayName":"WorkFlowEngine","variables":{"incoming":{"pass_on_null":false,"query":"hostname","obj":"$var.job.deviceData"},"outgoing":{"return_data":"$var.job.deviceName"},"error":"","decorators":[]},"groups":[],"actor":"Pronghorn","scheduled":false,"nodeLocation":{"x":600,"y":312}},"workflow_end":{"name":"workflow_end","groups":[],"nodeLocation":{"x":600,"y":420}}},"transitions":{"workflow_start":{"a1b2":{"type":"standard","state":"success"}},"a1b2":{"workflow_end":{"type":"standard","state":"success"}},"workflow_end":{}},"groups":[],"inputSchema":{"type":"object","properties":{"deviceData":{"title":"deviceData","type":"object"}},"required":["deviceData"]},"outputSchema":{"type":"object","properties":{"deviceName":{"title":"deviceName","type":"string"}}}}}
Update a workflow:
PUT /automation-studio/automations/{id}
{"update":{ ...same structure as automation object... }}
Project-scoped name required on PUT. If the workflow belongs to a project, the name field in the update body must include the @<projectId>: prefix — even if the workflow was created without it:
{"update":{"name":"@69f10abc: My Workflow","tasks":{...},"transitions":{...}}}
Sending the bare name ("name": "My Workflow") returns {"error": {"message": "Name must begin with '@projectId: '"}}.
Asymmetry: workflow CREATE (POST /automation-studio/automations) does NOT require the prefix — the platform applies it when the workflow is added to a project. But PUT-update always requires it for project-member workflows.
Always read the workflow before updating (GET /automation-studio/workflows/detailed/{name} or export the project) to get the current scoped name. See Rule 24 and issue #55.
Task Fields
Field
Application Tasks
Adapter Tasks
name
Method name from tasks.json
Method name from tasks.json
canvasName
From tasks.json canvasName field (may differ from name: arrayPush→push)
Same
location
"Application"
"Adapter"
locationType
null
Same as app
app
App name (e.g., WorkFlowEngine)
From apps.json (NOT tasks.json)
type
"automatic" or "operation" — read from tasks.json .type, do not guess
actor
"Pronghorn"
"Pronghorn"
displayName
App name
May differ from app
Adapter tasks also require adapter_id in incoming variables — the adapter instance name from health/adapters.
Task Access Control (groups)
The groups field on a task definition is task-level GBAC — group-based access control that restricts which IAP groups can see, claim, and complete a manual task in the Job Inbox.
Field
Type
Meaning
groups(plural)
string[]
GBAC. Each entry is a group's MongoDB _id (24-char hex). Empty [] means no task-level restriction.
group(singular, optional)
string
Canvas display category (e.g., "Tools", "JsonForms"). Set by the Studio canvas. NOT access control — easy to confuse with groups.
GET /authorization/groups — list groups (each has _id and name)
GET /authorization/groups/<id> — resolve a single group
Two GBAC scopes — both use the same string[] shape (group _ids) but apply at different levels:
Per-task groups (on the task definition, sibling of name/app/type) — gates access to a single manual task.
Top-level workflow groups (sibling of tasks/transitions at the workflow level) — gates access to the workflow as a whole.
Tasks of any type can carry groups, but only type: "manual" tasks surface in the Job Inbox where GBAC actually gates user access. Leave it as [] on automatic tasks unless platform-specific docs say otherwise.
Edge cases not yet documented — verify on your platform before relying on:
Semantics with multiple group IDs in the array (likely OR — any-of — but unverified)
Interaction between task-level and workflow-levelgroups (additive vs. override)
Whether groups accepts a $var job-variable for dynamic group resolution (almost certainly no — design-time only — but worth confirming)
Task IDs
Task IDs must be hex-only: [0-9a-f]{1,4}. Non-hex IDs (e.g., apush) cause $var references to silently fail.
success — task completed without error (all tasks)
error — task encountered errors (all tasks)
failure — evaluation didn't match or query returned undefined (evaluation/query only)
loop — forEach loop iteration (forEach only)
Transition types:
standard — moves forward
revert — moves backward to a previous task (retry loops)
MANDATORY: Every adapter/external task needs an error transition. Without one, errors cause "Job has no available transitions" and the job gets stuck forever.
JSON duplicate key problem: If both success and error need to go to workflow_end, you can't use workflow_end as a key twice. Route error to an intermediate task (e.g., newVariable to set error status), then route that to workflow_end.
Create Response Shape
Both workflow and template creation return {created, edit} — NOT {message, data, metadata}:
$var only resolves as direct top-level incoming variable values:
Wiring
Works?
Why
"deviceName": "$var.job.x"
Yes
Direct top-level value
"variables": {"key": "$var.job.x"}
NO
Nested inside object
"body": {"data": "$var.job.x"}
NO
Nested — stored as literal string
Workaround: Use merge, makeData, or query to build the nested object, then reference the task's output with $var.taskId.merged_object.
Task ID validation:$var.taskId.x only resolves when taskId matches [0-9a-f]{1,4}. Non-hex IDs silently fail.
Prefer task-to-task wiring: When a task's output feeds directly into the next task's input, wire it as $var.<taskId>.<outVar> instead of bouncing through $var.job.x. Only use job variables when: (a) values cross non-adjacent tasks, (b) values need to be visible in job output, or (c) multiple downstream tasks need the same value. Direct task-to-task wiring reduces clutter and makes data flow easier to trace.
incomingRefs cache — what PUT does and doesn't fix:
incomingRefs NOT regenerated — literals/changed taskRefs resolve to null
Open in Studio → Save
POST /workflow_builder/workflows/save
Does NOT regenerate incomingRefs either
Open in Studio → Save
Evaluation silently returns false after PUT
Stale operand cache
Constant-holder workaround below, or Studio save
Workflow hangs at workflow_start (status: running forever) after PUT
Any task's incomingRefs stale
Recreate via fresh POST — more PUTs won't fix it
Constant-holder workaround (API-only, no Studio save needed): store operand_2 literal values in a newVariable task and reference via {"task": "k_const", "variable": "value"} — taskRef resolution bypasses the cache.
makeData static input strings do NOT resolve after API create/PUT. The input and outputType fields are backed by job_data (type static). Workaround: use newVariable with value: [...] (array literal) — newVariable.value resolves correctly after API create without a Studio save.
task: "static" values broadly are backed by job_data written at Studio-save time. Any static value (template strings, query paths, model IDs, inline constants in childJob variables dicts) resolves as null at runtime on a freshly API-imported workflow until saved through Automation Studio.
Outgoing must write to job var for cross-task $var to be readable by downstream tasks. Pattern: "outgoing": {"result": "$var.job.raw_result"} then downstream: "obj": "$var.job.raw_result". If outgoing is null, the value is accessible via task iteration (GET /operations-manager/tasks/{iterationId}) but NOT via $var.taskId.result in downstream tasks at runtime. Use job vars for any result you need to pass forward.
POST /automation-studio/workflows/validate — runs pre-flight schema validation before create or update. Returns {errors: [], warnings: []}. An empty errors array means the workflow is schema-valid. Run this on every workflow before POSTing or PUTting.
Utility Tasks (WorkFlowEngine)
These are built-in tasks that require no adapter. They handle data manipulation and control flow.
query
Extract nested values from objects using dot-path syntax.
IMPORTANT: Don't guess the query path for adapter responses. Adapters transform upstream API responses — the field path in the adapter's output is NOT the same as the native API's response structure. The adapter's result outgoing is always a {response, headers, metrics} object, never a primitive. When the upstream API returns a simple string (like Infoblox's _ref), it's at result.response, not result directly. Always verify the actual response shape from a test job (GET /operations-manager/jobs/{jobId} → data.tasks) before wiring a path.
merge
Build an object from multiple resolved values. Primary workaround for $var not resolving inside nested objects.
Incoming:data_to_merge (array, min 2 items)
Outgoing:merged_object (object)
IMPORTANT: The field is "variable" NOT "value" in the reference objects inside data_to_merge.
Reference format in data_to_merge:
{"task": "job", "variable": "varName"} — pull from a user-supplied job variable (input to the workflow)
{"task": "static", "variable": "literalValue"} — literal value
{"task": "taskId", "variable": "outVar"} — pull from a previous task's output
WARNING — {task:"job"} references add fields to inputSchema.required.
The platform scans every data_to_merge entry in merge tasks (and every variables entry in childJob) for {task:"job"} references and automatically adds that variable name to inputSchema.required. This means using {task:"job", variable:"changeId"} for a variable that was produced internally by a query task will prompt operators to supply changeId as a workflow input — even though it should never come from the user.
Rule: only use {task:"job"} for variables that are genuine workflow inputs. For anything produced by an earlier task, use the producing task's ref directly:
Gotchas: Requires at least 2 items (1 item = silently null). Outgoing MUST declare "merged_object": null (empty {} makes it unreachable). Duplicate keys produce arrays — merging {"ip": "1.2.3.4"} and {"ip": "1.2.3.4"} yields {"ip": ["1.2.3.4", "1.2.3.4"]}, not an overwrite. To avoid this, pass a pre-built object as a single workflow input variable instead of merging multiple objects with the same keys.
parse
Convert a JSON string into a JavaScript object. Essential after extracting result.stdout from runService (which is always a string, even when the script printed valid JSON).
Incoming:stringToParse (string — the JSON string to parse)
Outgoing:result (object — the parsed object)
Operator enum — closed set. Only these 8 are valid:
contains, !contains, <, <=, >, >=, ==, !=
regex, match, matches, contains_key, in, startsWith — do not exist. An invalid operator silently returns false with empty outgoing and finish_state: failure. No error message. Always validate against this list before wiring. Source of truth: openapi.json at components/schemas/workflow_engine_wfEngineCommon_evaluationItem/properties/operator/enum.
contains is regex-based, not substring.operand_2 is interpreted as a regex pattern. A literal like 9.2(4) is parsed as regex — . matches any char, (4) becomes a capture group — and may match unintended strings or fail to match the intended one. Escape regex metacharacters in literal patterns: 9\.2\(4\) not 9.2(4).
contains also works for object-key presence — it is the universal "does X contain Y" operator. On a string operand it does regex matching; on an object operand it tests key presence. There is no separate contains_key operator.
Direct evaluation test (no workflow needed):
POST /workflow_engine/runEvaluationGroups
{"evaluation_groups":[{"operator":"AND","evaluations":[{"operand_1":"<test input>","operator":"contains","operand_2":"<pattern>"}]}]}
Returns true/false. Invalid operators silently return false. Use this to validate operators and escape patterns before wiring them into a workflow.
incomingRefs cache — API PUT does not regenerate it for existing task changes. See the incomingRefs table in $var Resolution Rules. Diagnostic sign: GET /operations-manager/tasks/{iterationUUID} shows incomingRefs[n].taskId: null or taskPointer: "/variables/outgoing/undefined".
Operand reference format (uses "variable", same as merge):
Loop modes:loopType: "" (single), "parallel" (multiple simultaneous), "sequential" (one at a time). With loops, use data_array (each element becomes a child job's variables) and set variables: {}.
incoming must only contain data_array — do NOT include job_id or any other field. Adding job_id causes errors at runtime.
$var.<taskId>.<output> does NOT resolve inside the loop body — string references like $var.n01.current_item silently resolve to null inside a forEach body. Use $var.job.<varName> instead (bind the forEach's outgoing to a job variable and reference that). This applies to ALL reference styles — even taskRef objects {"task": "outerTask", "variable": "current_item"} are unreliable inside a nested body.
Loop body tasks cannot transition to tasks outside the loop — no error transitions from loop body tasks to external error handlers. The forEach task itself handles exit via state: "error" on the forEach transition. Handle errors within the loop body, then let the forEach's error transition route out.
The last loop body task signals loop-back with an empty {} transition — do NOT add an explicit loop-back target pointing to forEach.
The variables field must be a resolved object. Use merge first to build it, then pass via $var.taskId.merged_object:
merge (build variables object) → makeData (use $var.taskId.merged_object as variables)
WARNING — makeData.incoming.variables cannot use $var references to a merge that sources childJob output.
When a merge task's data_to_merge contains a childJob reference (e.g., {"task": "childJobId", "variable": "job_details"}), the platform cannot compile $var.<mergeId>.merged_object as a taskRef for makeData.incoming.variables — it is stored as a literal static string. Template substitution then operates on the literal string and emits unresolved placeholders.
query.incoming.obj does NOT have this limitation — it resolves $var.<mergeId>.merged_object correctly even when the merge references childJob output.
Fix: extract individual values from the childJob-sourced merge using query tasks, then pass those resolved scalars to makeData via a second merge (that contains only non-childJob refs). Do NOT feed a childJob-sourced merge directly into makeData's variables.
Used in childJob mode 3 (loop with transformation) to reshape each data_array element before passing to the child.
decision
Multi-way branching based on conditions. Unlike evaluation (binary true/false), decision branches to different tasks based on multiple conditions.
Incoming:decisionArray (array of decision objects with conditions and target task IDs)
Outgoing:return_value (string — the ID of the next task)
restCall
Make external HTTP calls from within a workflow. Use when calling APIs not exposed through adapters.
Response shape — no wrapper.restCall returns the already-parsed JSON body directly as the outgoing value. There is no response or result wrapper. Query paths target body fields directly:
Correct: "query": "access_token"
Wrong: "query": "response.access_token" ← no response wrapper
Wrong: "query": "result.access_token" ← no result wrapper
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion.Ver en GitHub