Durations and deadline
Every case of a workflow can have a schedule: how long each step may take, when the whole case must be finished, and when it will probably be finished. Times are counted in working days (ADR 0017).
$workflow ->defaultDuration(2) // every step: 2 working days ->states([ State::make('draft')->initial(), // the clock starts after the draft State::make('submitted')->duration(1), State::make('in_review')->duration(5) ->whenOverdue(Escalation::make()->remind()->run('decline', 'Not reviewed in time')), State::make('waiting_supplier')->pausesClock(), State::make('approved')->locked(), // default: 2 State::make('ordered')->final()->goal(), // the regular end State::make('cancelled')->final(), ]) ->deadline(Deadline::sumOfDurations()) // the default: 1 + 5 + 2 = 8 working days ->whenAtRisk(Escalation::make()->notify(Actor::creator())) ->whenOverdue(Escalation::make()->notify(Actor::role('Department heads')));Tenants change all durations in the panel after activating the workflow, without code, see Tenant configuration.
| Term | Meaning |
|---|---|
| Duration | working days a state may take: the default of the workflow, or its own |
| Deadline | when the case must be finished; fixed when its clock starts |
| Forecast | when the case will probably reach its goal |
| Goal | the final state that counts as the regular end (goal()) |
| Risk | on_track, at_risk (forecast after the deadline) or overdue (deadline passed) |
Durations
Section titled “Durations”| Method | Meaning |
|---|---|
Definition::defaultDuration(2) |
working days of every state without a duration of its own |
State::duration(5) |
working days of this state (1 to 3650) |
State::pausesClock() |
the clock stops in this state, e.g. “waiting for the customer”; the deadline moves by the time spent there |
State::goal() |
the final state that counts as the regular end; one per workflow |
Initial and final states have no duration. A duration of 5 working days ends at the end of the fifth working day after the entry into the state: entered on Monday 10:00, due on the following Monday 23:59.
Without any duration (in the code or the tenant configuration) and without a deadline, cases have no schedule – everything below is off.
When the clock starts
Section titled “When the clock starts”The clock of a case starts when its record leaves the initial state – a
draft does not count. Definition::clockStartsIn('submitted') names
another state. Cases whose workflow gets durations later (e.g. set by the
tenant) get a clock on the next run of anvil:tick, counted from the
entry into their current state.
Deadline
Section titled “Deadline”| Deadline | Meaning |
|---|---|
Deadline::sumOfDurations() |
the sum of the durations of the states on the expected path to the goal (default) |
Deadline::within(10) |
10 working days after the clock started; tenants can change the days |
Deadline::field('needed_by') |
the date in a column of the record (end of that day); without a date the sum applies |
The deadline is fixed when the clock starts and does not move –
rework (a rejection, a step back) uses up time and makes the delay
visible. Only states that pause the clock move it, by the working days
spent there. Without a goal, sumOfDurations() is not possible.
Expected path and forecast
Section titled “Expected path and forecast”The remaining work of a case is the way from its current state to the goal:
Definition::expectedPath('submitted', 'in_review', 'approved', 'ordered')names the regular way; while a case is on it, the rest of it counts.- Otherwise (or off the path, e.g. after a rejection) the way to the goal with the smallest sum of durations counts. Optional transitions switched off by the tenant are left out.
Forecast = the later of the due date of the current state and the end of today, plus the durations of the remaining states. Steps done early bring the forecast forward; a step that hangs pushes it back every day.
Risk: on_track while the forecast is not after the deadline,
at_risk when it is, overdue when the deadline has passed. Finished
cases keep their deadline and their finish time (“finished on time”,
“finished 2 working days late”).
Example: submitted on Monday 9 November; deadline 1 + 5 + 2 = 8 working days later, Thursday 19 November. The review is finished already on 10 November: approved is due on 12 November, so is the forecast – “5 working days ahead”.
Escalations
Section titled “Escalations”State::make('in_review')->duration(5)->whenOverdue(Escalation::make()->remind()->notify(Actor::slot('escalation', default: Actor::role('Department heads')))->run('decline', 'Not reviewed in time')),
$workflow->whenAtRisk(Escalation::make()->notify(Actor::creator()));$workflow->whenOverdue(Escalation::make()->remind()->notify(Actor::role('Department heads')));Method of Escalation |
Meaning |
|---|---|
remind() |
notifies the actors of the transitions out of the current state |
notify(Actor ...) |
notifies further actors |
run('decline', 'reason') |
runs a transition as the system – only for an overdue state; the transition must start in the state and have no form |
Escalation::none() |
nothing, e.g. to switch off the default reminder |
| When | Default | Once |
|---|---|---|
A state is overdue (State::whenOverdue()) |
remind the actors of the transitions out of the state | per entry into the state |
A case is at risk (whenAtRisk()) |
nothing | per case |
A case is overdue (whenOverdue()) |
nothing | per case |
A failing run is logged and not retried. Deadline and forecast are
shown even without escalations.
Working days
Section titled “Working days”Saturdays, Sundays and public holidays are no working days. Configure the
calendar in app/Config/Anvil.php:
public array $workingDays = [1, 2, 3, 4, 5]; // ISO weekdays, Monday to Fridaypublic ?string $holidayProvider = \Anvil\Calendar\GermanHolidays::class;public ?string $holidayRegion = 'BY'; // the federal statepublic array $closedDays = ['12-24', '12-31', '2026-12-28']; // every year (m-d) or onceGermanHolidays knows the public holidays of Germany and of every federal
state (BW, BY, BE, BB, HB, HH, HE, MV, NI, NW, RP,
SL, SN, ST, SH, TH); holidays that only apply to parts of a
state are left out. Other countries implement
Anvil\Calendar\HolidayProvider (holidays(int $year): array of
Y-m-d => name).
In code: service('workingCalendar')->addWorkingDays($date, 3),
isWorkingDay($date), workingDaysBetween($from, $to).
Task due dates (Task::due()) are working days too; tasks without one
are due with their state.
Processing
Section titled “Processing”php spark anvil:tick (every minute) handles overdue states and the
escalations of cases at risk or overdue, updates forecasts once a day and
gives running cases a clock when durations were set later. Several
servers can run it at the same time.
Show the schedule with the columns and the filter of Lists and detail page.