# `Tempo.Schedule`
[🔗](https://github.com/kipcole9/tempo/blob/v0.20.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 **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 *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.

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

```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.Slot.t()}} | {:error, :infeasible}
```

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

### Arguments

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

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

* a `t:Tempo.Interval.t/0` covering 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"}

# `task`

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

---

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