Download OpenAPI specification:
phenix API
| kind | string Default: "all" Enum: "all" "topology" "scenario" "experiment" "image" "user" "role" limit configs to specified kind (defaults to all) |
{- "configs": [
- {
- "apiVersion": "string",
- "kind": "string",
- "metadata": {
- "name": "string",
- "annotations": {
- "builder-doc": {
- "digest": "string",
- "id": "string",
- "path": "string"
}, - "property1": "string",
- "property2": "string"
}
}, - "spec": { }
}
]
}Needs configs create on the config the body names, by its kind and name (such as Topology/lab). The server replaces each ${BRANCH_NAME} placeholder with a value from its environment. It stores every other ${NAME} placeholder as written.
phenix config creation parameters
| apiVersion required | string |
| kind required | string |
required | object |
required | object |
{- "apiVersion": "string",
- "kind": "string",
- "metadata": {
- "name": "string",
- "annotations": {
- "builder-doc": {
- "digest": "string",
- "id": "string",
- "path": "string"
}, - "property1": "string",
- "property2": "string"
}
}, - "spec": { }
}{- "message": "string",
- "cause": "string",
- "metadata": {
- "validation": "string",
- "validation-raw": "string",
- "property1": "string",
- "property2": "string"
}
}| kind required | string kind of phenix config to get |
| name required | string name of phenix config to get |
| Accept | string Default: application/json Enum: "application/json" "application/x-yaml" content format for response |
{- "apiVersion": "string",
- "kind": "string",
- "metadata": {
- "name": "string",
- "annotations": {
- "builder-doc": {
- "digest": "string",
- "id": "string",
- "path": "string"
}, - "property1": "string",
- "property2": "string"
}
}, - "spec": { }
}Can also be used to rename a config by changing the name in the metadata portion of the config header and using the old name in the request path. Renaming a Topology, or changing its kind, removes the Builder documents published under its old name, as deleting it does. The builder-doc annotation of the renamed config loses the id and the digest that Publish wrote. The annotation keeps a path, and the digest beside that path. The server removes the annotation when it is empty (see BuilderDocumentReference). Needs configs update on the config of the path and, when the body names another kind or name, on that config too. The server replaces each ${BRANCH_NAME} placeholder with a value from its environment. It stores every other ${NAME} placeholder as written.
| kind required | string kind of phenix config to get |
| name required | string name of phenix config to get |
phenix config update parameters
| apiVersion required | string |
| kind required | string |
required | object |
required | object |
{- "apiVersion": "string",
- "kind": "string",
- "metadata": {
- "name": "string",
- "annotations": {
- "builder-doc": {
- "digest": "string",
- "id": "string",
- "path": "string"
}, - "property1": "string",
- "property2": "string"
}
}, - "spec": { }
}{- "message": "string",
- "cause": "string",
- "metadata": {
- "validation": "string",
- "validation-raw": "string",
- "property1": "string",
- "property2": "string"
}
}Each version's schema file holds the schemas of the config kinds stored at that version, and a config is validated against the file of its own apiVersion: v0 holds Workflow; v1 holds Topology, Experiment, Image, User and Role; v2 holds Scenario.
| version required | string Enum: "v0" "v1" "v2" version of phenix config schema to get |
| Accept | string Default: application/json Enum: "application/json" "application/x-yaml" content format for response |
{ }A kind's schema exists only in the version it is stored at, the version its configs are validated against: Workflow in v0, Scenario in v2, and every other kind in v1.
| kind required | string Enum: "topology" "scenario" "experiment" "image" "user" "role" "workflow" kind of phenix config schema to get |
| version required | string Enum: "v0" "v1" "v2" version of phenix config schema to get |
| Accept | string Default: application/json Enum: "application/json" "application/x-yaml" content format for response |
{ }Requires builder use, as the document schema does. A template file holds one collection of device templates, in YAML or JSON. The Node Templates tab exports and imports this file. At start, phenix reads the template files in its template directory (--base-dir.builder-templates) as read-only server collections.
{ }Requires builder use, as the document schema does. A layer set file holds one layer set, in YAML or JSON, with copies of the custom icons that it names. The Layers tab of the Drafts page downloads and uploads this file.
{ }Requires builder use, as the document schema does. A Builder package is one JSON or YAML file (POST /builder/package makes one). It holds a Builder document, and the list of what the diagram needs on a phenix server. It also holds the Scenario and Topology configs and custom icons that the document names, when the request that made the package selected them.
{ }| screenshot | string whether or not to include VM screenshots (value is size to grab) |
{- "experiments": [
- {
- "id": 0,
- "name": "string",
- "topology": "string",
- "apps": [
- "string"
], - "start_time": "2019-08-24T14:15:22Z",
- "running": true,
- "vm_count": 0,
- "vlan_min": 0,
- "vlan_max": 0,
- "vlan_count": 0,
- "vlans": [
- {
- "vlan": 0,
- "alias": "string"
}
], - "vms": {
- "vms": [
- {
- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}
]
}
}
]
}phenix experiment creation parameters
| name | string |
| topology | string |
| apps | Array of strings |
| vlan_min | integer |
| vlan_max | integer |
{- "name": "string",
- "topology": "string",
- "apps": [
- "string"
], - "vlan_min": 0,
- "vlan_max": 0
}{- "id": 0,
- "name": "string",
- "topology": "string",
- "apps": [
- "string"
], - "start_time": "2019-08-24T14:15:22Z",
- "running": true,
- "vm_count": 0,
- "vlan_min": 0,
- "vlan_max": 0,
- "vlan_count": 0,
- "vlans": [
- {
- "vlan": 0,
- "alias": "string"
}
], - "vms": {
- "vms": [
- {
- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}
]
}
}| name required | string name of phenix experiment to get |
| screenshot | string whether or not to include VM screenshots (value is size to grab) |
{- "id": 0,
- "name": "string",
- "topology": "string",
- "apps": [
- "string"
], - "start_time": "2019-08-24T14:15:22Z",
- "running": true,
- "vm_count": 0,
- "vlan_min": 0,
- "vlan_max": 0,
- "vlan_count": 0,
- "vlans": [
- {
- "vlan": 0,
- "alias": "string"
}
], - "vms": {
- "vms": [
- {
- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}
]
}
}| name required | string name of phenix experiment to start |
{- "id": 0,
- "name": "string",
- "topology": "string",
- "apps": [
- "string"
], - "start_time": "2019-08-24T14:15:22Z",
- "running": true,
- "vm_count": 0,
- "vlan_min": 0,
- "vlan_max": 0,
- "vlan_count": 0,
- "vlans": [
- {
- "vlan": 0,
- "alias": "string"
}
], - "vms": {
- "vms": [
- {
- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}
]
}
}| name required | string name of phenix experiment to stop |
{- "id": 0,
- "name": "string",
- "topology": "string",
- "apps": [
- "string"
], - "start_time": "2019-08-24T14:15:22Z",
- "running": true,
- "vm_count": 0,
- "vlan_min": 0,
- "vlan_max": 0,
- "vlan_count": 0,
- "vlans": [
- {
- "vlan": 0,
- "alias": "string"
}
], - "vms": {
- "vms": [
- {
- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}
]
}
}| name required | string name of phenix experiment to update schedule for |
scheduling algorithm to use
| algorithm | string |
{- "algorithm": "string"
}{- "schedule": [
- {
- "vm": "string",
- "host": "string",
- "auto_assigned": true
}
]
}| exp_name required | string name of phenix experiment to get VMs for |
| screenshot | string whether or not to include VM screenshots (value is size to grab) |
{- "vms": [
- {
- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}
]
}| exp_name required | string name of phenix experiment to get VM from |
| vm_name required | string name of phenix experiment VM to get |
| screenshot | string whether or not to include VM screenshots (value is size to grab) |
{- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}| exp_name required | string name of phenix experiment to modify |
| vm_name required | string name of phenix virtual machine to modify |
phenix virtual machine patch parameters
| host | string |
| cpus | integer |
| ram | integer |
| disk | string |
| dnb | boolean |
object |
{- "host": "string",
- "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "interface": {
- "index": 0,
- "vlan": 0
}
}{- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}| exp_name required | string name of phenix experiment |
| vm_name required | string name of phenix VM to start |
{- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}| exp_name required | string name of phenix experiment |
| vm_name required | string name of phenix VM to stop |
{- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}| exp_name required | string name of phenix experiment |
| vm_name required | string name of phenix VM to redeploy |
{- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}| exp_name required | string name of phenix experiment |
| vm_name required | string name of phenix virtual machine to get screenshot from |
| size | string size of screenshot to grab |
| base64 | boolean whether or not to base64 encode the screenshot image |
| exp_name required | string name of phenix experiment |
| vm_name required | string name of phenix VM to start capture on |
phenix VM packet capture parameters
| interface | integer |
| filename | string |
{- "interface": 0,
- "filename": "string"
}| exp_name required | string name of phenix experiment |
| vm_name required | string name of phenix VM to snapshot |
phenix VM snapshot parameters
| filename | string |
{- "filename": "string"
}| screenshot | string whether or not to include VM screenshots (value is size to grab) |
{- "vms": [
- {
- "id": 0,
- "name": "string",
- "experiment": "string",
- "host": "string",
- "ipv4": [
- "string"
], - "cpus": 0,
- "ram": 0,
- "disk": "string",
- "dnb": true,
- "networks": [
- "string"
], - "taps": [
- "string"
], - "captures": [
- {
- "vm": "string",
- "interface": 0,
- "filepath": "string"
}
], - "running": true,
- "uptime": 0.1,
- "screenshot": "string"
}
]
}user details
| username | string |
| password | string |
| first_name | string |
| last_name | string |
| role_name | string |
| resource_names | Array of strings |
{- "username": "string",
- "password": "string",
- "first_name": "string",
- "last_name": "string",
- "role_name": "string",
- "resource_names": [
- "string"
]
}{- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "role": {
- "name": "string",
- "resources": [
- "string"
], - "resource_names": [
- "string"
], - "verbs": [
- "string"
]
}
}{- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "role": {
- "name": "string",
- "resources": [
- "string"
], - "resource_names": [
- "string"
], - "verbs": [
- "string"
]
}
}| username required | string username of user to modify |
user parameters
| first_name | string |
| last_name | string |
| role_name | string |
| resource_names | Array of strings |
{- "first_name": "string",
- "last_name": "string",
- "role_name": "string",
- "resource_names": [
- "string"
]
}{- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "role": {
- "name": "string",
- "resources": [
- "string"
], - "resource_names": [
- "string"
], - "verbs": [
- "string"
]
}
}user details
| username | string |
| password | string |
| first_name | string |
| last_name | string |
{- "username": "string",
- "password": "string",
- "first_name": "string",
- "last_name": "string"
}{- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "role": "string",
- "token": "string"
}login credentials
| user | string |
| pass | string |
{- "user": "string",
- "pass": "string"
}{- "username": "string",
- "first_name": "string",
- "last_name": "string",
- "role": "string",
- "token": "string"
}Creates the config if it does not exist and updates it otherwise. The server sets BRANCH_NAME to the branch before parsing the body, so ${BRANCH_NAME} placeholders expand to the branch name. The server stores every other ${NAME} placeholder as written. The Content-Type header must be exactly application/json or application/x-yaml. A Topology is also checked for a node an experiment could not use, such as a hostname used twice or a reserved hostname, before it is stored. With dryRun=true it runs the same checks, returns what it would do, and stores nothing. Needs configs create or update before the body is parsed, then configs create on a new config or update on a stored one, by the kind and name the body gives (such as Topology/lab), dry run or not.
| branch required | string workflow branch name |
| dryRun | boolean Default: false Checks the config as a real request would (placeholder expansion, the create or update decision, permissions, schema validation and, for a Topology, the node checks an experiment would fail on) and returns the result without storing the config or running config hooks. For an Experiment config only the schema is checked; the experiment checks run when the config is stored. The parameter's name is matched exactly, so a request that spells it differently is carried out for real. |
phenix config of a storable kind (e.g. Topology or Scenario)
| apiVersion required | string |
| kind required | string |
required | object |
required | object |
{- "apiVersion": "string",
- "kind": "string",
- "metadata": {
- "name": "string",
- "annotations": {
- "builder-doc": {
- "digest": "string",
- "id": "string",
- "path": "string"
}, - "property1": "string",
- "property2": "string"
}
}, - "spec": { }
}{- "action": "create",
- "kind": "string",
- "name": "string",
- "dryRun": true,
- "pending": "string"
}Validates the workflow config against the Workflow schema, finds the experiment mapped to the branch (phenix.workflow/branch annotation), plans an action, and checks the default bridge and the topology and scenario the action would use, including a stored topology's nodes, as creating an experiment would. Only then does it create, update, start, or restart the experiment. A failed check leaves a running experiment running. With dryRun=true the plan is returned after the same checks, and nothing is changed; a dry run before a first deploy names the configs the caller will store with pending. The server sets BRANCH_NAME to the branch before parsing the body, so ${BRANCH_NAME} placeholders expand to the branch name. The server keeps every other ${NAME} placeholder as written. The Content-Type header must be exactly application/json or application/x-yaml.
| branch required | string workflow branch name |
| tag | Array of strings key=value tag stored on the experiment, joined with commas into the phenix.workflow/tags annotation; repeat the parameter for more tags |
| dryRun | boolean Default: false Runs every check, a stored topology's node checks included, and returns the plan without creating, updating, starting, or stopping anything. The parameter's name is matched exactly, so a request that spells it differently is treated as a real apply, and refused with 400 when it also carries pending. |
| pending | Array of strings A config the caller will create or update before the real apply, usually the pending value a dry run of POST /workflow/configs/{branch} returned: |
| expect | string Enum: "none" "create" "createAndStart" "update" "updateAndStart" "restart" The action the caller expects, usually the action a previous dry run returned. If the server plans a different action, it returns 409 and changes nothing. It is checked on dry runs as well as real applies. A value that is not an action is a 400 with the message invalid expect value " |
phenix workflow config
| apiVersion required | string (API Version) Value: "phenix.sandia.gov/v0" The config API version; a workflow config is phenix.sandia.gov/v0. |
| kind required | string (Kind) Value: "Workflow" The config kind, which is always Workflow. |
| metadata | object or null (Metadata) Not read by the server; it is accepted so a workflow config has the same four top-level keys as every other phenix config, and so a later version can carry a name or annotations without a breaking change. |
required | object (Workflow Spec) What an apply may do and the experiment settings it sets; an empty spec applies every default. |
{- "apiVersion": "phenix.sandia.gov/v0",
- "kind": "Workflow",
- "metadata": { },
- "spec": {
- "auto": {
- "create": "string",
- "update": true,
- "restart": true
}, - "topology": "string",
- "scenario": "string",
- "vlans": {
- "property1": 0,
- "property2": 0
}, - "schedules": {
- "property1": "string",
- "property2": "string"
}, - "deployMode": "string",
- "useGREMesh": true,
- "vlanRange": {
- "min": 0,
- "max": 0
}, - "defaultBridge": "string"
}
}{- "action": "none",
- "experiment": "string",
- "reason": "string",
- "dryRun": true
}Server-side settings that clients read: every phenix CLI command fetches them over the unix socket when it starts, to inherit the server's bridge-mode, deploy-mode and use-gre-mesh, and the web UI's Experiments page reads them too. workflow-dry-run is true when POST /workflow/configs/{branch} and POST /workflow/apply/{branch} both support dryRun, and apply supports pending and expect. An older server ignores dryRun on /workflow/configs/{branch} and stores the config, so phenix workflow apply refuses servers without it.
{- "bridge-mode": "string",
- "deploy-mode": "string",
- "use-gre-mesh": true,
- "base-dir.injects": "string",
- "workflow-dry-run": true
}Builder drafts, snapshots, generation and publication. Every operation needs the one Builder permission: the verb use on the resource builder. A role that holds it may read, change and delete every draft, template, icon and layer set, whoever made them. No permission applies to one draft, template, icon or layer set. Some operations read or write phenix configs: publish, import, export, sources, packages, preflight checks and published documents. Each of these operations also needs the config permissions that its description names.
The icon library belongs to the server. Each icon has a name that is unique, ignoring case. Any caller may rename or delete an uploaded icon. No caller may rename or delete an icon of the server template files.
The template library belongs to the server. Built-in templates and the items of the server template files are read only.
Every response of a Builder operation carries X-Content-Type-Options: nosniff and Cache-Control: no-store, whatever its status, 304 included.
OPTIONS on the path of any Builder operation answers 204 with the Allow header of that path. It reads and changes nothing.
Requires builder use, as the document schema does. A template file holds one collection of device templates, in YAML or JSON. The Node Templates tab exports and imports this file. At start, phenix reads the template files in its template directory (--base-dir.builder-templates) as read-only server collections.
{ }Requires builder use, as the document schema does. A layer set file holds one layer set, in YAML or JSON, with copies of the custom icons that it names. The Layers tab of the Drafts page downloads and uploads this file.
{ }Requires builder use, as the document schema does. A Builder package is one JSON or YAML file (POST /builder/package makes one). It holds a Builder document, and the list of what the diagram needs on a phenix server. It also holds the Scenario and Topology configs and custom icons that the document names, when the request that made the package selected them.
{ }Requires builder use. Returns every draft, the most recently changed first, each with the user who created it. Also lists separately, as damaged, the drafts whose metadata this server can no longer read (for example, drafts that a newer phenix wrote). A caller can delete a damaged draft, but cannot open it. No row has experiment. limits says how many drafts the server holds at most.
{- "drafts": [
- {
- "id": "string",
- "createdBy": "string",
- "title": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "lastModifiedBy": "string",
- "cursor": 0,
- "snapshots": 0,
- "snapshotId": "string",
- "digest": "string",
- "size": 0,
- "historyBytes": 0,
- "dirty": true,
- "canUndo": true,
- "canRedo": true,
- "etag": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "stamp": {
- "createdBy": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedBy": "string",
- "updatedAt": "2019-08-24T14:15:22Z"
}, - "history": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "publication": {
- "mode": "topology",
- "topologyTarget": "string",
- "topologyAction": "string",
- "experimentTarget": "string",
- "scenarioTarget": "string",
- "snapshotId": "string",
- "digest": "string",
- "documentId": "string",
- "publishedAt": "2019-08-24T14:15:22Z",
- "publishedBy": "string"
}, - "forked": {
- "documentId": "string",
- "topologyTarget": "string",
- "experimentTarget": "string"
}, - "experiment": "string"
}
], - "damaged": [
- {
- "id": "string",
- "createdBy": "string",
- "title": "string",
- "updated": "2019-08-24T14:15:22Z",
- "etag": "string"
}
], - "limits": {
- "drafts": 0
}
}Requires builder use. The caller is the creator of the draft. The draft holds the document as its first snapshot. The operation publishes nothing.
With forkOf, the ID of a draft, that draft must exist. With a sourceToken that names a published document, the caller must hold configs get for the config that the document was published to. With a sourceToken that names the Builder file of a topology, the server reads the file as GET /builder/topologies/{topology}/document reads it, with the same permission. The answers are the same when the server cannot use the file. The server refuses the draft with 409 when the file no longer holds the document that the token names. It also refuses with 409 when the server no longer reads the topology from its file.
The server stamps the metadata of the document before it stores it. createdBy and createdAt stay as they are when the document has them. Otherwise they are the caller and the current time. updatedBy and updatedAt are always the caller and the current time. The server replaces the values sent in these two fields. The server stores one document as it is: the document that a sourceToken of the builder-doc/ or builder-file/ form names, returned unchanged. Thus the draft holds exactly what the caller opened, with the same digest. A value that is not valid in one of the four fields refuses the request (422). The response has no document. Its stamp holds the four values of the stored document.
A draft opened from a published document, or a draft that forks a draft, has experiment when that publication made an experiment. The experiment must still exist, and the caller must hold experiments get for it.
The server writes the draft and its document in one transaction, so a refused request stores nothing.
| title | string |
| summary | string summary of the first snapshot |
| sourceToken | string where the draft comes from. One of:
|
| sourceFile | string <= 255 characters the name of the uploaded file that the document came from, kept only for display. It is a file name of at most 255 bytes without |
| forkOf | string the ID of a draft that the new draft forks. The draft must exist (404 otherwise). The new draft takes its source token in place of |
required | object (Builder Document) A Builder document: the diagram of a topology, with its nodes, networks and connections, and how the canvas shows them. Under |
{- "title": "string",
- "summary": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "forkOf": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}
}{- "id": "string",
- "createdBy": "string",
- "title": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "lastModifiedBy": "string",
- "cursor": 0,
- "snapshots": 0,
- "snapshotId": "string",
- "digest": "string",
- "size": 0,
- "historyBytes": 0,
- "dirty": true,
- "canUndo": true,
- "canRedo": true,
- "etag": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "stamp": {
- "createdBy": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedBy": "string",
- "updatedAt": "2019-08-24T14:15:22Z"
}, - "history": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "publication": {
- "mode": "topology",
- "topologyTarget": "string",
- "topologyAction": "string",
- "experimentTarget": "string",
- "scenarioTarget": "string",
- "snapshotId": "string",
- "digest": "string",
- "documentId": "string",
- "publishedAt": "2019-08-24T14:15:22Z",
- "publishedBy": "string"
}, - "forked": {
- "documentId": "string",
- "topologyTarget": "string",
- "experimentTarget": "string"
}, - "experiment": "string"
}Requires builder use. experiment names an experiment that the draft published. For a draft opened from a published diagram, it can also name an experiment that the diagram published. It is present while that experiment exists and the caller holds experiments get for it.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
{- "id": "string",
- "createdBy": "string",
- "title": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "lastModifiedBy": "string",
- "cursor": 0,
- "snapshots": 0,
- "snapshotId": "string",
- "digest": "string",
- "size": 0,
- "historyBytes": 0,
- "dirty": true,
- "canUndo": true,
- "canRedo": true,
- "etag": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "stamp": {
- "createdBy": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedBy": "string",
- "updatedAt": "2019-08-24T14:15:22Z"
}, - "history": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "publication": {
- "mode": "topology",
- "topologyTarget": "string",
- "topologyAction": "string",
- "experimentTarget": "string",
- "scenarioTarget": "string",
- "snapshotId": "string",
- "digest": "string",
- "documentId": "string",
- "publishedAt": "2019-08-24T14:15:22Z",
- "publishedBy": "string"
}, - "forked": {
- "documentId": "string",
- "topologyTarget": "string",
- "experimentTarget": "string"
}, - "experiment": "string"
}Requires builder use. Any caller may delete any draft, also a draft whose metadata this server can no longer read. The delete does not change published configs. A delete of a draft during its publication waits until the publication ends. Then it answers 412 with the draft's new ETag.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
| If-Match required | string The ETag of the draft revision that the change is based on: exactly one strong, quoted entity tag, such as |
{- "message": "string",
- "cause": "string",
- "metadata": {
- "property1": "string",
- "property2": "string"
}, - "code": "publish.blocked",
- "issues": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
]
}Requires builder use. The list starts with the oldest snapshot. cursor is the index of the current snapshot.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
{- "snapshots": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "cursor": 0
}Requires builder use. Appends the document as a new snapshot, discards all snapshots after the cursor (the redo branch), and moves the cursor to the new snapshot. A draft keeps at most 50 snapshots, of at most 50 MiB in total. The server removes the oldest snapshots to make room for the new snapshot.
The server stamps the metadata of the document before it stores it. createdBy and createdAt are the values from the draft create. updatedBy and updatedAt are the caller and the current time. The server replaces the values sent in the four fields. A value that is not valid refuses the request (422). The response has no document. Its stamp holds the four values written.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
| If-Match required | string The ETag of the draft revision that the change is based on: exactly one strong, quoted entity tag, such as |
| summary | string |
required | object (Builder Document) A Builder document: the diagram of a topology, with its nodes, networks and connections, and how the canvas shows them. Under |
| opId | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ the client's id for this save, kept with the snapshot, so a client that never saw the response can tell that the server stored it |
{- "summary": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "opId": "string"
}{- "id": "string",
- "createdBy": "string",
- "title": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "lastModifiedBy": "string",
- "cursor": 0,
- "snapshots": 0,
- "snapshotId": "string",
- "digest": "string",
- "size": 0,
- "historyBytes": 0,
- "dirty": true,
- "canUndo": true,
- "canRedo": true,
- "etag": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "stamp": {
- "createdBy": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedBy": "string",
- "updatedAt": "2019-08-24T14:15:22Z"
}, - "history": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "publication": {
- "mode": "topology",
- "topologyTarget": "string",
- "topologyAction": "string",
- "experimentTarget": "string",
- "scenarioTarget": "string",
- "snapshotId": "string",
- "digest": "string",
- "documentId": "string",
- "publishedAt": "2019-08-24T14:15:22Z",
- "publishedBy": "string"
}, - "forked": {
- "documentId": "string",
- "topologyTarget": "string",
- "experimentTarget": "string"
}, - "experiment": "string"
}Requires builder use.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
| snapshot required | string snapshot ID, or |
{- "snapshot": {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}, - "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}
}Requires builder use. Removes the snapshot from the history and deletes its content. A caller cannot delete the snapshot that the cursor identifies (the current version). The cursor stays on it. When the last publication names the deleted snapshot, the publication record stays, and the draft is then dirty.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
| snapshot required | string snapshot ID, or |
| If-Match required | string The ETag of the draft revision that the change is based on: exactly one strong, quoted entity tag, such as |
{- "id": "string",
- "createdBy": "string",
- "title": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "lastModifiedBy": "string",
- "cursor": 0,
- "snapshots": 0,
- "snapshotId": "string",
- "digest": "string",
- "size": 0,
- "historyBytes": 0,
- "dirty": true,
- "canUndo": true,
- "canRedo": true,
- "etag": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "stamp": {
- "createdBy": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedBy": "string",
- "updatedAt": "2019-08-24T14:15:22Z"
}, - "history": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "publication": {
- "mode": "topology",
- "topologyTarget": "string",
- "topologyAction": "string",
- "experimentTarget": "string",
- "scenarioTarget": "string",
- "snapshotId": "string",
- "digest": "string",
- "documentId": "string",
- "publishedAt": "2019-08-24T14:15:22Z",
- "publishedBy": "string"
}, - "forked": {
- "documentId": "string",
- "topologyTarget": "string",
- "experimentTarget": "string"
}, - "experiment": "string"
}Requires builder use. The move discards no snapshot.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
| If-Match required | string The ETag of the draft revision that the change is based on: exactly one strong, quoted entity tag, such as |
| index | integer >= 0 index of the snapshot to move to, oldest first |
| snapshotId | string |
{- "index": 0,
- "snapshotId": "string"
}{- "id": "string",
- "createdBy": "string",
- "title": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "lastModifiedBy": "string",
- "cursor": 0,
- "snapshots": 0,
- "snapshotId": "string",
- "digest": "string",
- "size": 0,
- "historyBytes": 0,
- "dirty": true,
- "canUndo": true,
- "canRedo": true,
- "etag": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "stamp": {
- "createdBy": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedBy": "string",
- "updatedAt": "2019-08-24T14:15:22Z"
}, - "history": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "publication": {
- "mode": "topology",
- "topologyTarget": "string",
- "topologyAction": "string",
- "experimentTarget": "string",
- "scenarioTarget": "string",
- "snapshotId": "string",
- "digest": "string",
- "documentId": "string",
- "publishedAt": "2019-08-24T14:15:22Z",
- "publishedBy": "string"
}, - "forked": {
- "documentId": "string",
- "topologyTarget": "string",
- "experimentTarget": "string"
}, - "experiment": "string"
}The only Builder operation that creates or updates configs. (DELETE /builder/documents/{document} deletes a published topology.) It publishes the current snapshot of the draft. The request carries only the intent.
Requires builder use. The caller must also have its own configs create or update on each target config, as the action of the target requires. An experiment target also requires experiments create or update. A draft imported from a stored Topology or Experiment also needs configs get on it, and topologies get (or experiments get) on its name. The server uses them to check that the source did not change since the import.
In both modes, a draft whose document lists scenarios has a scenario stage before the experiment stage. Each listed Scenario config must exist and be readable (configs get and scenarios list). Otherwise the answer is 422 and names the scenario. The stage adds the topology to the topology annotation of each scenario that does not name it yet. This needs configs update on that scenario (otherwise 403). The stage does not write a scenario that already names the topology. The stage message tells which scenarios it changed. A publish never creates or replaces the spec of a scenario.
A repeat of a publication that completed, with the same If-Match, gets 200 with every stage skipped.
A draft updates an existing topology or experiment only in two cases. In the first case, the draft was imported from it. In the second case, the draft published it (or was opened from the published diagram that did), and nothing else changed it since then. The server refuses all other updates with 409.
Only the draft imported from a topology updates a topology that has a legacy Builder diagram (builder-xml). A draft imported from an experiment built from that topology does not. Also, the diagram must be the one that the import read. The server refuses with 409 a diagram that someone changed, added or removed since, as for a changed spec. The update removes builder-xml, writes builder-doc, and adds a warning to the result. The warning says that the update replaced the diagram or, if the import could not read it, removed it.
The server reads some topologies from the Builder file that their builder-doc annotation names. Only a draft opened from that file updates such a topology. The file must still hold what the draft was opened from, and the topology must still be what the file publishes. Such a draft updates the topology in no other way. It does not update it as a draft imported from the topology, even when the document of the file was imported from the topology. A publish never writes the file. The topology keeps its path, and the result has a warning that the file is now behind.
A published experiment records in its builder-experiment annotation which draft and published document published it. After the configure stage of its apps ran, it also records its digest there. An experiment update runs that stage, as an update through /configs does. When a stage fails after the server wrote earlier stages, the response is 409 or 500, with a result whose status is partial. Unless the draft changed meanwhile, the warnings of that result then have publish.retry.available. The same request, with the same If-Match, then finishes the publication. The stages that are already done answer skipped. When the server stored the published document but could not remove the content that it replaces, the publication continues. The answer then has a Warning header and a warning in the result.
With "dryRun": true, the request is a dry run. A dry run needs the same permissions, and makes every check of the publication on the current snapshot of the draft. It writes nothing (no published document, config, scenario annotation, experiment or draft record), and needs no If-Match. It answers 200 with a BuilderPublishPreview. The preview holds what the publication would change and the warnings it would give. Its errors hold the refusal that a publication would answer with 400, 409 or 422. A 403, a 404, a body that is not a publish request (400) and a server failure keep their status. The dry run takes no lock. Thus what it reports may change before the caller sends the publication. The Publish dialog shows it as What publishing changes.
| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
| If-Match | string The ETag of the draft revision that the publication is based on, as for every Builder mutation ( |
| mode required | string Enum: "topology" "topology-experiment" |
required | object (BuilderPublishTarget) |
object (BuilderPublishScenario) the scenario that a | |
object (BuilderPublishTarget) | |
| dryRun | boolean Default: false make every check of the publication, write nothing, and answer with what it would change ( |
{- "mode": "topology",
- "topology": {
- "name": "string",
- "action": "create"
}, - "scenario": {
- "name": "string"
}, - "experiment": {
- "name": "string",
- "action": "create"
}, - "dryRun": false
}{- "status": "succeeded",
- "stages": [
- {
- "name": "string",
- "status": "string",
- "message": "string",
- "config": "string"
}
], - "warnings": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
], - "errors": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
], - "topology": {
- "name": "string",
- "action": "create"
}, - "scenario": {
- "name": "string"
}, - "experiment": {
- "name": "string",
- "action": "create"
}, - "removedHostPins": [
- {
- "hostname": "string",
- "host": "string",
- "renamedTo": "string"
}
], - "draft": {
- "id": "string",
- "createdBy": "string",
- "title": "string",
- "sourceToken": "string",
- "sourceFile": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "lastModifiedBy": "string",
- "cursor": 0,
- "snapshots": 0,
- "snapshotId": "string",
- "digest": "string",
- "size": 0,
- "historyBytes": 0,
- "dirty": true,
- "canUndo": true,
- "canRedo": true,
- "etag": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "stamp": {
- "createdBy": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedBy": "string",
- "updatedAt": "2019-08-24T14:15:22Z"
}, - "history": [
- {
- "id": "string",
- "digest": "string",
- "size": 0,
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "summary": "string",
- "opId": "string",
- "current": true
}
], - "publication": {
- "mode": "topology",
- "topologyTarget": "string",
- "topologyAction": "string",
- "experimentTarget": "string",
- "scenarioTarget": "string",
- "snapshotId": "string",
- "digest": "string",
- "documentId": "string",
- "publishedAt": "2019-08-24T14:15:22Z",
- "publishedBy": "string"
}, - "forked": {
- "documentId": "string",
- "topologyTarget": "string",
- "experimentTarget": "string"
}, - "experiment": "string"
}
}Runs the checks that the request names on the current document of the draft, in the request order and all at the same time. Reports each check as passed, failed or unavailable, with a summary and its issues.
capacity compares the CPUs and memory of the devices that phenix starts with the free CPUs and memory of the schedulable cluster hosts. These devices are all devices except external devices. A device uses hardware.vcpus and hardware.memory, or 1 and 512 MB when the device does not set them. The check also makes sure that each device fits on one host. The vCPUs and memory of the device must both fit within the CPUs and memory of that host.network compares the VLANs that the devices use, and the VLAN aliases of the networks, with the VLAN range of the named experiment. No range applies without an experiment, or when the experiment sets no range. The check compares the aliases with the VLANs that running experiments hold. It compares the bridges that the interfaces name with the Open vSwitch bridges of the schedulable hosts. It also compares the default bridge of the experiment, or phenix when there is no default bridge. It lists these bridges with ovs-vsctl list-br through the minimega shell command, which changes nothing.disks finds each drive image by file name among the disk images of the server. It checks the image kind: a VM or ISO image for a kvm device, and a container image for a container.apps reads each Scenario config that the document lists. It finds each app that the scenario does not disable among the default, built-in and user apps. A listed scenario that does not exist is the error preflight.app.scenario-missing.
A check fails when one of its issues is an error. A check is unavailable when nothing failed, but the server could not make the check, or a part of it. The causes are:hosts list (capacity and bridges), disks list, applications list, or experiments list (VLANs in use).configs get and experiments get) or to a scenario (configs get and scenarios list).preflight.unavailable, or preflight.app.scenario-unreadable as a warning for a scenario). One check never stops the other checks. Requires builder use and configs get. The route writes nothing, and starts no VM or experiment.| draft required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ draft ID |
| checks required | Array of strings (BuilderPreflightCheckName) non-empty Items Enum: "capacity" "network" "disks" "apps" the checks to make, in the order of the report. Each at most once |
| experiment | string the experiment whose VLAN range and default bridge the network check uses. Empty or not present for none |
{- "checks": [
- "disks",
- "apps"
], - "experiment": "lab-exp"
}{- "checks": [
- {
- "name": "capacity",
- "status": "passed",
- "summary": "Found: 3 of 3 drive images, among the server's 12 disk images.",
- "issues": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
]
}
], - "passed": [
- "capacity"
], - "failed": [
- "capacity"
], - "unavailable": [
- "capacity"
]
}Requires builder use and configs list. POST /builder/generate can import topologies and experiments. Scenarios and images are for selection only. The server filters each config by configs list for its name. For topologies, experiments and scenarios, it also filters by the list permission of that resource. A topology also reports how many topologies it includes (includeCount), to a caller who holds configs get for its name.
{- "topologies": [
- {
- "kind": "string",
- "name": "string",
- "fullName": "string",
- "apiVersion": "string",
- "created": "string",
- "updated": "string",
- "digest": "string",
- "stored": true,
- "builder": "string",
- "generatable": true,
- "includeCount": 1
}
], - "experiments": [
- {
- "kind": "string",
- "name": "string",
- "fullName": "string",
- "apiVersion": "string",
- "created": "string",
- "updated": "string",
- "digest": "string",
- "stored": true,
- "builder": "string",
- "generatable": true,
- "includeCount": 1
}
], - "scenarios": [
- {
- "kind": "string",
- "name": "string",
- "fullName": "string",
- "apiVersion": "string",
- "created": "string",
- "updated": "string",
- "digest": "string",
- "stored": true,
- "builder": "string",
- "generatable": true,
- "includeCount": 1
}
], - "images": [
- {
- "kind": "string",
- "name": "string",
- "fullName": "string",
- "apiVersion": "string",
- "created": "string",
- "updated": "string",
- "digest": "string",
- "stored": true,
- "builder": "string",
- "generatable": true,
- "includeCount": 1
}
]
}Converts a stored config (source) or an uploaded config (content) into a document and returns it. The operation writes nothing. Requires builder use and configs get. A stored source also needs configs get for its name, and the topologies or experiments list permission for it. Uploaded content needs configs create, the permission of POST /configs, which also takes a config file. Unlike POST /configs, the server keeps each ${NAME} and ${NAME:default} placeholder in the content as written. It never reads its environment for them.
The server converts a Topology that has the builder-xml annotation and no builder-doc from its legacy Builder diagram, as POST /builder/legacy converts one. Its source then has builder builder-xml.
The server reads the topologies that a Topology includes (includeTopologies) from the config store with the permissions of the caller, never from a file. With includes keep (the default), their nodes are in the document, marked includedFrom and read only. The document lists the includes that the server could not read in its source.unresolvedIncludes. With includes combine, their nodes are nodes of the document. Only the includes that the server could not read stay in its source.includeTopologies.
copy asks for the same document as a plain import, not linked to the topology. A combined document and a copy name no config. Their source.kind is manual, and the document name is the name of the new topology. A draft made from such a document publishes a new topology, and never updates the topology it was imported from. Their metadata.notes end with the note Copied from <name>, where <name> is the name in the metadata of the source config. The new name is name, or else the source name with -combined or -copy added. The server does not search for a name that no topology has. The source of the response is still the config that the server read.
The device nodes keep the order of the config's nodes, then the nodes of each included topology in the order phenix merges them. A publish writes the nodes in that order.
From an Experiment, each node spec loses what phenix added to it when it created the experiment. The spec loses bridge_set_in_topo, and a bridge that phenix set to the default bridge of the experiment. It loses each field that phenix reads as unset: empty or null, and 0 or false in a field that cannot be null. A nullable field such as snapshot keeps an explicit 0 or false. The spec also loses each value equal to the default that phenix gives the field, such as vm_type kvm or memory 512. os_type, and the fields the topology schema requires, stay. The specs then read like the nodes of a topology.
The scenario of an Experiment is its scenario annotation. It becomes the scenarios of the document when this server stores a Scenario config of that name, and the caller may list it. To list it, the caller needs configs list and scenarios list for it. Otherwise, the document lists no scenario, and a warning says so. The scenario that an Experiment holds never goes into the document. For uploaded content, a listed scenario also adds a warning. The warning says that the scenario is the Scenario config of that name on this server, not the copy in the file.
Errors:
includes has a different value, or when the request gives name without copy or includes combine.copy or includes combine of an Experiment. The same status applies when the new name is not a config name of 1 to 512 bytes (import.name.invalid). It also applies when the new name is the name of the stored topology of the import (import.name.source).| source | string a stored Topology or Experiment config, as |
| content | string a Topology or Experiment config as JSON or YAML text |
| includes | string Enum: "keep" "combine" what happens to the nodes of the topologies that a Topology includes. |
| copy | boolean Default: false make the document a copy that is not linked to the Topology that it is generated from. |
| name | string <= 512 characters ^[A-Za-z0-9_@.-]*$ the name of the new topology, and of the document. The server accepts it only with |
{- "source": "string",
- "content": "string",
- "includes": "keep",
- "copy": false,
- "name": "string"
}{- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "warnings": [
- "string"
], - "source": {
- "kind": "string",
- "name": "string",
- "fullName": "string",
- "apiVersion": "string",
- "created": "string",
- "updated": "string",
- "digest": "string",
- "stored": true,
- "builder": "string",
- "generatable": true,
- "includeCount": 1
}
}Converts a diagram of the legacy Builder (the editor that phenix had before the Builder) and returns the document. The operation writes nothing. The server reads from the store only the topologies that a topology includes. content is the text of a file. The file is the mxGraph XML of the diagram, whose root element is mxGraphModel or root. Or the file is a Topology config, in YAML or JSON, that has the builder-xml annotation. The server reads no encoded or compressed form of the XML.
For a diagram alone, the node settings in the diagram are the nodes. Its $NAME placeholders get values from its own variables and the legacy defaults. The source of the document is manual, so a draft made from it publishes a new topology. For a Topology, its spec defines every node, and the diagram only places the nodes, matched by hostname. The response has its source, with stored false.
Requires builder use, configs get and configs create, because the content can be a config file, as for POST /configs. The server keeps each ${NAME} placeholder in it as written. content holds at most 5242880 bytes. Its XML nests at most 32 elements deep and holds at most 10000 cells. The server refuses a DOCTYPE or any other declaration. A refusal repeats nothing of the content but the name and kind of a config, or the name of the root element of the XML.
| content required | string the text of a legacy Builder diagram file: mxGraph XML, or a Topology config as YAML or JSON that has the |
| name | string the name of the document of a diagram that comes without a Topology ( |
{- "content": "string",
- "name": "string"
}{- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "warnings": [
- "string"
], - "source": {
- "kind": "string",
- "name": "string",
- "fullName": "string",
- "apiVersion": "string",
- "created": "string",
- "updated": "string",
- "digest": "string",
- "stored": true,
- "builder": "string",
- "generatable": true,
- "includeCount": 1
}
}Projects the document onto the phenix Topology config that a publish of it would write, and returns that config as YAML. The operation reads nothing and writes nothing. Requires builder use and configs get. The server checks the document as a draft save checks it, within the same size limits. The config has no builder-doc annotation. It names the included topologies of the document in includeTopologies, and does not merge them.
When the phenix config validation refuses the document, the answer is 422, as for a publish. A publish names these reasons in this order, before any other reason. First, it names interfaces without a VLAN, then addresses that interfaces share. Then it names hostnames that phenix refuses, then device settings that make the experiment fail to start. An export names interfaces without a VLAN only when a vlan is missing or null. It names hostnames only when one is a single character, which the validation also refuses. Otherwise it gives the reason of the validation. publishBlockers reports what only a publish refuses. Examples are an interface with a blank VLAN, and interfaces that share an IP or MAC address. Another example is a hostname that phenix refuses only when it creates an experiment (all, all digits, or phenix on a Windows node). A device setting that makes the experiment fail to start is also an example. One such setting is an ntp-server label that names no interface with an address.
required | object (Builder Document) A Builder document: the diagram of a topology, with its nodes, networks and connections, and how the canvas shows them. Under |
| name | string the name of the topology. When empty, the server uses the document name, as the Publish dialog proposes it. The name then has |
{- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "name": "string"
}{- "name": "string",
- "yaml": "string",
- "warnings": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
], - "publishBlockers": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
]
}Returns the package of the document in the request, with the edits not saved yet. The package holds the document, the sections that include names, and the requirements. The requirements list the items that follow, whatever the package carries. They list the scenarios of the document, its included topologies, its templates and its custom icons. They also list the apps of the scenarios that the server could read. They also list the files that the devices of the document and the packaged configs name (injection sources, app asset directories).
Requires builder use and configs get. The server reads from the config store each Scenario config that the document names, and, with topologies, each included topology. It reads them with the permissions of the caller (configs get on it, and scenarios or topologies list). The requirements name a config that does not exist, or that the caller may not read, but the package does not carry it. A warning says so, in the same words for both cases. The server never reads an include that is a file path. The icons section gives the document a copy of each custom icon that it names, from the icon library. Configs go in the package without their times and Builder annotations (builder-*). The package never holds the content of a file, a script or an app. The operation writes nothing.
POST /builder/package/resolve always accepts the package in the answer. The server omits from the requirements an entry that the package format refuses. A warning names that entry. Such an entry is blank or longer than 4096 bytes. Or it holds control characters (such as a newline in an injection source), or it comes after the first 1000 entries of its list. Each warning is an issue with its code (package.config.unreadable, package.include.file-path, package.icon.missing, package.icons.too-many, package.requirement.left-out, package.requirements.truncated). Anything else that would refuse the package, such as a stored config without a spec, gets 422 with the code package.invalid and the issues. A package larger than 5242880 bytes gets 413 with the code package.too-large.
required | object (Builder Document) A Builder document: the diagram of a topology, with its nodes, networks and connections, and how the canvas shows them. Under |
| include | Array of strings Items Enum: "scenarios" "topologies" "icons" "images" the sections that the package carries, each at most once. |
{- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}, - "include": [
- "scenarios"
]
}{- "package": { },
- "warnings": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
]
}Reports, for each entry of the requirements of the package, whether this server has it. Requires builder use and configs get. The package is the request body, at most 5242880 bytes. A larger body gets 413 with the code package.too-large. The server decodes the package strictly (it refuses unknown fields) and checks it as the package schema describes. Otherwise the answer is 422 with the code package.invalid. For a package that does not validate, issues has each problem. The server also refuses a carried config with a Builder annotation (builder-*).
present when the server has one of that name with the spec of the package. It is also present when the package carries no spec. It is different when its spec is not the same, and missing when there is none. It is unknown when the caller may not read it (configs get on it, and scenarios or topologies list). Thus the answer does not disclose whether it exists.configs list.GET /disks lists the caller (disks list, then by name). Thus an image that the caller may not see is missing, as an absent image is. A drive matches by the name or full path of the image, else by its file name. The detail then tells this (Matched by file name pkg.qc2. This server's image is /phenix/images/pkg.qc2.).applications list, also for the name.disks list or applications list, or when the server cannot list the disk images, they are unknown.present.unknown.
The operation writes nothing. The Builder creates a missing config that the user selects through POST /configs.| property name* additional property | any |
{ }{- "dependencies": [
- {
- "kind": "scenario",
- "name": "string",
- "status": "present",
- "packaged": true,
- "detail": "string"
}
]
}Requires builder use, as deleting any draft does. Deletes every draft that is published and unchanged. For such a draft, its current snapshot is the one it last published, and no newer snapshot follows it. Also, the topology it published to still references the document it published. A draft stays when its topology no longer exists, or references another document. Each delete names the revision that the cleanup read, so a draft changed in the meantime stays. The published configs and documents stay. A cleanup waits for a publication in progress, so it never deletes a draft while a publication of that draft is in progress.
Then it removes the orphaned storage that the server also removes when it starts and every hour after that. Orphaned storage is published documents that no topology references, and stored content that no draft or document names. It keeps what the server stored within the last hour, because a save or a publication may still be about to reference it.
With dryRun=true it removes nothing, and answers with what it would remove. The request has no body. A draft or a record that the server could not remove, or could not read, does not fail the request. The Warning header then says that removing some of it failed.
| dryRun | boolean
|
{- "dryRun": true,
- "drafts": [
- "string"
], - "documents": 0,
- "content": 0
}Requires builder use and configs list. Lists the documents that configs reference now in their builder-doc annotation, filtered by configs list for the config that each document was published to. The list shows a topology whose annotation names a Builder file (path), and no current stored document, with source file and the path only. The listing reads no file, so it lists such a row whether or not the server can use the file. A row with source store has experiment when the publication of the document made an experiment, and the caller holds experiments get for it. That experiment must still exist, and its topology must be the topology of the row.
{- "documents": [
- {
- "source": "store",
- "id": "string",
- "digest": "string",
- "size": 0,
- "target": "string",
- "kind": "string",
- "config": "string",
- "path": "string",
- "draftId": "string",
- "snapshotId": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "etag": "string",
- "topologyDiffers": true,
- "experiment": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}
}
]
}Requires builder use, and configs get for the config that the document was published to. A document that the caller may not read, or that no config references any more, gets 404.
| document required | string published document ID |
{- "source": "store",
- "id": "string",
- "digest": "string",
- "size": 0,
- "target": "string",
- "kind": "string",
- "config": "string",
- "path": "string",
- "draftId": "string",
- "snapshotId": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "etag": "string",
- "topologyDiffers": true,
- "experiment": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}
}Deletes the Topology config that the document is current for, as DELETE /configs/Topology/{name} does. Then deletes every published document of that topology. Drafts and experiments made from it do not change. Requires builder use, and configs delete for the topology. A document that the caller may not get, or that is no longer current, gets 404. A document is no longer current when the topology changed to reference another document, was published again, or was deleted. A published experiment gets 422. To delete an experiment, use the Experiments page. If-Match must name the etag of the document as the caller read it. A topology that changed since then, in any way, gets 412 with the current tag.
The list does not show a document whose stored metadata this server cannot read (for example one a newer phēnix wrote). The server logs its ID. Deleting it removes only that record and its content, and changes no config. When the record still names its config, the caller needs configs get and delete for it. Otherwise the caller needs them for every config (*/*). If-Match must name the tag of the stored record, which a 412 gives in its ETag header.
| document required | string published document ID |
| If-Match required | string The |
{- "message": "string",
- "cause": "string",
- "metadata": {
- "property1": "string",
- "property2": "string"
}, - "code": "publish.blocked",
- "issues": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
]
}Returns the document that the builder-doc annotation of the topology names, wherever the server keeps it. The stored published document has priority: the document that the id or digest of the annotation names for this topology. Without one, the server reads the Builder file at the path of the annotation, on every request. The file must:
--base-dir.phenix), and outside the directory that holds the mounted VM file systemsdigest beside path pins the file. The server refuses a file whose document has another digest. Requires builder use, and configs get for the topology, which says nothing about the file. A caller who may create or update any one topology can name in it any Builder file below the base directory. That caller can then read the file here. Three cases get the same 404. They are a topology that the caller may not get, a topology that does not exist, and a topology that references no document. When the server cannot use a file, the answer is a fixed sentence that names the path, never anything that the file holds.| topology required | string topology name |
{- "source": "store",
- "id": "string",
- "digest": "string",
- "size": 0,
- "target": "string",
- "kind": "string",
- "config": "string",
- "path": "string",
- "draftId": "string",
- "snapshotId": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "etag": "string",
- "topologyDiffers": true,
- "experiment": "string",
- "document": {
- "revision": 1,
- "metadata": {
- "id": "49d876d1-571a-5b5c-91b7-aadf5bb5209c",
- "name": "Pump station",
- "createdBy": "alice",
- "createdAt": "2026-10-01T15:04:05Z",
- "updatedBy": "bob",
- "updatedAt": "2026-10-02T09:30:00Z",
- "notes": [
- "Snapshot the PLC images before each run."
]
}, - "nodes": [
- {
- "id": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "kind": "device",
- "label": "router",
- "position": {
- "x": 0,
- "y": 0
}, - "device": {
- "hostname": "router",
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}, - "hardware": {
- "os_type": "vyos",
- "drives": [
- {
- "image": "vyos.qc2"
}
]
}, - "network": {
- "interfaces": [
- {
- "name": "eth0",
- "type": "ethernet",
- "proto": "static",
- "address": "10.0.0.1",
- "mask": 24,
- "vlan": "EXP"
}
]
}
}, - "interfaces": [
- {
- "id": "7f010dcd-e23d-514c-9206-3609df999026",
- "name": "eth0",
- "index": 0
}
]
}
}, - {
- "id": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "kind": "switch",
- "label": "EXP",
- "position": {
- "x": 160,
- "y": 256
}, - "switch": {
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
}
], - "networks": [
- {
- "id": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "name": "EXP",
- "alias": 101,
- "description": "Experiment network",
- "subnet": "10.0.0.0/24",
- "gateway": "10.0.0.1"
}
], - "edges": [
- {
- "id": "503c4f01-e7e0-5a2e-a07d-26e2776fea8f",
- "sourceNodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "sourceHandleId": "7f010dcd-e23d-514c-9206-3609df999026",
- "targetNodeId": "dee6bc70-8103-581e-8b31-6c95cda798a0",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44"
}
], - "viewport": {
- "x": 0,
- "y": 0,
- "zoom": 1
}, - "grid": {
- "enabled": true,
- "size": 16
}, - "scenarios": [
- "pump-station-ntp",
- "pump-station-attack"
], - "source": {
- "kind": "topology",
- "name": "pump-station",
- "apiVersion": "phenix.sandia.gov/v1",
- "importedAt": "2026-10-01T15:04:05Z",
- "digest": "sha256:1f7f8940e18192505e3ec1f993db7204b6baa8d6c1d9156917a82c663656a1b4",
- "updatedAt": "2026-09-30T08:00:00Z"
}, - "layout": "elk",
- "iconSize": "large",
- "layers": [
- {
- "id": 3,
- "label": "Operations"
}, - {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
], - "layerSet": {
- "name": "Purdue Model",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "source": {
- "id": "builtin:purdue",
- "version": 1
}
}, - "templates": [
- {
- "id": "0d5f8f4e-6a57-4b53-9d0a-5c0f4e3b2a11",
- "name": "Edge router",
- "description": "VyOS router with one static interface",
- "device": {
- "iconKey": "router",
- "spec": {
- "type": "Router",
- "general": {
- "hostname": "router"
}
}
}, - "source": {
- "kind": "library",
- "id": "6a1f0c2e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
- "version": 3,
- "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
], - "icons": {
- "plc": {
- "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
}
}
}
}Requires builder use. Returns every icon of the icon library of the server, sorted by name, ignoring case, without its image. Each icon has the ID of its image, its uploader, its aliases, its revision and its entity tag. The aliases are the earlier names from its renames, which still name it. Each icon also tells whether a caller may rename or delete it. A caller may not rename or delete an icon of the server template files. GET /builder/icons/{icon} gives the image of one icon. The ID is the hash of the image, so a client needs the image of an ID once. The response also tells the most icons that the library holds, and the entity tag of the list. A request whose If-None-Match names that tag (weak comparison) gets 304, with no body, while the list is the same. The server sends an image only as base64 inside JSON, never as an image. Every response of the icon operations also carries Content-Security-Policy: default-src 'none'; frame-ancestors 'none'.
| If-None-Match | string the entity tag of the list as last read |
{- "icons": [
- {
- "name": "string",
- "id": "string",
- "owner": "string",
- "width": 0,
- "height": 0,
- "bytes": 0,
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "aliases": [
- "string"
], - "canRename": true,
- "canDelete": true,
- "revision": 0,
- "etag": "string"
}
], - "maxIcons": 0,
- "etag": "string"
}Requires builder use. Adds a PNG under a name that the caller selects. The name has 1 to 64 letters, digits, _, @, . and -. It is not . or .., and no icon or alias has it, ignoring case. Takes a PNG of at most 96 by 96 pixels and 65536 bytes, as base64. Only the bytes tell what the image is. The server reads no file name, extension or declared type, and refuses all images that are not PNG (SVG, JPEG, GIF, WebP).
The server stores a PNG as it is when all these conditions are true. The PNG holds only the chunks IHDR, PLTE, tRNS, IDAT and IEND. It has a PLTE or tRNS only where its color type uses one. It has nothing after its pixels, and it is at most 40960 bytes. The server encodes all other PNGs again. This keeps the pixels and removes all else (text, metadata, color profiles, more frames, an unused palette, data after the pixels). Thus use the data that the response returns.
A name that already names an icon with the same bytes gets 200 with that icon. A name of another icon, or an alias of another icon, gets 409. The library holds at most 2000 icons, of all users together.
| name required | string^[A-Za-z0-9_@.-]{1,64}$ the icon's name: 1 to 64 letters, digits, |
| data required | string a PNG of at most 96 by 96 pixels and 65536 bytes, in standard base64 with padding and without line breaks |
{- "name": "string",
- "data": "string"
}{- "name": "string",
- "id": "string",
- "owner": "string",
- "width": 0,
- "height": 0,
- "bytes": 0,
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "aliases": [
- "string"
], - "canRename": true,
- "canDelete": true,
- "revision": 0,
- "etag": "string",
- "data": "string"
}Requires builder use. Returns the icon that the path names, with its image. name is the current name of the icon. A request whose If-None-Match names the icon's etag gets 304, with no body.
| icon required | string the name of the icon, or one of its aliases, in any case. A path that names no icon gets 404, whatever its form. |
| If-None-Match | string the entity tag of the icon as last read |
{- "name": "string",
- "id": "string",
- "owner": "string",
- "width": 0,
- "height": 0,
- "bytes": 0,
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "aliases": [
- "string"
], - "canRename": true,
- "canDelete": true,
- "revision": 0,
- "etag": "string",
- "data": "string"
}Requires builder use. An icon of the server template files gets 403. Gives the icon that the path names a new name. No other icon, and no alias of another icon, may have that name, ignoring case. The old name becomes an alias, so diagrams and templates that name it continue to show the icon. A change of case renames the icon in place. A rename to one of its aliases makes that alias its name again. An icon keeps at most 16 old names. A rename that would give it more gets 422, and the icon keeps all its old names.
| icon required | string the name of the icon, or one of its aliases, in any case. A path that names no icon gets 404, whatever its form. |
| name required | string^[A-Za-z0-9_@.-]{1,64}$ the new name of the icon, by the rules for the name of a new icon. The old name becomes an alias |
{- "name": "string"
}{- "name": "string",
- "id": "string",
- "owner": "string",
- "width": 0,
- "height": 0,
- "bytes": 0,
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "aliases": [
- "string"
], - "canRename": true,
- "canDelete": true,
- "revision": 0,
- "etag": "string",
- "data": "string"
}Requires builder use. An icon of the server template files gets 403. Deletes the icon that the path names, by its name or an alias, with all its aliases. Diagrams and templates that name it show their built-in icon. A downloaded document keeps its own copy. The server also removes a record of that name that it cannot read. When the server could not remove an alias, the answer is still 204, with a Warning: 199 header. If-Match must name the etag of the icon. A rename gives the icon a new tag, so a delete with the tag from before the rename gets 412 with the current tag. A record that the server cannot read has the tag of its record revision, which a 412 gives.
| icon required | string the name of the icon, or one of its aliases, in any case. A path that names no icon gets 404, whatever its form. |
| If-Match required | string The |
{- "message": "string",
- "cause": "string",
- "metadata": {
- "property1": "string",
- "property2": "string"
}, - "code": "publish.blocked",
- "issues": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
]
}Requires builder use. Returns the template library of the server, the built-in templates and the collections of the template files of the server. Every caller gets the same response. A template names its custom icon, which the icon library resolves.
templates and collections are the template library of the server (source library), in the order that callers added their items. Every user with Builder access changes the same library: no item has an owner. A server whose library never changed has an empty library. When this server cannot read the library record, the response has damaged true and none of its items. The server then refuses every change of it with 409.
builtins are the five built-in templates (server, workstation, router, firewall, external, source builtin, version 1). preloaded lists the collections that the server read at start from the template files of its template directory (--base-dir.builder-templates). There is one collection per file, with its templates (source preloaded, version 1). Built-in and preloaded items are read only. A write route that names one gets 409 with the code template.item.read-only. A caller can change a copy in the library.
A template that a diagram copied names its source in the diagram (templates[].source of the Builder document: kind, id and version). When the version of the item with that source and id here is higher, the library holds a newer version.
{- "templates": [
- {
- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "device": {
- "iconKey": "string",
- "icon": "string",
- "iconSize": "",
- "outlineColor": "string",
- "fillColor": "string",
- "hideDescription": true,
- "layer": 0,
- "spec": { }
}, - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "collections": [
- "string"
]
}
], - "collections": [
- {
- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "templateIds": [
- "string"
], - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}
], - "builtins": [
- {
- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "device": {
- "iconKey": "string",
- "icon": "string",
- "iconSize": "",
- "outlineColor": "string",
- "fillColor": "string",
- "hideDescription": true,
- "layer": 0,
- "spec": { }
}, - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "collections": [
- "string"
]
}
], - "preloaded": [
- {
- "collection": {
- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "templateIds": [
- "string"
], - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}, - "templates": [
- {
- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "device": {
- "iconKey": "string",
- "icon": "string",
- "iconSize": "",
- "outlineColor": "string",
- "fillColor": "string",
- "hideDescription": true,
- "layer": 0,
- "spec": { }
}, - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "collections": [
- "string"
]
}
]
}
], - "damaged": true,
- "limits": {
- "templates": 0,
- "collections": 0,
- "nameBytes": 0,
- "descriptionBytes": 0,
- "deviceBytes": 0
}
}Requires builder use. Adds 1 to 200 templates in one write, each with a new ID. With collection, the same write also makes a new collection that holds exactly those templates. The custom icon of a template (device.icon) is an icon name. The icon library resolves it, whether or not it holds that icon now. A template of the library has no source.
required | Array of objects [ 1 .. 200 ] items |
object a new collection to hold exactly the new templates |
{- "templates": [
- {
- "name": "string",
- "description": "string",
- "device": {
- "iconKey": "string",
- "icon": "string",
- "iconSize": "",
- "outlineColor": "string",
- "fillColor": "string",
- "hideDescription": true,
- "layer": 0,
- "spec": { }
}
}
], - "collection": {
- "name": "string",
- "description": "string"
}
}{- "created": [
- {
- "id": "string",
- "etag": "string"
}
], - "collection": {
- "id": "string",
- "etag": "string"
}
}Requires builder use. Any user with Builder access may replace any template of the library. Replaces the name, the description and the device of one template. The template keeps its ID, and its version increases by one. Content that is equal to the stored content changes nothing and keeps the entity tag. Devices already made from the template do not change. A diagram's copy of the template changes only when its user updates it.
| template required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ template ID |
| If-Match required | string The |
| name required | string 1 to 128 bytes, not blank, without control characters |
| description | string at most 1024 bytes, without control characters |
required | object (BuilderTemplateDevice) What a template sets: every field of a device node except its hostname, its connection points and where it was included from. The editor names a device made from the template after |
{- "name": "string",
- "description": "string",
- "device": {
- "iconKey": "string",
- "icon": "string",
- "iconSize": "",
- "outlineColor": "string",
- "fillColor": "string",
- "hideDescription": true,
- "layer": 0,
- "spec": { }
}
}{- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "device": {
- "iconKey": "string",
- "icon": "string",
- "iconSize": "",
- "outlineColor": "string",
- "fillColor": "string",
- "hideDescription": true,
- "layer": 0,
- "spec": { }
}, - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string",
- "collections": [
- "string"
]
}Requires builder use. A collection is a named group of templates of the library. A template can be in more than one collection. Every ID in templateIds must name a template of the library, one time. A built-in template or a template of a template file gets 409.
| name required | string 1 to 128 bytes, not blank, without control characters |
| description | string at most 1024 bytes, without control characters |
| templateIds | Array of strings <= 200 items templates of the library, each named once |
{- "name": "string",
- "description": "string",
- "templateIds": [
- "string"
]
}{- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "templateIds": [
- "string"
], - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}Requires builder use. Replaces the name, the description and the templates of one collection. The collection keeps its ID. To add a template to a collection, or to remove one from it, send this request with the changed list. Content that is equal to the stored content changes nothing and keeps the entity tag.
| collection required | string^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ collection ID |
| If-Match required | string The |
| name required | string 1 to 128 bytes, not blank, without control characters |
| description | string at most 1024 bytes, without control characters |
| templateIds | Array of strings <= 200 items templates of the library, each named once |
{- "name": "string",
- "description": "string",
- "templateIds": [
- "string"
]
}{- "id": "string",
- "source": "library",
- "name": "string",
- "description": "string",
- "templateIds": [
- "string"
], - "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}Requires builder use. Deletes the named templates and collections in one write. Each item names the etag of the content that the caller read, as a PUT names it in If-Match. An item without a strong, quoted tag gets 400. When the library holds any item at another tag, the whole request gets 412 with template.item.stale and the current tag of that item. The server then deletes nothing. The server ignores an ID that the library does not hold, so a repeat of the request is harmless. A deleted template leaves every collection that held it. A deleted collection leaves its templates. A built-in template or an item of a template file gets 409. Diagrams do not change: a diagram's copy of a deleted template stays, and its editor says that the library template was deleted.
Array of objects (BuilderTemplateRef) | |
Array of objects (BuilderTemplateRef) |
{- "templates": [
- {
- "id": "string",
- "etag": "string"
}
], - "collections": [
- {
- "id": "string",
- "etag": "string"
}
]
}{- "deleted": {
- "templates": 0,
- "collections": 0
}
}Requires builder use. The layer set library belongs to the server, and every user of the Builder sees every set. The built-in sets come first (the Purdue Model set, ID builtin:purdue). The other sets follow by name, ignoring case. defaultId is the default set of the server for new diagrams, or null. defaultEtag is the entity tag that a change of that choice names in If-Match. damaged counts the records that the server cannot read, which the list does not show.
{- "layerSets": [
- {
- "id": "builtin:purdue",
- "name": "string",
- "description": "string",
- "iconKey": "string",
- "icon": "string",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "layers": [
- {
- "id": 0,
- "label": "string",
- "iconKey": "string",
- "icon": "string",
- "color": "string",
- "matchers": {
- "property1": "string",
- "property2": "string"
}
}
], - "builtin": true,
- "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}
], - "defaultId": "string",
- "defaultEtag": "\"0\"",
- "damaged": 0,
- "limits": {
- "layerSets": 200,
- "layers": 32,
- "nameBytes": 128,
- "descriptionBytes": 1024,
- "labelBytes": 64,
- "matchers": 16,
- "matcherPathBytes": 128,
- "patternBytes": 256
}
}Requires builder use. Adds a set under a new ID, at version 1. Every user of the Builder sees every set. The library holds at most 200 sets, apart from the built-in sets. To duplicate a set, read it and send its content here.
| name required | string [ 1 .. 128 ] characters not blank, at most 128 bytes, with no control characters |
| description | string <= 1024 characters at most 1024 bytes, with no control characters |
| iconKey | string the built-in icon of the set. Empty for none |
| icon | string a custom icon name, shown in place of |
| matchConflict | string Enum: "" "lowest" "highest" the layer that a device gets when the node matchers of more than one layer match it: the lowest ID or the highest ID. Empty is |
| layerOutlines | boolean when true, the editor draws the outline of a node in the color of its layer, unless the node has its own outline color |
required | Array of objects (BuilderLayer) [ 1 .. 32 ] items 1 to 32 layers, each with an ID that no other layer has |
{- "name": "Plant levels",
- "description": "Levels of the water plant network.",
- "iconKey": "plc",
- "icon": "string",
- "matchConflict": "lowest",
- "layerOutlines": false,
- "layers": [
- {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
]
}{- "id": "builtin:purdue",
- "name": "string",
- "description": "string",
- "iconKey": "string",
- "icon": "string",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "layers": [
- {
- "id": 0,
- "label": "string",
- "iconKey": "string",
- "icon": "string",
- "color": "string",
- "matchers": {
- "property1": "string",
- "property2": "string"
}
}
], - "builtin": true,
- "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}Requires builder use. Returns the set that new diagrams of all users start with, or null. The server has one default set for all users. A selection of a set that no longer exists is null. A new blank diagram copies the default set and links to it. With no default, a new diagram shows the placeholder layers 0 to 6. etag is the entity tag of the selection, which a change of it names in If-Match.
{- "id": "builtin:purdue",
- "etag": "\"0\""
}Requires builder use. Selects the set that the new diagrams of all users copy. Each Builder user can change it. id null or empty removes the selection. If-Match must name the etag of the current selection. Another tag gets 412 (layerset.stale) with the current tag. A set that does not exist gets 422 with the code layerset.default.unknown.
| If-Match required | string The |
| id | string or null the ID of a layer set, or null for none |
{- "id": "builtin:purdue"
}{- "id": "builtin:purdue",
- "etag": "\"0\""
}Requires builder use. Returns the set with its entity tag.
| layerset required | string the ID of the layer set. A path that names no set gets 404, whatever its form |
{- "id": "builtin:purdue",
- "name": "string",
- "description": "string",
- "iconKey": "string",
- "icon": "string",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "layers": [
- {
- "id": 0,
- "label": "string",
- "iconKey": "string",
- "icon": "string",
- "color": "string",
- "matchers": {
- "property1": "string",
- "property2": "string"
}
}
], - "builtin": true,
- "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}Requires builder use. Any user with this permission may change any set, but nobody may change a built-in set (403, layerset.read-only). Replaces the content of the set. A change increases the version by one. Content that is equal to the stored content changes nothing and keeps the entity tag. Diagrams that copied the set keep their copies, and the editor offers them the update.
| layerset required | string the ID of the layer set. A path that names no set gets 404, whatever its form |
| If-Match required | string The |
| name required | string [ 1 .. 128 ] characters not blank, at most 128 bytes, with no control characters |
| description | string <= 1024 characters at most 1024 bytes, with no control characters |
| iconKey | string the built-in icon of the set. Empty for none |
| icon | string a custom icon name, shown in place of |
| matchConflict | string Enum: "" "lowest" "highest" the layer that a device gets when the node matchers of more than one layer match it: the lowest ID or the highest ID. Empty is |
| layerOutlines | boolean when true, the editor draws the outline of a node in the color of its layer, unless the node has its own outline color |
required | Array of objects (BuilderLayer) [ 1 .. 32 ] items 1 to 32 layers, each with an ID that no other layer has |
{- "name": "Plant levels",
- "description": "Levels of the water plant network.",
- "iconKey": "plc",
- "icon": "string",
- "matchConflict": "lowest",
- "layerOutlines": false,
- "layers": [
- {
- "id": 2,
- "label": "Supervisory control",
- "iconKey": "desktop",
- "color": "#15803d",
- "matchers": {
- "general.hostname": "^(hmi|scada)-"
}
}, - {
- "id": 1,
- "label": "Field devices"
}
]
}{- "id": "builtin:purdue",
- "name": "string",
- "description": "string",
- "iconKey": "string",
- "icon": "string",
- "matchConflict": "lowest",
- "layerOutlines": true,
- "layers": [
- {
- "id": 0,
- "label": "string",
- "iconKey": "string",
- "icon": "string",
- "color": "string",
- "matchers": {
- "property1": "string",
- "property2": "string"
}
}
], - "builtin": true,
- "version": 0,
- "etag": "string",
- "created": "2019-08-24T14:15:22Z",
- "updated": "2019-08-24T14:15:22Z",
- "createdBy": "string",
- "updatedBy": "string"
}Requires builder use. Any user with this permission may delete any set, but nobody may delete a built-in set (403, layerset.read-only). Diagrams that copied the set keep their copies. A default selection of the set becomes null. If-Match must name the etag of the current content. A record that the server cannot read has the tag of its record revision, "<revision>", which a 412 gives.
| layerset required | string the ID of the layer set. A path that names no set gets 404, whatever its form |
| If-Match required | string The |
{- "message": "string",
- "cause": "string",
- "metadata": {
- "property1": "string",
- "property2": "string"
}, - "code": "publish.blocked",
- "issues": [
- {
- "code": "node.hostname.duplicate",
- "severity": "error",
- "message": "duplicate hostname \"web\" (also nodes[1])",
- "path": "nodes[3].device.hostname",
- "nodeId": "4113dfc0-f4be-57f9-8115-6cecd1f1aea3",
- "edgeId": "7f010dcd-e23d-514c-9206-3609df999026",
- "networkId": "a8af611d-c68f-54e0-94a5-9176cea64e44",
- "field": "spec.network.interfaces.0.vlan"
}
]
}