Skip to content

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)
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.

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 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.

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”.

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.

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 Friday
public ?string $holidayProvider = \Anvil\Calendar\GermanHolidays::class;
public ?string $holidayRegion = 'BY'; // the federal state
public array $closedDays = ['12-24', '12-31', '2026-12-28']; // every year (m-d) or once

GermanHolidays 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.

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.