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

Written by

Published 7 min readTested in live Attio workspaces
Contents9 sections
  1. What the previousValue() function does
  2. The regression alert
  3. previousValue() vs. the rest of the history family
  4. Turning previous values into flags
  5. Blank-proofing: no history means empty
  6. Which attributes it works on
  7. CRM use cases that earn their keep
  8. Copy-paste formulas
  9. Final thoughts

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.

color:var(--color-text-heading)]">Prefer to watch? There is a short video for this one: [Attio previousValue(): see where a deal was before it moved. The whole library is also being built end to end in our first live session — Attio Formulas: Every Function, Built Live, free, no registration, Thursday 8 October 2026.

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.

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.

Ready when you are.

Book a call and we will tell you honestly whether this is worth doing, or start with the free 48-hour audit and decide afterwards.