Calculate Age from Date of Birth in XLSForm
The quick XLSForm formula for age in years is int((today() - ${dob}) div 365.25) in a calculate row, where dob is a date question. It is close but can be one year short on or near a birthday. For an exact age, calculate completed months from the year, month and day parts with format-date(), then take int(${age_months} div 12) for years. Every example below compiles with pyxform and ODK Validate, the check ODK Central runs when you upload a form.
What is the quick formula for age in XLSForm?
Subtracting two dates gives the number of days between them. Divide by 365.25 for years and cut off the decimals with int():
| type | name | label | required | constraint | constraint_message | calculation |
|---|---|---|---|---|---|---|
| date | dob | What is the respondent's date of birth? | yes | . <= today() | The date of birth cannot be in the future. | |
| calculate | age_approx | int((today() - ${dob}) div 365.25) | ||||
| note | show_age_approx | Age: ${age_approx} years |
Three rules cover most mistakes:
- Divide with
div, not/. XLSForm uses XPath, where/means a path. - Wrap the whole division in
int().int()drops the decimals, which gives completed years.round()would make a 17.6-year-old an adult. - Reference the question as
${dob}. A baredobcompiles without an error but reads nothing, so the age is blank.
This is the formula on the ODK and KoboToolbox forums, and it is fine for broad age groups. Use the exact method below when the age decides eligibility.
Why is the quick formula sometimes one year off?
A year has 365 or 366 days, and 365.25 is only the average. Someone born on 21 October 2025 is exactly 1 year old on 21 October 2026, but only 365 days have passed: 365 div 365.25 is 0.999, and int() returns 0. The same happens on many birthdays and the day or two around them, depending on how many leap days fall in between.
The mistake is rare per respondent, but a household survey records thousands of ages. Some children turn 5, some adults turn 18, and some women turn 49 on the day of the interview, and those are exactly the cut-offs that send people into or out of a module.
Avoid int((today() - ${dob}) div 30) for age in months. Months have 28 to 31 days, so dividing by 30 counts an extra month about every five years of age, and a child of 58 months can come out as 59.
How do I calculate exact age in completed months?
Read the year, month and day of both dates with format-date(), then count calendar months. %Y is the 4-digit year, %n the month number and %e the day of the month, all without leading zeros:
| type | name | label | required | constraint | constraint_message | calculation |
|---|---|---|---|---|---|---|
| today | today | |||||
| date | dob | What is the child's date of birth? | yes | . <= today() | The date of birth cannot be in the future. | |
| calculate | age_months | (int(format-date(${today}, '%Y')) - int(format-date(${dob}, '%Y'))) * 12 + int(format-date(${today}, '%n')) - int(format-date(${dob}, '%n')) - if(int(format-date(${today}, '%e')) < int(format-date(${dob}, '%e')), 1, 0) | ||||
| calculate | age_years | int(${age_months} div 12) | ||||
| note | show_age | Age: ${age_years} years (${age_months} months) |
The formula reads: whole years times 12, plus the difference in months, minus one month if the birthday's day of the month has not come yet. A child born on 25 March 2024 is 30 months old on 21 October 2026 (2 × 12 + 7 − 1).
int(${age_months} div 12) then gives exact completed years. There is no rounding anywhere, so the result is right on the birthday itself and in leap years. The today row is a metadata question: it records the date the form was first opened and is never shown to the enumerator.
How do I calculate age in months for children under 5?
Nutrition and immunisation surveys work in months. MUAC screening, for example, applies to children aged 6 to 59 months. Use the months calculate in the relevant column, and show the enumerator the result so they can check it with the caregiver:
| type | name | label | required | constraint | constraint_message | relevant | calculation |
|---|---|---|---|---|---|---|---|
| today | today | ||||||
| date | child_dob | What is the child's date of birth? | yes | . <= today() and . > today() - 1826 | The child must be under 5 years old. | ||
| calculate | child_age_months | (int(format-date(${today}, '%Y')) - int(format-date(${child_dob}, '%Y'))) * 12 + int(format-date(${today}, '%n')) - int(format-date(${child_dob}, '%n')) - if(int(format-date(${today}, '%e')) < int(format-date(${child_dob}, '%e')), 1, 0) | |||||
| note | confirm_age | The child is ${child_age_months} months old. Please confirm this with the caregiver. | |||||
| decimal | muac_cm | What is the child's MUAC in cm? | yes | . >= 6 and . <= 26 | MUAC must be between 6 and 26 cm. | ${child_age_months} >= 6 and ${child_age_months} <= 59 |
The constraint . > today() - 1826 keeps the date of birth within the last 1826 days (5 years including a leap day), so a typing slip in the year is caught on the spot. More date checks are in the XLSForm constraint examples.
How do I show age as years and months?
Split total months into whole years (int(... div 12)) and the remaining months (mod 12):
| type | name | label | required | constraint | constraint_message | calculation |
|---|---|---|---|---|---|---|
| today | today | |||||
| date | dob | What is the respondent's date of birth? | yes | . <= today() | The date of birth cannot be in the future. | |
| calculate | age_months | (int(format-date(${today}, '%Y')) - int(format-date(${dob}, '%Y'))) * 12 + int(format-date(${today}, '%n')) - int(format-date(${dob}, '%n')) - if(int(format-date(${today}, '%e')) < int(format-date(${dob}, '%e')), 1, 0) | ||||
| calculate | age_y | int(${age_months} div 12) | ||||
| calculate | age_m | ${age_months} mod 12 | ||||
| calculate | age_text | concat(${age_y}, ' years and ', ${age_m}, ' months') | ||||
| note | show_age_text | Age: ${age_text} |
Keep age_y and age_m as separate calculates. They export as numbers you can analyse, while age_text is only for the screen.
Why does the age change when I open the form again?
today() is recalculated every time the form is opened, edited or saved. A form filled in March and edited in May will show a new age, and in ODK Central or KoboToolbox an edit on the server recalculates it too. Two fixes:
- Use the
todaymetadata row, as in the examples above. It is set once, when the form is first opened, and every age on the form uses that date. - Use the interview date when interviews can be entered later from paper. Ask it as a date question and calculate from it instead of
${today}.
This version also handles respondents who do not know their date of birth, which is common in rural surveys. It takes the reported age when the date is unknown:
| type | name | label | required | constraint | constraint_message | relevant | calculation |
|---|---|---|---|---|---|---|---|
| date | interview_date | What is the date of the interview? | yes | . <= today() | The interview date cannot be in the future. | ||
| select_one yes_no | dob_known | Does the respondent know their date of birth? | yes | ||||
| date | dob | What is the respondent's date of birth? | yes | . <= ${interview_date} | The date of birth must be before the interview date. | ${dob_known} = '1' | |
| integer | age_reported | How old is the respondent in completed years? | yes | . >= 0 and . <= 120 | Age must be between 0 and 120. | ${dob_known} = '0' | |
| calculate | age_from_dob | ${dob_known} = '1' | int(((int(format-date(${interview_date}, '%Y')) - int(format-date(${dob}, '%Y'))) * 12 + int(format-date(${interview_date}, '%n')) - int(format-date(${dob}, '%n')) - if(int(format-date(${interview_date}, '%e')) < int(format-date(${dob}, '%e')), 1, 0)) div 12) | ||||
| calculate | age_final | if(${dob_known} = '1', ${age_from_dob}, ${age_reported}) | |||||
| note | show_age_final | Age used in this interview: ${age_final} years |
| list_name | name | label |
|---|---|---|
| yes_no | 1 | Yes |
| yes_no | 0 | No |
${age_final} is now the one age the rest of the form uses, for example ${age_final} >= 18 in the skip logic of an adult module. The if() that picks between the two is explained in the XLSForm if statement guide.
Common age calculation mistakes
These are the messages ODK Validate returns, which is what you see when ODK Central, KoboToolbox or the XLSForm converter rejects a form:
| You wrote | Write instead | Error you will see |
|---|---|---|
(today() - ${dob}) / 365.25 | (today() - ${dob}) div 365.25 | Unexpected beginning of path |
DATEDIF(${dob}, today(), 'Y') | the months formula above | cannot handle function 'DATEDIF' |
YEAR(today()) - YEAR(${dob}) | format-date(${today}, '%Y') | cannot handle function 'YEAR' |
=int((today() - ${dob}) div 365.25) | no leading = | Bad node ... in expression |
int(today() - ${dob}) div 365.25) | balance the brackets | Unbalanced brackets or parentheses! |
int((today() - dob) div 365.25) | ${dob} | No error, but the age is blank |
round((today() - ${dob}) div 365.25) | int(...) | No error, but ages are rounded up |
The last two rows are the dangerous ones: the form compiles and the age is silently wrong. For other messages, see common XLSForm errors, or paste your form into the free XLSForm validator.
Does this work the same in ODK, KoboToolbox and SurveyCTO?
The logic is the same on all three: today(), int(), div, mod, if() and once() are shared. Two details differ:
| Platform | What to know |
|---|---|
| ODK (XLSForm) | Use the formulas as written. format-date() and the today metadata row both work. |
| KoboToolbox | Same as ODK when you upload an XLSForm. In the Formbuilder, add a Calculate question and paste the expression. |
| SurveyCTO | Its function list documents format-date-time() for the date parts, with the same %Y, %n and %e codes, so write format-date-time(${dob}, '%Y'). Skip logic goes in a column named relevance. |
format-date-time() also works in ODK, so a form written with it runs on all three platforms.
Why calculate age in the form
An age typed by the enumerator is a second answer that can disagree with the date of birth, and nobody notices until cleaning. A calculated age is consistent by design, and a note such as "The child is 30 months old" lets the caregiver correct a wrong birth year while the enumerator is still in the household. That matters most at the cut-offs that route the interview: under 5, 6 to 59 months, 15 to 49 years.
You can describe it in plain language instead: "calculate the child's age in months from the date of birth and ask MUAC only for 6 to 59 months". LooprAI writes the calculate rows and the skip logic, then compiles the form with pyxform before anything is saved, so a / or an Excel DATEDIF never reaches your form.
Next: XLSForm calculation examples →
Frequently Asked Questions
How do I calculate age from date of birth in XLSForm?
Add a date question named dob and a calculate row with int((today() - ${dob}) div 365.25) for an approximate age in years. For an exact age, calculate completed months from the year, month and day parts with format-date(), then use int(${age_months} div 12).
Why is the calculated age one year less than it should be?
Dividing days by 365.25 uses the average year length, so on a birthday the result can be just under a whole number, such as 0.999, and int() cuts it to 0. Calculating from the year, month and day parts with format-date() is exact on every date.
How do I calculate a child's age in months in ODK or KoboToolbox?
Multiply the year difference by 12, add the month difference and subtract 1 when the birthday's day of the month has not come yet. Read each part with format-date(), using %Y, %n and %e. Dividing days by 30 drifts and is not exact.
Why does the age change when the form is edited later?
today() is recalculated each time the form is opened, edited or saved, including edits on the server. Calculate from the today metadata row, which is set once when the form is first opened, or from an interview date question.
Can I use DATEDIF or YEAR() in an XLSForm?
No. XLSForm uses XPath functions, not Excel functions, so DATEDIF fails with cannot handle function 'DATEDIF' and YEAR with cannot handle function 'YEAR'. Use format-date(${dob}, '%Y') to read the year and the months formula on this page for age.
Does the age formula work in SurveyCTO?
Yes, with one change: SurveyCTO documents format-date-time() rather than format-date() for reading the year, month and day, with the same %Y, %n and %e codes. format-date-time() also works in ODK and KoboToolbox, so one form can run on all three.
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.