Dates and Date Ranges
Agility stores a date range as a half-open interval:
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.
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.
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:
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:
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:
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.
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.
meta.v1 cannot tell you any of thisThis 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 — other reasons a figure disagrees with the product
- Moments and history —
asofand point-in-time queries - Filter expressions — comparison operators