All notable changes to this project are documented here. The format follows Keep a Changelog, and this project adheres to Semantic Versioning.

[v0.3.0] — 2026-11-01

Breaking changes

  • Agenda.Allocation's :role field is :requirement, and Agenda.claim/4 and Agenda.record/4 take :requirement where they took :role: it names the requirement a resource fills, which iCalendar's ROLE does not.

  • Agenda.Session.open_roles/1 is open_requirements/1.

Added

  • Agenda.free_busy/2 gives each resource's claims as {:busy | :tentative, interval} pairs, iCalendar's BUSY and BUSY-TENTATIVE, where busy/2 gives the intervals alone.

  • Agenda.cancel/2 cancels a session, freeing what it held or booked: the session's name for release/2, completing hold, confirm and cancel.

  • Agenda.Resource.cutype/1 and resource_type/1 read the conventional cutype: and resource_type: attributes, iCalendar's CUTYPE and RFC 9073's RESOURCE-TYPE; a resource with no cutype: is :unknown.

  • Agenda.Place.new/2 takes :location_type, RFC 9073's LOCATION-TYPE; the Agenda.Place moduledoc maps a place onto VLOCATION and says what an export loses.

  • An occurrence of Agenda.Series.expand/3 carries a recurrence_id, the start of its repetition (iCalendar's RECURRENCE-ID), into its arrangements and allocations, beside the series name.

Changed

[v0.2.0] — 2026-10-28

Breaking changes

  • Agenda runs on Tempo 2.1. A date written as an interval's end is read through, as it is said: "2026-06-15/2026-06-19" is Monday to Friday.

  • A claim, a refinement or a session's window combined with hours in UTC or a zone is written on that time line; a floating one beside them is a Tempo.FrameError. A within: window may still be floating.

  • Agenda.Limit.bucket/2 gives a week as {year, {:week, week}, nil}, the week as its calendar counts it, where it gave the date the week began on.

Changed

  • A period in a reconciliation's sentences is written as Tempo writes it (2026Y6M15D), and a span of a fiscal calendar as the Gregorian dates it falls on.

Fixed

  • A month in a reconciliation's breach sentence is named in full, where it was written "2026-06-".

  • A preference's context during Agenda.arrange/3 holds the pool the programme is arranged from, where it was always empty.

[v0.1.0] — 2026-09-03

First release. Resource-constrained scheduling on top of Tempo: Tempo answers when is this free?, Agenda answers what should I book, and where?

Added

  • The model. Agenda.Resource, Place, Session, Arrangement and Allocation — people and rooms are the same kind of thing, differing only in their attributes.

  • Requirements by description or by name. needs/3 matches on attributes — at_least/1, any_of/1, all_of/1; roster/3 names exact resources, and a person's own requires folds into their room.

  • Availability is derived, never stored. open/2 takes a Tempo value, an ISO 8601 string or an RRULE; free/2 computes open minus busy on demand, honouring concurrency and buffer_before/buffer_after.

  • Planning one session. Agenda.plan/3 ranks the ways a session could be held — attribute eligibility, then availability, then co-availability across the roster, then slotting.

  • Arranging a whole programme. Agenda.arrange/3 places every session with no resource in two places at once, no track clashing with itself, and consecutive sessions reachable across the place tree.

  • Failures are sentences. Every refusal names the resource and each reason it failed; Agenda.conflict/3 returns the minimal set of sessions or demands in tension, via QuickXplain.

  • When it will not all fit. unplaced: :allow returns {:partial, layout} with a reason per omission, and :pinned fixes announced placements while searching around them.

  • Preferences that cannot cost a placement. Agenda.prefer/3 scores layouts — :room_changes, :room_spread, or your own. Optimisation is two-pass, so the number placed is settled and proven first.

  • Order, load and wishes. Agenda.precede/4 sequences sessions with an optional :gap and :within — within: "PT0S" is "immediately after"; :limits caps claims or hours per period and is honoured by plan/3 as well as arrange/3, so booking one at a time cannot exceed a cap; :avoids and :prefers make a placement worse rather than impossible.

  • Required and optional participants. roster/3 names resources that must be free; Agenda.invite/3 names ones who need not be. Invitees never change whether a session can be held — only which time is best — and each arrangement records, in attending, which of them the chosen time suits.

  • Demand, not only supply. Agenda.interest/3 records that one resource would like a session with another, and Agenda.meetings/3 turns returned interest into sessions — one per mutually interested pair, each rostering both parties, so nobody being in two places at once is the constraint the library already enforces. Interest that is never returned is reported by Agenda.Interest.one_sided/1 rather than quietly scheduled.

  • The ledger. allocate/2, release/2 and diff/3 keep bookings true as sessions move, hold/3 and confirm/2 take a resource tentatively, and :tag records what the time was for.

  • Recurrence and reconciliation. Agenda.every/3 expands a session over an ISO 8601 recurrence or RRULE; Agenda.reconcile/3 reports unaccounted and overclaimed time as interval sets, not quantities.

  • Scale follows shape, not size. Candidate enumeration runs concurrently and sessions that cannot constrain each other are solved as separate problems: 1,200 sessions spread across twenty days lay out in about a second, while 500 competing for a single day exhaust the default :nodes budget — which reports itself rather than returning a partial answer.

  • Optional integrations. Agenda.from_ical/1 reads open hours from an RFC 7953 VAVAILABILITY — what a CalDAV server hands you when asked when someone is free.