Everything being laid out at once — the tracks, the standalone sessions, and the span they all fall inside.
Where Agenda.Planner.plan/3 answers "when could this one
session be held?", a programme asks the harder question: "is there
a placement for every session such that nothing clashes?" Those
are different problems. The first enumerates; the second searches,
and a choice made for one session forecloses choices for another.
The word is deliberately not "schedule" — that is triple-booked
already (Tempo.Schedule is critical-path planning,
elsewhere a Schedule is the bookable resource itself, and
colloquially it is this output).
Summary
Functions
Set the window every session must fall inside.
Add a session that belongs to no track.
Add every session in a list.
Add a track.
Every session in the programme, tracked or not.
Register that one resource would like a session with another.
Turn returned interest into sessions.
Build a programme.
Require that one session finishes before another starts.
Add a soft constraint.
The same programme cut down to the named sessions.
The track a session belongs to, or nil when it stands alone.
Types
@type t() :: %Agenda.Programme{ interests: [Agenda.Interest.t()], name: String.t(), precedences: [Agenda.Precedence.t()], preferences: [Agenda.Preference.t()], sessions: [Agenda.Session.t()], tracks: [Agenda.Track.t()], window: Agenda.Availability.pattern() | nil }
A whole layout waiting to be arranged.
Functions
@spec across(t(), Agenda.Availability.pattern()) :: t()
Set the window every session must fall inside.
Arguments
programmeis at/0.windowis a Tempo value or an ISO 8601 string.
Returns
- the programme, bounded.
Examples
iex> programme = Agenda.Programme.new("Conf")
iex> Agenda.Programme.across(programme, "2026-09-15/2026-09-17").window
"2026-09-15/2026-09-17"
@spec add_session(t(), Agenda.Session.t()) :: t()
Add a session that belongs to no track.
Arguments
programmeis at/0.sessionis aAgenda.Session.t/0.
Returns
- the programme, with the session added.
Examples
iex> programme = Agenda.Programme.new("Conf")
iex> Agenda.Programme.add_session(programme, Agenda.session("Registration"))
...> |> Map.get(:sessions) |> Enum.map(& &1.name)
["Registration"]
@spec add_sessions(t(), [Agenda.Session.t()]) :: t()
Add every session in a list.
A programme is usually built from a list of sessions rather than one
at a time, and folding add_session/2 over that list means writing
the fold — with the arguments the other way round from the reduce —
at every such site.
Arguments
programmeis at/0.sessionsis a list ofAgenda.Session.t/0, added in order.
Returns
- the programme, with the sessions added.
Examples
iex> programme = Agenda.Programme.new("Conf")
iex> sessions = [Agenda.session("Registration"), Agenda.session("Keynote")]
iex> Agenda.Programme.add_sessions(programme, sessions)
...> |> Map.get(:sessions) |> Enum.map(& &1.name)
["Registration", "Keynote"]
@spec add_track(t(), Agenda.Track.t()) :: t()
Add a track.
Arguments
programmeis at/0.trackis aAgenda.Track.t/0.
Returns
- the programme, with the track added.
Examples
iex> programme = Agenda.Programme.new("Conf")
iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
iex> Agenda.Programme.add_track(programme, track).tracks |> Enum.map(& &1.name)
["Elixir"]
@spec all_sessions(t()) :: [Agenda.Session.t()]
Every session in the programme, tracked or not.
Arguments
programmeis at/0.
Returns
- the sessions, standalone ones first, then each track's in order.
Examples
iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
iex> Agenda.programme("Conf")
...> |> Agenda.Programme.add_session(Agenda.session("Registration"))
...> |> Agenda.Programme.add_track(track)
...> |> Agenda.Programme.all_sessions()
...> |> Enum.map(& &1.name)
["Registration", "Keynote"]
@spec interest( t(), Agenda.Resource.t() | String.t(), Agenda.Resource.t() | String.t() ) :: {:ok, t()} | {:error, term()}
Register that one resource would like a session with another.
Interest is stated in one direction at a time. A meeting is held
where it is returned — see meetings/3 — so both parties must say
so, and interest that is never returned is reported rather than
scheduled.
Arguments
programmeis at/0.fromis the resource registering the interest, as aAgenda.Resource.t/0or its name.tois the resource it would like to meet, in the same two forms.
Returns
{:ok, programme}; or{:error, :self_interest}when a resource names itself.
Examples
iex> programme = Agenda.programme("Trade Show")
iex> {:ok, programme} = Agenda.Programme.interest(programme, "Kim", "Harbour Tours")
iex> length(programme.interests)
1
@spec meetings(t(), [Agenda.Resource.t()], keyword()) :: {:ok, t()} | {:error, term()}
Turn returned interest into sessions.
One session per mutually interested pair, each rostering both
parties, so the constraint that nobody is in two places at once is
the one the library already enforces rather than a rule anybody
writes. Interest that was never returned adds nothing and is left
for Agenda.Interest.one_sided/1 to report.
Adding meetings twice would double them, so this is a step that
produces a programme rather than something arrange/3 does on the
way past.
Arguments
programmeis at/0carrying the interests.poolis the resources the interests name.
Options
:durationis how long each meeting runs. Required.:windowis when meetings may be held. Defaults to the programme's own window.:needsis what every meeting requires beyond its two parties, as[role: predicates]— a table, most often.:asis the role both parties are rostered under. The default is:party, which says neither of them is substitutable.:nameis a two-argument function naming a meeting from the pair. The default reads"Kim with Harbour Tours".
Returns
{:ok, programme}with one session added per mutual pair; or{:error, {:missing_option, :duration}}; or{:error, {:unknown_resources, names}}when an interest names a resource the pool does not hold.
Examples
iex> kim = Agenda.resource("Kim")
iex> harbour = Agenda.resource("Harbour Tours")
iex> programme = Agenda.programme("Trade Show", across: "2027-06-15/2027-06-17")
iex> {:ok, programme} = Agenda.Programme.interest(programme, kim, harbour)
iex> {:ok, programme} = Agenda.Programme.interest(programme, harbour, kim)
iex> {:ok, programme} = Agenda.Programme.meetings(programme, [kim, harbour], duration: "PT15M")
iex> Enum.map(programme.sessions, & &1.name)
["Harbour Tours with Kim"]
Build a programme.
Arguments
nameis the programme's name.
Options
:acrossis the window every session must fall inside.
Returns
- a
t/0.
Examples
iex> Agenda.Programme.new("ElixirConf AU").name
"ElixirConf AU"
Require that one session finishes before another starts.
This is what makes a task graph out of a set of tasks. Both names must be sessions in the programme; naming one that is not is an error rather than a constraint silently doing nothing.
Arguments
programmeis at/0.firstis the name of the session that must finish first.thenis the name of the session that follows it.
Options
:gapis the least time that must pass between them.:withinis the most, measured from the end offirst.
Returns
{:ok, programme}; or{:error, reason}naming a session the programme does not have.
Examples
iex> programme =
...> Agenda.programme("Job")
...> |> Agenda.Programme.add_session(Agenda.session("Survey"))
...> |> Agenda.Programme.add_session(Agenda.session("Quote"))
iex> {:ok, programme} = Agenda.Programme.precede(programme, "Survey", "Quote", gap: "PT30M")
iex> Enum.map(programme.precedences, & &1.then)
["Quote"]
iex> Agenda.Programme.precede(Agenda.programme("Job"), "Survey", "Quote")
{:error, {:unknown_sessions, ["Survey", "Quote"]}}
Add a soft constraint.
A preference never makes a layout invalid, only worse.
Agenda.Arranger.arrange/3 places as many sessions as it can
first — that part is proven — and prefers a lower score only among
the layouts it reaches. See Agenda.Preference for what is and
is not promised.
Declaring even one preference changes how the search runs: it can no longer stop at the first layout that places everything it can, since a later one may score better. A programme with no preferences is unaffected.
Arguments
programmeis at/0.preferenceis a built-in name —:room_changesor:room_spread— or a{name, function}pair of your own.
Options
:weightis how much each violation costs. The default is1.
Returns
{:ok, programme}; or{:error, reason}when the preference is not recognised.
Examples
iex> programme = Agenda.programme("Conf")
iex> {:ok, programme} = Agenda.Programme.prefer(programme, :room_changes, weight: 10)
iex> Enum.map(programme.preferences, & &1.name)
[:room_changes]
iex> Agenda.Programme.prefer(Agenda.programme("Conf"), :teleportation)
{:error, {:unknown_preference, :teleportation}}
The same programme cut down to the named sessions.
Tracks keep only the sessions named, and a track left with none is dropped rather than kept empty — an empty track constrains nothing and would only be noise in the result.
This exists for Agenda.Arranger.conflict/3, which asks whether
smaller and smaller parts of a programme can be arranged in order to
find the part that cannot.
Arguments
programmeis at/0.namesis the list of session names to keep.
Returns
- the programme, holding only those sessions.
Examples
iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
iex> Agenda.programme("Conf")
...> |> Agenda.Programme.add_session(Agenda.session("Registration"))
...> |> Agenda.Programme.add_track(track)
...> |> Agenda.Programme.restrict_to(["Registration"])
...> |> Agenda.Programme.all_sessions()
...> |> Enum.map(& &1.name)
["Registration"]
iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
iex> Agenda.programme("Conf")
...> |> Agenda.Programme.add_track(track)
...> |> Agenda.Programme.restrict_to([])
...> |> Map.get(:tracks)
[]
@spec track_of(t(), String.t()) :: Agenda.Track.t() | nil
The track a session belongs to, or nil when it stands alone.
Arguments
programmeis at/0.session_nameis the session's name.
Returns
- a
Agenda.Track.t/0, ornil.
Examples
iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
iex> programme = Agenda.Programme.add_track(Agenda.programme("Conf"), track)
iex> Agenda.Programme.track_of(programme, "Keynote").name
"Elixir"
iex> Agenda.Programme.track_of(Agenda.programme("Conf"), "Keynote")
nil