Deploy a new version of a workflow
A deployed Workflow Definition is
immutable. To change what a workflow does, you edit its file and raise the
version. In this recipe version 1.0.0, which only created an account, becomes
1.1.0, which also sends a welcome mail. You learn which changes a minor or
patch version may make, because running instances continue on it.
Save the two files as new-version.workflow.ts and
new-version.workflow.case.ts.
import * as v from '@identity-flow/sdk/valibot';import { defineWorkflow } from '@identity-flow/sdk';
const Onboarding = v.object({ employeeId: v.string() });
/** * Version 1.0.0 of this workflow ran only the step `create account`. Version * 1.1.0 is the same definition, edited in place: the version was raised and one * step was added after the existing one. The parameters are unchanged. * * A running instance does not stay on the version it started with. Each time it * continues, it moves to the newest deployed version in the range * `^<its current version>`: from 1.0.0 on, every later minor and patch version * of the same major. Below 1.0.0 the range is narrower; 0.2.0 is not in ^0.1.0. * A minor or patch version must therefore accept every params value the older * versions accepted, and keep the name, kind and order of every step they * recorded. Any other change needs a new major version. */export const onboarding = defineWorkflow( { name: 'onboarding', version: '1.1.0', draft: false, schema: Onboarding }, async (flow) => { const account = await flow.do('create account', () => ({ employeeId: flow.params.employeeId }));
await flow.do('send welcome mail', () => ({ sentTo: flow.params.employeeId }));
return { ...account, welcomeMailSent: true }; },);
// The host deploys only the default export, so one file holds one workflow. The// test imports the named export; both are the same definition.export default onboarding;The host deploys only the default export of a workflow file, and one name
belongs to one file. So a new version replaces the old one in the same file;
you cannot keep 1.0.0 and 1.1.0 of onboarding side by side in your source.
If two files declare the same name, the host logs
workflow bundle name conflict, and which of them it deploys is not defined.
1.0.0 does not disappear. IdentityFlow keeps every deployed definition,
including its code, in its database and never deletes one. After a restart the
program deploys 1.1.0 next to the stored 1.0.0; see
Deploy Workflow Definitions.
New instances start on 1.1.0. Instances already running on 1.0.0 do not stay
there: at their next step they continue on 1.1.0, because 1.1.0 is in the
semver range ^1.0.0. Each time an instance continues, IdentityFlow runs it
with the newest deployed version in the range ^<its current version> and the
same release channel. Below 1.0.0 the range is narrower: 0.2.0 is not in
^0.1.0, and 0.0.4 is not in ^0.0.3.
A minor or patch version must therefore work for the instances that continue on it:
- Parameters. Its params schema accepts every value the older versions accepted. Parameters are validated again each time an instance continues, so a stricter params schema makes every running instance of the older version fail at its next step. Schemas that decode results already recorded must still accept them.
- Recorded steps. Steps that already ran keep their names, their kinds
(
do,sleep,dialog,request,start) and their order. New steps come only after them, or in branches that older instances never reached. Branching on recorded results stays deterministic. A step that replays with a different kind fails. - Everything else needs a new major version. Renaming, removing or
reordering steps, changing a step’s kind, or tightening the params schema
means
2.0.0. Instances on1.xstay on the newest stored1.xversion and keep running after the file holds2.0.0, because IdentityFlow loads their definition from its database.
1.1.0 follows these rules: its parameters are unchanged, and its new step
comes after create account.
import { workflowTest } from '@identity-flow/testing';import { expect } from 'vitest';
import { onboarding } from './new-version.workflow';
// What version 1.0.0 accepted and recorded. Instances started on 1.0.0 continue// on 1.1.0, so 1.1.0 has to accept the same parameters and run the same steps// first. Keep these values from the old version when you raise it.const acceptedBy100 = { employeeId: 'e-2041' };const recordedBy100 = ['create account'];
// A Workflow Test registers the definition it is given directly, and such a// definition never moves to a newer version. So this test checks 1.1.0 against// the values above; it does not show a running 1.0.0 instance switching over.const test = workflowTest({ workflow: onboarding, accounts: { operator: { providerId: 'acme', principals: ['role:hr-operator'] } },});
test('continues what 1.0.0 recorded', async ({ flow, accounts }) => { expect(onboarding.version).toBe('1.1.0');
const instance = await flow.actAs(accounts.operator).start(acceptedBy100); const completed = await flow.waitFor(instance).toBeCompleted();
expect(completed.data).toEqual({ employeeId: 'e-2041', welcomeMailSent: true });
const observed = await flow.observe(instance); const steps = observed.steps.map((step) => step.name); expect(steps.slice(0, recordedBy100.length)).toEqual(recordedBy100); expect(steps).toEqual(['create account', 'send welcome mail']);});The test pins what 1.0.0 accepted and recorded as two constants, and checks
that 1.1.0 accepts those parameters and runs those steps first, under the same
names. Do the same when you raise a version: before you edit the file, copy the
parameters and step names of the current version into the test.
The test does not show an instance switching from 1.0.0 to 1.1.0. A
Workflow Test registers the definition it is given directly, and such a
definition never moves to a newer version.
Run the test with the Workflow Test distribution, as described in Before you start.
Next: Versions and changed bundles for what the program checks at startup, and Replay rules for the rules every workflow follows.