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

The previousValue() function in Attio: catch deals moving backwards

·7 min read

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

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 alertsif(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" columnpreviousValue({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 detectionif(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 forensicsif({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 call