The power() function in Attio: the only way to model something that doesn't move in a straight line
Nearly every number in a CRM behaves linearly. Deal values add up. Days count off one at a time. sum(), avg(), dateDiff() — all of it moves in straight lines.
Some of the things you actually want to model do not. Revenue that expands 20% a year doesn't add a fixed amount, it multiplies. A lead's relevance doesn't drop by two points a week forever, it fades. power() is the only function in the Attio library that handles that shape, and as a bonus it's the only way to get a square root out of a workspace that has no sqrt().
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(), unique(), abs(), today()/now(), eomonth(), day()/hour(), and mod().
Table of contents
- What power() does
- Fractional exponents, and the missing sqrt()
- Compound growth projections
- CAGR: the number your board asks for
- Decay, and how to think in half-lives
- The exponent is where it goes wrong
- Composing with the rest of the library
- CRM use cases that earn their keep
- Copy-paste formulas
- Gotchas
- Final thoughts
What power() does
power(base, exponent) raises the first number to the second.
power(2, 8) → 256
power(1.2, 3) → 1.728
power(10, 0) → 1Three properties are worth holding onto, because every use below comes from one of them:
- An exponent above 1 compounds.
power(1.2, 3)is 1.728, not 1.6. The difference between multiplying and adding is the whole reason this function exists. - An exponent between 0 and 1 is a root.
power(x, 0.5)is the square root of x. - An exponent below 1 with a base below 1 decays.
power(0.9, 6)is 0.53 — a little over half of what you started with.
Anything raised to the power of 0 is 1. That's mathematically correct and occasionally surprising when an empty attribute defaults an exponent to zero and a whole column turns into 1s.
Fractional exponents, and the missing sqrt()
Attio's formula library has forty functions. 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">sqrt() is not one of them — I've checked the complete list, the same way I checked for month() and weekday() in the [day()/hour() article. There's no root function of any kind.
You don't need one. A root is a fractional exponent:
power(100000, 0.5) → 316.23
power({Value}, 1/3) cube root
power({Value}, 1/{Periods}) any root you likeThis is not trivia. The fractional exponent is what makes a compound *rate* calculable rather than just a compound *projection*, which is the difference between "here's what it'll be worth" and "here's how fast it's actually growing". That's the next two sections.
Compound growth projections
The pillar's own example is the simplest form: what an account is worth in three years if it expands at 20% a year.
{ARR} * power(1.2, 3)£50,000 becomes £86,400, not the £80,000 you'd get by adding 20% of the original three times. On one account the gap is rounding. Across a book of business it's the difference between an expansion target that's achievable and one that isn't.
The base is 1 + rate. 20% growth is 1.2, 7.5% is 1.075, a 10% *contraction* is 0.9. The exponent is the number of periods, and the period is whatever the rate is expressed in — get those two out of step and the answer is wrong by orders of magnitude, silently.
Monthly compounding over three years, at 0.75% a month:
{MRR} * power(1.0075, 36)That's a 30.9% total increase, which is a very different story from 0.75 × 36 = 27%. Small rates over many periods is exactly where linear intuition fails.
A rate that varies by segment belongs in an attribute rather than the formula:
{ARR} * power(1 + {Expansion rate}, 3)One honest caveat before anyone builds a board deck on this. A projection is an assumption, not a measurement. The formula is arithmetic; the 20% is a guess. Once it's a column in the CRM with a currency symbol on it, reps and execs read it as data. Name the attribute so it can't be misread — "Projected ARR (20% model)" rather than "3-year value" — and put the rate in a visible attribute rather than burying it in a formula nobody opens.
CAGR: the number your board asks for
Projection runs the compounding forward. CAGR runs it backwards: given where an account started and where it is now, what annual growth rate does that imply?
power({Current ARR} / {Starting ARR}, 1 / {Years}) - 1An account that went from £40,000 to £90,000 over three years: 90,000 ÷ 40,000 = 2.25, raised to the power of 1/3 is 1.3104, minus 1 is 31.0% a year.
As a percentage:
round((power({Current ARR} / {Starting ARR}, 1 / {Years}) - 1) * 100, 1)This is the calculation people export to a spreadsheet for, every quarter, by hand. It's one attribute. And unlike the projection above it isn't a model — it's a measurement of what actually happened, which makes it the more defensible number of the two.
If you don't have a "Years" attribute, derive it, but derive it carefully:
power({Current ARR} / {Starting ARR}, 1 / (dateDiff({Customer since}, today(), "days") / 365)) - 1Two conditions. Both values must be positive — a zero or negative starting ARR makes the division undefined. And the period has to be at least a few months; a customer of eleven days will produce an annualised rate in the thousands of percent, which is arithmetically correct and completely useless. Guard it:
if(dateDiff({Customer since}, today(), "days") < 365, 0, power({Current ARR} / {Starting ARR}, 1 / (dateDiff({Customer since}, today(), "days") / 365)) - 1)Decay, and how to think in half-lives
The other direction is more useful day to day. A lead that engaged eight months ago is not as interesting as one that engaged yesterday, and a lead score that ignores that is a lead score that ages into nonsense.
The temptation is to subtract a few points a week. Don't — linear decay hits zero and then goes negative, and you end up clamping it with max(). Exponential decay never reaches zero and behaves the way relevance actually behaves: fast at first, then a long tail.
Think in half-lives. Base 0.5, exponent measured in half-lives elapsed:
{Lead score} * power(0.5, dateDiff({Last touch}, today(), "days") / 90)Ninety days after the last touch the score is halved. After 180 days it's a quarter. After 45 days it's 71%. To change the aggressiveness you change the divisor and nothing else — 30 for a fast-moving inbound motion, 365 for enterprise where a year-old conversation still means something.
Rounded for display:
round({Lead score} * power(0.5, dateDiff({Last touch}, today(), "days") / 90), 0)Note what this inherits from 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">today(): it recalculates around midnight UTC, so the score drifts down on its own with no one touching the record. That's the opposite of the property [mod() gives you, and here the instability is the entire point — a decayed score that held still would be a stale score. Just don't build a stage automation that fires on crossing a threshold and then wonder why it triggered at 2am.
The same shape works for any relevance that fades: account health after the last QBR, referral value after the introduction, event lead quality after the conference.
The exponent is where it goes wrong
Almost every power() failure I've seen is an exponent problem, and it comes in two flavours.
The exponent is enormous. power(1.2, dateDiff({Created at}, today(), "days")) looks reasonable until you notice the exponent is 400. 1.2 to the power of 400 is a number with thirty-one digits. The field doesn't error in an obvious way; it just produces something absurd. If the exponent comes from a date difference, it almost always needs dividing — by 30 for months, 365 for years, or by a half-life as above.
The exponent doesn't match the rate. An annual rate with a monthly exponent overstates growth catastrophically. Write the unit into the attribute name so the mismatch is visible when someone reads the formula six months later.
There's a third, rarer one: a negative base with a fractional exponent. power(-8, 0.5) is the square root of a negative number, which has no real answer — expect an error or a blank. If the base is a computed difference that can go negative, wrap it in abs() and reapply the sign yourself, or guard with if().
Composing with the rest of the library
With round(), always, for anything a human reads — compounding produces long decimal tails and £86,399.99999 in a pipeline view is its own kind of credibility problem:
round({ARR} * power(1.2, 3), 0)With ?? so a blank doesn't take the field down:
({ARR} ?? 0) * power(1.2, 3)With if() to guard the domain — the pattern to reach for whenever a divisor or a base could be zero:
if({Starting ARR} > 0, power({Current ARR} / {Starting ARR}, 1 / {Years}) - 1, 0)With log(), which is the inverse operation and the subject of the next article in this series. Where power() amplifies spread, log() compresses it — the pillar's own example, log({Twitter follower count} + 1), exists to stop one celebrity account dominating a lead score. If you're reaching for power() to *reduce* the influence of a heavy-tailed number, you want log() instead.
And power(x, 0.5) alongside sum() for a crude size-weighting that isn't dominated by your largest account — the square root of revenue grows, but slowly.
CRM use cases that earn their keep
- Account CAGR. The measured growth rate per customer, live on the record. Sort descending and you have your expansion shortlist ranked by evidence rather than gut.
- Projected account value.
{ARR} * power(1 + {Expansion rate}, 3)for prioritising CS attention by future value, not current value. - Time-decayed lead score. The single highest-leverage use here. Scores that fade automatically stop reps working a list that was accurate in March.
- Engagement recency weighting. Same half-life shape applied to activity counts so recent meetings outweigh old ones.
- Account health fade. Health scored at the last QBR, decayed by time since, so silence registers as risk without anyone filing a report.
- Compound churn modelling.
power(1 - {Monthly churn}, 12)gives the annual survival rate implied by a monthly number — usually worse than people expect. - Normalised size bands.
power({Employee count}, 0.5)compresses a range that runs from 3 to 300,000 into something a scoring model can use.
Copy-paste formulas
Adjust attribute names to match your workspace.
power(2, 8)
power(100000, 0.5)
{ARR} * power(1.2, 3)
round({ARR} * power(1.2, 3), 0)
{MRR} * power(1.0075, 36)
{ARR} * power(1 + {Expansion rate}, 3)
power({Current ARR} / {Starting ARR}, 1 / {Years}) - 1
round((power({Current ARR} / {Starting ARR}, 1 / {Years}) - 1) * 100, 1)
if({Starting ARR} > 0, power({Current ARR} / {Starting ARR}, 1 / {Years}) - 1, 0)
{Lead score} * power(0.5, dateDiff({Last touch}, today(), "days") / 90)
round({Lead score} * power(0.5, dateDiff({Last touch}, today(), "days") / 30), 0)
power(1 - {Monthly churn rate}, 12)
power({Employee count}, 0.5)
({ARR} ?? 0) * power(1.2, 3)Gotchas
A huge exponent produces a huge number, quietly. Any exponent sourced from a date difference in days needs dividing by something. This is the single most common way this function goes wrong.
Rate and period must share a unit. An annual rate with a monthly exponent is wrong by a factor you won't notice until someone checks it in a spreadsheet.
Negative base plus fractional exponent is undefined. Roots of negatives have no real answer. Guard with abs() or if() if the base is computed.
Anything to the power of 0 is 1. A blank exponent that defaults to zero turns a whole column into 1s, which looks like data rather than an error.
A zero or negative starting value breaks CAGR. The division is undefined. Always wrap it in an if({Starting} > 0, …).
Very short periods annualise into nonsense. A three-week-old customer growing 5% implies a triple-digit annual rate. Floor the period at a year, or return 0.
Blanks propagate. As everywhere in the formula library, an empty attribute takes the whole field down. ?? before it reaches power().
Round the output and force the type. Compounding produces long decimals; currency needs 2 decimal places, rates usually 1. And set the output type explicitly — a rate left on Auto may be read as text and sort alphabetically, which puts 9% above 85%.
A projection is a model wearing a data costume. Once it's a currency column, people quote it. Name the attribute so the assumption travels with the number.
Three formula attributes deep is still the ceiling. Long expressions inside one editor are fine; chaining computed attributes is what caps out.
Final thoughts
power() is a small function with one big idea: some things multiply rather than add, and the difference compounds. Two formulas here earn their place in almost any workspace — a real CAGR per account, which replaces a quarterly spreadsheet ritual with a column, and a half-life decay on lead score, which stops your prioritisation quietly rotting.
The fractional exponent is the part worth remembering longest. Attio has no sqrt(), and it doesn't need one; power(x, 0.5) covers it, and power(x, 1/n) is what turns a before-and-after pair into a rate.
Formula layer grown past what one person can hold in their head? Get a free Attio audit and we'll map it — or see how we build this properly from the start in an 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.
Two ways in. Pick the friction that fits.