Skip to main content

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:

AttributeValue
BeginDate2020-01-01T00:00:00
EndDate2020-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.

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.

The filter that looks right is wrong
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 typeRangeAttributes
TimeboxSprints and iterationsBeginDate / EndDate
ScopeProject start and endBeginDate / EndDate
BudgetBudget periodBeginDate / EndDate
OkrSessionObjective periodBeginDate / EndDate
TimesheetTimesheet periodBeginDate / EndDate
CalendarEventEvent spanStartDate / EndDate
EpicPlanned delivery windowPlannedStart / 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.