The dateAdd() function in Attio: renewal dates nobody has to remember
Half the dates in a CRM are derived dates: the renewal is the close date plus a year, the trial ends fourteen days after signup, the follow-up is due thirty days after the last touch. Type them in by hand and two things happen — some records never get one, and the ones that do go stale the moment the underlying date moves. dateAdd() computes them instead, on every record, forever.
This is part of our function-by-function series on Attio formula attributes — previously: if(), timeSpentIn(), dateDiff(), the ?? operator, count(), hasBeenIn(), valueSetAt(), contains(), sum(), and previousValue(). This one covers dateAdd(): the syntax, the renewal-date machine, and the formulas worth copying.
Table of contents
- What the dateAdd() function does
- The syntax, piece by piece
- The renewal date nobody typed
- Computed dates move when reality moves
- Composing with dateDiff() and today()
- The empty-value trap
- CRM use cases that earn their keep
- Copy-paste formulas
- dateAdd() vs. dateDiff() vs. eomonth()
- Final thoughts
What the dateAdd() function does
dateAdd() takes a date and projects it forward:
dateAdd(date, units, unit_type)Give it a starting date, an amount, and a unit, and it returns the new date:
dateAdd({Close date}, 12, "months")That's a renewal date on every closed deal — computed, consistent, and present on records where a human would have skipped the field. The result behaves like any other date attribute: sort on it, filter on it, build views around it, trigger workflows off it.
The input can be any date: a native attribute like 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">{Created at}, a custom date attribute, today() or now(), a parsed literal via date(), or a timestamp pulled from history with [valueSetAt().
The syntax, piece by piece
Three arguments, in order:
- Date — the starting point. An attribute in curly braces (
{Close date}), a function liketoday(), or a parsed value likedate("2026-01-01"). - Units — a number: how far to project.
- Unit type — text:
"days","months","years", and friends.
Pick the unit that matches how the commitment is written. Contracts renew in "months" or "years"; trials and SLAs live in "days". And as with the whole date library, force the output type to a date explicitly rather than leaving it on Auto — downstream formulas and views behave better when the type is pinned.
If you'd rather skip the syntax, type *"close date plus 12 months"* into the AI prompt box under the formula editor — Attio writes the formula for you, without consuming workspace AI credits.
The renewal date nobody typed
The headline use case. Renewal dates are the classic manually-maintained field: critical to revenue, owned by nobody, and populated on whatever fraction of deals someone remembered. The fix is to stop treating it as data entry at all:
dateAdd({Close date}, 12, "months")Every deal that closes gets a renewal date in the same second, derived from the one date your team already maintains. Coverage goes to 100% by construction — there is no workflow to forget, no import to backfill, no "renewal date hygiene" project next quarter. The same shape handles contract ends ({Contract start} plus the term), trial expirations ({Created at} plus 14 days), and warranty windows.
Computed dates move when reality moves
The quiet advantage over a typed date: like every formula attribute, dateAdd() recalculates within seconds when a referenced attribute changes. Push the close date out a month during negotiation and the renewal date slides with it. Fix a wrong contract start from an import and every derived deadline corrects itself.
A manually entered renewal date has none of this. It's a snapshot of what someone believed on the day they typed it — and CRMs are full of renewal dates that disagree with the close dates sitting two columns away. A computed date *can't* disagree with its input. That consistency is worth more than the saved typing.
Composing with dateDiff() and today()
13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">dateAdd() returns a date, which means it slots into anywhere a date attribute goes — including inside [dateDiff():
dateDiff(today(), dateAdd({Close date}, 12, "months"), "days")That's a renewal countdown, in days, for a renewal date that no one ever entered — and because it references 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">today(), it recalculates daily around midnight UTC and counts down by itself. Wrap it in [if() and the CS queue builds itself:
if((dateDiff(today(), dateAdd({Close date}, 12, "months"), "days") ?? 999) < 60, "Renewal risk", "Healthy")One formula: derive the deadline, measure the distance, classify the record. This is the composition pattern the whole date library is built around — dateAdd() manufactures the dates the rest of your formulas measure against.
The empty-value trap
The standard failure mode of the series applies: an empty input makes an empty output. If {Close date} is blank — every open deal, for a start — dateAdd({Close date}, 12, "months") returns nothing, and any comparison built on top of it returns nothing too. The record silently drops out of both sides of your filter.
For the computed date itself, blank on open deals is usually correct — there *is* no renewal yet. The guard belongs on the downstream math, with ??:
(dateDiff(today(), dateAdd({Close date}, 12, "months"), "days") ?? 999) < 60Open deals behave as "renewal 999 days away" — safely out of the risk queue instead of invisibly absent from it. Decide what a missing input should mean, then make the fallback say it deliberately.
CRM use cases that earn their keep
- Auto-computed renewal dates —
dateAdd({Close date}, 12, "months")— important because renewal coverage stops depending on human memory, and the date can never contradict the close date it's derived from. - Contract end dates —
dateAdd({Contract start}, 24, "months")— important because term math done by hand is exactly the kind of arithmetic that ships off-by-a-month errors into billing conversations. - Trial expiration —
dateAdd({Created at}, 14, "days")— important because conversion plays are timed plays, and a computed expiry gives every trial the same clock. - Follow-up due dates —
dateAdd({Last interaction}, 30, "days")— important because "touch every account monthly" only happens when the deadline exists on the record instead of in someone's head. - Renewal countdown, no renewal field needed —
dateDiff(today(), dateAdd({Close date}, 12, "months"), "days")— important because the whole renewal motion runs off one maintained date instead of two. - SLA deadlines —
dateAdd({Created at}, 2, "days")— important because a visible due date on every new lead turns a response-time policy into something a view can enforce.
Copy-paste formulas
Swap in your attribute names and these work as-is:
Renewal date, one year after close (Date output):
dateAdd({Close date}, 12, "months")Contract end from start and a 24-month term (Date output):
dateAdd({Contract start}, 24, "months")Trial expiry (Date output):
dateAdd({Created at}, 14, "days")Renewal countdown in days — no renewal attribute required (Number output):
dateDiff(today(), dateAdd({Close date}, 12, "months"), "days")Renewal risk flag, blank-safe (Text output):
if((dateDiff(today(), dateAdd({Close date}, 12, "months"), "days") ?? 999) < 60, "Renewal risk", "Healthy")dateAdd() vs. dateDiff() vs. eomonth()
Three date functions, three different jobs:
| Question | Function | Returns |
|---|---|---|
| "What date is X units from this one?" | dateAdd({Close date}, 12, "months") | A date — projected forward |
| "How much time between these two dates?" | dateDiff({Created at}, {Close date}, "days") | A number — the gap in your unit |
| "What's the last day of this date's month?" | eomonth({Contract start}) | A date — snapped to month-end |
The mental model: dateAdd() manufactures dates, dateDiff() measures between them, eomonth() aligns them to the calendar. They chain — eomonth(dateAdd({Contract start}, 12, "months")) is a renewal aligned to month-end billing, and dateDiff() can count down to either.
Final thoughts
dateAdd() is the anti-data-entry function: every derived date it computes is a field your team no longer maintains, forgets, or lets drift out of sync with its source. Ship the renewal date first — it's one line, it covers every deal retroactively, and the countdown and risk flag stack straight on top of it.
For the rest of the library — every history, logic, math, date, and text function with CRM use cases — see the complete guide to Attio formula attributes.
And if you'd rather have your renewal machine, SLA deadlines, and follow-up cadences designed and shipped for you, that's literally what we do. Get a free workspace audit or see the AI-native Attio sprint.
Need help with your Attio setup?
We migrate teams, build data models, wire automations, and train Claude agents inside your workspace. Discovery call is free.
Book a free discovery callReady when you are.
Two ways in. Pick the friction that fits.