Template Format Compatibility
How Pulp Engine versions the template definition format, what compatibility you can rely on across upgrades, and how to pre-flight stored templates before upgrading a server.
The format generation is distinct from a template’s
versionfield.versionis the content semver — it bumps every time you edit a template.formatVersionis the format generation — it identifies which generation of the definition format the JSON document is written in.
The promise: additive-only within a format generation
Within format generation 1 (every release to date), schema changes are additive only:
- New node types, new optional fields, and new enum values may be added.
- Existing fields are never removed, renamed, or re-typed.
- A definition that parsed under an older server parses under every newer server in the same generation.
This is enforced in CI by a frozen fixture corpus
(apps/api/src/__tests__/fixtures/compat/ — including a production seed
template from v0.18.0 and representative starter-pack builds). Those files are
committed once and never regenerated; compat-fixtures.test.ts asserts they
parse under the current save-boundary schema on every build. A schema change
that breaks any frozen fixture cannot merge.
The reverse direction is not promised: a template that uses a node type introduced in v0.85.0 will be rejected by a v0.60.0 server. Downgrading a server below the version that authored a template is unsupported.
formatVersion semantics
| Value | Meaning |
|---|---|
| absent | Format 1. Every definition saved before v0.85.0 omits the field; every format-1 reader treats absence as 1. |
1 | Format 1, stamped explicitly. The API stamps formatVersion: 1 at the save boundary (create and update) from v0.85.0 onward, so all newly-saved definitions carry it. |
2+ | Reserved for a future breaking format change. A format-1 server rejects such documents loudly (HTTP 400 with a formatVersion issue) instead of silently misreading them. |
Notes:
- The stamp covers every save path — hand-authored API calls, the visual
editor, starter packs, and AI generation all pass through the same
POST /templates/PUT /templates/:keyvalidation and stamping. - Version restore is a faithful clone. Restoring a pre-v0.85.0 version
reproduces its stored bytes, including the absent
formatVersion— which still means format 1. Restore does not rewrite history. - Sending
formatVersion: 1yourself is valid and equivalent to omitting it.
Restore-on-drift behaviour
POST /templates/:key/versions/:version/restore re-validates the stored
historical definition against the current schema before restoring. If a
historical version predates the additive-only enforcement and no longer
conforms, the restore fails with 400 Stored Version Invalid and the
template’s history is left untouched. The error body lists the exact issues.
Because format changes are additive-only, this should not occur for any
version saved on v0.18.0 or later; it exists as a fail-closed guard.
Pre-flighting an upgrade
Before upgrading a server, you can validate exported/stored template JSON offline with the CLI — it applies the same Zod schema the API enforces at the save boundary:
# Validate one file or a directory of definitions (no API required)
pulp validate ./templates
pulp validate ./my-template.json --strict
A green pulp validate from the new release’s CLI against your stored
definitions means every one of them will load on the new server.
Where the schema lives
| Artifact | Role |
|---|---|
@pulp-engine/template-model | TypeScript interfaces (TemplateDefinition, node types). |
@pulp-engine/template-schema | The runtime Zod schema — single source of truth, used by the API save boundary, the CLI validate command, and the editor’s starter-pack parity test. |
apps/api/src/__tests__/fixtures/compat/ | Frozen cross-generation corpus; grows via node scripts/freeze-compat-fixture.mjs <pack-id> <fixture-name>. |