---
id: dates
slug: /apis/conventions/dates
title: 'Dates and Date Ranges'
description: 'How Agility stores date ranges. EndDate is exclusive — midnight of the day after the range ends — which is the source of most off-by-one errors against the API.'
sidebar_label: 'Dates and Ranges'
sidebar_position: 7
tags: ['api', 'conventions', 'dates', 'ranges', 'timebox']
---

Agility stores a date range as a **half-open interval**:

```text
BeginDate <= x < EndDate
```

`BeginDate` is included. **`EndDate` is excluded.** It is stored as midnight at
the *start of the day after* the range ends.

This one convention is behind most of the date confusion people hit against the
API, because it does not match what the product displays.

## What That Means in Practice

A Sprint that the UI shows as running **1 January to 7 January** is stored as:

| Attribute | Value |
| --- | --- |
| `BeginDate` | `2020-01-01T00:00:00` |
| `EndDate` | **`2020-01-08T00:00:00`** |

The `EndDate` is a day later than the date on screen, and it is a midnight
boundary rather than an end-of-day time.

:::info Why the API and the UI disagree

The product **translates in both directions and never shows you the stored
value.** Its end-date editor adds a day when saving what you typed, and subtracts
a day again when displaying it.

The API does neither. It returns the stored value, exclusive, untranslated — so
the first time most people meet this convention is when a date from the API is
one day later than the same date in the product, and nothing explains why.

Neither is wrong. The UI is showing you an inclusive date because that is what
people mean by "ends on the 7th"; the API is showing you the boundary that is
actually stored.

:::

:::danger The filter that looks right is wrong

```text
EndDate<='2020-01-07'
```

This **misses the sprint entirely**. Its `EndDate` is the 8th.

Worse, the mistake does not error and does not obviously misbehave — it just
quietly returns one fewer range than expected, which is exactly the kind of
discrepancy that surfaces as "the API disagrees with the report".

:::

### Testing Whether a Date Falls in a Range

Use the half-open form, and never `<=` on the upper bound:

```text
BeginDate<=$Date;EndDate>$Date
```

### Computing a Duration

Because the interval is half-open, the subtraction is direct — there is **no
off-by-one to correct**:

```text
days = EndDate - BeginDate
```

`2020-01-08` minus `2020-01-01` is 7 days, which is the right answer for a sprint
running the 1st through the 7th. If you find yourself adding one, you have
probably also converted `EndDate` to an inclusive date somewhere else, and the
two errors will cancel until they do not.

### Displaying a Range

To show a user the same dates the product shows, subtract a day from `EndDate`:

```text
displayed end = EndDate - 1 day
```

## Consecutive Periods Share a Boundary

The convention has a consequence that is easy to get wrong when you bucket time
yourself: **each period's end is the next period's begin**, not the day before it.

```text
period 0:  [ 2020-01-01 , 2020-01-08 )
period 1:  [ 2020-01-08 , 2020-01-15 )
period 2:  [ 2020-01-15 , 2020-01-22 )
```

`2020-01-08` appears twice — as the exclusive end of one period and the inclusive
begin of the next. It belongs to period 1 only, and no day is counted twice.

This is exactly how Agility builds its own time dimensions: it walks
`while (begin < endDate)`, computes each bucket's end, and then assigns
`begin = end` for the next iteration. Consecutive buckets sharing a value is only
correct because the end is exclusive.

**Do not subtract a day when generating adjacent periods.** If you build buckets
as `[Jan 1, Jan 7]`, `[Jan 8, Jan 14]` — inclusive, non-overlapping — they will
not line up with the product's, and items falling on a boundary will land in a
different bucket or none at all.

## Which Attributes Follow This

**Treat it as the pattern for any asset that defines a time range with a pair of
dates**, not as a quirk of one or two types. Where an asset has a beginning and
an end, the end is the exclusive boundary.

Seven asset types define such a pair, under three different naming conventions:

| Asset type | Range | Attributes |
| --- | --- | --- |
| `Timebox` | Sprints and iterations | `BeginDate` / `EndDate` |
| `Scope` | Project start and end | `BeginDate` / `EndDate` |
| `Budget` | Budget period | `BeginDate` / `EndDate` |
| `OkrSession` | Objective period | `BeginDate` / `EndDate` |
| `Timesheet` | Timesheet period | `BeginDate` / `EndDate` |
| `CalendarEvent` | Event span | **`StartDate`** / `EndDate` |
| `Epic` | Planned delivery window | **`PlannedStart`** / **`PlannedEnd`** |

The naming varies but the convention does not. `Epic`'s planned window is edited
by the same control that applies the day shift, so `PlannedEnd` is exclusive in
exactly the way `EndDate` is — do not assume the differently-named pairs behave
differently just because they read differently.

**Single dates are not affected.** `DoneDate`, `ClosedDate`, `RetireDate` and
`ChangeDate` are points in time rather than range boundaries, and carry no
exclusivity convention.

:::danger `meta.v1` cannot tell you any of this

This is the part that makes the convention genuinely hazardous. Schema discovery
reports `EndDate` as an attribute of type `Date`, and stops there. There is no
flag, no naming rule and no metadata of any kind marking a date as an exclusive
boundary.

So a client that introspects the model — the correct thing to do for asset types,
attributes and operations — **cannot learn this**. It has to be known in advance
and applied by hand, which is precisely why it keeps being rediscovered through a
support ticket.

If you are generating code or mappings from `meta.v1`, this is one of the rules
you have to encode yourself.

:::


## Related

- [Reproducing reported numbers](/guides/reproducing-reported-numbers) — other reasons a figure disagrees with the product
- [Moments and history](/concepts/moments-and-history) — `asof` and point-in-time queries
- [Filter expressions](/reference/query-grammar/filter-expressions) — comparison operators
