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

The hasBeenIn() function in Attio: query your pipeline's past, not just its present

·6 min read

Your stage column tells you where every deal is *right now*. It tells you nothing about where deals have *been* — and that's where the interesting truths hide. Which "active" deals were previously lost? Which customers have churned before? Which leads actually passed through Proposal on their way to Closed won? hasBeenIn() answers all of these with one function call, because Attio has been tracking every change to your select and status attributes all along.

This is part of our function-by-function series on Attio formula attributes — previously: if(), timeSpentIn(), dateDiff(), the ?? operator, and count(). This one covers hasBeenIn(): the syntax, the optional arguments most people miss, and the formulas worth copying.

Table of contents

What the hasBeenIn() function does

hasBeenIn() checks whether an attribute is currently — or has *ever* been — set to a value:

hasBeenIn(attribute, values)

It returns true or false. The canonical example:

hasBeenIn({Deal stage}, "Closed lost")

True for every deal that was marked lost at any point in its life — including deals that are active again today. That last part is the whole point: a normal stage filter can only see the present, so a deal that went Closed lost → reopened → Negotiation looks identical to one that's never stumbled. hasBeenIn() remembers.

Force the output type to Checkbox and the result is a clean, filterable tick on every record.

Why history functions are different

Most formula functions — math, text, dates — compute on values you can already see on the record. A spreadsheet could do the same after an export. The attribute history family is the category no spreadsheet can replicate, because it queries color:var(--color-text-heading)]">the change log, not the current value. Attio has been recording every transition of your select and status attributes since the data was created; hasBeenIn() and its siblings — [timeSpentIn(), valueSetAt(), valueAt(), previousValue() — finally let you compute on that record.

hasBeenIn() is the entry point to the family: the simplest question ("was it ever there?") with the broadest set of uses.

The arguments most people miss

The two-argument form covers most cases, but there are three refinements:

Arrays. The values argument accepts a list:

hasBeenIn({Deal stage}, ["Negotiation", "Signing"])

The match argument. With an array, an optional match argument controls the logic: "any" is true if the attribute has been in at least one of the values; "all" requires it to have visited every one. "Has this deal been through *both* Negotiation and Legal review?" is a one-liner with "all" — and a genuinely painful query anywhere else.

The dateRange argument. An optional dateRange limits which part of the history is checked. That turns "was this deal ever in Proposal?" into "was this deal in Proposal *within this window*?" — the difference between a lifetime fact and a time-boxed cohort.

The one big restriction

History functions only work on select and status attributes. hasBeenIn() isn't supported on text, number, date, or other attribute types — Attio only keeps the queryable change history for selects and statuses. The practical advice from the pillar guide: structure your pipeline stages as statuses (which you should be doing anyway) to get full value from the whole history family.

Inverting it with not()

Some of the best hasBeenIn() formulas are about *exclusion*. Wrap it in not():

not(hasBeenIn({Deal stage}, "Closed lost"))

True only for deals with a clean history. The classic use: keep previously lost deals out of automated re-engagement sequences, where a tone-deaf "great to connect!" email to someone who rejected you last quarter does real damage. The automation filters on the checkbox; the checkbox knows the history.

CRM use cases that earn their keep

  • Boomerang deal detectionhasBeenIn({Deal stage}, "Closed lost") filtered against active pipeline — deals that came back after a loss. Important because boomerang deals close differently: the objection that killed them last time is still in the room, and the rep should walk in knowing it.
  • True conversion cohorts — "ever reached Proposal" as a checkbox — the honest denominator for stage-conversion rates. Important because measuring Proposal→Won against *current* stage undercounts everyone who moved through quickly; history-based cohorts don't.
  • Re-engagement exclusionsnot(hasBeenIn({Deal stage}, "Closed lost")) as an automation filter. Important because the cost of re-engaging the wrong record isn't zero replies — it's brand damage with someone who already said no.
  • Churn-history flags on customershasBeenIn({Status}, "Churned") — current customers who have left before. Important because a returning customer's renewal deserves different handling than a first renewal, and nobody remembers this at scale without a flag.
  • Multi-stage journey checks — the array form with match: "all" — deals that touched every required stage. Important because process shortcuts (straight to Signing, skipping Legal) are where post-sale surprises come from, and this makes skipped steps filterable.
  • Time-boxed stage cohorts — the dateRange argument — "was in Negotiation during Q3." Important because pipeline-movement questions are almost always about a window, not a lifetime, and this answers them without an export.

Copy-paste formulas

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

Boomerang deal flag (Checkbox output):

hasBeenIn({Deal stage}, "Closed lost")

Clean-history filter for re-engagement (Checkbox output):

not(hasBeenIn({Deal stage}, "Closed lost"))

Ever reached mid-funnel (Checkbox output):

hasBeenIn({Deal stage}, ["Proposal", "Negotiation", "Signing"])

Previously churned customer (Checkbox output):

hasBeenIn({Status}, "Churned")

Boomerang label for views (Text output):

if(hasBeenIn({Deal stage}, "Closed lost"), "Returning deal", "First run")

Final thoughts

hasBeenIn() is the simplest member of Attio's most underrated function family: one question — "was it ever there?" — asked against a change log your CRM has been keeping whether you knew it or not. Start with the boomerang flag on your deals: it's one line, it needs a Checkbox output and a filtered view, and the first time it surfaces a previously lost deal mid-pipeline, it will have justified the whole history family.

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 boomerang detection, conversion cohorts, and history-aware automations 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