New to Attio?Get 10% off when you sign up through Craftt.Try Attio free →
All articles

The dateDiff() function in Attio: days of silence, sales cycles, and renewal countdowns

·8 min read

Most of the questions a CRM should answer are secretly date-gap questions. How long since we talked to this account? How long did that deal take to close? How far away is the renewal? How old is this lead? dateDiff() is the one function behind all of them — and once you pair it with today(), the answers update themselves every day without a human ever touching the record.

This is part of our function-by-function series on Attio formula attributes — previously: the if() function and the timeSpentIn() function. One article, one function: syntax, units, the absolute-difference nuance, and formulas you can paste straight in.

Table of contents

What dateDiff() does

dateDiff() measures the gap between two dates:

dateDiff(start, end, unit_type)

It returns the difference in whatever unit you ask for:

dateDiff({Created at}, {Close date}, "days")

Every closed deal now shows its sales cycle length in days — computed live, consistent across the pipeline, and usable everywhere an attribute is usable: view columns, sorts, filters, reports, and workflow triggers. No export, no spreadsheet, no =DATEDIF() that goes stale the moment it's pasted.

The inputs can be any two dates: native attributes like 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">{Created at} and {Last interaction}, custom date attributes, today() or now(), a parsed literal via date(), or a timestamp pulled from history with valueSetAt(). That flexibility is why dateDiff() shows up inside more client formulas than any function except [if().

The syntax, piece by piece

Three arguments, in order:

  1. Start — a date or timestamp. An attribute in curly braces ({Created at}), a function like today(), or a parsed value like date("2026-01-01").
  2. End — same rules as start.
  3. Unit — text, from "seconds" up to "years".

Choose the unit to match the question. Speed-to-lead lives in "hours" — by the time it's a "days" number, the lead is gone. Engagement and pipeline metrics live in "days". Tenure and contract math live in "months" or "years".

If you'd rather skip the syntax entirely, type *"days between created date and close date"* into the AI prompt box under the formula editor — Attio writes the formula for you, without consuming workspace AI credits.

Pairing with today() and now()

dateDiff() gets its real power from one property of today() and now(): any formula referencing them recalculates once daily around midnight UTC, on top of the usual within-seconds recalculation when a referenced attribute changes. That turns a static calculation into a live counter:

dateDiff({Last interaction}, today(), "days")

Days of silence on every account, ticking up by itself every day — in our experience the single best "who's going cold" signal in a workspace, and the backbone of re-engagement views and workflows.

Use today() when you're thinking in days and now() when you're thinking in hours:

dateDiff({Created at}, now(), "hours")

That's lead age in hours, the metric behind speed-to-lead SLAs. One caveat that follows from the recalculation rule: the daily refresh happens around midnight UTC, not continuously — so these formulas are perfect for "days since" and "hours old" fields, but don't build a minute-level countdown timer on them.

It's an absolute difference

The nuance to know before you build: dateDiff() returns the absolute difference between the two dates. Argument order doesn't flip the sign — there are no negative results.

Where this matters is anything that can cross zero. Take the renewal countdown:

dateDiff(today(), {Renewal date}, "days")

A renewal 30 days away reads 30. A renewal 30 days *overdue* also reads 30. For the classic CS use — filter for < 60 and work the queue — that's actually fine, because overdue renewals land in the same urgent bucket where they belong. But don't sort that column and assume the top is the future; a long-expired contract can sit next to next week's renewal.

If direction matters, make the surrounding formula encode it: keep the countdown for queue-building, and add a separate flag keyed off an attribute that only exists on one side of the line (a renewal-completed status, a closed stage) so past and future can't masquerade as each other.

The empty-value trap

The most common way a dateDiff() formula silently fails: an empty date. If {Last interaction} is blank — a fresh account, an imported record, a company nobody has emailed — the difference is nothing, and the formula returns nothing. Any comparison built on top of it returns nothing too, and the record quietly drops out of both sides of your filter.

The fix is the same ?? (null coalescing) guard we use everywhere:

(dateDiff({Last interaction}, today(), "days") ?? 999) > 30

A blank date now behaves as 999 days of silence — which is usually the honest interpretation: an account you've *never* touched belongs at the top of the going-cold list, not invisibly outside it. Rule of thumb: decide what a missing date should mean, then make the fallback say it deliberately.

CRM use cases that earn their keep

  • Days of silencedateDiff({Last interaction}, today(), "days") — a self-updating neglect counter on every account. Important because going-cold accounts surface themselves daily instead of waiting for someone to notice.
  • Sales cycle lengthdateDiff({Created at}, {Close date}, "days") on closed deals — average by rep, segment, or source. Important because velocity reporting starts with a per-deal number nobody has to compute by hand.
  • Speed-to-lead agedateDiff({Created at}, now(), "hours") — how old every unworked lead is, in the unit that decides conversion. Important because response-time decay is invisible until it's measured in hours.
  • Renewal countdowndateDiff(today(), {Renewal date}, "days") — filter < 60 and the CS queue builds itself. Important because renewal saves are won or lost on lead time, not effort.
  • Customer tenuredateDiff({Signup date}, today(), "months") — full relationship age on the record. Important because expansion plays, case-study asks, and loyalty pricing all key off tenure nobody wants to compute manually.
  • color:var(--color-text-heading)]">Current stage streakdateDiff(valueSetAt({Stage}, {Stage}), today(), "days") — days since the last stage change, composing with the history functions. Important because it's the streak counterpart to [timeSpentIn()'s cumulative total — and boomerang deals make the two diverge.

Copy-paste formulas

Swap in your attribute names and these work as-is:

Days since last interaction (Number output):

dateDiff({Last interaction}, today(), "days")

Going-cold flag, blank-safe (Text output):

if((dateDiff({Last interaction}, today(), "days") ?? 999) > 30, "Going cold", "Active")

Sales cycle length in days (Number output):

dateDiff({Created at}, {Close date}, "days")

Lead age in hours — the speed-to-lead metric (Number output):

dateDiff({Created at}, now(), "hours")

Renewal risk window (Text output):

if((dateDiff(today(), {Renewal date}, "days") ?? 999) < 60, "Renewal risk", "Healthy")

dateDiff() vs. dateAdd() vs. timeSpentIn()

Three functions live in the same neighborhood, and picking the right one keeps formulas honest:

QuestionFunctionReturns
"How much time between these two dates?"dateDiff({Created at}, {Close date}, "days")A number — the gap in your chosen unit
"What date is X units from this one?"dateAdd({Close date}, 12, "months")A date — e.g. an auto-computed renewal date
"How long has this sat in a stage?"timeSpentIn({Stage}, {Stage}, "days")A duration read from attribute history

The mental model: dateDiff() measures backward between dates you have, dateAdd() projects forward to a date you need, timeSpentIn() reads history you never recorded manually. They compose — dateDiff(today(), dateAdd({Close date}, 12, "months"), "days") is a renewal countdown for a renewal date that no one ever typed in.

Final thoughts

dateDiff() is the quiet workhorse of the date library: one three-argument function behind days-of-silence counters, sales-cycle reporting, lead-age SLAs, and renewal queues. Pair it with today() for self-updating counters, guard blank dates with ?? so records can't silently vanish from filters, and remember the result is an absolute gap — encode direction in the formula around it, not in a sign that isn't there.

For the rest of the library — every operator, logic, math, date, text, and history function with CRM use cases — see the complete guide to Attio formula attributes.

And if you'd rather have your engagement counters, velocity metrics, and renewal automations 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 call