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

The exp() and log() functions in Attio: how to score a company with three employees against one with 300,000

Written by

Published 12 min read

Tested in live Attio workspaces.

Open any enriched Attio workspace and look at employee count. You'll have companies with 3 people and companies with 300,000, sitting in the same column. Follower counts, funding raised, annual revenue, monthly pageviews — enrichment data is almost always like this: most records clustered at the bottom, a few enormous ones stretching the scale out to the horizon.

Now build a lead score on that column. What you have built is not a lead score. It is a list of your five biggest companies, with everything else rounded to zero.

log() is the fix, and it's the least-used function in the library that genuinely changes an outcome. exp() is its inverse, has one good use, and I'll be honest with you about how rare that is.

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(), mod(), and power().

Table of contents

What exp() and log() do

log(1000)      →   6.9078
exp(2.5)       →   12.1825

They undo each other. exp(log(x)) gives you back x, and log(exp(x)) does too. That relationship is why they're always documented together, and it's also why one of them is useful in a CRM and the other mostly isn't — compressing a runaway number is an everyday problem, and expanding one isn't.

The property that matters is what log() does to spacing. Feed it numbers that multiply and it returns numbers that add:

log(10 + 1)           →   2.40
log(100 + 1)          →   4.62
log(1000 + 1)         →   6.91
log(10000 + 1)        →   9.21

Each ten-fold jump in the input moves the output by the same fixed step of about 2.30. A range that spanned four orders of magnitude now spans seven points. That is the entire value proposition.

13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">exp() goes the other way: equal steps in, multiplying jumps out. Which is [power() territory from the last article, and if you want compounding you should use power() — it's clearer to read and you get to choose the base.

log() is the natural log, not the one Excel gave you

This is the one that bites, so it goes near the top.

Attio's log() is the natural logarithm, base e. Excel's LOG() defaults to base 10, and its natural log is called LN(). Same name, different function, no error message when you get it wrong — just answers that are all off by a factor of 2.3026.

log(1000)       →   6.9078        in Attio
LOG(1000)       →   3             in Excel

If you're rebuilding a scoring model that came out of a spreadsheet, this will silently rescale everything you did. Nothing will look broken, because a logarithm scaled by a constant still ranks records in exactly the same order — the numbers just don't mean what the spreadsheet's thresholds assumed, so every band boundary you copied across is now wrong.

There is no log10() or log2() in Attio either — I've checked the full forty-function list, same as with the missing sqrt() last time. You build them by changing base, which is just a division:

log({Value}) / log(10)      base 10
log({Value}) / log(2)       base 2

Base 10 is almost always what you want in a CRM, because "order of magnitude" is a thing people can reason about and "natural log units" is not. Divide by log(10) and the output becomes readable: 2 means hundreds, 3 means thousands, 6 means millions.

Orders of magnitude as the unit of measure

Here's the whole technique in one attribute:

round(log({Employee count} + 1) / log(10), 1)

Run it across a realistic spread:

EmployeesScore
30.6
501.7
5002.7
5,0003.7
300,0005.5

Look at what happened to the outlier. On the raw scale, the 300,000-employee company is 100,000 times the 3-person company, so in any weighted sum it eats the entire score. On the log scale it's about nine times — meaningfully bigger, which is true, rather than infinitely more important, which isn't.

Now it composes. A score with two inputs of wildly different scale is finally possible:

round(log({Employee count} + 1) / log(10), 1) * 10 + {Engagement score}

Without the log, employee count dominates and engagement is decoration. With it, both sit in a single-digit range and the weights you choose actually do something.

The same treatment applies to every heavy-tailed field you have: funding raised, ARR, pageviews, follower counts, app installs, headcount growth. If the biggest value in a column is more than about a hundred times the median, it's a candidate.

The +1 that keeps your formula alive

log(0) is undefined. So is log() of anything negative. Both will error or blank out the attribute, and enrichment data is *full* of zeros — companies with no recorded employee count, no funding, no followers, plus every record the enrichment provider simply hasn't reached yet.

Hence the + 1 in every formula above. It costs nothing and it changes the failure case into a sensible one:

log({Employee count} + 1)

Zero employees now scores log(1), which is exactly 0. The bottom of your scale is a clean zero rather than a broken field. The distortion at the top is negligible — adding 1 to 300,000 doesn't move the needle — and it only really nudges the very smallest values, where you don't have precision anyway.

Blanks are a separate problem from zeros, and + 1 doesn't save you there. A blank propagates through arithmetic and takes the field down, so pair it with ??:

log(({Employee count} ?? 0) + 1) / log(10)

That's the production-ready form. ?? handles missing, + 1 handles zero, / log(10) handles readability. Anything less than all three and you'll have a column with holes in it.

Saturation curves, and the only real job for exp()

I'll be straight: you will use log() twenty times for every exp(). But exp() has one shape that's hard to build any other way, and it's genuinely useful for scoring.

A saturation curve rises quickly, then flattens toward a ceiling without ever crossing it:

1 - exp(-{Meetings} / 5)
MeetingsScore
10.18
20.33
50.63
100.87
150.95

This is much closer to how multi-threading actually works than a straight count. The second meeting on an account tells you a lot. The fifteenth tells you almost nothing the fifth didn't — and a linear score would have the fifteenth-meeting account rated three times the five-meeting one, which no experienced AE believes.

The 5 is the shape control: it's the point where the curve reaches 63%. Drop it to 2 for something that saturates almost immediately, raise it to 10 for a slower climb. Multiply the whole thing by 100 if you want a percentage:

round((1 - exp(-{Meetings} / 5)) * 100, 0)

The same curve works for contacts per account, emails exchanged, sessions logged, or any "more is better, but with diminishing returns" number — which, if you think about it, is most engagement metrics in a CRM.

One limitation worth naming: you can't map log() across an array. It takes a single value, so there's no log() of a multi-value attribute and no geometric mean. Compress first at the record level, then aggregate.

Composing with the rest of the library

With round(), always — a raw log output like 5.4806 is not something to put in front of a rep:

round(log({ARR} + 1) / log(10), 1)

With ?? and + 1 together, as above, because the two failure modes are different and you need both guards.

With if() to turn a magnitude into a band people can filter on:

if(log(({Employee count} ?? 0) + 1) / log(10) >= 4, "Enterprise", if(log(({Employee count} ?? 0) + 1) / log(10) >= 2, "Mid-market", "SMB"))

That's 10,000+ employees as Enterprise and 100+ as Mid-market, expressed in a way that's trivial to retune — you're moving a single digit rather than rewriting thresholds.

With power(), as the exact inverse. log() compresses, power() compounds. If you find yourself reaching for power() to *reduce* the pull of a big number, you've got the wrong one; and exp(x) is the same thing as power(2.71828…, x), which is a good reason to just use power() whenever the base is something you'd rather state explicitly.

With sum() after the compression, never before — logging a total isn't the same as totalling logs, and the one you want is almost always compressing each record then adding.

CRM use cases that earn their keep

  • Company size scoring. round(log({Employee count} + 1) / log(10), 1) — the single most reusable formula in this article, and the fix for every lead score that's secretly ranking by headcount alone.
  • Funding-stage proxy. Order of magnitude of funding raised maps almost exactly onto seed / A / B / growth, without you maintaining a stage field that's always out of date.
  • Blended fit scores. The only way to combine headcount, revenue and engagement in one number without one of them swallowing the other two.
  • Taming enrichment outliers. The pillar's own example, log({Twitter follower count} + 1), exists precisely so that one celebrity-followed account doesn't rank above a hundred genuine prospects.
  • Diminishing-returns engagement. 1 - exp(-{Meetings} / 5) scores multi-threading the way it actually behaves.
  • Deal-size banding. Log of deal value gives you evenly spaced bands across a pipeline that runs from £2k to £2m, where fixed thresholds always end up lumpy.
  • Pageview and traffic tiers. Web enrichment is the most heavy-tailed data you'll import. It is unusable raw.

Copy-paste formulas

Adjust attribute names to match your workspace.

log(1000)

exp(2.5)

log({Value}) / log(10)

log({Value}) / log(2)

log({Employee count} + 1)

round(log({Employee count} + 1) / log(10), 1)

log(({Employee count} ?? 0) + 1) / log(10)

round(log(({ARR} ?? 0) + 1) / log(10), 1)

round(log({Employee count} + 1) / log(10), 1) * 10 + {Engagement score}

if(log(({Employee count} ?? 0) + 1) / log(10) >= 4, "Enterprise", if(log(({Employee count} ?? 0) + 1) / log(10) >= 2, "Mid-market", "SMB"))

1 - exp(-{Meetings} / 5)

round((1 - exp(-{Meetings} / 5)) * 100, 0)

round((1 - exp(-({Contacts} ?? 0) / 3)) * 100, 0)

Gotchas

log() is base e, not base 10. The Excel muscle memory is wrong here. Every threshold you port from a spreadsheet is off by a factor of 2.3026, and nothing will tell you.

There is no log10() or log2(). Change base with a division by log(10) or log(2).

Zero and negative inputs are undefined. Add 1. Every time, without thinking about it, on every log of a CRM field.

Blanks are not zeros. + 1 fixes zero; only ?? fixes blank. You need both guards, and most broken log columns I've seen are missing the second one.

Log output is not a percentage. It happens to land in a single-digit range, which makes it look like one. Label the attribute so nobody multiplies it by something expecting 0–100.

Compress before you aggregate. The log of a sum and the sum of logs are different numbers, and the second is usually the one you meant.

exp() explodes fast. exp(20) is roughly 485 million. If the input can be large, you almost certainly want it divided by something first, exactly as in the saturation curve.

You can't log an array. No multi-value inputs, no geometric mean. Compress at the record level.

Round the output and force the type. Long decimals undermine trust in a score; a numeric score left on Auto can be read as text and sort alphabetically, which puts 9 above 85.

Three formula attributes deep is still the ceiling, and in practice referencing another formula attribute is unreliable — inline the base expression rather than pointing at a computed one.

Final thoughts

log() earns its place for one reason: it's the only thing in the library that makes unequal data comparable. Any score you build on raw enrichment numbers is dominated by its largest records, and the failure is invisible — the column populates, the sort works, and the ranking is quietly meaningless. Wrapping the input in log(x + 1) / log(10) turns it into a real model, and it's a five-minute change.

exp() is the specialist. Learn the saturation curve, use it where more-is-better-but-less-so, and otherwise reach for power(), where you get to say what the base is.

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.