SurveyLoopr
Tips & Tricks
11 min read

Date Constraints in XLSForm: No Future Dates

Mehrab Ali
Mehrab AliPublished November 1, 2026

To stop future dates in XLSForm, put . <= today() in the constraint column of the date question. The dot is the date being entered and today() is the device's current date, so tomorrow is rejected and today is allowed. For a fixed range, wrap each limit in date('YYYY-MM-DD'): . >= date('2026-01-01') and . <= today(). ODK, KoboToolbox and SurveyCTO all read this syntax. Every example below compiles with pyxform and ODK Validate, the check ODK Central runs when you upload a form.

Download example form (.xlsx)All seven examples in one XLSForm, one group each. Tested with pyxform and ODK Validate; free to reuse under CC BY 4.0.Download

How do I stop future dates in XLSForm?

Compare the answer with today(). Date of birth, date of the last illness and the date a household moved in can never be after today, so each gets the same rule:

typenamelabelrequiredconstraintconstraint_message
datedobWhat is the respondent's date of birth?yes. <= today()The date of birth cannot be in the future.
dateillness_dateWhen did the child's fever start?yes. <= today()The date cannot be after today.
surveychoicessettings

Use <=, not <. With . < today() an illness that started this morning is rejected, and the enumerator has no way to enter it. For the opposite case, such as the date of a planned follow-up visit, flip the sign: . >= today() allows today and any later date.

Warning:

today() is the date set on the phone or tablet. If a device's clock is wrong, the limit moves with it. Check device dates during enumerator training. The start and end metadata rows come from the same clock, so compare them with the date the server received the submission if timing matters for your analysis.

How do I allow only today's date?

Write . = today(). A field team often asks for this for the interview date, so a back-dated interview cannot be entered:

typenamelabelrequiredconstraintconstraint_message
dateinterview_dateDate of interviewyes. = today()Enter today's date. Interviews cannot be back-dated.
surveychoicessettings

If the answer must always be today, you may not need a question at all. A today metadata row (type today, name today) records the date with no typing, and the enumerator cannot change it.

How do I limit a date to a fixed range?

Write each fixed limit as date('YYYY-MM-DD'), with a four-digit year, two-digit month and two-digit day. This baseline only accepts household members born from 1 January 1940 up to today:

typenamelabelrequiredconstraintconstraint_message
datemember_dobWhat is this person's date of birth?yes. >= date('1940-01-01') and . <= today()Enter a date between 1 January 1940 and today.
datedistribution_dateWhen did the household receive the last food distribution?yes. >= date('2026-07-01') and . <= date('2026-09-30')The distribution ran from 1 July to 30 September 2026.
surveychoicessettings

Two details make or break this rule:

  • Wrap the date in date(). A bare '2026-07-01' is text. pyxform and ODK Validate accept it, but ODK Collect compares a date with text and the rule never passes.
  • Use the ISO order. date('01/07/2026') also uploads without an error, but Collect cannot read it. A KoboToolbox forum user found the same rule working in Enketo and failing in Collect for exactly this reason.

How do I allow only the last 30 days?

Turn both dates into day numbers with decimal-date-time(), then subtract the number of days. decimal-date-time() counts days since 1 January 1970, so 30 days ago is decimal-date-time(today()) - 30. This recall question for a health facility visit accepts the last 30 days, including today:

typenamelabelrequiredconstraintconstraint_message
datevisit_dateWhen did the child last visit a health facility?yesdecimal-date-time(.) >= decimal-date-time(today()) - 30 and . <= today()The visit must be in the last 30 days.
daterain_dateWhen did it last rain enough to plant?yesdecimal-date-time(.) >= decimal-date-time(today()) - 365 and . <= today()Enter a date in the last 12 months.
surveychoicessettings

This is the form an ODK developer suggests on the forum when a rule like . >= today() - 180 does not behave as expected. Keep the . <= today() part: without it, any future date also passes, because a future date is always more recent than 30 days ago.

How do I check one date against another?

Reference the earlier question with ${name}. A treatment end date must be on or after its start date and not in the future:

typenamelabelrequiredconstraintconstraint_message
datetreat_startWhen did the treatment start?yes. <= today()The start date cannot be in the future.
datetreat_endWhen did the treatment end?yes. >= ${treat_start} and . <= today()The end date must be between the start date and today.
datefollow_upWhen is the follow-up visit planned?yesdecimal-date-time(.) >= decimal-date-time(${treat_start}) + 7 and decimal-date-time(.) <= decimal-date-time(${treat_start}) + 90Plan the follow-up 7 to 90 days after the treatment started.
surveychoicessettings

The follow_up rule adds days to another answer the same way the last-30-days rule subtracts them from today. The earlier question must come first in the form: a constraint that names a later, still empty question compares with nothing. The general pattern of comparing two answers is in the XLSForm constraint examples.

How do I require a minimum age from the date of birth?

Compare the birth date with the date exactly 18 years ago. Writing both as YYYYMMDD numbers with format-date() gives an exact birthday check, with no rounding of 365.25 days:

typenamelabelrequiredconstraintconstraint_message
dateresp_dobWhat is your date of birth?yesformat-date(., '%Y%m%d') <= concat(format-date(today(), '%Y') - 18, format-date(today(), '%m%d'))You must be 18 or older to take part in this survey.
surveychoicessettings

On 1 November 2026 the right side is 20081101, so anyone born on or before 1 November 2008 passes. Change the 18 for another threshold, for example 15 for women of reproductive age. To show the age itself, see calculating age from date of birth.

Does the same rule work for dateTime questions?

Yes, with now() in place of today(). A dateTime question records the time too, so . <= now() blocks a time later than the current moment:

typenamelabelrequiredconstraintconstraint_message
dateTimebirth_timeWhen was the baby born (date and time)?yes. <= now() and . >= date('2026-01-01')Enter a time in 2026 up to now.
surveychoicessettings

today() has no time part, so comparing a dateTime answer with today() treats today as midnight and rejects a birth earlier this morning. Use now() for dateTime and today() for date. To show a date as DD-MM-YYYY in a note, use format-date(), covered in formatting dates in XLSForm.

Common mistakes and how to fix them

We ran each of these through pyxform and ODK Validate. The messages are what you will see in ODK Central, KoboToolbox or the XLSForm validator:

You wroteWrite insteadError you will see
. => date('2021-01-01'). >= date('2021-01-01')contains invalid constraint expression [. => date('2021-01-01')] Bad node
. <= ${today} (no question named today). <= today()Could not find the name 'today'.
. >= ${start_date} where the question is treat_startThe real name: . >= ${treat_start}Could not find the name 'start_date'.
. <= today. <= today()No error, but today without brackets is read as a field path, not the function, so every date is rejected
. >= '2021-01-01'. >= date('2021-01-01')No error, but ODK Collect compares the date with text and rejects every date
. >= date('01/01/2021'). >= date('2021-01-01')No error at upload; Collect cannot read the date
int((today() - .) div 365.25) <= 28 for "last 4 weeks"decimal-date-time(.) >= decimal-date-time(today()) - 28No error, but it counts years, not days, so almost any date passes

The last four rows pass every check and fail in the field. Test each limit in the form preview before you deploy: one date just inside and one just outside. For other compile messages, see common XLSForm errors.

Does this work the same in ODK, KoboToolbox and SurveyCTO?

The syntax is shared. Where you type it differs:

PlatformWhere the rule goesNotes
ODK Collect and Centralconstraint and constraint_message columnstoday(), now(), date(), decimal-date-time() and format-date() as above
KoboToolbox (KoboCollect and Enketo)Same XLSForm columns; in the Formbuilder, the question's Validation CriteriaSame functions. Test date rules in KoboCollect as well as Enketo: forum threads report rules that behave differently between the two
SurveyCTOconstraint and constraint message columnstoday(), now(), date() and format-date() work the same. Check SurveyCTO's function list before copying a day-window rule

In all three, a constraint only runs when the question is shown and answered. If skip logic hides a date question, its rule never fires.

Why date rules are worth the effort

Dates feed the numbers people report: under-5 status, recall windows, time from symptom to treatment. A year typed as 2062 instead of 2026, or an illness dated next week, cannot be fixed after the household is gone. A date rule catches it while the respondent is still there to give the right answer, which is cheaper than any amount of cleaning later.

Tip:

You can describe the rule in plain language instead: "the visit date must be in the last 30 days and not in the future". LooprAI writes the constraint and its message, then compiles the form with pyxform before anything is saved, so a missing date() or a => never reaches your form.

Build my first form free

Next: XLSForm constraint examples →

Frequently Asked Questions

How do I stop future dates in XLSForm?

Put . <= today() in the constraint column of the date question. The dot is the date being entered and today() is the device's current date, so any later date is rejected while today itself is allowed. Add a constraint_message such as: The date cannot be in the future.

How do I restrict a date to a range in XLSForm?

Combine two comparisons with and, writing fixed dates with date() in YYYY-MM-DD order: . >= date('2026-07-01') and . <= date('2026-09-30'). For a range that ends today, replace the second date with today(). A bare quoted date is text and never matches.

How do I allow only dates in the last 30 days?

Compare day numbers with decimal-date-time(), which counts days since 1 January 1970: decimal-date-time(.) >= decimal-date-time(today()) - 30 and . <= today(). Change 30 to any number of days. The second part stops future dates from passing.

Why does my date constraint accept every date or reject every date?

Usually the fixed date is not wrapped in date(), or it is written as DD/MM/YYYY, or today is missing its brackets. All three upload without an error. Write date('2026-01-01') and today(), then test a date just inside and just outside the limit.

How do I make sure an end date is after a start date?

In the end date's constraint, reference the start question: . >= ${start_date} and . <= today(). The start question must come earlier in the form. To require a gap, compare day numbers: decimal-date-time(.) >= decimal-date-time(${start_date}) + 7.

Do date constraints work the same in KoboToolbox and SurveyCTO?

Yes. today(), now(), date() and format-date() behave the same, and the rule goes in the constraint column. In the KoboToolbox Formbuilder it goes in Validation Criteria. Test date rules in both KoboCollect and Enketo, since forum users report differences between them.

No Credit Card Required

Build the form first. Add hosting when you need it.

Start free with LooprAI, then deploy a managed ODK Central server or run DataSnap checks when your project is ready.

Automation handles the mechanical work.

Researchers decide what the evidence means.