Skip to content

Changing Workflows ​

A run can outlive the code that started it: it sleeps for a day, waits for an approval, or is mid-way through when you deploy. When it carries on, it runs under the workflow definition the new code registers, not the one it started under. This page is what happens then, and how to change a workflow safely.

How a run carries on under new code ​

A run's saved results are keyed by step name. When an engine picks a run up, it walks the workflow's steps from the top, as the current code defines them: a step whose name has a saved result is skipped and its result reused; the first step without one runs, and the run continues from there.

So what matters is which names have results, not where the steps sit or what their code does.

ChangeWhat a run already part-way through does
Add a step after the point the run has reachedRuns it, in order. Safe.
Change a step's code, keeping its nameUses the new code for steps it has not reached yet; steps it finished keep their saved results. Safe.
Remove a step, sleep, or waitSkips it. Safe, if nothing later reads its result.
Change the retry or timeout settings of a stepUses the new settings from its next attempt. Safe.
Rename a stepFinds no result under the new name and runs the step again. Renaming charge charges the card a second time.
Add a step before the point the run has reachedRuns the new step late — after the work it was meant to come before.
Change what a step returnsLater steps receive the old shape from runs that finished it before the deploy, whatever their types say.
Change the input schemaRuns keep the input they were enqueued with; it is not checked against the new schema.
Remove a workflow from the engineIts runs are never claimed again. They stay pending, sleeping, or waiting until it is registered once more.

Changing a workflow safely ​

Small changes in place. Adding steps at the end, fixing a step's code, tuning retries, and removing steps nothing depends on are all safe to deploy with runs in flight.

Anything else: a new version beside the old. Give the changed workflow a new name, and keep the old one registered until its runs have finished:

typescript
// The old definition, unchanged, until its runs drain
const orderV1 = createWorkflow({ name: 'order', input: OrderInput })
  .step('charge', charge)
  .step('ship', ship)

// The new one, under a new name
const order = createWorkflow({ name: 'order-v2', input: OrderInput })
  .step('validate', validate)
  .step('take-payment', charge)
  .step('ship', ship)

const engine = createEngine({ storage, workflows: [orderV1, order] })

// New runs go to the new version
await engine.enqueue('order-v2', input)

Runs started under order finish under the code they started with; new runs never see it. Once await engine.countRuns({ workflow: 'order' }) shows nothing pending, running, sleeping, or waiting, delete orderV1. The dashboard lists both versions side by side while they overlap.

Renaming a step without a new version is safe only once no run is part-way through the workflow — for example, after pausing new enqueues and letting every run finish.

Before you deploy ​

  • Does this rename a step, add one before others, or change what a step returns? Then deploy it as a new version.
  • Does a step have an effect that must not repeat? Pass its idempotencyKey to the API it calls, so a repeat — for any reason — is caught where it matters.
  • Are you removing a workflow? Check its runs have finished first.

Released under the MIT License.