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

The abs() function in Attio: measuring how wrong you were, not which way

Written by

Published 11 min read

Tested in live Attio workspaces.

There is a specific kind of bad number that survives every review, because averaging protects it. A rep forecasts 40% high in Q1 and 40% low in Q2. Average error: zero. On the dashboard they are the most accurate forecaster on the team. In reality nobody in the building can plan around anything they say.

abs() is a two-line function that fixes this class of problem. It strips the sign off a number, which sounds like nothing and turns out to be the difference between measuring *how wrong* someone was and measuring *whether their mistakes happened to point in opposite directions*.

This is part of our function-by-function series on Attio formula attributes — previously: if(), timeSpentIn(), dateDiff(), the ?? operator, count(), hasBeenIn(), valueSetAt(), contains(), sum(), previousValue(), dateAdd(), formatDate(), min()/max(), valueAt(), replace()/replaceAll(), avg()/median(), round()/ceil()/floor(), and()/or()/not(), length(), and unique().

Table of contents

What abs() does

One argument, one number:

abs(value)

abs(-42.5) returns 42.5. Positives pass through unchanged. That's the whole function.

Nobody writes abs(-42.5) in a CRM, though. The form that matters wraps a subtraction:

abs({Forecast} - {Actual})

Now you have the distance between two numbers, in whichever direction. {Forecast} - {Actual} alone gives you a signed gap, and a signed gap is the wrong shape for almost every question a revenue team asks about it — because sorting a column that contains both -50000 and +50000 puts the two worst estimates at opposite ends of the list, with the accurate ones in the middle.

Force the output type explicitly. Number for counts and units, Currency for money — a variance in dollars should render as dollars, not as a bare figure someone has to remember the unit for.

The averaging-out problem

Signed errors cancel. That is a mathematical property, not a reporting bug, and it quietly rewards inconsistency in every metric built on a difference.

QuarterForecastActualSigned errorabs() error
Q1140,000100,000+40,00040,000
Q260,000100,000-40,00040,000
Average040,000

Same rep, same two quarters, two completely different stories. The signed average says their forecasting is perfect. The absolute average says they are off by 40% every single quarter and the business cannot plan around them.

The version on the right is the one you want in a scorecard, and the version on the left is what you get by default in nearly every CRM, because subtraction is signed and nobody thinks to wrap it.

This applies well beyond forecasting. Any time you are measuring *agreement* — between an estimate and a result, between two systems, between a quote and an invoice — the sign is noise and the magnitude is the signal.

Forecast accuracy that survives a quarter

The dollar variance is the starting point:

abs({Forecast} - {Actual})

Useful, but not comparable across deal sizes: being $10,000 off on a $500,000 deal is excellent, and being $10,000 off on a $12,000 deal is a guess. The percentage version normalises it:

round(abs({Forecast} - {Actual}) / {Actual} * 100, 0)

That's percent error as a whole number, using round() so it doesn't render as 18.333333. Sort descending and you have your forecasting problem list — deals, reps, or segments where the number that went into the board deck bore no relation to what happened.

Two things to handle before this goes live. First, {Actual} of zero breaks the division, so guard it:

if(({Actual} ?? 0) == 0, 0, round(abs({Forecast} - {Actual}) / {Actual} * 100, 0))

Second, decide deliberately what a *missing* forecast should mean. Defaulting it to zero with ?? makes the variance equal the entire actual value, which reads as catastrophically bad forecasting when the truth is that nobody forecast at all. Those are different problems and they deserve different columns — one of them is an accuracy issue, the other is a process issue.

Then turn the number into a threshold anyone can act on with if():

if(abs({Forecast} - {Actual}) > 25000, "Material variance", "Within tolerance")

Magnitude and direction as two columns

abs() deliberately throws information away, and sometimes you want it back. Chronic over-forecasting and chronic under-forecasting are both accuracy failures, but they are *different* failures: one is happy ears and pipeline inflation, the other is sandbagging. Coaching the two identically helps nobody.

The fix isn't to abandon abs() — it's to ship two attributes.

abs({Forecast} - {Actual})
if({Forecast} > {Actual}, "Over-forecast", "Under-forecast")

Sort by the first, group by the second. Now "who is least reliable" and "which way do they lean" are separate, answerable questions, and a rep who is 40% high every quarter looks nothing like a rep who is 40% high and 40% low alternately — which is exactly the distinction the signed average was hiding.

If you want it in one field, and() composes the two conditions into a single flag for the case you care most about:

and(abs({Forecast} - {Actual}) > 25000, {Forecast} > {Actual})

Large *and* optimistic — the specific combination that inflates a board number, as a Checkbox you can filter a pipeline review by.

Catching disagreement between sources

The other durable use has nothing to do with forecasting. Enrichment providers, imports, and integrations all write numbers into your CRM, and they disagree constantly. abs() turns that disagreement into a sortable column.

abs({Enriched employee count} - {Stated employee count})

Where the enrichment provider and the number the prospect gave you diverge most. A small gap is normal. A gap of several thousand means one of the two is describing a different company — which usually means a bad domain match, and bad domain matches propagate into segmentation, routing, and pricing until somebody notices.

The billing version is the one finance cares about:

abs({Quoted amount} - {Invoiced amount})

Anything above zero is a quote-to-invoice discrepancy. Some are legitimate mid-cycle changes; some are revenue leaking through a manual step. Either way, a column that ranks them by size is a better month-end process than reading through both systems.

The same shape works for any two fields that *should* agree: seats sold against seats provisioned, contract value against sum of line items, the ARR on the account against the ARR rolled up from its deals.

Composing with the rest of the library

13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">abs() is an outer wrapper on a subtraction, which puts it downstream of most of the math family. sum() and avg() produce the numbers it compares; round() cleans up what it returns; [if() turns it into a label. The full rundown of the math functions lives in the pillar guide.

The one composition to *avoid* is with dates. dateDiff() already returns the absolute difference between two dates, so this:

abs(dateDiff({Created at}, {Close date}, "days"))

does nothing that 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">dateDiff() didn't already do. It's harmless, but it signals a misunderstanding worth clearing up: because dateDiff() is already unsigned, it cannot tell you whether a date is in the past or the future. If you need that, compare the dates directly rather than reaching for abs() — the [dateDiff() deep dive covers the pattern.

Blank-safety works the way the ?? deep dive described — an empty input evaluates to null and can blank the surrounding expression — but think before you default. ?? 0 on one side of a subtraction doesn't produce a missing value, it produces a *maximally wrong* value, and those will sit at the top of your sorted variance list crowding out the real ones.

CRM use cases that earn their keep

  • Forecast varianceabs({Forecast} - {Actual}) — how far off the estimate was, in either direction. Important because signed error averages to zero for the least consistent forecasters on the team.
  • Percent error — the rounded division version. Important because a $10k miss means something completely different on a $12k deal than on a $500k one.
  • Material-variance flagif(abs({Forecast} - {Actual}) > 25000, "Material variance", "Within tolerance"). Important because it makes a pipeline-review agenda that builds itself.
  • Optimism flag — the and() version combining size with direction. Important because over- and under-forecasting need opposite coaching.
  • Quote-to-invoice discrepancyabs({Quoted amount} - {Invoiced amount}). Important because revenue leaks through manual handoffs and nobody reconciles two systems by eye.
  • Enrichment disagreementabs({Enriched employee count} - {Stated employee count}). Important because a large gap is usually a bad domain match, and bad matches poison segmentation downstream.
  • Rollup reconciliation — account ARR against the summed ARR of its linked deals. Important because when those two disagree, every report built on either one is wrong and you don't know which.

Copy-paste formulas

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

Forecast variance in currency (Currency output):

abs({Forecast} - {Actual})

Percent error, divide-by-zero safe (Number output):

if(({Actual} ?? 0) == 0, 0, round(abs({Forecast} - {Actual}) / {Actual} * 100, 0))

Material-variance flag (Text output):

if(abs({Forecast} - {Actual}) > 25000, "Material variance", "Within tolerance")

Variance direction, to sit beside the magnitude (Text output):

if({Forecast} > {Actual}, "Over-forecast", "Under-forecast")

Large-and-optimistic flag (Checkbox output):

and(abs({Forecast} - {Actual}) > 25000, {Forecast} > {Actual})

Quote-to-invoice discrepancy (Currency output):

abs({Quoted amount} - {Invoiced amount})

Enrichment disagreement (Number output):

abs({Enriched employee count} - {Stated employee count})

Gotchas

Don't wrap dateDiff() in it. dateDiff() already returns an absolute difference. abs(dateDiff(...)) is redundant, and reaching for it usually means you were hoping for a signed result that dateDiff() never provides.

It throws away information on purpose. If anyone will ask "which direction", ship the direction as its own attribute rather than trying to recover it later. One formula, one question.

Think twice before defaulting the inputs. ?? 0 inside a subtraction converts "we don't know" into "wrong by the entire value". For variance columns, it's usually better to let blanks stay blank and build a separate flag for missing inputs.

Guard the denominator. Percent error divides by the actual, and an actual of zero is common — lost deals, unstarted contracts, new records. Check for it explicitly before dividing.

Force the output type. Currency for money variances, Number for counts. Auto usually guesses right, and a variance column that renders without its unit gets misread in exactly the meeting where it matters.

Three formula attributes deep is the ceiling. Nesting inside one editor is unlimited — if(... round(abs(...)) ...) is a single formula — but chaining separate formula attributes stops at three. If your accuracy metric is a formula on a formula on a formula, collapse it back into one expression.

A variance column is not an explanation. The list tells you where estimate and reality diverged, not why. Deals get descoped, contracts get renegotiated, enrichment providers refresh. Sorting by magnitude is how you decide what to look at first, not what to conclude.

Final thoughts

abs() is four characters and it changes what a metric measures. Any column in your workspace built on a subtraction — forecast against actual, quote against invoice, one system against another — is currently letting opposite mistakes cancel each other out and reporting the result as accuracy.

Pick the one comparison your team already argues about, ship abs() around it, and sort descending. The top of that list is usually a surprise, and it's usually the same three records every month.

For the rest of the library — every math, logic, date, text, and history function with CRM use cases — see the complete guide to Attio formula attributes.

And if you'd rather have forecast-accuracy metrics, reconciliation columns, and data-quality flags 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.

Book a free discovery call