App description templating
margot supports two forms of the Margo application descriptor inside your margo/
directory: a static app.yaml, or a Jinja2 template app.yaml.jinja. Use one — having
both present at the same time is a hard error.
When app.yaml.jinja is present, margot renders it with Jinja2 before every build,
verify, and describe invocation. This lets you derive the entire Margo application
description from the single source of truth you already maintain in margo.yaml —
versions, repositories, component names, and OCI references are never duplicated by hand.
How it works
margot reads margo.yaml, builds a context from every field it contains, and passes
that context to Jinja2 as the manifest namespace. The rendered output is treated
exactly like a static app.yaml.
Rules:
- Use
app.yaml.jinjaorapp.yaml— never both. Both present is a hard error. - Every variable reference uses
StrictUndefined: an unresolved variable is a build failure, never a silently empty string. Typos are caught immediately. margot buildwrites the renderedapp.yamlto the build output directory (.dist/by default) alongside the other artifact files. Theapp.yaml.jinjasource on disk is never modified.margot verifyandmargot describerender the template to a temporary file and discard it after the command completes — nothing is written to.dist/.
Template context reference
The full context available inside app.yaml.jinja:
manifest
├── id
├── name
├── version
├── appVersion
├── description
├── directory
├── repository
├── annotations (dict)
├── author (list)
│ └── [n]
│ ├── name
│ └── email
├── organization (list)
│ └── [n]
│ ├── name
│ └── site
├── compose (dict, present when compose is declared)
│ ├── directory
│ ├── repository
│ ├── version
│ ├── tag (computed)
│ ├── ref (computed)
│ ├── component (flat layout only)
│ ├── variants (list)
│ │ └── [n]
│ │ ├── name
│ │ ├── version
│ │ ├── tag (computed)
│ │ ├── ref (computed)
│ │ ├── repository
│ │ └── component
│ └── <variant-name> (direct access, e.g. manifest.compose.default)
│ └── (same fields as variants[n])
└── quadlet (dict, present when quadlet is declared)
└── (same shape as compose)
Top-level fields
All fields live under the manifest namespace.
| Variable | Type | Value |
|---|---|---|
manifest.id |
string | id from margo.yaml |
manifest.name |
string | name from margo.yaml |
manifest.version |
string | version from margo.yaml |
manifest.appVersion |
string | appVersion from margo.yaml, or "" if absent |
manifest.description |
string | description from margo.yaml |
manifest.directory |
string | directory (margo artifact source dir, default margo) |
manifest.repository |
string | Top-level repository, or "" if not set |
manifest.annotations |
dict | annotations key/value pairs, or {} if absent |
manifest.author |
list | List of {name, email} objects |
manifest.organization |
list | List of {name, site} objects |
Component fields
manifest.compose and manifest.quadlet are present when the corresponding component is
declared in margo.yaml. Both expose the same shape:
| Variable | Type | Value |
|---|---|---|
manifest.<type>.directory |
string | Component source directory |
manifest.<type>.repository |
string | Component OCI repository (inherited from top-level if not overridden) |
manifest.<type>.version |
string | Component base version (derivation base for variants) |
manifest.<type>.tag |
string | version with + replaced by _ (OCI-safe) |
manifest.<type>.ref |
string | <repository>:<tag>, or "" if either is empty |
manifest.<type>.component |
string | Margo component name (flat layout only) |
manifest.<type>.variants |
list | Ordered list of variant objects (empty list in flat layout) |
manifest.<type>.<name> |
dict | Direct access to a variant by name (e.g. manifest.compose.default) |
If a component is not declared in margo.yaml, manifest.compose / manifest.quadlet
resolves to an empty dict {}.
Variant object fields
Each object in manifest.<type>.variants (and each manifest.<type>.<name>) exposes:
| Field | Type | Derivation |
|---|---|---|
name |
string | As declared in margo.yaml |
version |
string | Authored value, or <component-version>+<type>-<name> if omitted |
tag |
string | version with + replaced by _ — OCI-safe. Computed, not authorable |
repository |
string | Inherited from component, or overridden per variant |
ref |
string | <repository>:<tag>. Computed, not authorable |
component |
string | Authored value, or <id>-<type>-<name> if omitted |
Note
tag and ref are derived fields — they cannot be set in margo.yaml. They exist
purely for use in templates so you never have to compute the OCI-safe tag yourself.
Basic usage
A minimal app.yaml.jinja using only the top-level fields:
apiVersion: v1
kind: ApplicationDescription
id: {{ manifest.id }}
metadata:
name: My App
description: {{ manifest.description }}
version: {{ manifest.version }}
catalog:
organization:
{%- for org in manifest.organization %}
- name: {{ org.name }}
site: {{ org.site }}
{%- endfor %}
deploymentProfiles: []
Running margot build with this margo.yaml:
apiVersion: v1
id: com-example-myapp
name: myapp
version: "1.0.0"
appVersion: "2.3.1"
description: "My application"
repository: public.ecr.aws/g2n4p2m7/margo
organization:
- name: Example Corp
site: https://example.com
Produces this app.yaml in .dist/1.0.0/margo/:
apiVersion: v1
kind: ApplicationDescription
id: com-example-myapp
metadata:
name: My App
description: My application
version: 1.0.0
catalog:
organization:
- name: Example Corp
site: https://example.com
deploymentProfiles: []
Referencing variants in loops
The main power of templating is generating deployment profiles automatically from the
variants list. Adding a new variant to margo.yaml requires zero edits to app.yaml.jinja.
margo.yaml:
apiVersion: v1
id: com-example-myapp
name: myapp
version: "1.0.0"
appVersion: "2.3.1"
description: "My application"
repository: public.ecr.aws/g2n4p2m7/margo
organization:
- name: Example Corp
site: https://example.com
compose:
version: 1.0.0
variants:
- name: default
- name: minimal
margo/app.yaml.jinja:
apiVersion: v1
kind: ApplicationDescription
id: {{ manifest.id }}
metadata:
name: My App
description: {{ manifest.description }}
version: {{ manifest.version }}
catalog:
organization:
{%- for org in manifest.organization %}
- name: {{ org.name }}
site: {{ org.site }}
{%- endfor %}
deploymentProfiles:
{%- for v in manifest.compose.variants %}
- type: compose
id: {{ manifest.id }}-compose-{{ v.name }}
components:
- name: {{ v.component }}
properties:
repository: oci://{{ v.repository }}
revision: {{ v.tag }}
{%- endfor %}
Rendered output:
apiVersion: v1
kind: ApplicationDescription
id: com-example-myapp
metadata:
name: My App
description: My application
version: 1.0.0
catalog:
organization:
- name: Example Corp
site: https://example.com
deploymentProfiles:
- type: compose
id: com-example-myapp-compose-default
components:
- name: com-example-myapp-compose-default
properties:
repository: oci://public.ecr.aws/g2n4p2m7/margo
revision: 1.0.0_compose-default
- type: compose
id: com-example-myapp-compose-minimal
components:
- name: com-example-myapp-compose-minimal
properties:
repository: oci://public.ecr.aws/g2n4p2m7/margo
revision: 1.0.0_compose-minimal
Notice that oci:// is prepended inside the template — the v.repository value contains
the bare registry/repository path (public.ecr.aws/g2n4p2m7/margo) without any scheme.
The Margo spec requires the oci:// prefix in properties.repository, so it belongs in
the template.
Accessing a variant by name
When you need to reference a specific variant (for example, to pin a known revision in a second profile), access it by name directly instead of iterating:
deploymentProfiles:
- type: compose
id: {{ manifest.id }}-compose-production
components:
- name: {{ manifest.compose.default.component }}
properties:
repository: oci://{{ manifest.compose.default.repository }}
revision: {{ manifest.compose.default.tag }}
This is equivalent to the loop result for the default variant, but explicit.
Iterating over both compose and quadlet variants
When a project ships both component types, combine both variant lists:
parameters:
nginxPort:
value: 8080
targets:
- pointer: NGINX_PORT
components:
{%- for v in manifest.compose.variants + manifest.quadlet.variants %}
- {{ v.component }}
{%- endfor %}
manifest.compose.variants + manifest.quadlet.variants is standard Jinja2 list
concatenation — no custom filter needed.
Using appVersion
appVersion tracks the version of the deployed application (like Helm's appVersion).
A common pattern is to pass it as the image tag for a Helm component:
deploymentProfiles:
- type: helm
id: {{ manifest.id }}-helm
components:
- name: nginx
properties:
repository: oci://registry-1.docker.io/bitnamicharts/nginx
revision: 18.1.7
parameters:
nginxImageTag:
value: "{{ manifest.appVersion }}"
targets:
- pointer: image.tag
components:
- nginx
Bumping appVersion in margo.yaml propagates to every parameter that references
manifest.appVersion — no other edits required.
Looping over catalog fields
manifest.author and manifest.organization are lists you can iterate over:
metadata:
catalog:
organization:
{%- for org in manifest.organization %}
- name: {{ org.name }}
site: {{ org.site }}
{%- endfor %}
author:
{%- for a in manifest.author %}
- name: {{ a.name }}
email: {{ a.email }}
{%- endfor %}
Conditional blocks
Use Jinja2 if to include a section only when a component is declared:
deploymentProfiles:
{%- if manifest.compose %}
{%- for v in manifest.compose.variants %}
- type: compose
id: {{ manifest.id }}-compose-{{ v.name }}
components:
- name: {{ v.component }}
properties:
repository: oci://{{ v.repository }}
revision: {{ v.tag }}
{%- endfor %}
{%- endif %}
{%- if manifest.quadlet %}
{%- for v in manifest.quadlet.variants %}
- type: quadlet
id: {{ manifest.id }}-quadlet-{{ v.name }}
components:
- name: {{ v.component }}
properties:
repository: oci://{{ v.repository }}
revision: {{ v.tag }}
{%- endfor %}
{%- endif %}
This is useful for shared template stubs that work regardless of which components are declared in a given project.
Annotations
manifest.annotations is a plain dict. Iterate over it with .items():
metadata:
extensions:
{%- for key, value in manifest.annotations.items() %}
{{ key }}: "{{ value }}"
{%- endfor %}
Error behavior
| Situation | Behavior |
|---|---|
Undefined variable (e.g. typo in {{ manifest.vesion }}) |
Hard error at build/verify/describe time. Never silent. |
Both app.yaml and app.yaml.jinja present |
Hard error. |
Only app.yaml present |
Copied verbatim — no rendering. |
app.yaml.jinja absent and app.yaml absent |
Hard error. |
See also
- margo.yaml reference — all
margo.yamlfields and their semantics. - Image search-and-replace — the
image.replacefield incomposeandquadletcomponents also uses the same template context. - Full example — a complete multi-component project with variants, Helm, compose, and quadlet in a
single
app.yaml.jinja.