# Recurring events

> Repeating events in Pidgeon as a rule rather than as rows, and what changing one date in a series actually means for the rest of it.

A repeating event is one row and a rule. Put an `RRULE` on it and the dates
are expanded when something asks for them.

```json
{
"title": "Standup",
"start": "2026-11-03T09:00:00",
"end": "2026-11-03T09:15:00",
"timezone": "Europe/London",
"recurrence": { "rule": "FREQ=WEEKLY;BYDAY=TU" }
}
```

The rule is [RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545)'s, the
same one every calendar speaks. It comes back **normalised**, so what you read
may not be character-for-character what you sent.

**It repeats in wall-clock time.** A weekly meeting at 09:00 in London is
09:00 in London in November and in June, across the clock change in between.
That is why the event stores a wall time and a zone rather than an instant —
adding seven days of milliseconds would drift it an hour twice a year.

## Most dates have no row

The series is stored; its dates are not. That is what makes a meeting that
runs for three years cheap, and it is why there is an endpoint for asking
which dates a rule actually produces:

```bash
curl "https://pidgeon.ai/api/v1/identities/0c939bd7-2d69-4150-a2ac-de994f0e4887/calendar/events/a093d804-b045-4909-a9ab-b359db151a7d/instances?after=2026-11-01T00:00:00Z&before=2026-12-01T00:00:00Z" \
-H "Authorization: Bearer $PIDGEON_API_KEY"
```

```ts
const { data } = await calendarListEventInstances({
path: {
  id: '0c939bd7-2d69-4150-a2ac-de994f0e4887',
  eventId: 'a093d804-b045-4909-a9ab-b359db151a7d',
},
query: { after: '2026-11-01T00:00:00Z', before: '2026-12-01T00:00:00Z' },
});
```

```python
from pidgeon.api.calendar import calendar_list_event_instances

dates = calendar_list_event_instances.sync(
  client=client,
  id="0c939bd7-2d69-4150-a2ac-de994f0e4887",
  event_id="a093d804-b045-4909-a9ab-b359db151a7d",
  after="2026-11-01T00:00:00Z",
  before="2026-12-01T00:00:00Z",
)
```

An instance's id is **`{seriesId}::{originalStart}`** — derived from the date
rather than allocated. It does not change when that date is edited, which an
id minted the first time somebody touched it could not promise.

The agenda expands series too, so you rarely need this directly. Reach for it
when you want the dates themselves rather than what is on a day.

## Changing one date, or the rest of them

A write against a series takes `mode`, and the id you name is **a date**, not
the series.

| `mode`             | What it means                     |
| ------------------ | --------------------------------- |
| `single` (default) | That date, and only that one      |
| `future`           | That date and everything after it |

**There is no `all`**, and the absence is deliberate: naming the *series* id
rather than an instance id already means the whole thing, so a third value
would be a second way to say something the API can already say.

```sh
PATCH /v1/identities/{id}/calendar/events/{seriesId}::{originalStart}?mode=single
```

`future` **splits the series**: the rule is truncated at that date and a new
series starts there carrying the change. A weekly meeting moved four times is
five series, for ever, which is the cost of being able to say "from here on"
without rewriting history.

## Deleting one date

`DELETE` with `mode=single` does not remove a row — there was no row. It
writes an **exception date** onto the rule, because a rule with nothing else
done to it would simply generate that date again the next time anybody asked.

You can see them: `recurrence.exdates` holds the dates that have been taken
out, and `rdates` holds any added to the series that the rule does not
produce.
