The previousValue() function in Attio: catch deals moving backwards
A deal sitting in Proposal might be on its way up — or it might have just fallen out of Negotiation. Your stage column can't tell the difference, and the two situations deserve opposite responses. previousValue() asks the question the column can't: *where did this record come from?* One step back into the attribute's history, and slipping deals stop hiding among the advancing ones.
This is part of our function-by-function series on Attio formula attributes — previously: if(), timeSpentIn(), dateDiff(), the ?? operator, count(), hasBeenIn(), valueSetAt(), contains(), and sum(). This one covers previousValue(): the syntax, the regression alert, and the formulas worth copying.
Table of contents
- What the previousValue() function does
- The regression alert
- previousValue() vs. the rest of the history family
- Turning previous values into flags
- Blank-proofing: no history means empty
- Which attributes it works on
- CRM use cases that earn their keep
- Copy-paste formulas
- Final thoughts
What the previousValue() function does
previousValue() returns the value an attribute held immediately before its current one:
previousValue(attribute)So if a deal moved Proposal → Negotiation, previousValue({Deal stage}) returns Proposal. Attio has been recording every change to your attributes all along; this function reads exactly one step back into that log. No configuration, no snapshot exports — the history is already there, and this is the one-line way to surface it.
Like the rest of the history family — hasBeenIn(), timeSpentIn(), valueSetAt() — it computes on data your spreadsheet never had.
The regression alert
The headline use case. A pipeline view shows every deal's current stage, and current stage is a lie of omission: a deal in Proposal that was *always* in Proposal and a deal that just slid back from Negotiation render identically. The second one is your loudest early-warning signal — something went wrong late in the deal — and it's invisible.
Compare the current value with the previous one:
if(and({Deal stage} == "Proposal", previousValue({Deal stage}) == "Negotiation"), "Moved backwards", "On track")Every deal that regressed self-flags, live, in a sortable column. Filter a saved view to "Moved backwards" and Monday's pipeline review starts with the deals that actually need the room's attention — before they finish the slide into Closed lost.
previousValue() vs. the rest of the history family
Four history functions, four different questions about the same change log:
previousValue()— *what came right before this?* One step back.valueAt()— *what was it on this date?* A point-in-time snapshot, however many changes ago.hasBeenIn()— *was it ever this value?* The whole history, yes or no.valueSetAt()— *when did it become this value?* A timestamp, not a value.
"Did this deal just fall out of Negotiation?" is 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">previousValue(). "Was it ever in Negotiation?" is [hasBeenIn(). "What stage was it in at quarter start?" is 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">valueAt(). They compose, too: previousValue() tells you where the record came from, and the self-referencing [valueSetAt() tells you when it arrived where it is now.
Turning previous values into flags
Raw, 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">previousValue() gives you a "came from" column — genuinely useful next to the stage column in any pipeline view. Wrapped in [if(), it becomes a signal:
if(previousValue({Plan}) == "Enterprise", "Former Enterprise", "No downgrade")Downgrade detection: accounts whose plan *used to be* bigger are churn risks wearing a normal-looking plan value, and CS should treat them differently. The same shape catches re-opened tickets, demoted lead statuses, and any select attribute where moving backwards means something — the == comparison against the previous value is the whole trick.
Blank-proofing: no history means empty
13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">previousValue() returns empty when there's no prior history — a record whose attribute has only ever held its current value has nothing to step back to. That's most new records, so a raw previousValue() column starts life mostly blank, and — the trap this series keeps flagging — empty values flowing through an expression can blank the whole result. Guard with [??:
previousValue({Deal stage}) ?? "No prior stage"Now fresh deals read as an explicit "No prior stage" instead of a hole, and comparison formulas built on top can't silently drop them. As always, force the output type explicitly rather than leaving it on Auto.
Which attributes it works on
A pleasant surprise: 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">previousValue() is *broader* than its siblings. [hasBeenIn(), timeSpentIn(), and valueSetAt() only work on select and status attributes. previousValue() supports most attribute types, with two exceptions: actor reference attributes (like record owner) and interaction attributes aren't supported.
That second point matters because "who was the previous owner?" is the first question everyone wants to ask — and it's the one this function can't answer. Stage regressions, plan downgrades, and status reversals are all fair game; ownership history isn't.
CRM use cases that earn their keep
- Stage regression alerts —
if(and({Deal stage} == "Proposal", previousValue({Deal stage}) == "Negotiation"), "Moved backwards", "On track")— important because a deal moving backwards is the strongest single predictor of a loss in progress, and no default view surfaces it. - A "came from" column —
previousValue({Deal stage})next to the stage column — important because pipeline reviews constantly ask "how did it get here?", and answering from memory is how deals get misjudged. - Downgrade detection —
if(previousValue({Plan}) == "Enterprise", "Former Enterprise", "No downgrade")— important because a downgraded account looks healthy in every current-state filter while being your most likely churn. - Lost-deal forensics —
if({Deal stage} == "Closed lost", previousValue({Deal stage}) ?? "Lost at entry", "Still active")— important because *where* deals die changes the fix: losses from Negotiation are a pricing conversation, losses from Discovery are a qualification one. - Status reversal triggers — previous status compared against current, feeding an automation filter — important because a record that moved backwards usually needs a human, and automations can route it to one the moment it happens.
- Paired with valueSetAt() —
previousValue()for where it came from,valueSetAt({Deal stage}, {Deal stage})for when it arrived — important because "slipped from Negotiation *three weeks ago*" and "slipped yesterday" are different emergencies.
Copy-paste formulas
Swap in your attribute names and these work as-is:
The "came from" column (Text output):
previousValue({Deal stage})Blank-safe version for views (Text output):
previousValue({Deal stage}) ?? "No prior stage"Stage regression alert (Text output):
if(and({Deal stage} == "Proposal", previousValue({Deal stage}) == "Negotiation"), "Moved backwards", "On track")Downgrade flag (Text output):
if(previousValue({Plan}) == "Enterprise", "Former Enterprise", "No downgrade")Lost-deal forensics (Text output):
if({Deal stage} == "Closed lost", previousValue({Deal stage}) ?? "Lost at entry", "Still active")Final thoughts
previousValue() is the direction-of-travel function: every other column in your CRM shows position, this one shows the vector. Ship the regression alert first — it's one line, and the first Monday it flags a slipping deal you didn't know about, it's paid for itself.
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 regression alerts, churn-risk flags, and pipeline instrumentation 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.