# `Agenda.Session`
[🔗](https://github.com/elixir-tempo/agenda/blob/v0.3.0/lib/agenda/session.ex#L1)

A session — the thing being scheduled.

A session states how long it runs, the window it must fall inside,
what it requires of resources, and what it would merely prefer. It
says nothing about *when* it actually happens: that is what planning
works out.

The word is deliberately not "meeting" (too narrow — a court hearing
and a conference talk are neither), not "event" (`Tempo.Event` is
taken, and it collides with event sourcing), and not "booking" (which
names the request in some scheduling libraries, not the thing
requested).

`new/2` builds one. `duration/2` and `window/2` say how long it runs
and where it must fall; `needs/3`, `roster/3` and `invite/3` add what
it requires and who may attend, and `prefers/2` adds soft preferences.
`rosters/1`, `open_requirements/1` and `named_resources/1` read its
requirements back.

### In iCalendar

A session is a `VEVENT`, and what it states lines up with iCalendar
and CalDAV as it is, with nothing renamed:

* **`duration`** is `DURATION`.

* **`window`** is the span planning searches, as CalDAV's
  `time-range` is the span a query searches.

* **A roster and an invitation** are the two kinds of attendee.
  `roster/3` is `ATTENDEE;ROLE=REQ-PARTICIPANT`: the session cannot
  be held without them, and they are allocated. `invite/3` is
  `ATTENDEE;ROLE=OPT-PARTICIPANT`: they make a time better and are
  never allocated, because an optional attendee who can cost a
  placement is not optional.

* **`Agenda.Arrangement`'s `attending`** has no counterpart. It says
  which invitees a candidate time suits, a planning result rather
  than a calendar fact; the nearest thing once the session is held is
  `PARTSTAT=ACCEPTED`.

# `t`

```elixir
@type t() :: %Agenda.Session{
  duration: Agenda.Availability.pattern() | nil,
  invitees: keyword([Agenda.Resource.t()]),
  name: String.t(),
  preferences: keyword(),
  recurrence_id: Tempo.t() | nil,
  requirements: [Agenda.Requirement.t()],
  series: String.t() | nil,
  window: Agenda.Availability.pattern() | nil
}
```

Something to be scheduled.

# `duration`

```elixir
@spec duration(t(), Agenda.Availability.pattern()) :: t()
```

Set how long the session runs.

### Arguments

* `session` is a `t:t/0`.

* `duration` is a `t:Tempo.Duration.t/0`.

### Returns

* the session, with its duration set.

### Examples

    iex> import Tempo.Sigils
    iex> session = Agenda.Session.new("Review")
    iex> Agenda.Session.duration(session, ~o"PT1H").duration
    ~o"PT1H"

# `invite`

```elixir
@spec invite(t(), atom(), [Agenda.Resource.t()]) :: t()
```

Invite resources who may attend but are not required.

The counterpart to `roster/3`. A rostered resource must be free or
the session cannot be held; an invitee never affects whether a
placement is possible, only how good it is — a time when more of
them can come scores better, and `Agenda.Arrangement` records which
of them the chosen time actually suits.

This is deliberately weaker than `roster/3` in a second way: an
invitee is **not allocated**. Their time is not taken and the ledger
does not know about them, because a placement that consumed an
optional person could make some *other* session impossible — and an
optional attendee that can cost a placement is not optional. Book
them with `Agenda.Ledger.allocate/3` once the time is settled, if
they are coming.

### Arguments

* `session` is a `t:t/0`.

* `name` names what the invitees would be there as, the same way
  `roster/3` does.

* `resources` is a list of `t:Agenda.Resource.t/0`.

### Returns

* The session, with the invitees added.

### Examples

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

# `named_resources`

```elixir
@spec named_resources(t()) :: [Agenda.Resource.t()]
```

Every resource named by a roster requirement.

These are the resources whose own `requires` tighten the open requirements —
see `Agenda.Requirement.induce/2`.

### Arguments

* `session` is a `t:t/0`.

### Returns

* the named resources, in requirement order.

### Examples

    iex> alice = Agenda.Resource.new("Alice")
    iex> session =
    ...>   Agenda.Session.new("Review")
    ...>   |> Agenda.Session.roster(:attendees, [alice])
    iex> Enum.map(Agenda.Session.named_resources(session), & &1.name)
    ["Alice"]

# `needs`

```elixir
@spec needs(t(), atom(), keyword()) :: t()
```

Add an attribute requirement under `name`.

### Arguments

* `session` is a `t:t/0`.

* `name` is the requirement's name, such as `:room`.

* `predicates` is a keyword list of attribute predicates.

### Returns

* the session with the requirement added.

### Examples

    iex> import Agenda.Predicate
    iex> session = Agenda.Session.new("Review")
    iex> session = Agenda.Session.needs(session, :room, seats: at_least(8))
    iex> Enum.map(session.requirements, & &1.name)
    [:room]

# `new`

```elixir
@spec new(String.t(), keyword()) :: t()
```

Build a session.

### Arguments

* `name` is the session's name.

* `options` is a keyword list of options.

### Options

* `:duration` is how long the session runs, a `t:Tempo.Duration.t/0`.

* `:window` is the interval it must fall inside, a
  `t:Tempo.t/0` interval.

### Returns

* a `t:t/0`.

### Examples

    iex> import Tempo.Sigils
    iex> Agenda.Session.new("Quarterly review", duration: ~o"PT1H").name
    "Quarterly review"

# `open_requirements`

```elixir
@spec open_requirements(t()) :: [Agenda.Requirement.t()]
```

The requirements described by attribute rather than named.

Each of these is a requirement that planning must *choose* a resource for.

### Arguments

* `session` is a `t:t/0`.

### Returns

* the attribute requirements.

### Examples

    iex> session =
    ...>   Agenda.Session.new("Review")
    ...>   |> Agenda.Session.needs(:room, seats: 8)
    iex> Enum.map(Agenda.Session.open_requirements(session), & &1.name)
    [:room]

# `prefers`

```elixir
@spec prefers(t(), keyword()) :: t()
```

Add soft preferences — they rank arrangements but never exclude one.

### Arguments

* `session` is a `t:t/0`.

* `preferences` is a keyword list. `within: place` prefers resources
  inside that place; any other key is matched against resource
  attributes.

### Returns

* the session, with the preferences added.

### Examples

    iex> sydney = Agenda.Place.new("Sydney")
    iex> session = Agenda.Session.new("Review")
    iex> Agenda.Session.prefers(session, within: sydney).preferences
    ...> |> Keyword.keys()
    [:within]

# `roster`

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

Add a requirement naming specific resources under `name`.

### Arguments

* `session` is a `t:t/0`.

* `name` is the requirement's name, such as `:attendees`.

* `resources` is the list of required
  `t:Agenda.Resource.t/0`. Naming nobody adds no requirement — a
  requirement filled by no one constrains nothing, and a requirement that
  neither names nor describes would otherwise let planning bind any
  resource at all to the requirement.

### Returns

* the session with the requirement added, or unchanged if
  `resources` is empty.

### Examples

    iex> alice = Agenda.Resource.new("Alice")
    iex> session = Agenda.Session.new("Review")
    iex> session = Agenda.Session.roster(session, :attendees, [alice])
    iex> Enum.map(session.requirements, & &1.name)
    [:attendees]

    iex> session = Agenda.Session.new("Review")
    iex> Agenda.Session.roster(session, :attendees, []).requirements
    []

# `rosters`

```elixir
@spec rosters(t()) :: [Agenda.Requirement.t()]
```

The requirements naming specific resources.

### Arguments

* `session` is a `t:t/0`.

### Returns

* the roster requirements.

### Examples

    iex> alice = Agenda.Resource.new("Alice")
    iex> session =
    ...>   Agenda.Session.new("Review")
    ...>   |> Agenda.Session.roster(:attendees, [alice])
    iex> Enum.map(Agenda.Session.rosters(session), & &1.name)
    [:attendees]

# `window`

```elixir
@spec window(t(), Agenda.Availability.pattern()) :: t()
```

Set the interval the session must fall inside.

### Arguments

* `session` is a `t:t/0`.

* `window` is a `t:Tempo.t/0` interval.

### Returns

* the session, bounded.

### Examples

    iex> import Tempo.Sigils
    iex> session = Agenda.Session.new("Review")
    iex> Agenda.Session.window(session, ~o"2026-06-15/2026-06-19").window
    ~o"2026-06-15/2026-06-19"

---

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