Versioning¶
A workflow is editable, but instances of it can run for a long time. Without versioning the engine would read the live workflow on every step, so editing a workflow would change the behavior of instances already running on it, and an edit that removed a node a token was sitting on would break them. Versioning pins each instance to the definition it started on.
How it works¶
When an instance starts it is pinned to an immutable version: a snapshot of the workflow's executable definition (its tenant, start node, nodes, flows and declared initial variables, but not the diagram layout). From then on the engine runs the instance against that snapshot, so editing the workflow afterwards never affects it. A new instance picks up the current definition.
Snapshots are stored as revisions of an orchestra_workflow_version content
entity, one per workflow, and an instance records the revision id it pinned.
Identical definitions share a snapshot (deduped by a hash of the executable
shape), so reverting a workflow to an earlier shape, or a save that only
moved the diagram, does not pile up versions.
Wording¶
The pin covers the wording too. A node's label is part of the executable definition, so renaming one cuts a new version: instances already running keep the label they started with, and only new instances show the new one.
Translations are the exception, and deliberately so. A language override is not part of the executable shape, so retranslating a label cuts no version. The text captured on the current snapshot is refreshed in place instead, and the instances pinned to it show the corrected wording straight away. A wording fix therefore reaches a running instance when it is made in a translation, and not when it is made in the workflow's own language.
The refresh writes the current snapshot and no older one, so a run left behind on an earlier version keeps the wording that version was cut with. Its task label is the exception: the inbox, the notification subject and the action link all resolve that from the live node, so they read the current translation whatever the run is pinned to.
Modes¶
The behavior is set by Workflow versioning on the Orchestra settings page
(or the versioning_mode setting), one of:
- Automatic (default): editing a workflow snapshots a new version the next time an instance starts on it, or when you migrate running instances to it. Merely editing, or viewing the versions or instances pages, never cuts a version. In-flight instances are untouched; new ones pick up the edit.
- Manual: instances run the last published version. Editing a workflow changes nothing, for running or new instances, until you publish it again. This is the deploy-style model: iterate on a draft freely, then publish when ready. The first start of a never-published workflow publishes it once to bootstrap.
- Off: no pinning. Instances always run the live workflow. Choose this only if you never edit a workflow while instances of it are running.
Whatever the mode, an instance always keeps the version it started on; the mode only affects which version a new instance pins.
Performance¶
Pinning is designed to stay cheap on the start path: the version a workflow pins is recorded, so starting an instance is a single lookup with no hashing or extra entity load. The snapshot's hash is only computed, and a new revision only considered, the first time an instance starts after an edit (in automatic mode) or when you publish (in manual mode). Advancing a token reads an immutable snapshot, which caches permanently, so it is no costlier than reading the live config.
Listing versions or instances only compares the live shape's hash to the stored versions to show which one is current, so viewing a page never cuts a version. A version is materialized only when an instance pins to it (a start or a migration).
Embedded children¶
A subprocess node running its child inside the parent's run (see Subprocesses) reads the child against one of its versions, and the parent's version records which, inside its hash. Nothing of the child is copied into the parent. When the child starts, the run records the child version it runs on, so a run keeps the parent and child versions it is on whatever is edited or published later, until it is migrated (see Versions and migration).
What a parent does when its child changes depends on the Workflow versioning setting (see Modes):
- Automatic: saving the child unpins every workflow embedding it, at any depth, so their next start cuts a version running the new child.
- Manual: publishing the child does not change what a published parent runs. The child's Versions tab lists the workflows whose published version still embeds an older version of it, with Update all, which publishes each ticked one again with the child's current published version and nothing else changed: edits to the parent that are not published yet stay unpublished. The parent's subprocess node also says when a newer child version is published.
- Off: nothing is pinned, and an embedded child is read live like its parent.
Versions and migration¶
Each workflow has a Versions tab listing its versions, when each was created, and how many running instances are pinned to each. In manual mode the tab also has a Publish current definition button. In automatic mode, if you have edited the workflow but no instance has started or migrated onto the new shape yet, the tab says the current shape is not versioned yet and offers Migrate running instances to the current shape, which cuts that version at that point rather than on merely viewing the page.
To move running instances from an old version onto a newer one, use Migrate instances here on the target version. Migration remaps each running instance's live tokens onto the target: nodes whose id is unchanged map automatically, and for any node that was renamed or removed you can choose where its tokens go.
A run migrates in parts: its own steps are one part, and each embedded child it is inside is another. Each part moves on its own. The run's own steps move to the target version. An embedded child moves to the child version the target embeds. A part that cannot move stays on its version, and does not hold back the other parts.
A part stays on its version when:
- one of its live steps has no counterpart in the new version, and you gave it no mapping;
- one of its parallel branches began at a step that the new version no longer reaches from that branch's split, a join of the new version counts that split's branches, and you gave that step no mapping. Map it to the step the branch begins at in the new version, even though no token stands on it any more;
- a split sent two branches down two flows to the same step, a join of the new version counts that split's branches, and the new version would give the two one name: it draws a single flow there, or your mapping sends two steps to one. The join could no longer tell the two branches apart;
- for an embedded child, the target no longer embeds the same workflow at that node;
- a step waits on an embedded child, and the node it would move to embeds no child in the new version.
A step also records the flow it arrived on. When the new version has a flow between the same two nodes, the step takes that flow. When there are several, it takes the one at the same position: the second flow stays the second. When it has none, the step keeps its old flow and still moves. A join counts branches, not flows, so the step is still counted when it reaches the join.
A part that stays finishes on its version. The run then carries on in the version its own steps are on. Every migration reports each part of each run it looked at: what moved, from which version to which, and what stayed and why. Migrating the run again later, with the missing mapping, moves what stayed.
A step inside an embedded child is a node of the child version, and is named
after the node that embeds it: n_check/n_review is n_review inside the
child embedded at n_check.
The same migration is available from the command line, for deploy scripts, headless sites or bulk runs:
drush orchestra:migrate-versions WORKFLOW [--to=VERSION] [--map=old:new,old2:new2] [--dry-run]
--to defaults to the current published version; --map carries the
renamed/removed-node mappings, a child's nodes named as above; --dry-run
reports how many instances would move, which nodes have no mapping, which
parts of runs would stay, and the joins where a waiting branch would no longer
be counted, without changing anything. The command then
logs every part of every run it migrated.
A single instance can also be moved on its own. On the process instance list
(orchestra_ui), an instance pinned behind its workflow's
current version shows a Migrate to current operation that migrates just
that instance, and the list's Migrate selected to current bulk action runs
the same per-instance migration over the selection as a batch. Both report
every part of every run; a part left on its version for want of a node mapping
moves once the mapping is given on the per-workflow Migrate instances here
form. The same list also offers per-instance Cancel and Delete.
The list's Version column makes the pinning visible: each row shows the version the instance is pinned to, marked N (current) when it is the workflow's current version (in automatic mode the version matching the live shape, in manual the published version; the same marking as the versions page), and when no embedded child it is inside was left on an older version, so an instance behind it, the candidate for the migrate operations above, stands out at a glance. The list's columns sort, and a collapsed Filter narrows it by workflow and state (and, for an administrator, tenant); the filter lives in the URL, so a filtered view can be shared.
Instances that pin nothing¶
An instance pins no version when it started under the Off mode, the one mode that never pins. Such an instance falls back to the live workflow, and the instance list shows it as Live in its Version column. The migrate operations above are the way to put it on a version: pinning is never applied retroactively.
The reverse cannot happen: a version an instance pins is never pruned out from under it, nor deleted with its workflow. Retention skips any snapshot revision an instance references, along with each workflow's current and published version, and it asks nothing about the instance's state, so a run that ended long ago protects its snapshot exactly as a live one does. An instance keeps the shape it started on, and its history keeps reporting that shape rather than whatever the workflow has become since.