# Core concepts

> The model behind the Reassign API: the day plan, local datetimes and spans, names and formats, validation, recurring events, categories, projects and priority, checklists, focus intervals, change polls, the Inbox, trackers, reviews, calendars, undo, and scopes.

Source: https://reassign.dev/concepts/

This page explains the model behind the Reassign API. Read it before you write to a day plan. Each section links to its reference page.

## The day plan

Reassign is a 24-hour circular day planner. A day plan is the list of events on one date.

- Each event has a span: a `start` and an `end`. Refer to [Spans](https://reassign.dev/concepts.md#spans).
- An event that you write is 5 minutes to 168 hours (7 days) long. This rule applies to `POST /events`, `POST /schedule/plan`, and the backlog `schedule` op.
- The server keeps the exact minute that you send. It does not move times to a grid.
- `GET /schedule` gives the events of each day, the free time of each day (`freeSlots`), and the planned minutes for each area, activity type, and project.

| `kind`                   | Conflicts                                                                  |
| ------------------------ | -------------------------------------------------------------------------- |
| `blocking` (the default) | Two blocking events cannot overlap. Events that only touch do not overlap. |
| `non_blocking`           | Never conflicts, and does not block other events                           |
| `reference`              | Never conflicts, and does not block other events                           |

An event write that overlaps a blocking event fails with the error code `conflict`. The backlog `schedule` op also checks for conflicts. For the HTTP status, refer to [Errors](https://reassign.dev/errors.md). Refer to [Events](https://reassign.dev/reference/events.md) and [Scheduling](https://reassign.dev/reference/scheduling.md).

## Dates, times, and time zones

| Value                          | Format                                                                                                            | Example                      |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| Date                           | `"YYYY-MM-DD"`. The API refuses a date that does not exist, for example `"2027-02-29"`.                           | `"2026-09-23"`               |
| Local datetime                 | `"YYYY-MM-DDTHH:MM"`, in the time zone of the account. No seconds, no offset, no `Z`. Hours go from `00` to `23`. | `"2026-09-23T18:30"`         |
| Instant (a point in real time) | ISO 8601 in UTC, with milliseconds                                                                                | `"2026-09-23T16:30:00.000Z"` |
| Duration                       | Whole minutes, in a field that ends with `Minutes`                                                                | `90`                         |

- A span, a moment, and `now` use local datetimes. A time stamp (a field that ends with `At`, for example `updatedAt` or `expiresAt`) uses an instant.
- All local datetimes are in the time zone of the account.
- The API does not take a time zone for each request.
- If the account has no valid time zone, the server uses `UTC`.
- A response with a local datetime gives the zone one time, at the top level, as an IANA name. For example, `"timezone": "Europe/Ljubljana"`.
- On a daylight saving day, a start time can fall into the hour that the clock skips. The event then has a `warning` text.

Refer to [Events](https://reassign.dev/reference/events.md).

## Spans past midnight and multi-day spans

A span is two local datetimes: `start` and `end`. The `end` must be after the `start`. An event that you write is also 5 minutes to 168 hours long.

| To write                     | Send                                                       |
| ---------------------------- | ---------------------------------------------------------- |
| 09:00 to 10:30               | `"start": "2026-09-23T09:00"`, `"end": "2026-09-23T10:30"` |
| 23:00 to 01:00               | `"start": "2026-09-23T23:00"`, `"end": "2026-09-24T01:00"` |
| 22:00 to midnight            | `"start": "2026-09-23T22:00"`, `"end": "2026-09-24T00:00"` |
| Friday 18:00 to Sunday 18:00 | `"start": "2026-09-25T18:00"`, `"end": "2026-09-27T18:00"` |

- The day of an event is the date part of `start`. An event from 23:00 to 01:00 belongs to the first date.
- An event past midnight, or an event of more than one day, needs no extra field. Its `end` is on a later date.
- A span to midnight ends at `T00:00` of the next date. The API refuses `T24:00`.
- An `end` that is not after `start` gets HTTP status 422 with the error code `validation`.
- A span has no `date` field and no field for the next day.
- The same rule applies to all spans: events, free slots, conflicts, and the actual times of a review (`actualStart` and `actualEnd`).

Refer to [Events](https://reassign.dev/reference/events.md).

## Names and formats

| Item                                         | Style                  | Example                       |
| -------------------------------------------- | ---------------------- | ----------------------------- |
| Keys in a body, a query, and a nested object | camelCase              | `activityTypeId`, `requestId` |
| Enum values and error codes                  | lowercase snake\_case  | `non_blocking`, `not_found`   |
| Path segments                                | lowercase kebab-case   | `/api/v1/schedule/plan`       |
| Color                                        | Lowercase `#rrggbb`    | `"#3b82f6"`                   |
| Boolean query value                          | `true` or `false` only | `?includeBacklog=true`        |

- Each concept has one name. The same key is in the request and in the response. Thus, you can copy a field from a read into a write.
- A key has no alias. The API does not change a value to a different format.
- The object's own id is `id`. A reference to a different object is `<entity>Id`, for example `areaId`.
- OAuth 2.0 fields (`access_token`), HTTP headers, scope strings (`events:read`), and RRULE text keep their standard names.

## Validation

- A request with a bad value gets HTTP status 422 with the error code `validation`.
- Each route checks its query and its body against a schema. The message names the first bad field by its path, for example `` `from` ``. `error.issues` lists up to 20 bad fields.
- A query parameter that the route does not know, or a query parameter that you send two times, gets HTTP status 422.
- A body that is not valid JSON gets HTTP status 422. An empty body reads as `{}`.
- A write that sends an output-only field fails. The output-only fields are `source`, `meeting`, `location`, `warning`, `readOnly`, `completedFocusIntervals`, and `reflect`.
- In `POST /events`, a bad op does not fail the full request. It becomes an error row in `results`. In an atomic batch, the bad op still rejects the batch, and the response has HTTP status 422 with the rows.

Refer to [Errors](https://reassign.dev/errors.md).

## Unknown fields

- Responses can contain more fields than these docs list. Ignore fields that you do not know.
- Requests are different. The API refuses a key that it does not know with HTTP status 422, for example ``Unknown body key `color`.``
- A key that has no effect in its context is also an error. For example, `recurrenceEnd` without `recurrence` fails.

## Areas and activity types

| Category      | What it is                            | Shown as                   |
| ------------- | ------------------------------------- | -------------------------- |
| Area          | A group of events, for example "Work" | A color (`color`)          |
| Activity type | A tag, for example "Focus"            | A fill pattern (`pattern`) |

- An event has zero or one area and zero or one activity type.
- An event refers to them by id only: `areaId` and `activityTypeId`. There are no name fields.
- `GET /schedule` gives the `areas` and `activityTypes` lists. Use them to find the name and the color of an id.
- Some areas and activity types are shared defaults (`isGlobal: true`). An edit of a shared default makes your own copy with a new id. A delete of a shared default hides it for your account only.
- An event has no color field. Its color comes from its area.

A read always has `areaId` and `activityTypeId`. The value is `null` when the event has no area or no activity type.

| You send                               | On `create`                  | On `update`                                                 |
| -------------------------------------- | ---------------------------- | ----------------------------------------------------------- |
| An id, for example `"areaId": "3c9d…"` | Sets the link                | Sets the link                                               |
| `null`                                 | No link (the same as no key) | Removes the link. AI classification does not fill it again. |
| No key                                 | No link                      | Keeps the current link                                      |

On `create`, an id that you send is a manual choice. AI classification can fill only a field that has no id. Refer to [Rules of the fields](https://reassign.dev/reference/events.md#rules-of-the-fields).

Refer to [Categories](https://reassign.dev/reference/categories.md).

## Projects and priority

A project is a named goal, for example "Launch". It can be in one area.

- A project belongs to your account only. It has no shared default and no copy.
- An event and an Inbox item have zero or one project: `projectId`. A read always has `projectId`. The value is `null` when the item has no project.
- `GET /schedule` gives the `projects` list: `{ id, name, areaId }`. An archived project also has `archived: true`. `GET /categories` lists all projects, also the archived projects.
- A project has no color. Its color comes from its area.
- `projectId` must be a live project of your account. An unknown or archived id fails with `not_found`. An archived project that is already on the item can stay. A `POST /categories` project `update` with `archived: false` restores an archived project.
- When a write sets a new project and no `areaId`, the item gets the area of the project, if the project has an area. This also replaces an area that the user set.
- Two live projects cannot have the same name, also in different areas. A duplicate name fails with `conflict`, for example "A project named Launch exists in Work."
- `POST /categories` changes projects with the `projects` list. Its ops are `create`, `update`, `delete`, `split`, and `relink`.
- Each day of `GET /schedule` has `loadByProject`: the blocking minutes for each project. `projectId: null` is the time with no project. A read of more than one day also has a top-level `loadByProject` with the total.

| You send `projectId` | On `create`                     | On `update`                          |
| -------------------- | ------------------------------- | ------------------------------------ |
| An id                | Sets the project                | Sets the project                     |
| `null`               | No project (the same as no key) | Removes the project. The area stays. |
| No key               | No project                      | Keeps the current project            |

`priority` is a whole number from 0 to 4: `0` none, `1` low, `2` medium, `3` high, `4` urgent.

- A read always has `priority`.
- `null` is not a valid value. Send `0` to remove the priority.
- On `create`, no key gives `0`. On `update`, no key keeps the current value.

Refer to [Categories](https://reassign.dev/reference/categories.md) and [Backlog](https://reassign.dev/reference/backlog.md).

## Recurring events and series

A recurring event is a series. Each date of the series is an occurrence.

- Set `recurrence` to an iCalendar RRULE, for example `"FREQ=WEEKLY;BYDAY=MO,WE,FR"`. The API does not accept preset names.
- The API refuses a rule with `FREQ=HOURLY`, `FREQ=MINUTELY`, or `FREQ=SECONDLY`, and a rule with `BYHOUR`, `BYMINUTE`, or `BYSECOND`.
- In an update, send `"recurrence": null`. The series then does not repeat.
- `recurrenceEnd` sets the last date of the series. The date is included. Send it with `recurrence` in the same op.
- The change poll shows a series as one row. `GET /schedule` shows each occurrence.

The id selects what a change applies to:

| You send                                                                 | The change applies to                     |
| ------------------------------------------------------------------------ | ----------------------------------------- |
| The series id, for example `"c4d5…"`                                     | The full series                           |
| An occurrence id `seriesId@YYYY-MM-DD`, for example `"c4d5…@2026-09-24"` | That occurrence only                      |
| An occurrence id and `"scope": "future"`                                 | That occurrence and all later occurrences |

- `scope` has one value: `"future"`. It is valid only with an occurrence id.
- Use the occurrence id that a read gives. Do not make the date part yourself.
- After you move an occurrence to another day, its id keeps the original date. Thus, the date part of the id can differ from the date of `start`.
- An occurrence id on an event that does not repeat fails. A date that is not an occurrence of the series also fails.
- A changed occurrence keeps its id `seriesId@YYYY-MM-DD` after the change.
- Each occurrence of a series has the fields `seriesId` and `originalDate`. They are the two parts of its id, also on a moved occurrence.
- A changed occurrence carries the series' `recurrence`. It inherits the series' `calendarId`, `mirrorCalendarIds`, and `mirrorStyles` unless it chose its own calendar.
- `recurrence`, `calendarId`, `mirrorCalendarIds`, `mirrorStyles`, and `sourceUrl` belong to the full series. On an occurrence id, the current value is not a change. A new value fails.

Refer to [Events](https://reassign.dev/reference/events.md).

## Checklists

An event can have a checklist of small tasks: `checklist: { items: [{ id, text, done }] }`.

- A `create` op can include a first checklist.
- After that, use the `checklist` op of `POST /events`. `items` replaces the full list. `check` and `uncheck` take item ids.
- On an occurrence id, `check` and `uncheck` change that occurrence only. On a series id, `items` changes the list of the full series.
- The `checklist` op has no `scope`.

Refer to [Events](https://reassign.dev/reference/events.md).

## Focus intervals

A blocking event can have a focus rhythm: `focusIntervals: { focusMin, breakMin }`, for example `{ "focusMin": 25, "breakMin": 5 }`.

- `focusMin` is 5 to 180. `breakMin` is 1 to 60.
- The breaks are between the focus intervals. The block always ends on a focus interval.
- A `non_blocking` or `reference` event refuses `focusIntervals`.
- In an update, send `"focusIntervals": null` to remove the rhythm.
- A read gives `completedFocusIntervals` when one or more intervals are done.

Refer to [Events](https://reassign.dev/reference/events.md).

## Poll for changes (Beta)

`GET /events` is a change poll. It returns the stored events that changed after your last poll.

- On the first poll, do not send `cursor`.
- The response has `events`, `cursor`, `hasMore`, and `timezone`. Send the `cursor` back as `?cursor=` on the next poll.
- The cursor is opaque. Do not parse it or make it yourself. A value that the API did not write gets HTTP status 422.
- When `hasMore` is `true`, poll again at once with the new `cursor`.
- `limit` sets the page size: 1 to 500. The default is 100.
- The poll does not skip or repeat a row.
- A series is one row. The poll does not expand its occurrences.
- A changed occurrence is a separate row. Its `id` is the stored id of that row, not `seriesId@YYYY-MM-DD`. No other read gives this id.
- `GET /events/{id}` with that stored id returns the occurrence with the id `seriesId@YYYY-MM-DD`.
- The poll does not show deleted events.
- To read a day, use `GET /schedule?from=YYYY-MM-DD&to=YYYY-MM-DD`.

Refer to [Events](https://reassign.dev/reference/events.md).

## The Inbox (backlog)

The Inbox holds blocks that do not have a time yet, for example "Wash the car". The route is `/backlog`.

- `capture` adds a block to the Inbox.
- `schedule` puts a block on the day plan at a `start` (a local datetime). The block gives the length.
- `schedule` obeys the rules of an event write. The span is 5 minutes to 168 hours. A blocking block that overlaps a blocking event fails with `conflict`.
- `durationMinutes` of a block is 5 to 1440.
- A bad op becomes an error row with the code `validation`. It does not fail the full request.
- `park` moves an event from the day plan back to the Inbox.
- `capture`, `capture_text`, and `update` take `projectId` and `priority`. Refer to [Projects and priority](https://reassign.dev/concepts.md#projects-and-priority).
- On `GET /schedule` with `includeBacklog=true`, `backlogProjectId` keeps the items of one project. Send `none` for the items with no project. `backlogMinPriority` keeps the items with that priority or a higher priority.

Refer to [Backlog](https://reassign.dev/reference/backlog.md).

## Moments and trackers

| Item    | What it is                                            |
| ------- | ----------------------------------------------------- |
| Tracker | A thing that you log, for example "Coffee" or "Mood". |
| Moment  | One logged entry of a tracker, at a point in time.    |

| Tracker `scale` | Each log                                                                                            |
| --------------- | --------------------------------------------------------------------------------------------------- |
| `tally`         | Adds the stored `step`, or 1 when no `step` is stored. The API refuses `step` on a `tally` tracker. |
| `rating`        | A value on a scale, 1 to 5 by default                                                               |
| `quantity`      | An amount in `unit`                                                                                 |

- The point of a moment is `time`, a local datetime. The day of a moment is the date part of `time`.

Refer to [Moments and trackers](https://reassign.dev/reference/moments-and-trackers.md).

## Day reviews

A day review records how a past day went.

1. Mark each event: `kept`, `skipped`, `changed`, or `added`. Use the `reflect` op of `POST /events`. For `changed` and `added`, you can also send `actualStart` and `actualEnd`.
2. Confirm the day with `POST /reviews/{date}` and `"action": "confirm"`. This keeps a fixed copy of the results.

- You can confirm only a date before today. You can discard the review of any day.
- `"action": "discard"` removes the review and the marks of that day. It also deletes the events marked `added`.
- The review gives two adherence values from 0 to 1. `event` is the score of each event, weighted by its length. `layer` compares the planned areas with the actual areas.
- There is no separate route to read a review. `GET /schedule` gives a `review` block on each confirmed day.
- Each event of that day also has a `reflect` block with its mark.

Refer to [Day reviews](https://reassign.dev/reference/day-reviews.md).

## Calendars and read-only events

A user can connect calendars (Google or Microsoft) and task lists. `GET /calendars` lists them.

- The events that you create, edit, or delete also sync to the connected calendars.
- REST writes, MCP writes, and changes in the app use the same sync path.
- The sync runs after the server sends the response. Thus, the calendar can show the change a short time later.
- `calendarId` is the home calendar of an event. Only a calendar with `writable: true` can take events.

| `calendarId` on `create` | Result                                                                                                                                                                                     |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| A calendar id            | The event goes to that calendar.                                                                                                                                                           |
| `null`                   | The event stays on the dial only.                                                                                                                                                          |
| No key                   | The event goes to the default calendar of the account, if one is set. The default calendar must be writable, and its source must be connected. Otherwise the event stays on the dial only. |

- `mirrorCalendarIds` adds one-way copies to more calendars. It must not contain the home `calendarId`. A task list cannot take a copy.
- On `update`, `mirrorCalendarIds` replaces the set. `[]` removes all copies.
- On `update`, the API checks only the ids that the op adds to `mirrorCalendarIds`. An id that is already on the event can stay, also when its calendar is read-only or disconnected now.
- `mirrorStyles` sets how much each copy shows: `full`, `private`, or `busy`. Refer to [Copy styles](https://reassign.dev/reference/events.md#copy-styles).
- A one-off event on the dial only can have copies. A series with copies needs a home calendar. A series that came from a connected calendar is an exception. Refer to [Rules of the fields](https://reassign.dev/reference/events.md#rules-of-the-fields).
- On `update`, an omitted `calendarId` keeps the home calendar.

| `calendarId: null` on `update`                                                                            | Result                                                                                         |
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| On a calendar event                                                                                       | The event stays on the dial only. The API removes the home link and deletes all mirror copies. |
| On a dial-only event                                                                                      | No change. The copies stay.                                                                    |
| With an occurrence id or `scope`                                                                          | Fails with `validation`. Use the series id.                                                    |
| With `start`, `end`, `name`, `notes`, `areaId`, `activityTypeId`, `projectId`, `priority`, or `kind`      | The API applies these edits first, then removes the links, in one step.                        |
| With `mirrorCalendarIds`, `mirrorStyles`, `recurrence`, `recurrenceEnd`, `sourceUrl`, or `focusIntervals` | Fails with `validation`. Send that change in a separate op.                                    |

- An event from a calendar that the user does not own has `readOnly: true`.
- The API refuses an edit or a delete of a read-only event with the error code `read_only` (HTTP 403). Change it in its calendar.
- One exception: an `update` with `calendarId: null` works on a read-only event. The edits that it carries also apply, because the event is local after the op.

Refer to [Calendars](https://reassign.dev/reference/calendars.md).

## Undo tokens

| Token       | Comes from                                                                                                                                               | Lifetime                      | Use                                            |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------- |
| `undoToken` | A write that records an undo receipt, for example `POST /events`, `POST /schedule/plan`, `POST /backlog`, `POST /categories`, and `POST /reviews/{date}` | 30 minutes, until `expiresAt` | Send it to `POST /actions/undo`. One use only. |

- When present, `undoToken` and `expiresAt` come together, at the top level of the response. One token covers all the changes of the call.
- A write that changed nothing gives no `undoToken`. A rejected batch also gives no `undoToken`. A write also gives no `undoToken` when the server cannot record the undo. The change then stays. Thus, a response with no `undoToken` does not prove that nothing changed.
- Tracker and moment writes give no `undoToken`. To reverse one, send another write.
- `POST /actions/undo` takes 1 to 20 tokens. An undo gives no new token. You cannot undo an undo.
- After 30 minutes, the change stays. The undo row then has the error "The 30-minute undo window has passed; the change stays."
- An undo can fail with `conflict` while a calendar sync sends the same change. The undo then changes nothing. Send it again after a short time.
- An undo fails with `stale` when a row that the write changed has a newer change. The undo then changes nothing. The row can list the changed rows in `conflictingRows`. Refer to [Stale tokens](https://reassign.dev/reference/scheduling.md#stale-tokens).

Refer to [Scheduling](https://reassign.dev/reference/scheduling.md).

## Scopes

| Scope          | Permits                                                                                |
| -------------- | -------------------------------------------------------------------------------------- |
| `events:read`  | All read calls, except `GET /me`                                                       |
| `events:write` | Read, create, change and delete events and other plan data. It includes `events:read`. |

- A personal access token with **Read only** access has `events:read`. **Read and write** access has both scopes.
- `GET /me` needs no scope. `events:read` covers the other read calls.
- A call without the necessary scope fails with the error code `scope` (HTTP 403).
- A write route checks the scope before the body. Thus, a token with only `events:read` gets `scope`, also when the body is not correct.
- Most calls also need a trial or Pro plan. The 7-day free trial includes Reassign Pro. `GET /me` works on all plans.

Refer to [Authentication](https://reassign.dev/authentication.md).
