Core concepts · Versioning

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.

WORKFLOW VERSIONS · APPEND-ONLYlivecanonicalVersionId2026.05.01 · rev 1a3f1c9…2026.07.24 · rev 27b02de…2026.09.10 · rev 3e41a77…draftnot runnableeach run pins workflowVersion + specVersionHash at creationrun-a1b2c3d4run-88fe01aarun-0c7d9e21
Immutable versions, one movable pointerRuns hold a dashed line to the version they executed. Moving the live pointer does not move them — which is exactly the point.
FieldWhat it does
versionThe human label, dated — 2026.09.10. What you quote when discussing behaviour.
revisionIncrements on every edit within a version label.
canonicalVersionIdPoints at the version new runs will use. Moving it is how you ship.
contentHashSHA-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.

fields present on every runjson
{
  "id": "run-fb0677b2",
  "templateId": "generate-illustration",
  "workflowVersion": "2026.09.09",
  "specVersionHash": "sha256:eb3ced0c91a625e7ad47beaa28f5ff7b6db933ca5f6cac78fb93c1b030852fdb"
}
FieldTypeRequiredDescription
workflowVersionstringoptionalThe version label this run executed — not necessarily the one that is live now.
specVersionHashstringoptionalHash of the frozen definition plus the resolved intake. Two runs with the same hash had identical inputs in every respect.
specIdstring | nulloptionalIdentifier 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.

There is no “Workflow Specification” object

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

GET /v1/workflows/{workflow_id}/versionsbash
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 changeDo thisWhy
Better prompt, same promise and same inputsNew version of the same workflowCallers need do nothing; saved intakes keep working.
A better mechanism for a promise already madeNot a version change at allCapability flag plus conditional step logic. See Intake.
New required intake field, or a different output shapeA new workflow id, and deprecate the old oneExisting 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 workflowVersion and specVersionHash beside 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.
In short
  • Versions are append-only; a movable pointer decides which one is live.
  • Every run pins workflowVersion and specVersionHash at creation.
  • A new promise needs a new workflow; a better mechanism for the same promise needs neither.