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:
| Version | Applies to | Changes when |
|---|---|---|
Workflow version (u32) | One workflow | You save a change |
| File schema version | The workflow.json format | The format itself evolves |
| ChengOS release version | The product | A 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:
| State | Meaning |
|---|---|
draft (default) | Being edited; not intended for production traffic |
active | Executable, the normal running state |
archived | Retired 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:
- 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. - Unknown top-level keys are ignored. A file exported by an older server that still carries
nodeCount,createdAt, orisDraftdecodes cleanly. workflowIdis 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:
- The filesystem watcher picks up changes asynchronously under the workspace root.
- The import API runs the same importer synchronously, so a frontend or LLM that has just
written
workflow.jsongets an immediate structured result instead of waiting.
Import errors are stable machine-readable codes mapped to HTTP statuses:
| Code | Status |
|---|---|
WORKFLOW_WORKSPACE_CONFLICT, WORKFLOW_SOURCE_CONFLICT, WORKFLOW_ID_MISMATCH, WORKFLOW_FILE_ADOPTION_REQUIRED | 409 Conflict |
WORKFLOW_NOT_FOUND, WORKSPACE_NOT_FOUND, WORKFLOW_FILE_NOT_FOUND | 404 Not Found |
IMPORT_INTERNAL_ERROR | 500 |
INVALID_WORKFLOW_FILE, UNSUPPORTED_*, WORKFLOW_FILE_NOT_STABLE, WORKFLOW_FILE_PATH_INVALID | 400 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
- Keep production workflows in
activestate; do experimental work in adraftcopy. - Use
change_noteon every save — it is the only per-save annotation the system stores. - Export
workflow.jsonand commit it. Git gives you the history, blame, and rollback that the editor does not. - Re-import explicitly when you want a committed version to become the running one.
Next steps
- Workflow Templates — content hashing applied to built-in workflows.
- Upgrades & Rollback — versioning the product itself.
- Development Setup — working with workflow files on disk.