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.
app.Workflow = new WorkflowConfig { Retention = TimeSpan.FromDays(90) };Signature#
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 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#
What "finished" means, and why age alone is never enough#
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#
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#
app.Audit.WorkflowAuditRecord.Retention (see audit read access (app.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:
`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#
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#
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#
Ninety days of finished runs, kept bounded without touching anything still in flight:
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:
[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#
- audit read access (app.Audit) —
app.Audit.WorkflowAuditRecord, the transition trail this window is a ceiling on - Workflow.Run (start a workflow) — the run this window measures, and what reaching a terminal state means
- use — bringing the workflow capability into the app