# workflow run retention (app.Workflow)

> `app.Workflow` declares how long the app keeps FINISHED workflow runs. Without it a completed run is kept for ever, along with everything it owns — its work items, its timers and its transition trail. With it, a run whose terminal state is older than the window is reaped, and its children go with it. A run still in progress is never reaped, whatever its age.

<!-- id: config-workflow · area: config · stability: stable · html: https://osysharp.com/reference/config/workflow/ -->

## Summary        {#summary}
`app.Workflow` declares how long the app keeps **finished** workflow runs. A run that reaches a terminal state still
occupies a row, and so does everything it owns — its work items, its timers, its stored step results and its
transition trail. Nothing removes them unless you say so, so an app accumulates every run it has ever executed.

Declaring a `Retention` window changes that: a run whose completion is older than the window is reaped on a
background sweep, and its children are reaped with it. Two things are guaranteed and neither is configurable — **a
run that has not finished is never reaped, however old it is**, and **with no window declared nothing is ever
reaped**. Deleting an app's data on a default nobody chose is not a decision the platform makes for you.

```osy syntax
app.Workflow = new WorkflowConfig { Retention = TimeSpan.FromDays(90) };
```

## Signature      {#signature}
```osy
app.Workflow = new WorkflowConfig {
  Retention = TimeSpan.FromDays(90),   // (optional) how long a FINISHED run is kept after it completes
};
```

`Retention` takes the same constant `TimeSpan.From…` factory that [`app.Audit`](https://osysharp.com/reference/config/audit/) retention takes —
`FromDays`, `FromHours`, `FromMinutes`, `FromSeconds`. It must be a constant and it must be positive; a computed
value or a bare number is a compile error rather than a window nobody can predict.

## Description    {#description}

### What "finished" means, and why age alone is never enough   {#finished}
The window is measured from the moment a run **completed** — not from when it started, and not from when it was last
touched. A run that succeeded, failed or was cancelled is finished; a run that is still executing or waiting on an
event is work in progress and is out of scope entirely.

This distinction is the whole safety property. A long-running workflow — an annual review, a multi-year warranty, a
contract that waits on a renewal that has not come — can easily be older than any window you would pick, and it must
survive. Age is only ever consulted for a run that has already reached a terminal state, so a live run cannot be
selected at all.

### What goes with the run   {#owned-rows}
A finished run owns the rows that describe how it ran: the work items it opened, the timers governing them, the
results of the steps it executed, and its transition trail. These are **structurally owned** — they describe that one
run and mean nothing without it — so reaping a run reaps them together. There is no state in which the run is gone
and its work items remain pointing at nothing, which is worse than either keeping or removing the lot.

A run that another run still names as its parent is left alone until that child has itself been reaped. Lineage stays
intact; the sweep simply picks it up on a later pass.

### ⚠ It is a CEILING on your workflow audit history   {#audit-ceiling}
`app.Audit.WorkflowAuditRecord.Retention` (see [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/)) sets how long the **transition trail** is kept. The
trail belongs to its run, so **the run window takes it with it**: once the run is reaped its trail is gone, whatever
the audit window says.

That means an audit window **longer** than the run window cannot be honoured, and the compiler refuses the pair
rather than quietly obeying the shorter one:

```text
`app.Audit.WorkflowAuditRecord.Retention` is TimeSpan.FromDays(365), but `app.Workflow.Retention` is
TimeSpan.FromDays(30) — and the run window takes the audit trail with it. A workflow audit event belongs to its run
(the run is reaped, its events go too), so the longer window cannot be honoured. Raise `app.Workflow.Retention` to
at least TimeSpan.FromDays(365), or lower this one.
```

An equal window is fine — the run and its trail expire together. A **shorter** audit window is also fine, and is how
you keep runs longer than you keep their transition detail. If you need the trail to outlive the run, do not set a
run window at all.

### Removing the block stops the reaping   {#removing}
`app.Workflow` is reconciled on every compile, so deleting it from source removes the policy — it does not leave the
last window it ever had quietly in force. For a setting whose job is deleting data, the difference between "we
stopped reaping" and "we go on reaping on a rule nobody can see any more" is the whole point.

### When to reach for it   {#when}
Reach for a window when runs are numerous and short-lived and their history has no ongoing value — a per-request
approval, a per-order fulfilment, a notification flow. Leave it undeclared when the run IS the record: anything you
would expect to look up years later, or anything whose trail answers a compliance question. Keeping data costs
storage; a window you set too tight costs you the answer to a question you have not been asked yet.

## Examples       {#examples}

Ninety days of finished runs, kept bounded without touching anything still in flight:

```osy title="basic" test app=config-workflow-retention
entity Order {
  [Required, MaxLength(200)] string Reference;
  OrderStage Stage = OrderStage.Placed;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

enum OrderStage { Placed, Shipped }

workflow Fulfilment {
  Tracks  = Order.Stage;
  Initial = Placed;
  event Ship();
  state Placed {
    subscribe Ship();
    on Ship { goto Shipped; }
  }
  terminal success Shipped { }
}

app.Workflow = new WorkflowConfig { Retention = TimeSpan.FromDays(90) };
```

Keeping the runs for a year but their transition detail for only a month — the audit window is *shorter*, which the
run window permits:

```osy title="trail-expires-first" test app=config-workflow-trail
[Principal] entity Staff {
  [Required, MaxLength(200)] string Name;
  bool IsAuditor;
}

app.Workflow = new WorkflowConfig { Retention = TimeSpan.FromDays(365) };

app.Audit = new AuditConfig {
  WorkflowAuditRecord = new AuditSurface { Read = user => user.IsAuditor, Retention = TimeSpan.FromDays(30) }
};
```

## See also       {#see-also}
- [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/) — `app.Audit.WorkflowAuditRecord`, the transition trail this window is a ceiling on
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — the run this window measures, and what reaching a terminal state means
- [use](https://osysharp.com/reference/types/use/) — bringing the workflow capability into the app
