This skill is the source of truth for the JSON shape accepted by the Unity Pipeline Automation v1 API (and exposed by the Pipeline Automation MCP server in this repo). Most fields are not discoverable from the tool schema alone — picker metadata, action IDs, and several "looks fine but returns HTTP 400" traps live here.
For the REST API behind the MCP tools — endpoint routes, job/step response schemas, status enums, list filters, log formats, and error semantics — see automation-api.md next to this file.
Top-level structure
{
"name": "My Pipeline",
"description": "20–256 character description of what the pipeline does.",
"version": "1.0.0",
"tags": ["tag-a", "tag-b"],
"inputParameters": [ /* see Input Parameters */ ],
"outputParameters": [ /* see Output Parameters */ ],
"secrets": [ /* see Secrets */ ],
"storages": [ /* see Storages */ ],
"steps": [ /* see Steps */ ],
"metadata": { "key": "value" }
}
Constraints
name: 2–57 chars, alphanumeric / hyphens / spacesdescription: 20–256 charsversion: semver (MAJOR.MINOR.PATCH)
Input Parameters
Each entry defines a value the caller must (or may) supply when triggering the pipeline.
| Field | Notes |
|---|---|
name |
Machine name; used in {{inputParameters.name}} references |
displayName |
Human-readable label shown in the UI |
type |
String (only observed type) |
required |
true / false |
isList |
true for multi-value params (e.g. a list of files) |
metadata.picker |
Optional UI picker hint (see Pickers below) |
Standard Asset Manager fields (hardcoded — copy verbatim)
The projectId, assetId, and assetVersionId fields have fixed canonical definitions. Always use these exact objects — do not change names, descriptions, types, or picker metadata.
projectId
{
"name": "projectId",
"displayName": "Project",
"type": "String",
"description": "The project containing the asset to process.",
"required": true,
"isList": false,
"metadata": {
"picker": "assetManagerProjectPicker"
}
}
assetId
{
"name": "assetId",
"displayName": "Asset",
"type": "String",
"description": "The asset containing the files to process.",
"required": true,
"isList": false,
"metadata": {
"picker": "assetManagerAssetPicker",
"projectField": "projectId"
}
}
assetVersionId
{
"name": "assetVersionId",
"displayName": "Asset Version",
"type": "String",
"description": "The asset version containing the files to process.",
"required": true,
"isList": false,
"metadata": {
"picker": "assetManagerVersionPicker",
"projectField": "projectId",
"assetField": "assetId"
}
}
Additional Asset Manager pickers
For optional dataset and file selection, add these after the three standard fields:
[
{
"name": "datasetId",
"displayName": "Dataset",
"type": "String",
"description": "The dataset containing the files to process.",
"required": false,
"isList": false,
"metadata": {
"picker": "assetManagerDatasetPicker",
"projectField": "projectId",
"assetField": "assetId",
"versionField": "assetVersionId"
}
},
{
"name": "inputFiles",
"displayName": "Files",
"type": "String",
"description": "The files to process. If no file is selected, all the files are included.",
"required": false,
"isList": true,
"metadata": {
"picker": "assetManagerFilePicker",
"projectField": "projectId",
"assetField": "assetId",
"versionField": "assetVersionId",
"datasetField": "datasetId"
}
}
]
Picker reference
| Picker | Purpose | Required metadata fields |
|---|---|---|
assetManagerProjectPicker |
Project selector | — |
assetManagerAssetPicker |
Asset selector | "projectField": "projectId" |
assetManagerVersionPicker |
Version selector | "projectField", "assetField" |
assetManagerDatasetPicker |
Dataset selector | "projectField", "assetField", "versionField" |
assetManagerFilePicker |
File selector | "projectField", "assetField", "versionField", "datasetField" |
Steps
Steps are the execution units of a pipeline. All steps share these fields:
{
"id": "my-step",
"name": "My Step",
"continueOnFail": false,
"timeout": 300,
"dependencies": ["previous-step-id"]
}
| Field | Notes |
|---|---|
id |
Unique within the pipeline; used in dependency references and template expressions |
name |
Display name shown in the UI |
continueOnFail |
If true, the pipeline continues even if this step fails |
timeout |
Seconds before the step is force-killed (default 300) |
dependencies |
List of step ids that must complete before this step runs; empty array = runs immediately |
Step type: action
Runs a named action (e.g. an Asset Manager operation).
{
"id": "get-asset",
"name": "Get asset",
"action": {
"actionId": "ad7cbc97-36d5-46ac-99fc-89ea88025b26-1.1.2-Get-asset",
"inputParameters": {
"projectId": "{{inputParameters.projectId}}",
"assetId": "{{inputParameters.assetId}}",
"assetVersionId": "{{inputParameters.assetVersionId}}",
"datasetDescriptor": "{{inputParameters.datasetId}}",
"Files": "{{inputParameters.inputFiles}}",
"downloadDirectory": "/workspace/input",
"downloadDependencies": true
},
"configurationId": "21a311c6-da72-488a-9688-374824e1c751"
},
"continueOnFail": false,
"timeout": 300,
"dependencies": ["init"]
}
| Field | Notes |
|---|---|
actionId |
Format: <uuid>-<version>-<Action-Name> |
inputParameters |
Key/value map; values can be literals or template expressions |
configurationId |
Optional; ties the action to a specific infrastructure configuration |
Step type: suspend
Pauses the pipeline until a human approves or rejects via approve_job_step.
{
"id": "await-approval",
"name": "Await Approval",
"suspend": {},
"continueOnFail": false,
"timeout": 86400,
"dependencies": ["get-asset"]
}
Step type: pipeline (sub-pipeline)
Runs another pipeline as a nested step.
{
"id": "run-sub-pipeline",
"name": "Run Sub-Pipeline",
"pipeline": {
"id": "<uuid>",
"version": "1.0.0",
"inputParameters": {
"projectId": "{{inputParameters.projectId}}"
}
},
"continueOnFail": false,
"dependencies": ["get-asset"]
}
Critical: The fields inside
pipelineareidandversion— NOTpipelineId/pipelineVersion. Using the wrong names causes HTTP 400. Critical: Pipeline reference steps cannot have atimeoutfield. Remove it entirely or the API returns HTTP 400.
Fan-out for sub-pipeline steps
Use the top-level fanOut field to run a sub-pipeline once per item in a JSON array.
{
"id": "process-each-item",
"name": "Process Each Item",
"pipeline": {
"id": "<sub-pipeline-uuid>",
"version": "1.0.0",
"inputParameters": {
"projectId": "{{inputParameters.projectId}}",
"itemName": "_OPENING_BRACKETS_item.name_CLOSING_BRACKETS_",
"itemDir": "_OPENING_BRACKETS_item.glb_dir_CLOSING_BRACKETS_"
}
},
"fanOut": {
"loopThroughList": "{{steps.previous-step.outputParameters.customOutput}}",
"maximumParallelCount": 5
},
"continueOnFail": false,
"dependencies": ["previous-step"]
}
| Field | Notes |
|---|---|
fanOut.loopThroughList |
Template expression resolving to a JSON array |
fanOut.maximumParallelCount |
Concurrent iterations — must be 1–5 (HTTP 400 if outside this range) |
- Per-item values in
pipeline.inputParametersuse_OPENING_BRACKETS_item.field_CLOSING_BRACKETS_syntax (same as action fan-out) fanOutis a top-level step field, not nested insidepipeline- Sub-pipeline fan-out steps still cannot have
timeout
Template expressions
Use {{ }} to reference values dynamically at runtime.
| Expression | Resolves to |
|---|---|
{{inputParameters.paramName}} |
A pipeline-level input parameter |
{{steps.step-id.outputParameters.outputName}} |
An output from a completed step |
{{steps.step-id.outputParameters.outputFile1Field2}} |
Specific field within a structured step output |
Fan-out with withParam
Pass withParam to run a step once per item in a JSON array, running iterations in parallel.
{
"id": "process-each-file",
"name": "Process Each File",
"action": {
"actionId": "<action-id>",
"inputParameters": {
"input1": "_OPENING_BRACKETS_item.sourceName_CLOSING_BRACKETS_",
"withParam": "{{steps.find-files.outputParameters.outputFile1}}"
}
},
"continueOnFail": false,
"timeout": 300,
"dependencies": ["find-files"]
}
withParamvalue must be a JSON array (or a template resolving to one)- Per-item values use the
_OPENING_BRACKETS_item.field_CLOSING_BRACKETS_syntax (literal{{ item.field }}is escaped this way by the API) - Use
"[1]"aswithParamto run a single-iteration fan-out (no parallelism, just satisfies the required field)
Action IDs — discover them, don't hardcode
actionIds (<app-uuid>-<app-version>-<Action-Name-slug>) are
deployment-specific: orgs can see duplicate registrations of the same app
(one of which may only have Draft versions — pipelines referencing those can
never become Available, error 53), name slugs differ between public cloud and
private-cloud deployments, and the embedded app version must be Available in
the target org. Discover the ID in the org you're authoring for:
list_apps— find the app by name (keep all ids if the name appears twice)list_app_versions— keep versions withlifecycle.lifecycleStatus=Available, prefer the newest- Read that version's
actionsarray (orget_app_action) — it carries the exactactionIdplus the input/output parameter schema - Use the discovered
actionIdverbatim
known-action-ids.mdnext to this file has the full guidance, the IDs the example pipelines validate against (observed on public cloud — re-discover before use), and per-action parameter tables.
Get asset — input parameters
| Key | Required | Description |
|---|---|---|
projectId |
Yes | Project the asset belongs to |
assetId |
Yes | Asset to download |
assetVersionId |
No | Specific version; latest if omitted |
datasetDescriptor |
No | Dataset within the asset version |
Files |
No | Specific files to download; all if omitted |
downloadDirectory |
No | Local path (default /workspace/input) |
downloadDependencies |
No | true / false |
Execute custom script — input parameters
| Key | Required | Description |
|---|---|---|
script |
Yes | Python source code. Written as a static file artifact — template expressions are NOT resolved inside the script body. |
parameters |
No | Command-line argument string appended after the script path. Template expressions ARE resolved here. Access in-script via sys.argv[1]. |
Passing dynamic pipeline inputs to a script: The
scriptfield is written to disk before execution, so{{inputParameters.x}}inside the script body is literal text — it will not be substituted. To inject a runtime value, put the template expression inparametersand read it fromsys.argvin the script."inputParameters": { "script": "import sys\nlevel = sys.argv[1]", "parameters": "{{inputParameters.myParam}}" }
Create asset — input parameters
| Key | Required | Description |
|---|---|---|
projectId |
Yes | Target project |
assetName |
Yes | Name for the new asset |
assetType |
No | e.g. "3D Model" |
assetTags |
No | Must be an array (e.g. ["tag1"]). Passing a string causes HTTP 400 (code 55). |
collectionPath |
No | Collection to place the asset in |
Create asset — output parameters
| Key | Description |
|---|---|
assetId |
ID of the created asset |
assetVersionId |
Version ID of the created asset |
datasetId |
Primary dataset ID |
projectId |
Project the asset was created in |
assetName |
Name of the created asset |
Add file — input parameters
| Key | Required | Description |
|---|---|---|
projectId |
Yes | Project the asset belongs to |
assetId |
Yes | Asset to upload files into |
assetVersionId |
No | Specific version; uses current draft if omitted |
datasetDescriptor |
No | Dataset to upload into |
uploadDirectory |
No | Local directory whose contents are uploaded (default: all files in workspace) |
freezeAsset |
No | true to freeze the asset version after upload |
Execute custom script — configurationId
Use 21a311c6-da72-488a-9688-374824e1c751 (standard Asset Transformer infrastructure).
Lifecycle & visibility
Lifecycle status transitions
Draft → Available → Deprecated → Archived
- These are the API enum values (
lifecycleStatus); theAvailablestate is what the UI presents as an active/published version. - Only
Draftversions can be updated or deleted. - Use
change_pipeline_status/change_project_pipeline_statusto advance.
Visibility levels
| Value | Who can see |
|---|---|
Private |
Organisation members only |
Unlisted |
Anyone with the direct link |
Public |
Listed in the public pipeline catalogue |
Minimal working example
{
"name": "My Asset Pipeline",
"description": "Downloads an asset from Asset Manager and processes it.",
"version": "1.0.0",
"inputParameters": [
{
"name": "projectId",
"displayName": "Project",
"type": "String",
"description": "The project containing the asset to process.",
"required": true,
"isList": false,
"metadata": { "picker": "assetManagerProjectPicker" }
},
{
"name": "assetId",
"displayName": "Asset",
"type": "String",
"description": "The asset containing the files to process.",
"required": true,
"isList": false,
"metadata": {
"picker": "assetManagerAssetPicker",
"projectField": "projectId"
}
},
{
"name": "assetVersionId",
"displayName": "Asset Version",
"type": "String",
"description": "The asset version containing the files to process.",
"required": true,
"isList": false,
"metadata": {
"picker": "assetManagerVersionPicker",
"projectField": "projectId",
"assetField": "assetId"
}
}
],
"steps": [
{
"id": "get-asset",
"name": "Get asset",
"action": {
"actionId": "ad7cbc97-36d5-46ac-99fc-89ea88025b26-1.1.2-Get-asset",
"inputParameters": {
"projectId": "{{inputParameters.projectId}}",
"assetId": "{{inputParameters.assetId}}",
"assetVersionId": "{{inputParameters.assetVersionId}}",
"downloadDirectory": "/workspace/input",
"downloadDependencies": true
},
"configurationId": "21a311c6-da72-488a-9688-374824e1c751"
},
"continueOnFail": false,
"timeout": 300,
"dependencies": []
}
]
}
Examples
Vetted, end-to-end pipeline JSONs live in examples/ (valid JSON, correct
constraints, only documented action IDs):
download-and-inspect.json— minimalGet asset→Execute custom script(read-only scene stats to the job log).optimize-and-republish.json— full roundtrip:Get asset→Execute custom script(tessellate + decimate to GLB) →Create asset→Add file, showing how to pass a runtime value viaparametersand how to wirecreate-assetoutputs into a downstreamadd-filestep.
Prefer adapting an example over authoring a pipeline from scratch.
The script field of an "Execute custom script" step
This skill covers the pipeline JSON around the step, not the Python inside it.
That script body is Asset Transformer (pxz) code. It is authored with the
at-upa-scripting skill, which ships with the Unity Asset Transformer
plugin (/plugin install uat-mcp@industry-ai-workflows) and
carries the authoritative pxz API reference. The two plugins install
separately, so that reference is not reachable from here by path — ask for the
skill by name.
If the Asset Transformer plugin is not installed: author the step's JSON here,
leave script as a placeholder, and tell the user that writing the body needs
that plugin. Do not invent pxz calls from memory. The SDK surface is
version-specific, and a wrong call fails at pipeline runtime — minutes later,
in a container log, after the earlier steps have already run.