# `Tempo.Schedule`
[🔗](https://github.com/elixir-tempo/tempo_network/blob/v1.0.0/lib/tempo/schedule.ex#L1)

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.

# `t`

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

A schedule under construction.

# `critical_path`

```elixir
@spec critical_path(%{optional(term()) =&gt; 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`

```elixir
@spec new() :: t()
```

An empty schedule.

### Returns

* an empty `t:t/0`.

### Examples

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

# `solve`

```elixir
@spec solve(t()) ::
  {:ok, %{optional(term()) =&gt; Tempo.Schedule.ScheduledTask.t()}}
  | {:error, :infeasible | Exception.t()}
```

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

### Arguments

* `schedule` is a `t: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`

```elixir
@spec span(%{optional(term()) =&gt; 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 `t: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`

```elixir
@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 `t: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]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
