JSON Forms (itential-json-forms)
JSON Forms are reusable form definitions stored in the json-forms application. They drive structured input panels across IAP — manual triggers in Operations Manager, the JsonForms/ShowJsonForm manual task, and any workflow surface that prompts a user for typed input.
A form is a single document with four cooperating schemas — struct (UI rendering), schema (data contract / validation), uiSchema (per-field widget hints), and bindingSchema (live-data binding for REST dropdowns). Get the relationships wrong and the form will render but break silently at runtime.
Customization
Before using this skill, check custom/org/, custom/team/, and custom/dev/
in this skill's own directory. Read every .md file found, in that order
(any folder may be empty or absent). Apply them in addition to everything
below — where a file overrides a specific rule from this document, prefer
the override; more specific wins (dev over team over org). See
.claude/CUSTOMIZATION.md for the full framework and what belongs in
which layer.
Concepts
struct— the UI definition.struct.typeis always"array";struct.items[]is the list of fields.customKeyon each field becomes the property key inschemaand the variable key when the form's data is consumed.schema— the data contract.schema.properties.<customKey>must exist for every field instruct.items[](and stay in sync with the field's type, enum values, etc.).schema.requiredlists mandatorycustomKeys.uiSchema— per-customKeywidget hints: placeholder text,ui:widgetoverrides, disabled flags. Required for cascading dropdowns (see below).bindingSchema— empty{}for static-enum forms; see REST-bound dropdowns below for the mirroring requirement when a field is REST-bound.- Static vs. REST-bound dropdowns — static dropdowns hardcode the list via
enum/enumNames. REST-bound dropdowns pull options live from an IAP endpoint at form-render time. - Cascading dropdowns (aka field dependency in the Studio UI) — a REST-bound dropdown whose URL path parameter is filled from another field's current value. The dependent field re-fetches when the source field changes.
API Reference
| Method | Endpoint | Description |
|---|---|---|
| GET | /json-forms/forms |
List all JSON forms |
| GET | /json-forms/forms/{id} |
Fetch a single form |
| POST | /json-forms/forms |
Create a JSON form |
| PUT | /json-forms/forms/{id} |
Update a JSON form (full replacement — see Update below) |
| DELETE | /json-forms/forms |
Bulk delete — body {"ids":["...","..."]}. There is no per-id DELETE endpoint; per-id calls 404. |
| GET | /automation-studio/json-forms/method-options |
Canonical list of bindable endpoints (same list Studio shows in its dropdown picker) |
Create a JSON Form
POST /json-forms/forms
Choose the helper that matches your form's dropdown needs:
| Use case | Annotated scaffold (fill in the blanks) | Real export (read to see the actual shape) |
|---|---|---|
| Static-enum dropdowns only (hardcoded option lists) | ${CLAUDE_PLUGIN_ROOT}/helpers/create/create-json-form.json |
${CLAUDE_PLUGIN_ROOT}/helpers/assets/json-form-example-static-enum.json — real Cisco IOS "Port Turn Up" form, 8 fields incl. static dropdown, number updown widgets, ipv4 format validation |
| REST-bound dropdowns (live data from IAP endpoints) | ${CLAUDE_PLUGIN_ROOT}/helpers/create/create-json-form-rest-bound.json |
${CLAUDE_PLUGIN_ROOT}/helpers/assets/json-form-example-rest-bound.json — real Cisco IOS "Compliance" form, one REST-bound dropdown pulling tree names live from GET /configuration_manager/configs |
| Cascading dropdowns (field dependency) | ${CLAUDE_PLUGIN_ROOT}/helpers/create/create-json-form-rest-bound.json (Inventory Manager Site/Device cascade worked example) |
(no real cascading export on hand yet — the scaffold is hand-built but follows the same field shapes as the two real exports above) |
The scaffolds are annotated with _comment_* fields to fill in; the real exports are genuine POST /json-forms/forms payloads pulled from a live platform (IDs/timestamps/createdBy stripped since the server assigns those on create) — read one when you want to see exactly what a working form looks like end-to-end, not just the shape.
Static-enum dropdowns
enum/enumNames arrays appear in both struct.items[i] and schema.properties.<customKey> — they must stay in sync.
struct.items[i].enum/enumNamesare arrays of{id, label, value}objects.schema.properties.<customKey>.enum/enumNamesare flat string arrays of the same values (not objects).
Leave bindingSchema: {} for static-enum forms.
REST-bound dropdowns
When options should reflect live platform state (devices, inventories, projects, templates) instead of a hardcoded list, the dropdown pulls from a GET against an IAP endpoint at render time. Three things must line up:
struct.typeMUST be"array"(not"object"). The static-enum scaffold uses"array"too — keep it.bindingSchema.properties.<customKey>must mirror every REST-bound field. Studio reverse-engineersbindingSchemafromstructin the GUI, but the server does not — leavingbindingSchema: {}produces dropdowns that render but never fetch.- Endpoint discovery:
GET /automation-studio/json-forms/method-optionsreturns the canonical list of bindable endpoints — the same list Studio shows in its dropdown picker.
Per-field shape in struct.items[i]:
{
"type": "string",
"title": "Site",
"binding": true,
"rel": "collection",
"targetPointer": "/enum",
"method": "GET",
"base": "/inventory_manager",
"href": "/v1/inventories",
"sourcePointer": "/result/data",
"sourceKeyPointer": "/name",
"customKey": "site"
}
base + hrefis the endpoint.sourcePointeris a JSON pointer into the response, walked to the array of items.sourceKeyPointeris the per-item field that becomes BOTH the value and the label.labelKeyPointeris unused — both columns come fromsourceKeyPointer.
Cascading dropdowns (aka field dependency)
The Studio UI labels this pattern field dependency; the form JSON calls it cascading. Same feature, two names. A REST-bound dropdown whose URL path parameter is filled from another field's value (e.g., dropdown 2 lists devices from the inventory selected in dropdown 1):
- The dependent dropdown's
hrefstays as a TEMPLATE with placeholders:/v1/inventories/:inventoryIdentifier/nodes. Path params use:namecolon syntax, NOT{name}curly braces. - A
variablesarray maps each placeholder to a JSON pointer into form data:[{"name": "inventoryIdentifier", "reference": "/site"}]substitutes:inventoryIdentifierwith whatever value the field whosecustomKeyissitecurrently holds. - The same
variablesarray must appear in both places:struct.items[i].variablesbindingSchema.properties.<dependentKey>.binding:hyperSchema.links[0].variables
- Both the source and the dependent field need
ui:widget: "DependencyWidget"inuiSchema— without it, the runtime will not re-fetch when the source changes.
Update a JSON Form
PUT /json-forms/forms/{id}
- Body MUST be wrapped in
{"options": {...}}. - Include ALL fields the form already has:
created,createdBy,lastUpdated,lastUpdatedBy,name,description,struct,schema,uiSchema,validationSchema,bindingSchema,version. - This is a full replacement — omitting any field clears it.
Delete JSON Forms
Bulk-only:
DELETE /json-forms/forms
Body: {"ids": ["<id1>", "<id2>", ...]}
Per-id calls (DELETE /json-forms/forms/<id>) return 404 — the endpoint does not exist.
Wiring to a Manual Trigger
A JSON Form is consumed by an Operations Manager manual trigger that hands the user's form input to a workflow as job variables. See builder-agent for the trigger creation details.
Critical flag — legacyWrapper: false: The default is true, which wraps form field values under a formData object and breaks the mapping to workflow job variables. Set legacyWrapper: false so each form field maps directly to a workflow input variable by name (i.e., field customKey: "device_name" → job variable device_name).
Required trigger fields: name, type ("manual"), enabled, actionType ("automations"), actionId, formId, legacyWrapper.
Helper for the wired-up trigger: ${CLAUDE_PLUGIN_ROOT}/helpers/create/create-ops-manager-trigger-manual.json.
Common Gotchas
struct.typeis"array", not"object". Forms with"object"render empty.bindingSchemamirroring is mandatory for REST-bound fields. Studio hides this in the GUI; an API-created form needs bothstruct.items[i]ANDbindingSchema.properties.<customKey>populated, or the dropdown renders and never fetches.:namecolon syntax inhref, not{name}. Curly-brace placeholders are silently ignored.enum/enumNamesflat-vs-object asymmetry. Instruct.items[i]they're{id, label, value}objects; inschema.properties.<customKey>they're flat string arrays.DependencyWidgetrequired on BOTH ends of a cascade. Easy to remember to set it on the dependent field and forget the source — without the source-side widget the dependent won't re-fetch.labelKeyPointeris a red herring. Don't bother setting it; both label and value come fromsourceKeyPointer.- Bulk DELETE has no per-id alternative. Always send
{"ids":[...]}to/json-forms/forms.
See Also
builder-agentfor workflows that consume form output, manual-trigger wiring, and project-level component management.- Helper files in
${CLAUDE_PLUGIN_ROOT}/helpers/:create/create-json-form.json— static-enum scaffoldcreate/create-json-form-rest-bound.json— REST-bound + cascading scaffold (Inventory Manager Site/Device cascade worked example)create/create-ops-manager-trigger-manual.json— manual trigger that consumes a formassets/json-form-example-static-enum.json— real export, static-enum (Cisco IOS Port Turn Up)assets/json-form-example-rest-bound.json— real export, REST-bound (Cisco IOS Compliance)