Blueprint Syntax and Best Practices
Last updated: March 4, 2026
Table of Contents
Why a Dedicated Syntax Article
The Nutanix Blueprint 101 article covers concepts and a working example. But when I actually started writing blueprints as code with calm-dsl, I spent a lot of time digging through the DSL source code to understand exactly what syntax was valid where.
This article is a structured syntax reference with explanations of the choices I make, written the way I wish the documentation had been laid out.
Blueprint JSON/YAML Structure
When you export a blueprint from Prism Central (Blueprint Designer β Download), you get a JSON file. This is the raw data model that everything else β the UI and calm-dsl β generates from.
Understanding the top-level structure helps when reading API responses or debugging DSL-generated blueprints.
Top-Level Keys
The spec.resources object is the blueprint body. Everything else is metadata wrapping.
app_profile_list Items
app_profile_list ItemsEach profile contains:
nameβ profile identifieruuidvariable_listβ profile-level variable overridesdeployment_listβ links services to substrates with replica countsaction_listβ profile-level actions
service_definition_list Items
service_definition_list Itemsdepends_on_list is how service dependency ordering is declared β it is the JSON equivalent of the arrow you draw in the Blueprint Designer canvas.
substrate_definition_list Items (AHV)
substrate_definition_list Items (AHV)The user_data in guest_customization.cloud_init is base64-encoded cloud-init YAML. This is important when debugging: decode it to see the actual script.
The calm-dsl Python DSL Syntax Reference
calm-dsl is the official Python library that generates the JSON blueprint spec. Install it:
Blueprint Skeleton
Every calm-dsl blueprint file follows this structure:
The ordering matters for readability but not execution β Python class references resolve at parse time.
Credentials
Key rule: Only one credential can be default=True per blueprint. This default is used for service readiness probes unless overridden per substrate.
Variables
Blueprint variables are defined as class attributes on Service, Profile, or Blueprint classes.
Variable Scoping Rules
Blueprint-level
Blueprint class
All profiles, all services
Profile-level
Profile class
Services within that profile
Service-level
Service class
Tasks within that service only
Action-level
Inline in CalmTask
That task only
Services
A Service class defines behavior β tasks that run at lifecycle events. It does not define the VM spec (that is the Substrate).
Built-in Action Names
These names are reserved and trigger automatically at lifecycle events:
__create__
After VM provisioning (via substrate)
__install__
Synonym for __create__ in package context
__delete__
Before VM deprovisioning
__start__
When application is started
__stop__
When application is stopped
__restart__
When restart is invoked
Custom action names (anything else) appear as manually-triggered Day 2 actions.
Substrates and AHV Spec YAML
Rather than embedding the full VM spec in Python code, I keep it in a YAML file per substrate and reference it with read_ahv_spec().
The specs/app-vm.yaml file:
Important: The user_data in the YAML spec can contain macros (@@{...}@@). This is evaluated at launch time, not at blueprint definition time.
Packages and Actions
A Package connects a Service to its install/uninstall actions and to the VM image reference. In most single-image scenarios, the Package class is minimal:
Profiles and Deployments
A Deployment links a Package (application) to a Substrate (VM definition) and sets replica count:
A Profile groups deployments and defines profile-level variables:
Multiple profiles in the same blueprint share Services but can reference different Substrates, enabling different VM sizing per environment without duplicating logic.
Task Types Reference
SSH exec
CalmTask.Exec.ssh(script=..., target=...)
Run shell script on a Linux VM
PowerShell exec
CalmTask.Exec.powershell(script=..., target=...)
Run script on Windows VM
HTTP call
CalmTask.HTTP.post(url=..., headers=..., body=...)
Call an API endpoint
Set variable
CalmTask.SetVariable.ssh(script=..., variables=[...])
Capture script output into a variable
Delay
CalmTask.Delay(delay_seconds=30)
Wait between tasks
Execute runbook
CalmTask.RunbookTask(runbook=ref(...))
Trigger a separate Runbook
While loop
CalmTask.WhileLoopTask(...)
Retry logic with condition
Set Variable Task β Capturing Script Output
This pattern is how I pass dynamic values between tasks and between services at runtime:
HTTP Task β Calling External APIs
Macro Substitution Syntax
Nutanix blueprints use @@{variable_name}@@ for macro substitution. This is replaced at runtime before a task script or cloud-init config is executed.
Built-in Macros
@@{calm_application_name}@@
The application name chosen at launch
@@{calm_array_index}@@
0-based index of the current VM replica (for replicated services)
@@{calm_unique_hash}@@
A short unique hash β useful in hostnames to avoid collisions
@@{calm_jwt}@@
A short-lived JWT for calling back to the NCM Self-Service API from within a task
@@{address}@@
The primary IP address allocated to the VM
@@{name}@@
The VM name (as set in substrate spec)
Cross-Service Macro Access
To access a variable that belongs to a different service, prefix with the service name:
This only works with service-level variables and the built-in address and name macros, not with profile-level variables (those are global and need no prefix).
Macro in cloud-init
Macros work in the user_data field of the substrate spec:
This means the entire application config can be injected at VM boot β no need for a separate configure task just to write a config file.
Blueprint Best Practices
These are practices I settled on after iterating through several blueprint versions in my home-lab.
1. Use Name References, Not UUID References, in Substrate Specs
Subnet and image UUIDs are cluster-specific. A blueprint that hard-codes UUIDs breaks when imported into a different cluster.
The same applies to data_source_reference for images. Use name unless you have a specific reason to pin to a UUID.
2. Always Set a Readiness Probe with Retries
Without a readiness probe, the blueprint engine fires install tasks before SSH is available on the VM. This produces misleading "connection refused" errors that look like task failures.
For Windows VMs, use "connection_type": "WinRM" and port 5985 or 5986.
3. Separate VM Spec from Blueprint Logic
Keep AHV VM specs in specs/*.yaml files and reference them with read_ahv_spec().
Benefits:
The Python DSL file focuses on behavior (tasks, variables, actions)
VM sizing is easy to diff in version control
You can have
specs/app-vm-dev.yamlandspecs/app-vm-prod.yamlwith different CPU/memory, referenced from different Profiles
4. Use Secret Variables for All Sensitive Values
Any value that should not appear in plain text in the Prism Central UI or audit logs must be declared as a Secret type variable:
Never pass a secret value through a non-secret variable even "temporarily."
5. Write Idempotent Task Scripts
Each task script should be safe to run more than once. This matters because:
Blueprint retries will re-run failed tasks
You may trigger the same Day 2 action multiple times
For package installations, apt-get install -y is already idempotent. For services, use systemctl enable --now rather than start on its own.
6. Include a __delete__ Action that Cleans Up
__delete__ Action that Cleans UpThe __delete__ action on a Service runs before the VM is powered off and deleted. Use it to:
Deregister the VM from a load balancer
Remove DNS records
Revoke any service tokens
Skipping this means stale entries accumulate in supporting systems.
7. Pin the calm-dsl Version in Your Project
calm-dsl releases can introduce breaking syntax changes. Pin the version in requirements.txt:
And document in your README.md which Prism Central version and calm-dsl version the blueprint was tested against. A blueprint generated with calm-dsl 3.9 may not import cleanly into a PC running a much older version.
8. Version Blueprints with Git Tags
Each time you make a structural change to the blueprint (not just a script tweak), bump the version and tag it in Git:
In Prism Central, use the same version string when publishing to the Marketplace so it is clear which Git state corresponds to the published version.
9. Test Blueprint Actions in Isolation Before Publishing
Before publishing to the Marketplace, test each action:
Launch the blueprint manually from the Blueprint Designer ("Launch" button, not Marketplace)
With the application running, trigger each Day 2 action individually from the Application view (Actions tab)
Check the task execution log for errors β not just "success" status
The Marketplace launch does not show task-level logs easily, so debugging published blueprints is harder than debugging from the designer.
10. Avoid Hardcoding IP Addresses in Tasks
Hardcoding IP addresses in task scripts is the fastest way to create a blueprint that only works in your cluster. Use macros or dynamic variables:
Define monitoring_server_ip as a profile-level variable with a sensible default. Different environments can override it at launch without changing the blueprint.
Version Control Workflow with calm-dsl
My workflow for managing blueprints in Git:
Makefile targets:
Blueprint Linting and Validation
calm-dsl does not have a dedicated lint command, but I use two checks before pushing:
1. Python Parse Check
If the DSL class hierarchy has errors (missing references, wrong types), this raises a Python exception immediately without needing a PC connection.
2. Dry-Run JSON Dump
calm compile generates the JSON that would be sent to Prism Central. Pipe it through json.tool to catch any malformed output. Redirect to /dev/null since the goal is just the exit code.
3. GitLab CI Validation Job
In the project's .gitlab-ci.yml, I run both checks automatically:
This catches syntax errors on every merge request before anything reaches Prism Central.
Next Steps
Now that the blueprint syntax and best practices are clear, see how blueprints integrate into automated pipelines: GitLab CI/CD Integration with Nutanix Blueprint.
Go back to the Nutanix 101 series index.
Last updated