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

The today() and now() functions in Attio: the only formulas that change a record while you sleep

Written by

Published 12 min read

Tested in live Attio workspaces.

Every formula in this series so far has been reactive. Change a deal's stage and the flag updates. Close a deal and the variance recalculates. Useful, but it means the CRM only tells you things when someone has already done something.

The two functions in this article are the exception. today() and now() are the only ones in Attio's library whose value changes without anybody touching a record — and that single property is what turns a database of what happened into a system that raises its hand about what isn't happening. Accounts going quiet. Deals ageing in a stage. Renewals approaching. None of those events *are* events. They are the absence of events, and the only way a CRM notices them is if time itself is an input.

This is part of our function-by-function series on Attio formula attributes — previously: if(), timeSpentIn(), dateDiff(), the ?? operator, count(), hasBeenIn(), valueSetAt(), contains(), sum(), previousValue(), dateAdd(), formatDate(), min()/max(), valueAt(), replace()/replaceAll(), avg()/median(), round()/ceil()/floor(), and()/or()/not(), length(), unique(), and abs(). This one takes both clock functions together, because the only real decision is which of the two you reach for.

Table of contents

What today() and now() do

Neither takes an argument:

today()
now()

today() returns the current date, without a time component. now() returns the current date and time as a timestamp. That's the entire difference in what they return.

The difference in what they *do* is bigger, and it's the same for both: Attio documents that using either one adds a daily recalculation around midnight UTC to the formula. The field doesn't sit there holding the value it had when the record was created. It updates itself, every day, on every record, forever, with nobody doing anything.

The formulas that move on their own

It's worth sitting with what that means, because it's the reason these two functions matter more than their one-line definitions suggest.

A normal formula attribute is a function of the record. Change the record, the formula recalculates; leave the record alone and the value is frozen. That's fine for abs({Forecast} - {Actual}) or count({Team}), where nothing should change until the underlying data does.

But the most expensive things in a pipeline are the ones where *nothing happens*. A prospect who stops replying doesn't fire an event. A deal sitting in Negotiation for seven weeks doesn't generate a record update — the absence of updates is the entire problem. A renewal that's forty days out becomes a renewal that's ten days out purely by the passage of time.

Put today() in a formula and time becomes an input to the record. The number climbs on its own, crosses your threshold on its own, and lands the record in a saved view on its own. Nobody has to remember to check. That is the difference between a CRM you interrogate and a CRM that tells you things.

Daily recalculation is the whole contract

Once. Per. Day. This is the part most people get wrong, and it's worth being blunt about because now() returning a *timestamp* strongly implies live values, and it isn't.

What Attio documents is a daily recalculation around midnight UTC. So dateDiff({Created at}, now(), "hours") doesn't return the lead's age at the instant you look at the column — it returns the age as computed at the last recalculation. That distinction is invisible on a "days since last interaction" field, where a once-a-day refresh is exactly the right cadence, and very visible on a "minutes since this lead came in" field, where it isn't.

Design accordingly:

  • Day-scale and slower — silence, ageing, tenure, countdowns. Daily recalculation is ideal, and this is where these functions earn their keep.
  • Hour-scale — lead age, SLA windows measured in hours. Workable, as long as everyone reading the column knows how fresh the number actually is.
  • Minute-scale — speed-to-lead alerting where seconds decide conversion. Don't build this on a formula field. Use a workflow triggered by the record's creation instead, which fires when the event happens rather than when the clock rolls over.

The second half of that contract is *whose* midnight. The rollover is documented at midnight UTC, not in your workspace's local time — so for a team in California, the day boundary lands mid-afternoon the day before, and for a team in Sydney it lands mid-morning. It rarely matters for a 30-day silence threshold. It matters a great deal if someone builds a "leads created today" formula and wonders why the count resets at 4pm. setTimezone() converts a date value into another timezone; it does not move when the recalculation happens.

Days of silence

If you ship one formula from this article, ship this one:

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

Days since anyone last talked to this account, live on every record, refreshed daily, maintained by nobody. It is the single best "who's going cold" signal a CRM can produce, and it costs one line.

The number is diagnostic. The label is actionable:

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

Tiers work even better, because "cold" and "abandoned" deserve different responses:

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

Save a view filtered to "Going cold", sort by the raw day count descending, and you have a follow-up queue that rebuilds itself every morning. The accounts arrive at the top of it by doing nothing, which is precisely the behaviour you were failing to notice.

The same shape, pointed at a deal's creation date, gives you pipeline ageing:

dateDiff({Created at}, today(), "days")

On open deals, that's how long each one has been alive. Combined with a stage filter, it's the stalled-pipeline report that most teams rebuild by hand every quarter. For time in a *specific* stage rather than total age, timeSpentIn() is the sharper instrument.

Countdowns and the past-future problem

Running the difference the other way gives you a countdown:

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

Days until renewal — and here is the trap. dateDiff() returns an absolute difference. It has no sign. A renewal thirty days from now and a renewal thirty days overdue both return 30, and your "renewing soon" view will quietly fill up with contracts that lapsed last month.

The fix is to get direction from a comparison rather than from the subtraction:

if({Renewal date} < today(), "Overdue", "Upcoming")

And then combine the two, because the useful column says both things at once:

if({Renewal date} < today(), "Overdue", if(dateDiff(today(), {Renewal date}, "days") <= 30, "Renewing soon", "Later"))

Read the order carefully — the overdue check has to come first, because once you're past the date the day count alone can't be trusted to mean what it looks like it means. This is the same unsigned-difference behaviour covered in the abs() deep dive: dateDiff() is already absolute, so wrapping it in abs() adds nothing, and no amount of arithmetic will recover a sign that was never there.

Choosing between today() and now()

The rule is short: count days with today(), count hours with now().

today() has no time component, so a day count computed from it can't be thrown off by partial days. now() carries the time, which is what you want when the unit is hours or finer.

Where this bites is mixing them. dateDiff({Created at}, today(), "hours") compares a timestamp against a bare date, and the missing time component has to be resolved somehow — which is exactly the sort of thing that produces an off-by-one nobody can reproduce on demand. Keep the units and the function aligned: today() with "days", "weeks", "months", "years"; now() with "hours", "minutes", "seconds".

Composing with the rest of the library

These two are the leftmost or rightmost argument in someone else's function far more often than they're used alone. dateDiff() is the usual partner, but the pairings are worth knowing:

  • With dateAdd()dateAdd(now(), 7, "days") for a rolling one-week horizon that moves forward every day.
  • With if() — every threshold in this article.
  • With formatDate()formatDate(today(), "'Q'Q yyyy") gives you the *current* quarter as text, useful for comparing against a deal's own quarter label.
  • With 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">floor()floor(dateDiff({Signup date}, today(), "months")) for whole months of tenure, covered in the [round/ceil/floor deep dive.
  • With and()and(dateDiff({Last interaction}, today(), "days") > 30, {Deal value} > 50000) for the flag that actually deserves an alert: big *and* quiet.

Blank inputs behave the way the ?? deep dive described. A record with no {Last interaction} at all produces null, which can blank the whole expression — and those records are usually the worst offenders, not the safest ones. Guard them explicitly rather than letting them vanish from both sides of your filter:

if({Last interaction} == null, "Never contacted", if(dateDiff({Last interaction}, today(), "days") > 30, "Going cold", "Active"))

CRM use cases that earn their keep

  • Days of silencedateDiff({Last interaction}, today(), "days"). Important because going quiet is the most common way a deal dies and the only one that generates no event to notice.
  • Going-cold tiers — the nested if() version. Important because a 31-day gap and a 90-day gap need completely different follow-up.
  • Pipeline ageingdateDiff({Created at}, today(), "days") on open deals. Important because deal age is the cheapest proxy for a forecast that's quietly rotting.
  • Renewal countdown with direction — the overdue-first nested if(). Important because an unsigned countdown puts lapsed contracts in your "upcoming" view.
  • Customer tenurefloor(dateDiff({Signup date}, today(), "months")). Important because tenure-based segmentation beats size-based segmentation for expansion timing.
  • Lead age in hoursdateDiff({Created at}, now(), "hours"). Important for SLA reporting, as long as everyone knows the number refreshes daily rather than continuously.
  • Big-and-quiet flag — the and() combination. Important because "no contact in 30 days" is noise on a $2k deal and an emergency on a $200k one.

Copy-paste formulas

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

Days since last contact (Number output):

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

Going-cold tiers (Text output):

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

Never-contacted-safe version (Text output):

if({Last interaction} == null, "Never contacted", if(dateDiff({Last interaction}, today(), "days") > 30, "Going cold", "Active"))

Deal age in days (Number output):

dateDiff({Created at}, today(), "days")

Renewal status with direction (Text output):

if({Renewal date} < today(), "Overdue", if(dateDiff(today(), {Renewal date}, "days") <= 30, "Renewing soon", "Later"))

Customer tenure in whole months (Number output):

floor(dateDiff({Signup date}, today(), "months"))

Lead age in hours (Number output):

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

Big and quiet (Checkbox output):

and(dateDiff({Last interaction}, today(), "days") > 30, {Deal value} > 50000)

Gotchas

now() is not a live clock. Attio documents a daily recalculation around midnight UTC. The timestamp shape suggests real-time and it isn't. Anything that needs to fire within minutes belongs in a workflow triggered by the event, not in a formula field read by a human.

The rollover is UTC. Not your workspace timezone, not the record owner's. A "today" boundary that lands mid-afternoon local time is confusing rather than broken, but it will get reported as a bug at least once. setTimezone() converts a value; it doesn't move the recalculation.

dateDiff() has no sign. Overdue and upcoming look identical. Get direction from a direct comparison — {Renewal date} < today() — and put that check first in any nested if().

Don't mix the two functions inside one difference. today() with day-scale units, now() with hour-scale units. Comparing a bare date against a timestamp is where unreproducible off-by-ones come from.

Blank dates disappear, and they're the important ones. A record with no last-interaction date isn't safe, it's untouched. Test for the null explicitly rather than defaulting it into a number.

A recalculating field is not a notification. The value changes overnight; nobody is told. Pair the formula with a saved view someone actually opens, or with a workflow that watches the field — the formula creates the signal, it doesn't deliver it.

Force the output type. Number for the counts, Text for the tiers, Checkbox for the flags. Auto guesses, and filters built on a guessed type break in confusing ways.

Three formula attributes deep is the ceiling. Nesting inside a single editor is fine — the tiered if() above is one formula. The limit applies to formula attributes referencing other formula attributes.

Final thoughts

today() and now() take no arguments and return the most obvious value imaginable, which makes them easy to skim past in the function list. They're the two that change what a CRM is for. Every other formula answers a question you thought to ask. These ones raise the question while you're asleep.

Ship dateDiff({Last interaction}, today(), "days") on your primary object this afternoon, save one view filtered to anything over 30, and check it tomorrow morning. The accounts sitting in it got there by doing nothing at all — which is exactly why you hadn't noticed them.

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

And if you'd rather have going-cold alerts, renewal countdowns, and pipeline-ageing views designed and shipped for you, that's literally what we do. Get a free workspace audit or see the AI-native Attio sprint.

Official sources

Attio documentation used to verify this guide:

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