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

The valueSetAt() function in Attio: the exact moment anything happened in your pipeline

·6 min read

Every SLA argument, every "how long has this been sitting here," every stage-conversion report starts with the same question: *when did this record enter that state?* Your CRM has always known the answer — it recorded the timestamp the moment someone moved the deal. valueSetAt() is the function that finally hands the timestamp back to you.

This is part of our function-by-function series on Attio formula attributes — previously: if(), timeSpentIn(), dateDiff(), the ?? operator, count(), and hasBeenIn(). This one covers valueSetAt(): the syntax, the self-reference trick for the current value, first-versus-last entries, and the dateDiff() pairing that makes it sing.

Table of contents

What the valueSetAt() function does

valueSetAt() returns the timestamp an attribute was set to a value:

valueSetAt(attribute, value)

The canonical example:

valueSetAt({Status}, "MQL")

The exact moment that lead became an MQL — not "sometime last week," not a rep's recollection, the recorded timestamp from Attio's change history. Like the rest of the attribute history family, it works because Attio has been logging every change to your select and status attributes all along; this function just queries the log.

Force the output type to Timestamp (or Date if you don't need the time) and the result behaves like any other date attribute — sortable, filterable, usable in views.

Targeting the current value

Pass the attribute itself in place of a literal value and valueSetAt() targets whatever the attribute is set to right now:

valueSetAt({Deal stage}, {Deal stage})

The second 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">{Deal stage} evaluates to the record's current stage, so "when did this deal enter the stage it's in?" gets answered for every deal at once, no matter which stage each one occupies. This is the same self-reference trick [timeSpentIn() uses, and it's what makes one formula serve the whole pipeline instead of needing one per stage.

First entry or latest entry

By default, valueSetAt() returns when the attribute was first set to the value. The optional position argument — "first" or "last" — matters the moment records can revisit a state.

A deal that went 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">Negotiation → Closed lost → Negotiation has two Negotiation entries. "first" gives you the original attempt; "last" gives you the current run. For SLA clocks and staleness counters you almost always want "last" on re-enterable stages — measuring today's negotiation against a timestamp from the previous, failed one quietly inflates every age metric for exactly the [boomerang deals that deserve accurate handling.

The dateDiff() pairing

A timestamp is a fact. Paired with dateDiff(), it becomes a live clock:

dateDiff(valueSetAt({Deal stage}, {Deal stage}), today(), "days")

Days in current stage, recalculated daily thanks to 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">today(). Wrap it in [if() and the clock becomes a judgment:

if((dateDiff(valueSetAt({Status}, "MQL"), today(), "days") ?? 999) > 2, "Past SLA", "In SLA")

Every MQL now carries its own SLA verdict — filter a view to "Past SLA" and the morning stand-up runs itself. Note the ?? fallback: a record that was never an MQL has no timestamp, and without the guard it would silently drop out of both labels.

valueSetAt() vs. timeSpentIn()

The two functions sound interchangeable and aren't:

  • valueSetAt() returns *when* the attribute entered a value — a point in time.
  • timeSpentIn() returns *how long* it has spent there — a duration, summed across every visit in the history.

For a deal that's never bounced around, "days since entering the stage" and "time spent in the stage" agree. For a boomerang deal they diverge: 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">timeSpentIn() counts both visits' accumulated time, while the self-referencing valueSetAt() with dateDiff() measures only the current run. Staleness questions ("how long has it been sitting here *this time*?") want valueSetAt; effort and cost questions ("how much total time has this stage consumed?") want [timeSpentIn().

The one big restriction

Same rule as the whole history family: select and status attributes only. valueSetAt() isn't supported on text, number, date, or other attribute types, because those don't carry the queryable change history. Structure your pipeline stages as statuses — which you should be doing anyway — and every stage transition becomes a timestamp you can compute on.

CRM use cases that earn their keep

  • SLA clocks on inbound leadsdateDiff(valueSetAt({Status}, "MQL"), today(), "days") — elapsed time since the lead became an MQL. Important because speed-to-first-touch is the highest-leverage conversion variable in inbound, and an unenforced SLA is a suggestion.
  • Days-in-stage countersdateDiff(valueSetAt({Deal stage}, {Deal stage}), today(), "days") — the staleness metric for the current run specifically. Important because "how long has it sat here this time" is the question pipeline reviews actually ask.
  • Stage-entry dates for reportingvalueSetAt({Deal stage}, "Negotiation") as a Date column — when each deal reached mid-funnel. Important because stage-velocity and cohort reports need entry dates, and this beats reconstructing them from activity feeds.
  • Customer anniversary datesvalueSetAt({Status}, "Customer") — the day each account actually became a customer. Important because renewal conversations, tenure segmentation, and "1 year with us" touches all key off a date most CRMs never store cleanly.
  • Re-entry tracking on boomerang deals — the "last" position on re-enterable stages — when the current run actually started. Important because measuring a revived deal from its first, failed attempt makes every age metric lie.
  • Pipeline cohorts by entry month — stage-entry timestamps grouped by month — who entered the pipeline in each period. Important because conversion rates only mean something relative to a cohort, and entry timestamps are the cohort.

Copy-paste formulas

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

When the deal entered its current stage (Timestamp output):

valueSetAt({Deal stage}, {Deal stage})

Days in current stage (Number output):

dateDiff(valueSetAt({Deal stage}, {Deal stage}), today(), "days")

When the lead became an MQL (Timestamp output):

valueSetAt({Status}, "MQL")

MQL SLA verdict (Text output):

if((dateDiff(valueSetAt({Status}, "MQL"), today(), "days") ?? 999) > 2, "Past SLA", "In SLA")

Stale-deal flag on the current run (Text output):

if((dateDiff(valueSetAt({Deal stage}, {Deal stage}), today(), "days") ?? 0) > 14, "Stale", "Active")

Final thoughts

valueSetAt() is the history family's timekeeper: every stage change your team has ever made left a timestamp, and this function turns those timestamps into SLA clocks, staleness counters, anniversary dates, and honest cohorts. Start with dateDiff(valueSetAt({Deal stage}, {Deal stage}), today(), "days") on your deals — it's the days-in-stage metric every pipeline review wants, and it recalculates itself daily.

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

And if you'd rather have SLA tracking, stage-velocity reporting, and history-aware formulas 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