Versioning
Workflow definitions are append-only. Editing one publishes a new immutable version and moves a pointer; it never rewrites what came before. Every run pins the version it executed, so a result from last year is still explainable today.
Versions never change
A workflow has a stable id — generate-illustration — and a stack of versions beneath it. Publishing an edit appends; it does not mutate. A separate movable pointer, canonicalVersionId, decides which version is live for new runs.
| Field | What it does |
|---|---|
version | The human label, dated — 2026.09.10. What you quote when discussing behaviour. |
revision | Increments on every edit within a version label. |
canonicalVersionId | Points at the version new runs will use. Moving it is how you ship. |
contentHash | SHA-256 over the key-sorted definition. Identical hashes mean identical definitions. |
The frozen spec
When you create a run, Esy snapshots the workflow definition and combines it with your resolved intake into a single hash. That pair is the run’s spec, and it is fixed for the life of the run.
{
"id": "run-fb0677b2",
"templateId": "generate-illustration",
"workflowVersion": "2026.09.09",
"specVersionHash": "sha256:eb3ced0c91a625e7ad47beaa28f5ff7b6db933ca5f6cac78fb93c1b030852fdb"
}| Field | Type | Required | Description |
|---|---|---|---|
workflowVersion | string | optional | The version label this run executed — not necessarily the one that is live now. |
specVersionHash | string | optional | Hash of the frozen definition plus the resolved intake. Two runs with the same hash had identical inputs in every respect. |
specId | string | null | optional | Identifier for the frozen snapshot, where one was persisted separately. |
This is what makes a six-month-old run answerable. You can tell whether two outputs differ because the inputs differed or because the workflow changed underneath them — compare the hashes.
Earlier docs described a first-class Workflow Specification resource. It does not exist. The real frozen instance is the workflowVersion + specVersionHash pair above, held on the run.
There is an unrelated legacy /v1/specs endpoint with a different shape entirely. It is not this, and you should not build against it.
Listing versions
curl -s https://api.esy.com/v1/workflows/generate-illustration/versions \
-H "Authorization: Bearer $ESY_API_KEY"Bump, or make a sibling?
The decision that matters when a workflow needs to change:
| The change | Do this | Why |
|---|---|---|
| Better prompt, same promise and same inputs | New version of the same workflow | Callers need do nothing; saved intakes keep working. |
| A better mechanism for a promise already made | Not a version change at all | Capability flag plus conditional step logic. See Intake. |
| New required intake field, or a different output shape | A new workflow id, and deprecate the old one | Existing saved intakes would break. Deprecation names the successor. |
Deprecation is not deletion. A deprecated workflow refuses new runs with a 400 that carries supersededBy, while every historical run still resolves and still explains itself.
Reproducibility in practice
- Record the version with the output. If you store artifacts in your own system, store
workflowVersionandspecVersionHashbeside them. - Pin models for batch work. Version pinning covers the workflow, not the provider’s model. For a catalog, also pin a dated model snapshot via provider overrides.
- Compare hashes before blaming the model. Different output with the same hash is model non-determinism; different hashes mean the inputs or the definition changed.
- Versions are append-only; a movable pointer decides which one is live.
- Every run pins
workflowVersionandspecVersionHashat creation. - A new promise needs a new workflow; a better mechanism for the same promise needs neither.
