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

currency(), checkbox() and rating() in Attio: the three conversions that cannot fail, and why that is the problem

Written by

Published 21 min readTested in live Attio workspaces
Contents19 sections
  1. What currency(), checkbox() and rating() do
  2. Three functions with no error state
  3. They do not convert values, they assert types
  4. The rendering is the risk
  5. Only one of the three leaves the number alone
  6. currency() changes what other functions give back
  7. There is no currency code anywhere in the function
  8. What this means in a multi currency workspace
  9. Where checkbox() is honest
  10. A comparison is a better checkbox
  11. rating() and the problem with stars
  12. Rescaling is a decision, not a conversion
  13. The documentation is deepest where the risk is lowest
  14. Where the error actually goes
  15. The output type is usually the better tool
  16. CRM use cases
  17. Copy-paste formulas
  18. Gotchas
  19. Final thoughts

There is one function in Attio's type conversion category that can return an error. 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">number() refuses input it cannot parse, and [that refusal is the most useful thing in the category. There is one that fails on input that is completely correct: phoneNumber(), which rejects a real, dialable London landline because the country code was never in the string.

That leaves three. currency(), checkbox() and rating() have no failure mode at all. Feed them anything of roughly the right shape and you get a value back, every time, on every record.

The natural reading is that these are the safe ones. They are not. They are the three you should be most careful with in a live view, and the reason is not that they are buggy or badly designed. It is that they never check anything, because there is nothing for them to check.

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(), power(), exp()/log(), split()/splitPart(), startsWith()/endsWith(), lower()/upper(), left()/right(), date()/timestamp(), setTimezone(), number()/text(), year()/quarter()/month(), random(), and phoneNumber(). This one closes the type conversion category.

color:var(--color-text-heading)]">Prefer to watch? Every function in this series gets its own short video in the [Every Attio formula function playlist. 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 currency(), checkbox() and rating() do

Three single-argument functions, documented in one line each:

currency(value)
checkbox(value)
rating(value)

Attio's docs describe them as follows. currency(value): "Converts a number to a currency value." checkbox(value): "Converts a value to a checkbox. Non-empty or non-zero values become true. Empty values stay empty." rating(value): "Converts a number to a rating from 0 to 5. Numbers outside this range are clamped to the nearest valid value."

That is the entire specification. Two of the three descriptions contain the behaviour that will catch you, stated plainly and in passing.

Three functions with no error state

Put the category side by side and one column does all the work:

FunctionInput it rejectsWhat you get when you are wrong
number()Anything unparseableA visible error
phoneNumber()Anything without a country codeA failed conversion on correct data
text()NothingText, possibly not the text you expected
currency()NothingAn amount, in a currency you did not choose
checkbox()Nothingtrue, including for the word "false"
rating()NothingA clamped number that looks legitimate

The bottom three share a property worth stating outright: there is no input you can hand them that produces a complaint. Not a malformed one, not an out-of-range one, not one in the wrong unit. The function does something reasonable with whatever arrives and moves on.

A function that cannot complain cannot warn you. That much has been the running theme of this series. What makes these three different from every other quiet failure in the library is *why* they cannot complain.

They do not convert values, they assert types

number("1,200.00") does real work. It parses. Parsing is a process that can succeed or fail, which is exactly why number() has an error to return.

text({Deal value}) also does real work. It serialises. Serialising cannot fail, because every value has some textual representation, even if it is not the one you hoped for.

These three do neither. Look at what each one actually asks of the input:

  • rating(4) asks nothing. 4 is a number. It is already a valid rating. The function's job is to declare that this number lives on a 0-to-5 scale.
  • checkbox({Something}) asks nothing about truth. It asks whether a value is present, and presence is not a question any real value can fail.
  • currency(1200) asks nothing at all. 1200 is a number and stays 1200. The function's job is to declare that this number is money.

In each case the function is not transforming your data. It is making a claim about your data: this number is on a 0-5 scale, this value's presence means yes, this number is an amount of money in the workspace currency.

Every one of those claims is either true or false about your actual data, and the function has no way whatsoever to find out which. A 1-10 satisfaction score is a perfectly good number; nothing about it announces its range. A text column of the words true and false is perfectly good text; nothing about it announces that half the cells mean no. A column of EUR amounts is perfectly good numbers; nothing about them announces the currency.

So the functions cannot error, and the reason is not leniency. The information needed to detect the mistake is not in the value. It is in your head, the same way the country code was in the head of whoever typed the London phone number.

The rendering is the risk

Here is the part that makes this specifically a live-view problem rather than a data problem.

number() and text() produce types with no visual opinion. A number renders as digits. Text renders as characters. Neither carries any claim beyond itself, and if the value is wrong it usually *looks* a bit wrong — a stray comma, a truncated string, a suspicious zero.

These three produce the three most visually confident types in the CRM:

  • A currency value renders with a symbol. The symbol is a statement about the unit.
  • A checkbox renders as a box that is ticked. There is no element in any CRM that looks more definite than a ticked box.
  • A rating renders as a rating, not as the digit 5.

None of those renderings can express doubt. A checkbox has no "derived from a string that said false" state. A rating cannot display "this was clamped". A currency symbol cannot indicate "the unit here is a guess". The visual affordance is fully confident by construction, and it will be just as confident when the underlying assertion is wrong.

That is the answer to why a function that cannot return an error is more dangerous in a live view than one that can. It is not only that the error is missing. It is that the output is rendered by a control whose entire visual job is to communicate certainty. A wrong assertion does not look wrong. It looks authoritative.

Only one of the three leaves the number alone

Worth separating, because it changes how you audit them:

FunctionChanges the value?Changes the type?
rating()Yes, by clampingYes
checkbox()Yes, by collapsing to true or emptyYes
currency()NoYes

currency() is the only lossless one. currency(1200) is still 1200. Nothing is clamped, nothing is collapsed, nothing is discarded.

And it is still the most dangerous of the three in a report, for exactly that reason. A clamped rating at least leaves a trace — a flattened distribution, a suspicious pile of 5s, something a careful person might notice. A wrong currency leaves no trace at all, because the number is *correct*. It survives every sanity check you would think to run. The total adds up. The average is plausible. The per-deal figures match the source system to the penny.

The only thing wrong is the unit, and the unit is the one thing the data never carried and the rendering will never question.

currency() changes what other functions give back

This is the genuine reason currency() exists, and it is not cosmetic.

Attio's docs are specific about what the math functions return. sum(): "Returns the sum of an array of numbers or currency values. Returns a single value of the same type as the input." avg() carries the same sentence. min(), max() and median() are documented as operating on "numbers, currency values, or dates".

Read that as a rule about types rather than about arithmetic. The aggregation functions are type-preserving, so the type of the thing you feed them decides the type of the answer:

sum({Fee})

If {Fee} is a plain number, that total is a number, and it will render as a number everywhere it appears. If {Fee} is a currency value, the total is a currency value, and it renders as money.

So when you compute an amount and it comes back as a number, currency() is how you put it back into the money type before it reaches anything downstream:

currency({Deal value} * 0.15)

Whether arithmetic on a currency attribute already returns currency, or drops to a plain number, is worth testing on your own workspace rather than assuming — the docs specify the return type for the aggregation functions and say nothing about the operators. Build the formula, look at what the column renders as, and only reach for currency() if the money type did not survive.

There is no currency code anywhere in the function

The documented signature is currency(value). One argument. There is no second argument for a currency code, no currency(value, "EUR"), and nothing anywhere on the functions page that lets a formula name a currency.

That is a deliberate and reasonable design — the currency belongs to the attribute, not to the expression — but it has a consequence people discover late:

currency() converts the type. It does not set the unit. The unit comes from the configuration of the attribute the formula writes into, which means the same function call produces "$1,200" in one workspace and "€1,200" in another, from the identical input.

If you need an amount rendered in a specific currency, that is an attribute configuration task, not a formula task. No amount of nesting will get you there.

What this means in a multi currency workspace

Now the failure case. Suppose deal amounts arrived from a previous system as plain numbers, some recorded in USD and some in EUR, with the currency held in a separate text column.

currency({Amount raw})

Every record renders as money in the workspace currency. Every EUR deal is now displayed as though it were USD. Nothing errors, because nothing can — the numbers are all valid numbers, and currency() was never given anything to compare them against.

And it gets worse downstream, because the amounts now aggregate. sum() over that column adds USD to EUR and returns a single confident total that corresponds to no quantity that exists in the world. The pipeline number in the dashboard is not off by a rounding error; it is a sum of incommensurable things, presented with a currency symbol in front of it.

There is no formula fix. The language cannot select a currency, so it cannot repair this. What a formula *can* do is measure it, which is the same honest fallback the phoneNumber() article landed on:

if({Source currency} == "USD", {Amount raw}, 0)

Sum that and you have the portion of the total you can actually trust, next to the portion you cannot. That is a real number you can take to a meeting, which a blended total is not.

Where checkbox() is honest

The 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">checkbox() trap has been [covered in this series already: it tests for presence rather than truth, so imported text columns of true and false become uniformly true, and "empty values stay empty" makes the result three-valued rather than boolean.

The point worth adding here is the positive one, because checkbox() does have a job it does well. It is honest precisely where presence and truth are the same question:

checkbox({Open pipeline})

A "has live pipeline" flag, derived from an amount. Zero or empty is genuinely false. Any amount is genuinely true. The assertion the function makes about the data is correct, so the confident rendering is earned.

That is the whole test, and it generalises to all three functions. Do not ask "will this convert?" — it always will. Ask: is the claim this function is about to make about my data actually true? For checkbox() over a number or currency, usually yes. For checkbox() over free text, almost never.

A comparison is a better checkbox

When what you want is a flag for a condition, checkbox() is the wrong instrument, because it can only express one condition: presence. Write the condition instead:

{Deal value} > 50000

Set the formula attribute's output type to Checkbox and you get a real boolean column, filterable and groupable in every view, that says exactly what makes it true. A reviewer reading the formula six months from now sees the threshold. Reading checkbox({Deal value}) they see nothing except that somebody wanted a tick.

This is the same move as the comparison operators, which are the next thing in this series, and it is worth knowing before you reach for a conversion: any threshold is already a checkbox. It does not need converting into one.

rating() and the problem with stars

The clamp is documented and has been covered: numbers outside 0 to 5 are clamped to the nearest valid value, so a 1-10 score becomes a column where everything from 5 upwards is a 5.

What the rendering adds is the reason this one survives review. A number column full of 5s looks odd — five, five, five, five is a visible pattern, and someone eventually asks about it. A rating column full of 5s does not look odd at all. It looks like a set of records that are doing well. Ratings in a CRM are normally entered by a human as a judgement, so a top rating reads as somebody's assessment rather than as an arithmetic artefact, and nobody interrogates a compliment.

Then it reaches a report. The average of a clamped column is wrong in a specific and flattering direction: always too high, never too low, because clamping only ever pulls values toward the ceiling. An honest-looking satisfaction chart that is biased upward by construction is worse than no chart, and there is nothing in the column to indicate it.

Rescaling is a decision, not a conversion

If the source range is not already 0 to 5, the conversion has to be preceded by arithmetic, and the arithmetic is a choice:

rating(round({Score 1 to 10} / 2, 0))

Swap in ceil() to round generously or floor() to round conservatively. That is not a formatting preference — it decides whether a 7 out of 10 is reported as a 4 or a 3, which is the difference between "good" and "middling" in every view that column appears in. Make it deliberately.

Guard the input too, since a blank source with a nested calculation is its own quiet problem:

rating(round(({Score 1 to 10} ?? 0) / 2, 0))

Use the ?? operator at the attribute, as everywhere else in this series, and decide on purpose whether "no score" should read as 0 or stay empty. For a satisfaction rating, empty is almost always the truthful answer, and a forced 0 is a bad review you invented.

The documentation is deepest where the risk is lowest

A pattern in Attio's own documentation, and it points the wrong way.

The FAQ at the bottom of the functions page contains a full itemised answer to "What does text() return for each attribute type?" — five distinct cases spelled out, including that a record reference converts to the linked record's ID and not its name, that an actor reference gives a display name and resolves to empty for a deactivated member, and that an interaction gives the owning member rather than the interaction type.

That is genuinely good reference material. It is also documentation for the single function in the category that cannot hurt you. text() produces plain text, carries no visual claim, and is trivially verifiable by looking at the column.

The three functions that make unverifiable assertions into confident renderings get one sentence each and no FAQ entry at all. There is nothing on what 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">checkbox() does with each attribute type, nothing on what rating() accepts besides numbers, nothing on how currency() interacts with a multi-currency workspace. Combined with the structural gap this series noted in [the phoneNumber() article — the type conversion table is the only function table on the page without an Example column — the documentation depth across this category runs almost exactly inverse to the risk.

Not a complaint about effort. A reminder about where to spend your own testing time: the functions with the least written about them are the ones whose behaviour you most need to verify yourself, because their output looks correct by default.

Where the error actually goes

A function that cannot return an error does not remove the error. It relocates it.

With number(), the error surfaces on the record, at the moment the formula runs, in front of the person who built the formula. That person is holding the context, is expecting to be wrong, and is in a position to fix it.

With these three, nothing surfaces anywhere. The mistake travels intact through the formula, through the column, through the view, into the dashboard, and finally lands on whoever glances at it in a meeting and draws a conclusion. The error is not gone. It has been moved to the one place in the chain where nobody is checking.

You review formulas. You review imports. Nobody reviews a ticked box.

That is the practical rule for the whole category: the further a conversion pushes its failure from the point of authorship, the more testing it needs before it goes into anything anyone reads.

The output type is usually the better tool

The closing observation, and the one that changes what you build.

Every formula attribute in Attio has an output type, and this series has said from the beginning that forcing it beats leaving it on Auto. Set against that, these three functions look different — in a lot of cases they are doing a job the output type already does, only less visibly.

Compare the two ways to get a boolean flag:

checkbox({Deal value} > 50000)

against the comparison alone, with the attribute's output type set to Checkbox:

{Deal value} > 50000

The second is better in every respect. It is shorter, it says the same thing, and the type decision is declared once at the attribute level where anyone can see it, rather than buried in the middle of an expression. The type is metadata, and metadata belongs in the configuration, not in the formula body.

So the honest guidance for this whole category: color:var(--color-text-heading)]">reach for the output type first, and for a conversion function only when you need the type changed part way through an expression — inside a nested call, or on one branch of an [if() and not another. Check what your output type dropdown actually offers on a formula attribute before you build around a conversion function, because if the target type is available there, that is where the decision should live.

rating() keeps a real job regardless, because clamping is behaviour and not just a type declaration. But checkbox() around a comparison, and currency() on a value that is already the attribute's own type, are usually a sign that the work is happening in the wrong place.

CRM use cases

  • Has live pipeline flag — checkbox({Open pipeline}) — important because presence and truth genuinely coincide on an amount, which is the one situation where checkbox() asserts something true.
  • Commission or fee as money — currency({Deal value} * 0.15) — important because a computed amount that stays a plain number will not aggregate or render as money downstream.
  • Trustworthy portion of a multi-currency pipeline — if({Source currency} == "USD", {Amount raw}, 0) — important because a blended total across currencies is confidently wrong, and the trustworthy subtotal is a number you can actually defend.
  • Satisfaction rating from a wider scale — rating(round({Score 1 to 10} / 2, 0)) — important because rating() on the raw score silently clamps every high scorer to 5 and biases every average upward.
  • Threshold flag without a conversion — {Deal value} > 50000 with the output type set to Checkbox — important because the formula then states its own condition instead of hiding it behind a presence test.
  • Conversion-rate style ratio kept as a number — leave it alone rather than wrapping it in currency() — important because a ratio is not money and a currency symbol in front of it makes a column that reads as revenue.

Copy-paste formulas

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

Has live pipeline (Checkbox output):

checkbox({Open pipeline})

Threshold flag, no conversion needed (Checkbox output):

{Deal value} > 50000

Commission as money (Currency output):

currency({Deal value} * 0.15)

Trustworthy slice of a multi-currency amount (Currency output):

if({Source currency} == "USD", {Amount raw}, 0)

Rescaled satisfaction rating (Rating output):

rating(round({Score 1 to 10} / 2, 0))

Rescaled rating, blank-safe (Rating output):

rating(round(({Score 1 to 10} ?? 0) / 2, 0))

Honest boolean from an imported text flag (Checkbox output):

lower({Raw flag} ?? "") == "true"

Gotchas

  • None of the three can error. There is no input that produces a complaint, so there is no version of "it worked, so it must be right" that holds here.
  • currency() sets the type, never the unit. One argument, no currency code. The unit comes from the attribute's configuration.
  • Summing mixed currencies produces a confident nonsense total. sum() is type-preserving, not unit-aware. Split by source currency before you total.
  • checkbox() tests presence, not truth. The string "false" is non-empty, therefore true. Never run it over imported text; compare explicitly instead.
  • checkbox() output is three-valued. Empty stays empty, and empty is not false. Default at the input with ?? if you need a boolean on every row.
  • rating() clamps silently and only upward. Any source scale wider than 0-5 biases every average high. Rescale first.
  • A clamped rating column does not look wrong. Ratings read as human judgement, so a wall of 5s reads as good news rather than as an artefact.
  • Do not wrap ratios or counts in currency(). The symbol makes a non-money column read as revenue in every view it appears in.
  • Prefer the output type to the conversion function. A declared type on the attribute is visible; a conversion buried in an expression is not.
  • Do not point one of these at another formula attribute. As everywhere in this series, referencing a formula attribute is unreliable in practice — inline the base expression instead.

Final thoughts

This closes the type conversion category, and the category turned out to be about something other than conversion. number() parses and can fail. text() serialises and cannot. phoneNumber() demands a precondition your data probably does not meet. And these last three make claims — this is money, this means yes, this is out of five — that no function could ever verify, then hand the result to a control designed to look certain.

The rule that falls out of it is worth keeping past this article: a conversion that cannot fail has not validated anything. It has agreed with you. Before you put one in a live view, check the claim it is making on your behalf, because the rendering will not.

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

Next: comparison operators, which turn any threshold into a checkbox without a conversion function anywhere in sight.

And if you would rather have your amounts, flags and scores modelled correctly the first time, that is exactly 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.