Summary#
In production, work that falls due later — a state's Expire, a Deadline, a reminder, a schedule's next occurrence —
is run by the platform's dispatcher on its own cadence. A test does not wait for it. Workflow.Settle(row) runs
that row's workflow until nothing is due now; Background.Settle() does it for the whole app. Move the clock
first with TestClock.Advance when the work is due later than now.
Signature#
Workflow.Settle(<row>); // that row's run: due timers fire, routes run, until nothing is due now
Background.Settle(); // everything due in the app, including schedule occurrencesDescription#
When do I need it?#
Only for work that waits for a clock. A row a commit creates has already entered its workflow when
UnitOfWork.Commit() returns — an autostarted enter needs no settle (Tracks and Initial (the field a workflow drives)). What a settle adds is the
sweep: every timer due at the test's current time fires once, its route runs, and the run moves on.
Which one?#
| you want | write |
|---|---|
a named row's timer, reminder, Expire or Deadline to fire | Workflow.Settle(row); |
| a schedule's occurrence — the row it makes does not exist yet | Background.Settle(); |
| several rows' runs at once | Background.Settle(); — one sweep, not one settle per row |
Workflow.Settle reads only its row's run: it is cheap, and it cannot fire something that belongs to another row.
What does it not do?#
- It does not move the clock. Work due tomorrow stays due tomorrow;
TestClock.Advance(TimeSpan.FromDays(1))first, then settle. - One settle fires a recurring reminder once, the way a periodic sweep does — settle again to see the next.
- It is test-only. App code has no clock to drain; the dispatcher does it.
using Osysharp.Workflow;
app Holds {
model "**/*.osy";
use Osysharp.Workflow;
}
enum HoldState { Held, Lapsed }
entity Hold {
[MaxLength(80)] string Title;
HoldState Status;
security { allow read, create when IsAnonymous || IsAuthenticated; }
}
workflow HoldLife {
Tracks = Hold.Status;
Autostart = true;
Initial = Held;
state Held {
Expire { Within = TimeSpan.FromDays(1); Accrues = false; }
on Expire { goto Lapsed; }
}
terminal success Lapsed { }
}
[Test]
void A_hold_lapses_once_its_day_has_passed() {
var h = new Hold { Title = "Blue kayak" };
UnitOfWork.Commit();
Workflow.Settle(h); // nothing is due yet
Assert.Equal(HoldState.Held, Hold.Single(x => x.Id == h.Id).Status);
TestClock.Advance(TimeSpan.FromHours(25));
Workflow.Settle(h); // now the Expire is due, and its route runs
Assert.Equal(HoldState.Lapsed, Hold.Single(x => x.Id == h.Id).Status);
}See also#
- TestClock.Advance and TestClock.Set — moving the clock a settle reads
- Remind (milestone reminders) — reminders, asserted on the audit timeline after a settle
- Tracks and Initial (the field a workflow drives) — why an autostarted
enterneeds no settle - Schedule (recurring work) — schedules, settled with
Background.Settle()