Versioning
Since workflows can run for long periods, sometimes months or even years, it's common to need to make changes to a workflow definition while executions are still in progress. Without versioning, modifying workflow code that affects the execution path would cause non-determinism errors during replay.
The getVersion() helper function allows you to safely introduce changes to running workflows by creating versioned branch points.
Using getVersion
getVersion() is a replay-safe straight-line helper. Each change point records one typed VersionMarkerRecorded history event for the selected run, and later workflow replay or query replay reuses that committed value instead of recalculating the branch from today's code. New runs also snapshot a start-time workflow_definition_fingerprint on WorkflowStarted, so when a run reaches a newly introduced branch point with no recorded marker yet, the runtime can distinguish "this run started before that definition existed" from "this is a fresh execution on the current definition" without relying only on the compatibility marker.
use Workflow\V2\Workflow;
use Workflow\V2\WorkflowStub;
use function Workflow\V2\{activity, getVersion};
final class MyWorkflow extends Workflow
{
public function handle(): void
{
$version = getVersion(
'my-change-id',
WorkflowStub::DEFAULT_VERSION,
1
);
if ($version === WorkflowStub::DEFAULT_VERSION) {
activity(OldActivity::class);
} else {
activity(NewActivity::class);
}
}
}
How It Works
The getVersion() method takes three parameters:
- changeId - A unique identifier for this change point
- minSupported - The minimum version this code still supports
- maxSupported - The maximum (current) version for new executions
When a workflow encounters getVersion():
- New executions append a typed
VersionMarkerRecordedhistory event with thechange_id, chosenversion, and supported range, then returnmaxSupported - Replaying executions return the previously recorded version from that history marker
- Runs without a marker yet fall back to
WorkflowStub::DEFAULT_VERSIONwhen the runtime determines the branch predates the current workflow definition. It first checks the run's compatibility marker against the current worker, then compares the run's durably snappedworkflow_definition_fingerprintto the current loadable workflow class. If the fingerprints match, the run is on the same definition and the marker is recorded fresh; if they differ, the run predates the change and the fallback fires. Runs whoseWorkflowStartedhistory predates the fingerprint snapshot (no recorded fingerprint) conservatively receiveDEFAULT_VERSIONbecause the runtime cannot prove the definition has not changed. The fallback does not append a synthetic marker or consume a new workflow step - Query methods replay the same committed version marker before invoking the annotated method, so queries see the same branch the workflow task saw
- Waterline exposes recorded markers in the selected-run timeline with
version_change_id,version,version_min_supported, andversion_max_supported, and selected-run detail now also surfacesworkflow_definition_fingerprint,workflow_definition_current_fingerprint, andworkflow_definition_matches_currentso operators can see when a long-lived run started on an older definition even before a version marker was committed
This allows new workflows to use the latest code path while existing workflows continue using their original path.
Adding a New Version
Suppose you have an existing workflow that calls prePatchActivity:
use Workflow\V2\Workflow;
use function Workflow\V2\activity;
final class MyWorkflow extends Workflow
{
public function handle()
{
$result = activity(PrePatchActivity::class);
return $result;
}
}
To replace it with postPatchActivity without breaking running workflows:
use Workflow\V2\Workflow;
use Workflow\V2\WorkflowStub;
use function Workflow\V2\{activity, getVersion};
final class MyWorkflow extends Workflow
{
public function handle()
{
$version = getVersion(
'activity-change',
WorkflowStub::DEFAULT_VERSION,
1
);
$result = $version === WorkflowStub::DEFAULT_VERSION
? activity(PrePatchActivity::class)
: activity(PostPatchActivity::class);
return $result;
}
}
When you roll out a deployment that introduces a new getVersion() branch point, keep rotating WORKFLOW_V2_CURRENT_COMPATIBILITY for that build wave. The runtime uses the run's start-time workflow_definition_fingerprint as its primary authority when deciding whether a missing version marker belongs to a fresh execution or to an older run that should stay on DEFAULT_VERSION. The compatibility marker still matters for mixed-fleet worker routing and fires before the fingerprint check. Runs that predate the fingerprint snapshot (no recorded fingerprint) conservatively stay on DEFAULT_VERSION since the runtime cannot verify the definition has not changed.
Adding More Versions
When you need to make additional changes, increment maxSupported:
$version = getVersion(
'activity-change',
WorkflowStub::DEFAULT_VERSION,
2
);
$result = match($version) {
WorkflowStub::DEFAULT_VERSION => activity(PrePatchActivity::class),
1 => activity(PostPatchActivity::class),
2 => activity(AnotherPatchActivity::class),
};
Deprecating Old Versions
After all workflows using an old version have completed, you can drop support by increasing minSupported. This removes the need to maintain old code paths.
// After all DEFAULT_VERSION workflows have completed:
$version = getVersion(
'activity-change',
1, // No longer supporting DEFAULT_VERSION
2
);
$result = match($version) {
1 => activity(PostPatchActivity::class),
2 => activity(AnotherPatchActivity::class),
};
If a workflow with a version older than minSupported tries to replay, it will throw a VersionNotSupportedException. That includes older-compatibility runs whose safe fallback is still WorkflowStub::DEFAULT_VERSION.
If you accidentally reuse the same changeId for a different branch point later, the runtime treats that as a determinism error instead of silently accepting the mismatch. Keep one stable changeId per logical code change.
Multiple Change Points
You can use multiple getVersion() calls in the same workflow for independent changes:
use Workflow\V2\Workflow;
use Workflow\V2\WorkflowStub;
use function Workflow\V2\getVersion;
final class MyWorkflow extends Workflow
{
public function handle(): void
{
$version1 = getVersion('change-1', WorkflowStub::DEFAULT_VERSION, 1);
$version2 = getVersion('change-2', WorkflowStub::DEFAULT_VERSION, 1);
// Each change point is tracked independently
}
}
Important: Each changeId should be unique within a workflow. The chosen version is recorded as typed workflow history and replayed deterministically on later workflow tasks, queries, and Waterline detail views. When Waterline shows no VersionMarkerRecorded entry for a change point on an older run, that means replay stayed on the legacy DEFAULT_VERSION path without backfilling a new marker into existing history. The selected-run detail fields workflow_definition_fingerprint, workflow_definition_current_fingerprint, and workflow_definition_matches_current tell you whether that run started on a different workflow definition than the one your current build can load today.