YAML Format Specification
The YAML format mimics the Odoo database structure and data, providing a convenient way to export and import data to/from Odoo and store it in version control systems.
Please refer to the YAML Export/Import page for the information on how to export and import YAML files.
Each YAML file contains the following sections:
- Header —
cetmix_tower_yaml_version(optional) - Manifest — optional metadata for Hangar publishing
- Records — required; list of Tower records to import
Below is an example of a YAML file which contains a Command record:
# This file is generated with Cetmix Tower.
# Details and documentation: https://cetmix.com/tower
# This is a command example
cetmix_tower_yaml_version: 1
records:
- cetmix_tower_model: command
access_level: manager
reference: add_user_to_docker_group
name: Add user to "docker" group
action: ssh_command
allow_parallel_run: false
note: Adds current user to "docker" group to allow running Docker commands without
'sudo'
tag_ids:
- reference: docker
name: Docker
color: 4
- reference: system
name: System
color: 1
path: false
code: |-
{% if tower.server.os == "Debian 11" %}
/usr/sbin/usermod -a -G docker {{ tower.server.username }}
{% else %}
usermod -a -G docker {{ tower.server.username }}
{% endif %}
server_status: false
Header
cetmix_tower_yaml_version
Type: Number
This is an optional field, used to identify the version of the YAML format. If the YAML file version is higher than the version supported by Cetmix Tower, an exception will be raised during import.
YAML file version
Current supported version is 1
manifest
YAML field type: Mapping (optional)
Optional metadata block used to publish YAML snippets in the Hangar. The manifest section is not required for import/export; it is mainly used when sharing snippets publicly.
Manifest is NOT a record
The manifest is a top-level mapping (sibling of records), not an item in the records list.
Do not use cetmix_tower_model: manifest inside records.
The manifest is read only from the top-level manifest: key. Putting it inside records will cause the import to fail.
Manifest for better UX
A complete manifest improves the experience for anyone importing your snippet. Use the description field (multi-line) to include full usage instructions.
Supported keys:
| Key | Type | Description |
|---|---|---|
name |
String | Snippet name |
summary |
String | Short summary |
description |
String | Full description. Use multi-line for complete usage instructions (see below). |
author |
String or List | Author(s) name(s) |
version |
String | Version (e.g. 1.0.0) |
website |
String | Website URL |
license |
String | License identifier. See Available licenses below. |
license_text |
String | Custom license text when license is custom |
price |
Number | Price (for paid snippets) |
currency |
String | Currency code (e.g. EUR, USD) |
Manifest description — complete usage instructions
When writing the description field, include a complete usage guide so users know how to configure, use, and troubleshoot the snippet. A well-structured description should cover:
- What this snippet does — Clear summary of the purpose (e.g. deploys WordPress with Traefik and Let's Encrypt).
- Technology stack — List the software and versions (e.g. Ubuntu 22.04, Docker, Traefik, MariaDB 10.11).
- Configuration required before use — Steps the user must complete first (e.g. create a Server in Tower, install the Jet Template on it, configure secrets).
- How to use — Step-by-step workflow (e.g. create Jet from template → Run Prepare → Run Build → Run Start → open URL). Mention that the Create Jet wizard can select target state "Running" to auto-run Prepare→Build→Start — one-click deploy.
- Troubleshooting — Common issues and how to fix them (e.g. SSL fails → check port 80; DB connection fails → verify secret).
Use a multi-line string (| or >) to keep the description readable.
Available licenses
The license key accepts one of: agpl-3, lgpl-3, mit, or custom. Values like proprietary are invalid — for proprietary licenses, use license: custom with license_text.
| Value | Description |
|---|---|
agpl-3 |
AGPL-3 (GNU Affero General Public License v3) |
lgpl-3 |
LGPL-3 (GNU Lesser General Public License v3) |
mit |
MIT License |
custom |
Custom license; you must also provide license_text with the full license text. |
Example:
manifest:
name: Docker Setup Commands
summary: Common Docker installation and configuration
description: |
Jet Template that deploys a Docker Compose stack on Ubuntu 22.04.
Technology stack:
- Ubuntu 22.04
- Docker Engine
- Traefik v2.10
- MariaDB 10.11
- WordPress (latest)
Configuration required before use:
1. Create a Server in Cetmix Tower (OS: Ubuntu 22.04, SSH configured).
2. Install this Jet Template on the server.
3. When creating a Jet, provide: domain_name, db_password, letsencrypt_email.
How to use:
1. Create Jet from template.
2. Run Prepare.
3. Run Build.
4. Run Start.
5. Open https://yourdomain and complete WordPress setup.
Troubleshooting:
- If SSL fails: Ensure port 80 is open publicly.
- If DB connection fails: Check db_password secret.
- If 502 error: Check WordPress container logs.
author: Cetmix
version: 1.0.0
license: mit
records:
- cetmix_tower_model: command
reference: example_command
...
Wrong — manifest must not be inside records:
records:
- cetmix_tower_model: manifest # Invalid — causes import to fail
reference: my_manifest
name: My Snippet
records
YAML field type: List
Required for import
The records key is required for import. If your YAML file does not have a top-level records: key containing a list of records, the importer will fail with "YAML file doesn't contain any records".
Use the Tower format only
Only the Cetmix Tower YAML structure is supported. Do not use alternative schemas with top-level keys such as variables, commands, file_templates, flight_plans, or jet_templates. All entities (commands, plans, variables, file templates, jet templates, etc.) must be defined as items in the records list, each with cetmix_tower_model: <model_name>. See the supported models for valid model names.
A YAML file can contain multiple records, which may belong to different models.
You can organize records in two ways:
-
Nested (single root): Put all records under one root record. Child records (tags, commands, plans, variables, etc.) are defined inline within their parent. This keeps the whole hierarchy in a single tree—common for Jet Templates or Server Templates with many related entities.
-
Flat (one record per list item): Create a separate list item for each record. In this case, each record must appear before any reference to it—define tags, variables, and other dependencies before the records that use them.
-
Keys/secrets: Define keys/secrets before any jet_template, command, or file_template that references them in
secret_ids; otherwise the importer will try to create stub keys without the requirednamefield and fail. -
Jet template dependencies: When a jet_template uses
template_requires_idsto reference another jet_template, that required template must either already exist in the database (e.g. imported earlier) or be defined earlier in the same YAML file. Otherwise the importer will try to create a stub jet_template without the requirednamefield and fail.
-
Record Structure
cetmix_tower_model
YAML field type: String
Odoo model name.
Odoo model names are translated to YAML model names using the following rule: cx.tower.model.name → model_name.
List of supported models
| YAML Model | Odoo Model |
|---|---|
| command | cx.tower.command |
| plan (Flight plan) | cx.tower.plan |
| plan_line | cx.tower.plan.line |
| plan_line_action | cx.tower.plan.line.action |
| variable | cx.tower.variable |
| variable_option | cx.tower.variable.option |
| variable_value | cx.tower.variable.value |
| server | cx.tower.server |
| server_template | cx.tower.server.template |
| server_log | cx.tower.server.log |
| shortcut | cx.tower.shortcut |
| file_template | cx.tower.file.template |
| key | cx.tower.key |
| os | cx.tower.os |
| tag | cx.tower.tag |
| jet_template | cx.tower.jet.template |
| jet_state | cx.tower.jet.state |
| jet_action | cx.tower.jet.action |
| jet_waypoint_template | cx.tower.jet.waypoint.template |
| jet_template_dependency | cx.tower.jet.template.dependency |
| webhook | cx.tower.webhook |
| webhook_authenticator | cx.tower.webhook.authenticator |
| git_project | cx.tower.git.project |
| git_source | cx.tower.git.source |
| git_remote | cx.tower.git.remote |
This list can be extended in custom modules by inheriting from the cx.tower.yaml.mixin model. Check the Entity Diagram for the model relations.
Manifest is not a model
manifest is not in this list because it is a top-level YAML key, not an importable record.
Do not add cetmix_tower_model: manifest to the records list.
Invalid model name
If the model name is invalid, the record will not be imported.
Common mistake: flight_plan
Use plan for Flight Plans, not flight_plan. The YAML model name is plan (the last segment of cx.tower.plan). The conceptual term "Flight plan" refers to this model.
Common mistake: plan line_ids use command_id
Plan lines in line_ids use command_id to reference the command, not command. Each plan line must have command_id (reference or exploded record).
Variable has no default value field
The variable model defines metadata only (reference, name, variable_type, etc.).
It does not have default_value_char or any value field.
Values are stored in variable_value records, each with variable_id and value_char.
Use variable_value_ids on the context: jet_template, server_template, server, or jet.
Global values are variable_value records not linked to any entity.
Example:
cetmix_tower_model: command
access_level
YAML field type: String
Access level required for the record (commands, plans, jet states, etc.). Used for permission checks during execution.
| YAML value | Odoo value | Description |
|---|---|---|
user |
"1" |
User level |
manager |
"2" |
Manager level |
root |
"3" |
Root level |
Example:
access_level: manager
reference
YAML field type: String
Unique identifier of the record. If not specified, it will be generated automatically.
Example:
reference: add_user_to_docker_group
Missing reference
If no reference is provided, the record will be created each time the file is imported. This may result in multiple copies of the same record being stored in the database.
Base Odoo Fields
Char
YAML field type: String.
Example:
name: Add user to "docker" group
Text
YAML field type: String.
Example:
code: |-
{% if tower.server.os == "Debian 11" %}
/usr/sbin/usermod -a -G docker {{ tower.server.username }}
{% else %}
usermod -a -G docker {{ tower.server.username }}
{% endif %}
Integer
YAML field type: Number.
Example:
priority: 1
Float
YAML field type: Number.
Example:
percentage: 100.0
Boolean
YAML field type: Boolean.
Example:
active: true
Date
YAML field type: Date.
Example:
date: 2021-01-01
Datetime
YAML field type: Datetime.
Example:
datetime: 2021-01-01 12:00:00
Selection
YAML field type: String.
Odoo Selection field values are represented in the YAML file by selection keys.
For example, the following Odoo field:
use_sudo = fields.Selection(
string="Use sudo",
selection=[("n", "Without password"), ("p", "With password")],
help="Run commands using 'sudo'. Leave empty if 'sudo' is not needed.",
)
Will be represented in the YAML file as:
use_sudo: p # "p" is the key for the "With password" selection value
Relational Fields
Supported relational fields:
Many2manyMany2oneOne2many
Relational fields are represented in YAML in two ways:
Many2one
For Many2one fields, you can use:
- Reference only: A string with the reference, or a dict with a
referencekey pointing to an existing or earlier-defined record. - Exploded: A full nested record dict (including
cetmix_tower_model,reference, and other fields). The related record is created or updated before linking.
Examples:
# Reference only (string)
command_id: very_much_command_test
# Reference only (dict)
command_id:
reference: very_much_command_test
# Exploded (full nested record)
command_id:
reference: very_much_command_test
name: Very much command
action: ssh_command
code: Such much code
variable_ids:
- cetmix_tower_model: variable
reference: test_plan_dir
name: Test Plan Directory
Many2many
For Many2many fields, the value is a list. Each item can be:
- Reference only: A string with the reference, or a dict with a
referencekey only (for existing or earlier-defined records). - Exploded: A dict with
referenceand additional fields. The related record is created or updated before linking.
Examples:
# Reference only (plain strings)
tag_ids:
- docker
- system
# Reference only (dicts with reference key)
tag_ids:
- reference: docker
- reference: system
# Exploded (create/update related records)
tag_ids:
- reference: docker
name: Docker
color: 4
- reference: system
name: System
color: 1
Reference mode
Record is represented as a reference or as a list of references.
Use this mode when:
- The related record(s) already exist in the database, or
- The related record(s) were defined earlier in the same YAML file (in another record's exploded/inline section)
You can use a plain reference string or {reference: x} if the same entity was described in the same file before—no need to repeat the full definition.
Exploded mode
Each related record is represented as an entire record or as a list of entire records.
Use this mode when the related record(s) don't exist in the database or need to be updated.
See the Many2one and Many2many sections above for examples.
Info
If a reference is not specified, a new record will be created. Once a record is defined in the file (in any record), you can reference it elsewhere in the same file using reference mode.
Unknown keys
Unknown or extra keys in records are ignored during import. Only keys that map to valid Odoo fields are processed.
Import conflict handling
When importing, records are matched by reference. If a record with the same reference already exists in the database, the import wizard lets you choose:
| Option | Behavior |
|---|---|
| Skip | Do not import the record; keep the existing one unchanged. |
| Update | Update the existing record with values from the YAML file. |
| Create | Create a new record (reference will be auto-generated if needed). Related records referenced in the YAML may also be created instead of linked to existing ones. |
The chosen policy applies to all records in the file during that import run.
Jet Template lifecycle in YAML
The Jet lifecycle (Prepare, Build, Start, Stop, Remove, Destroy) is defined via Jet States and Jet Actions, not via fields on the jet template.
Common mistake: plan_prepare_id, plan_build_id, etc.
The cx.tower.jet.template model does not have fields like plan_prepare_id, plan_build_id, plan_start_id, plan_stop_id, or plan_remove_id.
These fields do not exist and will be ignored during import.
To define the lifecycle in YAML:
- Create jet_state records for each state (e.g.
removed,preparing,building,stopped,running,removing). - Create jet_action records for each transition, each with:
state_from_id,state_transit_id,state_to_idplan_id— the Flight Plan to run when the action is triggered
state_transit_id is required
Every jet_action must have state_transit_id. Transit states (e.g. preparing, building, starting) must be defined as jet_state records and referenced. Import will fail without state_transit_id.
Build and Start need different plans
Start must not reuse the Build plan. Build creates resources (containers, binaries, configs); re-running the same plan on Start fails because resources already exist. Start must use a different plan suited to starting existing resources (e.g. docker start for Docker). Apply the same logic for Stop, Remove, Destroy — each action type needs a plan suited to its purpose. Actions that do the same thing in different contexts (e.g. two Start actions in different template variants, or the same template with different initial states) can share the same plan.
Full lifecycle for production
Production templates should define Stop, Remove, and Destroy actions, not just Prepare/Build/Start. Include at least one action with empty state_from_id for the Create Jet wizard.
Jet template fields for install/uninstall (different from lifecycle):
plan_install_id— Installation Flight Plan (when template is installed on a server)plan_uninstall_id— Uninstallation Flight Planplan_clone_same_server_id— Clone on same serverplan_clone_different_server_id— Clone to different server
These are different from the Prepare/Build/Start/Stop actions, which are driven by action_ids (jet_action records). See wordpress-docker-chatgpt.yaml for a complete example.
Entity Diagram
The entity diagram is a visual representation of the YAML file structure.
- command
- tag # Tags assigned to command
- variable # Variables used by the command
- os # Operating systems associated with the command
- plan # Child plan executed by the command
- file_template # Template used to upload file from the command
- plan
- plan_line # Lines of the plan
- command_id # Command executed in the plan line (use command_id, NOT command)
- plan_line_action # Actions depending on command output
- variable_value # Variable values set by the action
- tag # Tags assigned to the plan
- server_template
- variable_value # Configuration settings
- shortcut # Shortcuts to quickly trigger actions
- tag # Tags for categorization
- os # Default operating system
- server_log # Logs associated with the template
- plan # Plans executed when server is created/deleted
- file_template
- tag # Tags related to the template
- variable # Variables used in template
- variable
- variable_option # Available options (dropdowns)
- variable_value # Assigned values
- variable_value
- variable # Inline variables used in variable values
- jet_template
- jet_state # Lifecycle states (Draft, Running, Stopped, etc.)
- jet_action # Transitions (Prepare, Build, Start, Stop, etc.)
- plan # Flight Plan run when action is triggered
- variable_value # Template configuration
- template_requires_ids # Other jet_templates this one depends on
- waypoint_template_ids
- key # SSH key or Secret
- os # Operating system
- tag # Global tagging entity
- git_project
- git_source # Sources
- git_remote # Source remotes