The timeSpentIn() function in Attio: deal velocity and stale-deal alerts
Ask any sales leader what they want from their CRM and "which deals are stuck?" is in the top three. Ask the CRM and, historically, you got silence — stage columns show where a deal *is*, never how long it's *been* there. timeSpentIn() is the Attio formula function that finally answers the question, and it's the one we deploy first in almost every client workspace.
This is part of our function-by-function series on Attio formula attributes — previously: the if() function. One article, one function: syntax, the self-reference trick for the current stage, the select/status restriction, and velocity formulas you can paste straight in.
Table of contents
- What timeSpentIn() does
- The syntax, piece by piece
- Targeting the current stage
- It only works on select and status attributes
- Cumulative time, not latest visit
- The stale-deal flag
- CRM use cases that earn their keep
- Copy-paste formulas
- timeSpentIn() vs. valueSetAt() vs. dateDiff()
- Final thoughts
What timeSpentIn() does
timeSpentIn() belongs to Attio's attribute history functions — the category no spreadsheet can replicate, because it queries the history of an attribute rather than its current value. Attio has been recording every change to your select and status attributes all along; this function computes on that record.
timeSpentIn(attribute, value, unit)It returns the total time the attribute has spent at the given value, in the unit you choose:
timeSpentIn({Deal stage}, "Negotiation", "days")Every deal now shows exactly how many days it has spent in Negotiation — across its whole lifetime, computed live, recalculated the moment the stage changes. That single number is the raw material for deal velocity, stage benchmarks, and the stale-deal triage list your Friday pipeline review has been reconstructing by memory.
The syntax, piece by piece
Three arguments, in order:
- Attribute — a select or status attribute, referenced in curly braces:
{Deal stage},{Status},{Priority}. Type{in the formula editor to pick from every attribute on the object. - Value — the option you're measuring, as text:
"Negotiation". Or the attribute itself again —{Deal stage}— to target whatever it holds right now. - Unit —
"hours","days","weeks", or"months". A month is approximated as 30 days.
Choose the unit to match the question. Speed-to-lead SLAs live in "hours". Pipeline reviews live in "days". Enterprise cycles and CS health live in "weeks" or "months".
If you'd rather skip the syntax entirely, type *"how many days has this deal been in its current stage"* into the AI prompt box under the formula editor — Attio writes the formula for you, without using workspace AI credits.
Targeting the current stage
The second argument's superpower is self-reference — pass the attribute itself in place of a named value:
timeSpentIn({Stage}, {Stage}, "days")The second {Stage} evaluates to whatever the record holds right now, so instead of naming a stage, the formula targets whichever one the deal is in. That one trick is the difference between building a separate formula per stage and building one formula that works across the entire pipeline — when the deal moves from Proposal to Negotiation, the same attribute automatically starts measuring time in Negotiation.
Rule of thumb: use the self-reference for monitoring ("is this deal stuck?"), and a named value for measurement ("how long do deals spend in Negotiation?"). Most workspaces want both — they're different attributes answering different questions.
It only works on select and status attributes
The restriction to know before you build: timeSpentIn() — like its siblings hasBeenIn() and valueSetAt() — only works on select and status attributes. Text, number, date, and other attribute types aren't supported, because Attio maintains the change history this function reads on selects and statuses only.
In practice this is less limiting than it sounds — the things worth timing (stage, status, priority, tier, health) belong in selects anyway. But it does have a design consequence: if you want to time it, model it as a select or status attribute. A free-text "Stage" column can't power any of this. If your pipeline state lives in text fields today, that's the migration to do first — and it's worth doing for a dozen other reasons too.
Cumulative time, not latest visit
One nuance that matters for interpretation: timeSpentIn() returns the total time across the attribute's entire history, not just the current streak. A deal that sat in Negotiation for 10 days, dropped back to Proposal, then re-entered Negotiation for 5 more days shows 15 days for "Negotiation".
For velocity benchmarks, that's exactly what you want — total cost of a stage, including rework. For "how long since the last stage change," pair the history functions differently: valueSetAt({Stage}, {Stage}) gives the timestamp the current value was set, and dateDiff() from there gives the current streak. Cumulative and streak are both useful; just be deliberate about which one your formula reports, because a boomerang deal makes them diverge.
The stale-deal flag
The signature timeSpentIn() pattern — and probably the highest-value ten seconds of configuration in Attio:
if(timeSpentIn({Stage}, {Stage}, "days") > 14, "Stale", "Active")Wrap the measurement in an if(), set the output type to Text, and every deal in every view now carries a "Stale" or "Active" label. Filter a view to Stale and you have a standing triage list; trigger a workflow when the value changes to Stale and the deal owner gets pinged without anyone watching the pipeline.
Two refinements worth making once the basic flag works:
Different stages have different natural dwell times — early stages tolerate more sitting than Contract sent. Nest the threshold per stage:
if({Stage} == "Contract sent", if(timeSpentIn({Stage}, {Stage}, "days") > 5, "Stale", "Active"), if(timeSpentIn({Stage}, {Stage}, "days") > 14, "Stale", "Active"))And exclude closed deals from the flag entirely, so your Stale filter never fills with won business:
if({Stage} == "Closed won" or {Stage} == "Closed lost", "Closed", if(timeSpentIn({Stage}, {Stage}, "days") > 14, "Stale", "Active"))CRM use cases that earn their keep
- Stale-deal triage —
if(timeSpentIn({Stage}, {Stage}, "days") > 14, "Stale", "Active")— stuck deals surface themselves in every view. Important because slipped deals get found by the system on day 15, not by the pipeline review three weeks later. - Stage velocity benchmarking —
timeSpentIn({Deal stage}, "Negotiation", "days")on every closed deal — sort, average, and compare by rep or segment. Important because "our Negotiation stage takes 23 days" turns pipeline coaching from vibes into numbers. - Speed-to-lead enforcement —
timeSpentIn({Lead status}, "New", "hours")— how long leads sit untouched in the intake stage. Important because response time is one of the strongest conversion levers, and the decay is invisible without measurement. - Bottleneck diagnosis — one named-stage formula per key stage, then compare columns across the pipeline. Important because "we lose deals in legal review" becomes provable — and budgetable — when the stage shows triple the dwell time of any other.
- CS health monitoring —
timeSpentIn({Account health}, "At risk", "weeks")— total time an account has spent flagged, across every episode. Important because an account that's been At risk for six cumulative weeks is a different renewal conversation than one that dipped for two days. - Process-debt detection —
timeSpentIn({Approval status}, "Pending", "days")on internal workflows like discount or contract approvals. Important because internal latency kills deals as surely as competitor pressure, and nobody owns a delay no one can see.
Copy-paste formulas
Swap in your attribute names and these work as-is:
Days in current stage (Number output):
timeSpentIn({Stage}, {Stage}, "days")Stale-deal flag, closed deals excluded (Text output):
if({Stage} == "Closed won" or {Stage} == "Closed lost", "Closed", if(timeSpentIn({Stage}, {Stage}, "days") > 14, "Stale", "Active"))Hours a lead has sat in intake (Number output — the speed-to-lead metric):
timeSpentIn({Lead status}, "New", "hours")Total days ever spent in Negotiation, including re-entries (Number output):
timeSpentIn({Deal stage}, "Negotiation", "days")Deal health from dwell time (Text output):
if(timeSpentIn({Stage}, {Stage}, "days") > 30, "Critical", if(timeSpentIn({Stage}, {Stage}, "days") > 14, "Warning", "Healthy"))timeSpentIn() vs. valueSetAt() vs. dateDiff()
Three functions answer time questions in Attio, and picking the right one keeps formulas honest:
| Question | Function | Returns |
|---|---|---|
| "How long has this spent in stage X, ever?" | timeSpentIn({Stage}, "X", "days") | A duration, cumulative across history |
| "How long in the current stage?" | timeSpentIn({Stage}, {Stage}, "days") | A duration for the present value |
| "When did it enter the current stage?" | valueSetAt({Stage}, {Stage}) | A timestamp |
| "How many days between two dates?" | dateDiff({Created at}, {Close date}, "days") | A duration between two date attributes |
The mental model: timeSpentIn() for durations from history, valueSetAt() for timestamps from history, dateDiff() for arithmetic between dates you already have. They compose — dateDiff(valueSetAt({Stage}, {Stage}), today(), "days") is the current-streak counter that ignores earlier visits to the stage.
Final thoughts
Deal velocity used to be a BI project: export the pipeline, reconstruct stage transitions, maintain the dashboard nobody trusts. timeSpentIn() collapses all of it into one attribute that's always current, filterable in any view, and ready to trigger workflows. Model your pipeline states as select or status attributes, ship the stale-deal flag first, and add named-stage benchmarks once the triage habit sticks.
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 velocity metrics, health flags, and pipeline hygiene 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.