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?
falseHere design → build/docs → ship, 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
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
@type t() :: %Tempo.Schedule{network: Tempo.Network.t()}
A schedule under construction.
Functions
@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
planis the map returned bysolve/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]
@spec new() :: t()
An empty schedule.
Returns
- an empty
t/0.
Examples
iex> schedule = Tempo.Schedule.new()
iex> map_size(schedule.network.periods)
0
@spec solve(t()) :: {:ok, %{optional(term()) => Tempo.Schedule.Slot.t()}} | {:error, :infeasible}
Solve the schedule, finding each task's early and late position.
Arguments
scheduleis at/0.
Returns
{:ok, plan}whereplanis a map ofid => 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"}
@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
planis the map returned bysolve/1.
Returns
- a
Tempo.Interval.t/0covering the whole project.
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"}
Add a task to the schedule.
Arguments
scheduleis the schedule to extend.idis any term uniquely identifying the task.
Options
:durationis the task's duration — an exactTempo.Duration.t/0or a{min, max}range.:afteris a task id, or list of ids, this task depends on: it starts no earlier than each of them finishes.:startpins the task's start to an exact date (an anchor).:earliestrequires the task to start on or after a date.:deadlinerequires the task to finish on or before a date.:withinis 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]