# Deploying while workflows are running

> A workflow run can outlive the deploy that started it. Deploying with --new-version freezes the code and data shape the running app has, so runs already in flight finish on what they started with while new runs use what you just deployed.

<!-- id: project-app-versions · area: project · stability: stable · html: https://osysharp.com/reference/project/app-versions/ -->

## Summary        {#summary}
A workflow run can last far longer than the gap between two deploys — an approval that waits three days, an onboarding
saga that waits for a signature, a fan-out that waits on a hundred children. So a deploy will land **while runs are
partway through**, and the question that decides whether your app is correct is: *when that run wakes up, whose code
does it execute?*

Deploy with `--new-version` and the answer is **its own**. The deploy is frozen as a new app version; every run already
in flight goes on resolving and executing against the code and the data shape it started under, and every run started
after the deploy uses what you just deployed. Both versions run side by side until the older runs drain.

## Signature      {#signature}
```osy syntax
osy compile --new-version           // local
osyrin app compile --new-version    // a deployed app
```

## Description    {#description}

### What a version is   {#what}
An app version is a frozen snapshot of what the app **is**: its entities, functions, workflows, screens, and the shape
of its data. Deploying with `--new-version` records one and makes it current. Nothing is copied and nothing is
duplicated — your rows stay exactly where they are, in one place, shared by every version. What is frozen is the
*description* of them.

That split is what makes the guarantee cheap. A run pinned to an older version reads and writes the same rows as
everything else; it just reads them through the shape it was compiled against.

### What a run in flight keeps   {#in-flight}
A run that was already going when you deployed keeps:

- **its logic** — the body of the workflow, and of every function that workflow calls, exactly as it was written when
  the run started. Editing what a step does will not change what a run halfway through that step is going to do;
- **its data shape** — the properties it knew about. A property you removed in the new version is still there for that
  run, and its column is retained until no run needs it any more.

A run started *after* the deploy gets the new logic and the new shape. Nothing you do to source affects a run that has
already begun.

### When you need the flag   {#when-needed}
- **Long-running workflows.** Anything that waits: an approval, a scheduled reminder, a saga waiting on a child, an
  external callback. The longer it waits, the more likely a deploy lands under it.
- **Any change to a workflow, or to a function a workflow calls.** Body edits count — changing what a step *does* is
  precisely the change a run in flight must not see.

You do not need it for a change nothing is waiting on. Deploying a new screen, a new report or an unrelated entity is
an ordinary deploy.

### When it happens without you asking   {#automatic}
A **production** app decides for itself: it freezes a version when the change would affect a run in flight, and
otherwise deploys in place. `--new-version` overrides that and always freezes one.

A **development** app never freezes a version unless you ask. That keeps the inner loop fast — you edit, compile, and
run against exactly what you last wrote. Pass `--new-version` when you specifically want to rehearse the upgrade: park
a run, deploy, and watch it finish on its old body.

### Seeing what versions you have   {#listing}

```console
$ osy versions            # local
$ osyrin app versions     # a deployed app
```

```text
NewAdmin — application versions
╭─────────┬────────┬───────────┬──────────────────────────┬────────────────────────╮
│ Version │ Schema │ In flight │ Oldest run               │ State                  │
├─────────┼────────┼───────────┼──────────────────────────┼────────────────────────┤
│ 1.0.0   │ app    │         2 │ 2026-07-25 16:47 (3h ago)│ held by runs in flight │
│ 1.1.0   │ app_v2 │         0 │ —                        │ current                │
╰─────────┴────────┴───────────┴──────────────────────────┴────────────────────────╯
```

**In flight** is the number that matters. It is why a version is still there, and it is the only thing standing between
an old version and being cleaned up. `Oldest run` tells you how long you have been waiting for the last stragglers to
finish. Add `--json` for a machine-readable form.

### Old versions clean themselves up   {#reclaiming}

You do not accumulate a version per deploy. Once **nothing is running against** a version any more, the next deploy
reclaims it, and `versions` shows it as `reclaimed`. Two versions are never touched:

- the **current** one, which new runs start on;
- **any version with a run still in flight** — dropping it would strand that run with nothing to resolve against, which
  is the exact failure versioning exists to prevent.

So the usual steady state is one version, plus however many are still finishing work. A long-waiting run is the normal
reason to see an older version hanging around, and the `In flight` column tells you which one.

Reclaiming a version also eventually reclaims the **columns of properties you removed**. Deleting a property does not
drop its column straight away: runs on older versions may still be reading and writing it. Once every version that
declared the property is gone, the column can go too — a separate, explicit step, because unlike a version schema a
column holds the only copy of its data. Only columns whose removal your app actually recorded are taken; anything else
that turns up on a table is reported to you and left alone.

Removing a property or an entity is itself a change you have to acknowledge — see
[Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/).

### The one change that is refused   {#refused}
Changing the stored **type**, **width** or **precision** of a property that already exists is refused on an app with
data, whether or not you version it. There is no shape a column can take that is simultaneously the old type for a run
still pinned to it and the new type for the code you just deployed.

Evolve the type additively instead:

1. add a **new** property of the new type — an additive change deploys cleanly and disturbs nothing;
2. copy the data across;
3. once every reader **and writer** uses the new property, remove the old one — a removal like any other, so it needs
   its one line of acknowledgement (see [Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/)).

> While both properties exist, a write to the old one is **not** reflected in the new one. Move all writes over before
> the final copy, or write to both during the transition — anything written to the old property in that window is lost.

Removing a **required** property in step 3 makes its retained data optional. It has to: the version you just deployed no
longer knows about the property, so it has nothing to put there, and rows created from now on simply leave it empty. A
run still pinned to an older version will therefore find that value missing on any row created after the removal, even
though the version it was compiled against believes the property is always present. Finish the cutover — step 2 — before
anything you care about starts depending on the old property being filled in.

### Deploying is still one command   {#deploy}
Everything else a deploy does is unchanged when it freezes a version: your icons, artwork, control packages and
per-environment configuration are part of the deploy and land with it, and the app's generated API description is
refreshed to match what you just shipped.

## Examples       {#examples}

A workflow whose run can easily outlive a deploy — it waits for a human:

```osy title="a run that will still be waiting when you deploy" test app=app-versions
enum ApprovalStage { Pending, Approved, Rejected }

entity Invoice {
  [Required, MaxLength(120)] string Description;
  [Required] decimal Amount;
  [MaxLength(200)] string? Outcome;
  ApprovalStage Stage = ApprovalStage.Pending;
}

workflow ExpenseApproval {
  Tracks = Invoice.Stage;
  Initial = Pending;

  event Decide();

  state Pending {
    subscribe Decide();
    on Decide {
      when (this.Item.Amount <= 500) {
        this.Item.Outcome = "auto-approved under the original policy";
        goto Approved;
      }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal success Rejected { }
}
```

Now raise the auto-approval threshold to `1000`, reword the outcome, and deploy:

```bash
osy compile --new-version
```

An invoice already sitting in `Pending` when that deploy landed is still judged at `500` when the decision finally
arrives, and still writes *"auto-approved under the original policy"* — it finishes under the policy it entered. An
invoice submitted after the deploy is judged at `1000`. Neither one had to know the other existed.

## See also       {#see-also}
- [Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/) — renaming or removing something that already holds rows
- [Stopping runs after a bad deploy](https://osysharp.com/reference/project/cancel-runs/) — stopping runs that are still executing a version you want rid of
- [app.osy](https://osysharp.com/reference/project/manifest/) — what a deploy is built from
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting a child workflow and waiting for it
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — long-running sagas, the runs most likely to span a deploy
