Workflow Versioning

Applies to: ChengOS v0.1.0+ | Last verified: 2026-08-12 | Source: crates/cheng-core/src/domain/workflow.rs, crates/cheng-core/src/workflow_file, crates/cheng-api/src/rest/handlers/workflow_file/mod.rs, crates/cheng-common/src/version.rs

Three different things in ChengOS are called a "version", and conflating them causes real confusion:

VersionApplies toChanges when
Workflow version (u32)One workflowYou save a change
File schema versionThe workflow.json formatThe format itself evolves
ChengOS release versionThe productA release is tagged

This page covers the first two. For the third, see Upgrades & Rollback.

Workflow version and change notes

Every workflow carries a version counter starting at 1, and an optional change_note. Saving an edit increments the counter; the change note records why.

The counter is author-visible metadata, not a snapshot store. ChengOS does not keep a history of previous graph revisions you can roll back to from the editor. If you need that, the answer is the file contract below plus your own version control — which is exactly what the file contract was designed for.

Lifecycle state

Separate from version, a workflow has a state:

StateMeaning
draft (default)Being edited; not intended for production traffic
activeExecutable, the normal running state
archivedRetired but retained

And a visibility: private (default), public, unlisted, or shared.

Promoting draft → active is the closest thing to "publishing a version" the editor has.

The workflow file contract

A workflow can live as a workflow.json file — the format the frontend writes, the LLM generator produces, explicit import consumes, and the filesystem watcher reads.

{
  "schemaVersion": 1,
  "workflowId": "fead748f-…",
  "name": "my-workflow",
  "description": "…",
  "state": "draft",
  "version": 3,
  "tags": ["generated"],
  "visibility": "private",
  "definition": { "nodes": [], "edges": [], "notes": [], "containers": [] }
}

This external contract is deliberately distinct from the internal domain shape and from the HTTP DTOs. Three rules govern it:

  1. Identity and tenancy are never read from the file. workspaceId, tenantId, permissions, favourite/template flags, computed counts, and timestamps come from the authenticated import context and the directory layout — not from an untrusted file body. A file cannot smuggle itself into another workspace.
  2. Unknown top-level keys are ignored. A file exported by an older server that still carries nodeCount, createdAt, or isDraft decodes cleanly.
  3. workflowId is optional only for a brand-new file. Omit it and the importer allocates one; every file the server encodes carries an id.

Because identity is trusted context rather than file content, a workflow.json is safe to commit to Git, review in a pull request, and diff between branches. That is the practical version-history story for workflows.

File schema versions and upgrades

schemaVersion is the format's own version. Only V1 exists today; a legacy nested envelope with no schemaVersion decodes as V1 and is canonicalised on the next encode.

The upgrade policy is the interesting part:

A passive watcher may decode an older supported version, but it must not silently rewrite a user's already-versioned file merely because the server was upgraded.

Rewriting is an explicit operation. upgrade_to_latest decodes (validating the graph shape), re-encodes canonically, and reports from_version, to_version, and whether the on-disk bytes would actually change. Future V1 → V2 transformations go there as explicit steps rather than being scattered across serde aliases.

So upgrading your server never rewrites your workflow files behind your back. Your Git diff stays clean until you deliberately import or write back.

Content hashing

Change detection uses a normalised content hash (sha256:…) rather than the version counter or byte equality. The hash is identity-independent: workflowId is excluded, so it describes the graph, not the installation.

This is what makes template sync's three-way comparison possible — see Workflow Templates — and it is why a workflow whose version field never changes still produces a correct "changed / unchanged" answer.

Server-managed metadata keys record the file relationship: META_FILE_HASH, META_FILE_SCHEMA_VERSION, META_FILE_SOURCE, META_FILE_MANAGED, META_FILE_IMPORTED_AT, plus template-origin keys for workflows installed from the catalog. These are server-owned and are stripped from the file contract.

Import: synchronous vs the watcher

Two paths bring a file into the database, sharing one importer:

Import errors are stable machine-readable codes mapped to HTTP statuses:

CodeStatus
WORKFLOW_WORKSPACE_CONFLICT, WORKFLOW_SOURCE_CONFLICT, WORKFLOW_ID_MISMATCH, WORKFLOW_FILE_ADOPTION_REQUIRED409 Conflict
WORKFLOW_NOT_FOUND, WORKSPACE_NOT_FOUND, WORKFLOW_FILE_NOT_FOUND404 Not Found
IMPORT_INTERNAL_ERROR500
INVALID_WORKFLOW_FILE, UNSUPPORTED_*, WORKFLOW_FILE_NOT_STABLE, WORKFLOW_FILE_PATH_INVALID400 Bad Request

WORKFLOW_FILE_NOT_STABLE deserves a note: it means the file was still being written when the importer read it. Retry; do not treat it as a validation failure.

The ChengOS release version

For completeness: every Rust component reads one release version from cheng-common, resolved at compile time from CHENGOS_VERSION (set by CI from the immutable Git tag), the nearest VERSION file, or the workspace package version — in that order.

Components must not read their own CARGO_PKG_VERSION: that is a per-package build detail that can drift from the release identity. The canonical form is a bare SemVer (0.1.0); the v prefix belongs to Git tags and release asset names only.

A practical workflow

  1. Keep production workflows in active state; do experimental work in a draft copy.
  2. Use change_note on every save — it is the only per-save annotation the system stores.
  3. Export workflow.json and commit it. Git gives you the history, blame, and rollback that the editor does not.
  4. Re-import explicitly when you want a committed version to become the running one.

Next steps