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

The round(), ceil(), and floor() functions in Attio: board-deck numbers, seat bundles, and honest tenure

Written by

Published 7 min read

Tested in live Attio workspaces.

Formulas produce exact numbers, and exact numbers are usually the wrong deliverable. A weighted pipeline of $19,847.3333 can't go in a board deck; 4.2 seat bundles can't go on an invoice; 5.8 months as a customer is not six months, whatever rounding says. round(), ceil(), and floor() are the three ways to trim a number — and choosing between them is really choosing *which direction it's safe to be wrong in*.

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(), and avg()/median(). This one covers the three rounding functions together, because the interesting part is the choice between them.

Table of contents

What round(), ceil(), and floor() do

One rounds to the nearest, one always up, one always down:

round(value, num_digits)
ceil(value)
floor(value)

round(1234.5678, 2) returns 1234.57. ceil(7.2) returns 8. floor(9.8) returns 9. Only round() takes a digits argument — ceil() and floor() always land on a whole number, which fits their jobs: things you count in units.

Force the output type to Number (or Currency for money) rather than leaving it on Auto.

round(): numbers for humans

round() is presentation. The math behind a weighted pipeline is exact and should stay exact — but the number a human reads shouldn't be:

round({Deal value} * 0.4, 0)

$19,847.3333 becomes $19,847, and the board deck stops looking like a debugger. Two digits for currency that keeps cents, zero for anything a person scans in a view. The rule: round() never goes into further math if you can help it — it's the last step before eyes, not an input to the next formula. Round once, at the end.

ceil(): numbers for billing

ceil() is the commercial one. Anything sold in units — seats in packs, licenses, storage blocks, support hours — has the same shape: the customer's need is fractional, the thing you sell isn't, and the fraction always costs a whole unit.

ceil({Users} / 10)

42 users, packs of 10: ceil(4.2) = 5 bundles. round() here would say 4 — and undercharge every customer whose need lands in the bottom half of a pack. That's the reason ceil() exists as a separate function: when the direction of the error has a price, "nearest" is not neutral.

floor(): numbers for progress

floor() is the honest one. Elapsed time, completed units, anything earned — the partial one doesn't count until it's finished:

floor(dateDiff({Signup date}, today(), "months"))

A customer 5.8 months in has completed 5 months, not 6. Tenure segments, anniversary triggers, "eligible for renewal pricing after 12 full months" — all of these break subtly with round(), because for half of each period it grants the milestone early. floor() is also the "full quarters as a customer" and "complete years since founding" function, for the same reason.

Picking the direction

The three functions are one decision table:

The number is for…Wrong-direction costUse
Reading — decks, views, reportsNone; it's displayround(x, 0)
Charging — seats, packs, blocksRounding down underchargesceil(x)
Progress — tenure, completed unitsRounding up overstatesfloor(x)

If trimming the number changes what someone pays or qualifies for, it's not formatting — pick the direction deliberately. If it doesn't, it's round().

Composing with the rest of the library

Rounding is a finishing move, so it wraps the others. Clean up an avg() rollup for display:

round(avg({Deal value}) ?? 0, 0)

Floor a dateDiff() for tenure, then route on it with if():

if(floor(dateDiff({Signup date}, today(), "months")) >= 12, "Anniversary pricing", "Standard")

The ?? guard goes *inside* the rounding — blank-proof the input, then trim the result. And the pillar's random() sampler is the same pattern: round(random() * 100, 0) turns a raw random into a clean 0–100 score for picking audit samples.

CRM use cases that earn their keep

  • Board-deck weighted pipelineround({Deal value} * 0.4, 0) — important because stakeholders read the number, and $19,847.3333 reads as noise, not precision.
  • Seat bundles to quoteceil({Users} / 10) — important because you never sell 4.2 bundles, and rounding down is a discount nobody approved.
  • Full months as a customerfloor(dateDiff({Signup date}, today(), "months")) — important because tenure milestones granted early — even by half a month — compound across every account.
  • Anniversary-pricing flagif(floor(dateDiff({Signup date}, today(), "months")) >= 12, "Anniversary pricing", "Standard") — important because eligibility rules need completed months, and this one can't fire early.
  • Clean average deal sizeround(avg({Deal value}) ?? 0, 0) — important because the rollup's long tail of decimals hides the actual magnitude at a glance.
  • Support blocks consumedceil({Hours used} / 5) — important because a 5-hour block that's 20 minutes used is a consumed block, and billing math has to agree with the contract.

Copy-paste formulas

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

Weighted pipeline, deck-ready (Currency output):

round({Deal value} * 0.4, 0)

Seat bundles needed, packs of 10 (Number output):

ceil({Users} / 10)

Full months as a customer (Number output):

floor(dateDiff({Signup date}, today(), "months"))

Anniversary-pricing flag (Text output):

if(floor(dateDiff({Signup date}, today(), "months")) >= 12, "Anniversary pricing", "Standard")

Average deal size, clean and blank-safe (Currency output):

round(avg({Deal value}) ?? 0, 0)

Gotchas

  • Only round() takes digits. ceil() and floor() always go to the nearest integer. To ceil to one decimal, scale: ceil({x} * 10) / 10.
  • Round last. Rounding an input and then multiplying compounds the error. Keep intermediate formulas exact; trim in the final, human-facing attribute.
  • round() at the midpoint. Halfway values have to break one way; if a threshold sits exactly on a .5 boundary, test a record on it rather than assuming.
  • color:var(--color-text-heading)]">Blank in, blank out. All three pass null through. Guard the input with [?? before rounding, not after.
  • Daily refresh for date math. Anything built on today() recalculates around midnight UTC, so a tenure floor flips the day the month completes — fine for milestones, not for minute-level countdowns.
  • Nesting limit. Three formula attributes deep. Wrap the rounding in the same formula as the math rather than adding a rounding-only attribute on top of a stack.

Final thoughts

round(), ceil(), and floor() look like formatting; two of them are policy. Trim for the reader with round(), charge in whole units with ceil(), grant milestones on completed periods with floor() — and any time the direction of the error has a cost, make sure the function you picked errs the way the business does.

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

And if you'd rather have your billing math, tenure segments, and deck-ready rollups 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