Agenda (Agenda v0.1.0)

Copy Markdown View Source

Resource-constrained scheduling for Tempo.

Tempo answers when is this free? This library answers what should I book, and where? — it adds the named resources, the attributes describing them, the places containing them, and the requirements a session places on them.

The vocabulary

  • Resource — a named thing that can be allocated. People and rooms differ only in their attributes. See Agenda.Resource.

  • Place — a container of resources and other places, forming a tree. Travel between resources is derived from the tree rather than configured. See Agenda.Place.

  • Requirement — what a session demands, written in the predicate vocabulary of Agenda.Predicate. See Agenda.Requirement.

  • Session — something to be held: how long it runs, the window it must fall inside, and what it demands of resources. A session is a request, not a booking. See Agenda.Session.

  • Programme — sessions to be laid out together, with whatever relates them: tracks, orderings, preferences and interests. See Agenda.Programme.

  • Track — sessions that may not overlap each other, and that somebody following them must be able to travel between. Not overlapping is intrinsic to a track rather than an option on one: a set of sessions that may collide is already expressible, as a list. See Agenda.Track.

  • Interest — one resource would like a session with another. The demand side of the model: where interest is returned, meetings/3 writes the sessions. See Agenda.Interest.

  • Arrangement — one workable way to hold a session: a particular interval, and the particular resources it would take. A proposal rather than a commitment — plan/3 returns many, and none of them is booked. See Agenda.Arrangement.

  • Allocation — one resource, held for one session, over one interval. The unit a ledger stores, and what a :tag labels. See Agenda.Allocation.

  • Ledger — the record of what is allocated and to whom, and the only authority on it. Nothing stores availability: free/2 is open hours minus what the ledger holds, computed when asked, so a booking cannot drift out of step with the calendar it came from.

    A ledger is an ordinary immutable value rather than a running store, and that is deliberate — it is what lets arrange/3 fork one, try a layout, abandon it and try another. Durable shared state belongs in whatever persists the answer, not here. See Agenda.Ledger.

Reading a match aloud

iex> import Agenda.Predicate
iex> boardroom = Agenda.resource("Boardroom", seats: 8, video_conferencing: true)
iex> small = Agenda.resource("Meeting room 2", seats: 4)
iex> needs_a_room = Agenda.needs(:room, seats: at_least(8), video_conferencing: true)
iex> Agenda.eligible(needs_a_room, [boardroom, small]) |> Enum.map(& &1.name)
["Boardroom"]

"Of the two rooms, only the boardroom seats eight and has video conferencing."

And when a resource does not qualify, the reason is a sentence:

iex> import Agenda.Predicate
iex> small = Agenda.resource("Meeting room 2", seats: 4)
iex> needs_a_room = Agenda.needs(:room, seats: at_least(8))
iex> Agenda.explain(needs_a_room, small)
"Meeting room 2: seats is 4 — needs at least 8"

Status

First release. Resources and places, matching by description or by name, derived availability, single-session planning, whole-programme arrangement, ordering, tracks, load limits, preferences, the allocation ledger, recurrence, reconciliation, and meetings generated from mutual interest are all here, and each has a guide.

Two things are deliberately outside it. Booking — durable shared state, a clock, and concurrent claims — belongs in a sibling library, because the arranger's freedom to fork and abandon ledgers depends on a ledger being an ordinary value. And holiday calendars are accepted as data rather than resolved here, since jurisdiction rules have far broader use than scheduling. An adapter for Ash is planned; see the plan.

Summary

Functions

Find a placement for every session in a programme. Delegates to Agenda.Arranger.arrange/3.

Rebuild what a ledger holds as arrangements, ready to pin. Delegates to Agenda.Ledger.arrangements/3.

What each resource is already claimed for. Delegates to Agenda.Ledger.busy/2.

Book one resource over one interval, refusing what it cannot honour. Delegates to Agenda.Ledger.claim/4.

Turn a session's hold into a firm allocation. Delegates to Agenda.Ledger.confirm/2.

The smallest set of things that cannot hold together — sessions for a programme, demands for a session.

How many allocations a ledger holds. Delegates to Agenda.Ledger.count/1.

Keep only free time inside any of constraints. Delegates to Agenda.Refine.during_any/2.

The resources satisfying requirement. Delegates to Agenda.Requirement.eligible/2.

Expand a repeating session into its occurrences. Delegates to Agenda.Series.expand/3.

Drop every hold that has lapsed by a moment. Delegates to Agenda.Ledger.expire/2.

Describe an arrangement, a partial layout, or an infeasible result as a sentence.

Why resource does or does not satisfy requirement, as a sentence.

What each preference contributed, as sentences.

When a resource is open and not already taken. Delegates to Agenda.Availability.free/2.

Read open hours from an RFC 7953 VAVAILABILITY. Delegates to Agenda.Availability.from_ical/1.

Claim resources tentatively, until a moment. Delegates to Agenda.Ledger.hold/3.

Every allocation that is still only a hold. Delegates to Agenda.Ledger.holds/1.

Register that one resource would like a session with another. Delegates to Agenda.Programme.interest/3.

Invite resources who may attend but are not required. Delegates to Agenda.Session.invite/3.

Keep only windows of at least duration. Delegates to Agenda.Refine.lasting_at_least/2.

An empty ledger. Delegates to Agenda.Ledger.new/0.

Turn returned interest into sessions. Delegates to Agenda.Programme.meetings/3.

Build an attribute requirement. Delegates to Agenda.Requirement.new/2.

Keep only free time inside constraint. Delegates to Agenda.Refine.only_during/2.

Set when a resource is open. Delegates to Agenda.Availability.open/2.

Open hours on a resource, raising on a pattern it cannot read. Delegates to Agenda.Availability.open!/2.

Build a place. Delegates to Agenda.Place.new/2.

Rank the ways a session could be held. Delegates to Agenda.Planner.plan/3.

Require that one session finishes before another starts. Delegates to Agenda.Programme.precede/4.

Add a soft constraint to a programme. Delegates to Agenda.Programme.prefer/3.

Build a programme. Delegates to Agenda.Programme.new/2.

Require that a delegate can get between consecutive track sessions. Delegates to Agenda.Track.reachable/2.

What would change if a session moved. Delegates to Agenda.Ledger.diff/3.

Whether a resource's claims account for the time it owed. Delegates to Agenda.Reconciliation.reconcile/3.

Record one resource over one interval, checking nothing. Delegates to Agenda.Ledger.record/4.

Free everything a session holds. Delegates to Agenda.Ledger.release/2.

Free everything held by a whole series. Delegates to Agenda.Ledger.release_series/3.

Build a resource. Delegates to Agenda.Resource.new/2.

Build a requirement naming specific resources. Delegates to Agenda.Requirement.roster/2.

What a layout costs against a programme's preferences.

Build a session. Delegates to Agenda.Session.new/2.

Build a track — sessions that cannot clash with each other. Delegates to Agenda.Track.new/2.

How long it takes to get from one resource to another.

Functions

allocate(ledger, arrangement, options \\ [])

@spec allocate(Agenda.Ledger.t(), Agenda.Arrangement.t(), keyword()) ::
  {:ok, Agenda.Ledger.t()}

Record an arrangement. Delegates to Agenda.Ledger.allocate/3.

Examples

iex> import Tempo.Sigils
iex> arrangement = %Agenda.Arrangement{
...>   session: "Review",
...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...>   allocations: %{room: [Agenda.resource("Boardroom")]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement)
iex> Agenda.count(ledger)
1

Tagging what the time was for:

iex> import Tempo.Sigils
iex> arrangement = %Agenda.Arrangement{
...>   session: "Discovery",
...>   interval: ~o"2026-06-16T09:00:00/2026-06-16T17:00:00",
...>   allocations: %{consultant: [Agenda.resource("Dana")]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement, tag: {:project, "ACME"})
iex> ledger |> Agenda.Ledger.to_list() |> Enum.map(& &1.tag)
[project: "ACME"]

arrange(programme, pool, options \\ [])

Find a placement for every session in a programme. Delegates to Agenda.Arranger.arrange/3.

Examples

iex> room = Agenda.resource("Hall", seats: 100)
iex> {:ok, room} = Agenda.open(room, "2026-09-15T09:00:00/2026-09-15T11:00:00")
iex> talk = Agenda.session("Keynote", duration: "PT1H")
...>         |> Agenda.Session.needs(:room, seats: 100)
iex> programme =
...>   Agenda.programme("Conf", across: "2026-09-15/2026-09-16")
...>   |> Agenda.Programme.add_session(talk)
iex> {:ok, [only]} = Agenda.arrange(programme, [room])
iex> only.session
"Keynote"

arrangements(ledger, pool, options \\ [])

@spec arrangements(Agenda.Ledger.t(), [Agenda.Resource.t()], keyword()) ::
  {:ok, [Agenda.Arrangement.t()]} | {:error, Agenda.Infeasible.t()}

Rebuild what a ledger holds as arrangements, ready to pin. Delegates to Agenda.Ledger.arrangements/3.

Examples

iex> import Tempo.Sigils
iex> boardroom = Agenda.resource("Boardroom")
iex> arrangement = %Agenda.Arrangement{
...>   session: "Review",
...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...>   allocations: %{room: [boardroom]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement)
iex> {:ok, [pinned]} = Agenda.arrangements(ledger, [boardroom])
iex> pinned.session
"Review"

busy(ledger, options \\ [])

@spec busy(
  Agenda.Ledger.t(),
  keyword()
) :: %{optional(String.t()) => [Tempo.Interval.t()]}

What each resource is already claimed for. Delegates to Agenda.Ledger.busy/2.

Examples

iex> Agenda.busy(Agenda.ledger())
%{}

claim(ledger, resource, interval, options \\ [])

@spec claim(
  Agenda.Ledger.t(),
  Agenda.Resource.t(),
  Agenda.Availability.pattern(),
  keyword()
) ::
  {:ok, Agenda.Ledger.t()} | {:error, term()}

Book one resource over one interval, refusing what it cannot honour. Delegates to Agenda.Ledger.claim/4.

record/4 writes down what happened; claim/4 books what is to happen, and checks. See Agenda.Ledger.claim/4 for why a timesheet wants the first and a schedule wants the second.

Examples

iex> import Tempo.Sigils
iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
iex> {:ok, ledger} = Agenda.claim(Agenda.ledger(), dana, ~o"2026-08-10T09:00:00/2026-08-10T12:00:00", tag: {:project, "ACME"})
iex> ledger |> Agenda.Ledger.to_list() |> Enum.map(& &1.tag)
[project: "ACME"]

iex> import Tempo.Sigils
iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
iex> Agenda.claim(Agenda.ledger(), dana, ~o"2026-08-10T16:00:00/2026-08-10T19:00:00")
{:error, "Dana is not open for 2026Y8M10DT17H0M0S/T19H0M0S"}

confirm(ledger, session)

@spec confirm(Agenda.Ledger.t(), String.t()) :: {:ok, Agenda.Ledger.t()}

Turn a session's hold into a firm allocation. Delegates to Agenda.Ledger.confirm/2.

Examples

iex> {:ok, ledger} = Agenda.confirm(Agenda.ledger(), "Review")
iex> Agenda.count(ledger)
0

conflict(subject, pool, options \\ [])

@spec conflict(
  Agenda.Programme.t() | Agenda.Session.t(),
  [Agenda.Resource.t()],
  keyword()
) ::
  {:ok, [term()]} | :none

The smallest set of things that cannot hold together — sessions for a programme, demands for a session.

Delegates to Agenda.Arranger.conflict/3 or Agenda.Planner.conflict/3 according to what it is given. Reach for it when arrange/3 or plan/3 has failed and the question is what to change.

Arguments

Options

Returns

  • :none when there is nothing to explain; or

  • {:ok, conflict} — a minimal set of session names, or of demands.

Examples

iex> room = Agenda.resource("Hall", seats: 100)
iex> {:ok, room} = Agenda.open(room, "2026-09-15T09:00:00/2026-09-15T10:00:00")
iex> talk = fn name ->
...>   Agenda.session(name, duration: "PT1H", window: "2026-09-15/2026-09-16")
...>   |> Agenda.Session.needs(:room, seats: 100)
...> end
iex> programme =
...>   Agenda.programme("Conf")
...>   |> Agenda.Programme.add_session(talk.("Keynote"))
...>   |> Agenda.Programme.add_session(talk.("Deep dive"))
iex> Agenda.conflict(programme, [room])
{:ok, ["Keynote", "Deep dive"]}

count(ledger)

@spec count(Agenda.Ledger.t()) :: non_neg_integer()

How many allocations a ledger holds. Delegates to Agenda.Ledger.count/1.

Examples

iex> Agenda.count(Agenda.ledger())
0

during_any(free, constraints)

@spec during_any(Agenda.Refine.refinable(), [Agenda.Availability.pattern()]) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}

Keep only free time inside any of constraints. Delegates to Agenda.Refine.during_any/2.

Examples

iex> {:ok, room} = Agenda.open(Agenda.resource("R"), "2027-03-02T09:00:00/2027-03-02T17:00:00")
iex> {:ok, staffed} =
...>   room
...>   |> Agenda.free(within: "2027-03-02/2027-03-03")
...>   |> Agenda.during_any(["2027-03-02T09:00:00/2027-03-02T10:00:00"])
iex> Tempo.IntervalSet.count(staffed)
1

eligible(requirement, candidates)

The resources satisfying requirement. Delegates to Agenda.Requirement.eligible/2.

Examples

iex> boardroom = Agenda.resource("Boardroom", seats: 8)
iex> Agenda.eligible(Agenda.needs(:room, seats: 8), [boardroom])
...> |> Enum.map(& &1.name)
["Boardroom"]

every(session, pattern, options \\ [])

@spec every(Agenda.Session.t(), Agenda.Availability.pattern(), keyword()) ::
  {:ok, [Agenda.Session.t()]} | {:error, term()}

Expand a repeating session into its occurrences. Delegates to Agenda.Series.expand/3.

Examples

iex> standup = Agenda.session("Stand-up", duration: "PT15M")
iex> {:ok, occurrences} = Agenda.every(standup, "R3/2027-03-02T09:00:00/P1W")
iex> length(occurrences)
3

expire(ledger, now)

@spec expire(Agenda.Ledger.t(), Agenda.Availability.pattern()) ::
  {:ok, Agenda.Ledger.t()} | {:error, term()}

Drop every hold that has lapsed by a moment. Delegates to Agenda.Ledger.expire/2.

Nothing expires on its own — this is what advances time, and it takes the moment as an argument so that no function in this library reads a clock.

Examples

iex> {:ok, ledger} = Agenda.expire(Agenda.ledger(), "2026-06-15T10:15:00")
iex> Agenda.count(ledger)
0

explain(arrangement)

Describe an arrangement, a partial layout, or an infeasible result as a sentence.

Arguments

Returns

  • a sentence.

Examples

iex> reason = Agenda.Infeasible.new("Review", ["no room seats 8"])
iex> Agenda.explain(reason)
"Review cannot be held: no room seats 8"

iex> reason = Agenda.Infeasible.new("Workshop", ["no room seats 8"])
iex> Agenda.explain(Agenda.Layout.new("Conf", [], [reason]))
"Conf: 0 of 1 sessions placed. Workshop cannot be held: no room seats 8"

explain(requirement, resource)

Why resource does or does not satisfy requirement, as a sentence.

Arguments

Returns

  • a sentence naming the resource and every unmet attribute, or stating that it qualifies.

Examples

iex> import Agenda.Predicate
iex> boardroom = Agenda.resource("Boardroom", seats: 8)
iex> Agenda.explain(Agenda.needs(:room, seats: at_least(8)), boardroom)
"Boardroom qualifies"

explain_score(arrangements, programme, options \\ [])

@spec explain_score(
  [Agenda.Arrangement.t()] | Agenda.Layout.t(),
  Agenda.Programme.t(),
  keyword()
) :: [
  String.t()
]

What each preference contributed, as sentences.

A score is a number, and a number says a layout is worse without saying how. This says how.

Arguments

Options

  • :pool is the resources the layout drew on. The default is [].

Returns

  • one sentence per preference, in the order they were declared.

Examples

iex> {:ok, programme} =
...>   Agenda.Programme.prefer(Agenda.programme("Conf"), :room_changes, weight: 10)
iex> Agenda.explain_score([], programme)
["room_changes: 0 × 10 = 0"]

free(resource, options)

@spec free(
  Agenda.Resource.t(),
  keyword()
) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}

When a resource is open and not already taken. Delegates to Agenda.Availability.free/2.

Examples

iex> boardroom = Agenda.resource("Boardroom")
iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T17:00:00")
iex> {:ok, free} = Agenda.free(boardroom, within: "2026-06-15/2026-06-16")
iex> Tempo.IntervalSet.count(free)
1

from_ical(ics)

@spec from_ical(String.t()) ::
  {:ok, Agenda.Availability.imported()}
  | {:error, :ical_not_available | String.t()}

Read open hours from an RFC 7953 VAVAILABILITY. Delegates to Agenda.Availability.from_ical/1.

This is what a CalDAV server hands you when asked when someone is available. The result is a pattern for open/2.

Examples

iex> ics = """
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> BEGIN:VAVAILABILITY
...> UID:consulting-room
...> DTSTAMP:20260601T000000Z
...> BEGIN:AVAILABLE
...> UID:tuesdays
...> DTSTAMP:20260601T000000Z
...> DTSTART:20260602T090000Z
...> DTEND:20260602T170000Z
...> RRULE:FREQ=WEEKLY;BYDAY=TU
...> END:AVAILABLE
...> END:VAVAILABILITY
...> END:VCALENDAR
...> """
iex> {:ok, hours} = Agenda.from_ical(ics)
iex> {:ok, room} = Agenda.open(Agenda.resource("Consulting room"), hours)
iex> {:ok, free} = Agenda.free(room, within: "2026-06-01/2026-07-01")
iex> Tempo.IntervalSet.count(free)
5

hold(ledger, arrangement, options)

@spec hold(Agenda.Ledger.t(), Agenda.Arrangement.t(), keyword()) ::
  {:ok, Agenda.Ledger.t()} | {:error, term()}

Claim resources tentatively, until a moment. Delegates to Agenda.Ledger.hold/3.

Examples

iex> import Tempo.Sigils
iex> boardroom = Agenda.resource("Boardroom")
iex> arrangement = %Agenda.Arrangement{
...>   session: "Review",
...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...>   allocations: %{room: [boardroom]}
...> }
iex> {:ok, ledger} = Agenda.hold(Agenda.ledger(), arrangement,
...>                   until: "2026-06-15T10:15:00")
iex> Agenda.holds(ledger) |> length()
1

holds(ledger)

@spec holds(Agenda.Ledger.t()) :: [Agenda.Allocation.t()]

Every allocation that is still only a hold. Delegates to Agenda.Ledger.holds/1.

Examples

iex> Agenda.holds(Agenda.ledger())
[]

interest(programme, from, to)

@spec interest(
  Agenda.Programme.t(),
  Agenda.Resource.t() | String.t(),
  Agenda.Resource.t() | String.t()
) ::
  {:ok, Agenda.Programme.t()} | {:error, term()}

Register that one resource would like a session with another. Delegates to Agenda.Programme.interest/3.

Interest is one-directional. A meeting is held where it is returned, so both parties must say so — see meetings/3.

Examples

iex> {:ok, programme} = Agenda.interest(Agenda.programme("Show"), "Kim", "Harbour")
iex> {:ok, programme} = Agenda.interest(programme, "Harbour", "Kim")
iex> Agenda.Interest.mutual(programme.interests)
[{"Harbour", "Kim"}]

invite(session, role, resources)

Invite resources who may attend but are not required. Delegates to Agenda.Session.invite/3.

The counterpart to roster/2. An invitee never affects whether a session can be held, only which time is best: Agenda.plan/3 scores a placement higher when more of them are free, and the arrangement records which of them it suits. They are not allocated — an optional attendee that could cost some other session its placement would not be optional.

Examples

iex> bob = Agenda.resource("Bob")
iex> session = Agenda.session("Review", duration: "PT1H")
iex> Agenda.invite(session, :optional, [bob]).invitees |> Keyword.keys()
[:optional]

lasting_at_least(free, duration)

@spec lasting_at_least(Agenda.Refine.refinable(), Agenda.Availability.pattern()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}

Keep only windows of at least duration. Delegates to Agenda.Refine.lasting_at_least/2.

Examples

iex> {:ok, room} = Agenda.open(Agenda.resource("R"), "2027-03-02T09:00:00/2027-03-02T09:30:00")
iex> {:ok, usable} =
...>   room
...>   |> Agenda.free(within: "2027-03-02/2027-03-03")
...>   |> Agenda.lasting_at_least("PT1H")
iex> Tempo.IntervalSet.empty?(usable)
true

ledger()

@spec ledger() :: Agenda.Ledger.t()

An empty ledger. Delegates to Agenda.Ledger.new/0.

Returns

Examples

iex> Agenda.count(Agenda.ledger())
0

meetings(programme, pool, options \\ [])

@spec meetings(Agenda.Programme.t(), [Agenda.Resource.t()], keyword()) ::
  {:ok, Agenda.Programme.t()} | {:error, term()}

Turn returned interest into sessions. Delegates to Agenda.Programme.meetings/3.

One session per mutually interested pair, each rostering both parties, so nobody being in two places at once is the constraint the library already enforces rather than one anybody writes.

Examples

iex> kim = Agenda.resource("Kim")
iex> harbour = Agenda.resource("Harbour")
iex> programme = Agenda.programme("Show", across: "2027-06-15/2027-06-17")
iex> {:ok, programme} = Agenda.interest(programme, kim, harbour)
iex> {:ok, programme} = Agenda.interest(programme, harbour, kim)
iex> {:ok, programme} = Agenda.meetings(programme, [kim, harbour], duration: "PT15M")
iex> length(programme.sessions)
1

needs(name, predicates \\ [])

@spec needs(
  atom(),
  keyword()
) :: Agenda.Requirement.t()

Build an attribute requirement. Delegates to Agenda.Requirement.new/2.

Examples

iex> Agenda.needs(:room, seats: 8).name
:room

only_during(free, constraint)

@spec only_during(Agenda.Refine.refinable(), Agenda.Availability.pattern()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}

Keep only free time inside constraint. Delegates to Agenda.Refine.only_during/2.

Examples

iex> {:ok, room} = Agenda.open(Agenda.resource("R"), "2027-03-02T09:00:00/2027-03-02T17:00:00")
iex> {:ok, clinic} =
...>   room
...>   |> Agenda.free(within: "2027-03-02/2027-03-03")
...>   |> Agenda.only_during("2027-03-02T13:00:00/2027-03-02T16:00:00")
iex> Tempo.IntervalSet.count(clinic)
1

open(resource, pattern)

@spec open(Agenda.Resource.t(), Agenda.Availability.pattern()) ::
  {:ok, Agenda.Resource.t()} | {:error, term()}

Set when a resource is open. Delegates to Agenda.Availability.open/2.

Examples

iex> boardroom = Agenda.resource("Boardroom")
iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T17:00:00")
iex> is_nil(boardroom.open)
false

open!(resource, pattern)

Open hours on a resource, raising on a pattern it cannot read. Delegates to Agenda.Availability.open!/2.

The ! companion to open/2, for a pattern that is a literal in the source — a guide, a notebook, a fixture — where the tuple is ceremony around what cannot fail except by typo.

Examples

iex> import Tempo.Sigils
iex> Agenda.open!(Agenda.resource("Boardroom"), ~o"2026-06-15T09:00:00/2026-06-15T17:00:00").open
~o"2026Y6M15DT9H0M0S/2026Y6M15DT17H0M0S"

place(name, options \\ [])

@spec place(
  String.t(),
  keyword()
) :: Agenda.Place.t()

Build a place. Delegates to Agenda.Place.new/2.

Examples

iex> Agenda.place("Level 2").name
"Level 2"

plan(session, pool, options \\ [])

@spec plan(Agenda.Session.t(), [Agenda.Resource.t()], keyword()) ::
  {:ok, [Agenda.Arrangement.t()]} | {:error, Agenda.Infeasible.t()}

Rank the ways a session could be held. Delegates to Agenda.Planner.plan/3.

Examples

iex> boardroom = Agenda.resource("Boardroom", seats: 8)
iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T11:00:00")
iex> session =
...>   Agenda.session("Review", duration: "PT1H", window: "2026-06-15/2026-06-16")
...>   |> Agenda.Session.needs(:room, seats: 8)
iex> {:ok, [best | _]} = Agenda.plan(session, [boardroom])
iex> Agenda.explain(best)
"2026Y6M15DT9H0M0S/T10H0M0S — room: Boardroom"

precede(programme, first, then, options \\ [])

@spec precede(Agenda.Programme.t(), String.t(), String.t(), keyword()) ::
  {:ok, Agenda.Programme.t()} | {:error, term()}

Require that one session finishes before another starts. Delegates to Agenda.Programme.precede/4.

Examples

iex> programme =
...>   Agenda.programme("Job")
...>   |> Agenda.Programme.add_session(Agenda.session("Survey"))
...>   |> Agenda.Programme.add_session(Agenda.session("Quote"))
iex> {:ok, programme} = Agenda.precede(programme, "Survey", "Quote", gap: "PT30M")
iex> length(programme.precedences)
1

prefer(programme, preference, options \\ [])

@spec prefer(Agenda.Programme.t(), atom() | {atom(), function()}, keyword()) ::
  {:ok, Agenda.Programme.t()} | {:error, term()}

Add a soft constraint to a programme. Delegates to Agenda.Programme.prefer/3.

Examples

iex> {:ok, programme} = Agenda.prefer(Agenda.programme("Conf"), :room_spread)
iex> Enum.map(programme.preferences, & &1.name)
[:room_spread]

programme(name, options \\ [])

@spec programme(
  String.t(),
  keyword()
) :: Agenda.Programme.t()

Build a programme. Delegates to Agenda.Programme.new/2.

Examples

iex> Agenda.programme("ElixirConf AU").name
"ElixirConf AU"

reachable(track, options)

@spec reachable(
  Agenda.Track.t(),
  keyword()
) :: {:ok, Agenda.Track.t()} | {:error, {:missing_option, :within}}

Require that a delegate can get between consecutive track sessions. Delegates to Agenda.Track.reachable/2.

Examples

iex> import Tempo.Sigils
iex> {:ok, track} = Agenda.track("Elixir") |> Agenda.reachable(within: ~o"PT10M")
iex> track.reachable_within
~o"PT10M"

rearrange(ledger, session, arrangement)

What would change if a session moved. Delegates to Agenda.Ledger.diff/3.

Examples

iex> import Tempo.Sigils
iex> arrangement = %Agenda.Arrangement{
...>   session: "Review",
...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...>   allocations: %{room: [Agenda.resource("Boardroom")]}
...> }
iex> Agenda.rearrange(Agenda.ledger(), "Review", arrangement)
...> |> Enum.map(&elem(&1, 0))
[:allocate]

reconcile(ledger, resource, options)

@spec reconcile(Agenda.Ledger.t(), Agenda.Resource.t(), keyword()) ::
  {:ok, Agenda.Reconciliation.t()} | {:error, term()}

Whether a resource's claims account for the time it owed. Delegates to Agenda.Reconciliation.reconcile/3.

The schedule and the timesheet are the same ledger read forwards and backwards, so reconciling is a set difference rather than a sum: unaccounted names which time is missing, not merely how much.

Holidays are not this library's business — pass them in through :excluding as an ordinary interval set, from wherever they are resolved.

Examples

iex> import Tempo.Sigils
iex> {:ok, dana} =
...>   Agenda.open(Agenda.resource("Dana"), ~o"2026-06-16T09:00:00/2026-06-16T17:00:00")
iex> arrangement = %Agenda.Arrangement{
...>   session: "Acme",
...>   interval: ~o"2026-06-16T09:00:00/2026-06-16T12:00:00",
...>   allocations: %{consultant: [dana]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement, tag: {:project, "ACME"})
iex> {:ok, report} = Agenda.reconcile(ledger, dana, within: ~o"2026-06-16/2026-06-17")
iex> Agenda.Reconciliation.explain(report)
["Dana: 5 hours unaccounted — 2026Y6M16DT12H0M0S/T17H0M0S"]

record(ledger, resource, interval, options \\ [])

@spec record(
  Agenda.Ledger.t(),
  Agenda.Resource.t(),
  Agenda.Availability.pattern(),
  keyword()
) ::
  {:ok, Agenda.Ledger.t()} | {:error, term()}

Record one resource over one interval, checking nothing. Delegates to Agenda.Ledger.record/4.

The unchecked half of the pair claim/4 completes: a schedule is a request and must be validated, a timesheet is a record and the world is not obliged to match the contract. See Agenda.Ledger.record/4.

Examples

iex> import Tempo.Sigils
iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
iex> {:ok, ledger} = Agenda.record(Agenda.ledger(), dana, ~o"2026-08-15T09:00:00/2026-08-15T12:00:00", tag: {:project, "ACME"})
iex> ledger |> Agenda.Ledger.to_list() |> Enum.map(& &1.tag)
[project: "ACME"]

release(ledger, session)

@spec release(Agenda.Ledger.t(), String.t()) :: {:ok, Agenda.Ledger.t()}

Free everything a session holds. Delegates to Agenda.Ledger.release/2.

Examples

iex> {:ok, ledger} = Agenda.release(Agenda.ledger(), "Review")
iex> Agenda.count(ledger)
0

release_series(ledger, series, options \\ [])

@spec release_series(Agenda.Ledger.t(), String.t(), keyword()) ::
  {:ok, Agenda.Ledger.t()} | {:error, term()}

Free everything held by a whole series. Delegates to Agenda.Ledger.release_series/3.

Examples

iex> Agenda.release_series(Agenda.ledger(), "Stand-up")
{:ok, Agenda.ledger()}

resource(name, options \\ [])

@spec resource(
  String.t(),
  keyword()
) :: Agenda.Resource.t()

Build a resource. Delegates to Agenda.Resource.new/2.

Examples

iex> Agenda.resource("Boardroom", seats: 8).attributes
%{seats: 8}

roster(name, resources)

@spec roster(atom(), [Agenda.Resource.t()]) :: Agenda.Requirement.t()

Build a requirement naming specific resources. Delegates to Agenda.Requirement.roster/2.

Examples

iex> alice = Agenda.resource("Alice")
iex> Agenda.roster(:attendees, [alice]).name
:attendees

score(arrangements, programme, options \\ [])

What a layout costs against a programme's preferences.

arrange/3 already prefers a lower score, so this is for comparing layouts you are choosing between yourself — or for reading the score of an {:ok, arrangements} result, which returns the placements without one.

Arguments

Options

  • :pool is the resources the layout drew on, for preferences that consult them. The default is [].

Returns

  • the total penalty, where 0 is ideal.

Examples

iex> {:ok, programme} = Agenda.Programme.prefer(Agenda.programme("Conf"), :room_changes)
iex> Agenda.score([], programme)
0

session(name, options \\ [])

@spec session(
  String.t(),
  keyword()
) :: Agenda.Session.t()

Build a session. Delegates to Agenda.Session.new/2.

Examples

iex> Agenda.session("Review", duration: "PT1H").name
"Review"

track(name, options \\ [])

@spec track(
  String.t(),
  keyword()
) :: Agenda.Track.t()

Build a track — sessions that cannot clash with each other. Delegates to Agenda.Track.new/2.

Examples

iex> Agenda.track("Elixir", of: [Agenda.session("Keynote")]).name
"Elixir"

travel_time(from, to, options \\ [])

@spec travel_time(Agenda.Resource.t(), Agenda.Resource.t(), keyword()) ::
  {:ok, Tempo.Duration.t()} | {:error, :unknown}

How long it takes to get from one resource to another.

Derived from the resources' separation in the place tree — the further up you must climb to get from one to the other, the longer the journey. Resources in unrelated trees, or with no place at all, return {:error, :unknown} rather than a guess: nothing can be said about a journey between two places that are not related.

Arguments

Options

  • :between is a keyword-style list of {{from_name, to_name}, duration} overrides, consulted in either direction before the table. Use it wherever the building disagrees with the geometry — two adjacent rooms separated by a locked fire door, for instance.

  • :levels is a map of separation to duration, replacing the default table (0PT0M, 1PT5M, 2PT10M).

  • :distant is the duration used for separations beyond the table. The default is PT20M.

Returns

  • {:ok, duration} where duration is a Tempo.Duration.t/0; or

  • {:error, :unknown} when the two resources share no place.

Examples

iex> import Tempo.Sigils
iex> sydney = Agenda.place("Sydney Convention Centre")
iex> level_2 = Agenda.place("Level 2", within: sydney)
iex> level_3 = Agenda.place("Level 3", within: sydney)
iex> boardroom = Agenda.resource("Boardroom", within: level_2)
iex> annexe = Agenda.resource("Annexe", within: level_3)
iex> Agenda.travel_time(boardroom, annexe)
{:ok, ~o"PT5M"}

iex> here = Agenda.resource("Here")
iex> there = Agenda.resource("There")
iex> Agenda.travel_time(here, there)
{:error, :unknown}