Tempo.Schedule (Tempo Network v1.0.0)

Copy Markdown View Source

Constraint-based project scheduling over Tempo.Network.

A schedule is a set of tasks — each with a duration — joined by dependencies (task B starts no earlier than task A finishes) and bounded by fixed starts and deadlines. solve/1 finds, for every task, the earliest and latest it can run and whether it sits on the critical path. This is the classic project-scheduling / critical path method, expressed as the Simple Temporal Problem Tempo.Network already solves: tasks are time-periods, dependencies are boundary relations, and the solver's propagation is the forward/backward pass.

Example

iex> import Tempo.Sigils
iex> {:ok, plan} =
...>   Tempo.Schedule.new()
...>   |> Tempo.Schedule.task(:design, duration: ~o"P2D", start: ~o"2026-06-01")
...>   |> Tempo.Schedule.task(:build, duration: ~o"P3D", after: :design)
...>   |> Tempo.Schedule.task(:docs, duration: ~o"P1D", after: :design)
...>   |> Tempo.Schedule.task(:ship, duration: ~o"P2D", after: [:build, :docs], deadline: ~o"2026-06-08")
...>   |> Tempo.Schedule.solve()
iex> plan[:ship].early
~o"2026Y6M6D/7D"
iex> plan[:docs].critical?
false

Here design → build/​docs → ship, with ship due by the 8th. The solver schedules ship for the 6th to the 8th, and finds docs has slack (it is not on the critical path) while design, build, and ship are.

What it does not do

Scheduling around a busy calendar — "fit this task into the first free gap, avoiding existing meetings" — is a disjunctive problem ("the task is before that meeting or after it") that lies outside the Simple Temporal Problem. For that, work with the free regions directly using the set operations (Tempo.difference/3, Tempo.intersection/3) and Tempo.IntervalSet.slots/3. Tempo.Schedule is for dependency scheduling, where the constraints compose by conjunction.

Summary

Types

t()

A schedule under construction.

Functions

The critical path of a solved plan — the task ids with no slack, in start order.

An empty schedule.

Solve the schedule, finding each task's early and late schedule.

The project span of a solved plan — the interval from the earliest task start to the latest task finish.

Add a task to the schedule.

Types

t()

@type t() :: %Tempo.Schedule{network: Tempo.Network.t()}

A schedule under construction.

Functions

critical_path(plan)

@spec critical_path(%{optional(term()) => Tempo.Schedule.ScheduledTask.t()}) :: [
  term()
]

The critical path of a solved plan — the task ids with no slack, in start order.

A task is critical when its early and late schedules start together, so any delay to it delays the whole project. Requires a plan with a deadline; without one no task is critical and the list is empty.

Arguments

  • plan is the map returned by solve/1.

Returns

  • the critical task ids, ordered by start.

Examples

iex> import Tempo.Sigils
iex> {:ok, plan} =
...>   Tempo.Schedule.new()
...>   |> Tempo.Schedule.task(:a, duration: ~o"P2D", start: ~o"2026-06-01")
...>   |> Tempo.Schedule.task(:b, duration: ~o"P3D", after: :a, deadline: ~o"2026-06-06")
...>   |> Tempo.Schedule.solve()
iex> Tempo.Schedule.critical_path(plan)
[:a, :b]

new()

@spec new() :: t()

An empty schedule.

Returns

Examples

iex> schedule = Tempo.Schedule.new()
iex> map_size(schedule.network.periods)
0

solve(schedule)

@spec solve(t()) ::
  {:ok, %{optional(term()) => Tempo.Schedule.ScheduledTask.t()}}
  | {:error, :infeasible | Exception.t()}

Solve the schedule, finding each task's early and late schedule.

Arguments

  • schedule is a t/0.

Returns

  • {:ok, plan} where plan is a map of id => t:Tempo.Schedule.ScheduledTask.t/0;

  • {:error, :infeasible} when the dependencies, durations, and bounds cannot all be satisfied; or

  • {:error, reason} for an option or a value task/3 could not read.

Examples

iex> import Tempo.Sigils
iex> {:ok, plan} =
...>   Tempo.Schedule.new()
...>   |> Tempo.Schedule.task(:a, duration: ~o"P2D", start: ~o"2026-06-01")
...>   |> Tempo.Schedule.task(:b, duration: ~o"P3D", after: :a)
...>   |> Tempo.Schedule.solve()
iex> {plan[:a].early, plan[:b].early}
{~o"2026Y6M1D/2D", ~o"2026Y6M3D/5D"}

iex> import Tempo.Sigils
iex> {:error, %ArgumentError{} = error} =
...>   Tempo.Schedule.new()
...>   |> Tempo.Schedule.task(:a, duration: ~o"P2D", earliest: ~o"2026-06-01")
...>   |> Tempo.Schedule.solve()
iex> Exception.message(error)
"Tempo.Schedule.task/3 takes :not_before where 1.x took :earliest."

span(plan)

@spec span(%{optional(term()) => Tempo.Schedule.ScheduledTask.t()}) ::
  Tempo.t() | {:error, Exception.t()}

The project span of a solved plan — the interval from the earliest task start to the latest task finish.

Arguments

  • plan is the map returned by solve/1.

Returns

  • a Tempo.t/0, the interval covering the whole project; or

  • {:error, reason} when the plan has no tasks, or no fixed start or :not_before date to place them by.

Examples

iex> import Tempo.Sigils
iex> {:ok, plan} =
...>   Tempo.Schedule.new()
...>   |> Tempo.Schedule.task(:a, duration: ~o"P2D", start: ~o"2026-06-01")
...>   |> Tempo.Schedule.task(:b, duration: ~o"P3D", after: :a)
...>   |> Tempo.Schedule.solve()
iex> span = Tempo.Schedule.span(plan)
iex> Tempo.Interval.endpoints(span)
{~o"2026Y6M1D", ~o"2026Y6M5D"}

task(schedule, id, options \\ [])

@spec task(t(), term(), keyword()) :: t()

Add a task to the schedule.

An option task/3 does not take, or a value it cannot read, is recorded on the schedule rather than raised, and solve/1 returns it.

Arguments

  • schedule is the schedule to extend.

  • id is any term uniquely identifying the task.

  • options is a keyword list of options.

Options

  • :duration is the task's duration — an exact Tempo.Duration.t/0 or a {min, max} range.

  • :after is a task id, or list of ids, this task depends on: it starts no earlier than each of them finishes.

  • :start fixes the task's start on an exact date.

  • :not_before requires the task to start on or after a date.

  • :deadline requires the task to finish on or before a date.

  • :within is the window the task lies in, a Tempo value or an interval as every :within is: the task starts no earlier than the window does and finishes no later than it ends, so within: ~o"2026-06" is a task done in June.

Returns

  • the schedule with the task added, or with an error recorded.

Examples

iex> import Tempo.Sigils
iex> schedule =
...>   Tempo.Schedule.new()
...>   |> Tempo.Schedule.task(:a, duration: ~o"P2D")
iex> Map.keys(schedule.network.periods)
[:a]