Tempo.Schedule (Tempo v0.20.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 anchors 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 tightening 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].start
~o"2026Y6M6D"
iex> plan[:docs].critical?
false

Here designbuild/​docsship, with ship due by the 8th. The solver schedules ship to start on the 6th, 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/2, Tempo.intersection/2) 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 position.

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.Slot.t()}) :: [term()]

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

A task is critical when its earliest and latest starts coincide, 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.Slot.t()}} | {:error, :infeasible}

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

Arguments

  • schedule is a t/0.

Returns

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

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

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].start, plan[:b].start}
{~o"2026Y6M1D", ~o"2026Y6M3D"}

span(plan)

@spec span(%{optional(term()) => Tempo.Schedule.Slot.t()}) :: Tempo.Interval.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

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> {span.from, span.to}
{~o"2026Y6M1D", ~o"2026Y6M6D"}

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

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

Add a task to the schedule.

Arguments

  • schedule is the schedule to extend.

  • id is any term uniquely identifying the task.

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 pins the task's start to an exact date (an anchor).

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

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

  • :within is a {earliest_start, latest_finish} window the task must fall inside.

Returns

  • the schedule with the task added.

Examples

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