The min() and max() functions in Attio: next renewal, biggest deal, live on the record
sum() answers "how much is in play at this account." A different question runs the Monday review: *which one* — which renewal comes first, which deal was the biggest, which contract ends soonest. min() and max() answer it: they scan an array and return the extreme, and because they work on dates as well as numbers, the extreme is often the next action itself.
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(), and formatDate(). This one covers min() and max() together — they're the same move in two directions.
Table of contents
- What the min() and max() functions do
- The rollup trick, one step further
- min() on dates: the next-renewal unlock
- max() on values: the biggest-deal anchor
- Blank-proofing
- Composing with the rest of the library
- CRM use cases that earn their keep
- Copy-paste formulas
- min()/max() vs. sum()/avg()/median()
- Final thoughts
What the min() and max() functions do
Each takes an array and returns one value from it:
min(values)
max(values)So min([10, 25, 3]) returns 3 and max([10, 25, 3]) returns 25. The part that matters: the array can hold numbers, currency, or dates. On dates, min() means *earliest* and max() means *latest* — and that's where these two stop being spreadsheet trivia and start being CRM fields.
As with the whole series: force the output type explicitly — a date type when the input is dates, a number or currency type when it's amounts — rather than leaving it on Auto.
The rollup trick, one step further
Like every array function, min() and max() pair with relationship and multi-value attributes. A formula on a Company can reach into its linked records: type { in the editor and the attributes exposed through your relationships appear alongside the Company's own. Point the function at one of them:
max({Deal value})That's the largest deal across everything linked to the account. It's the same graph-walk that makes 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">count({Team}) and [sum({Deal value}) work — but where sum aggregates the pile into a total, min and max reach in and pull out one record's value: the extreme.
min() on dates: the next-renewal unlock
The headline use case, straight from the "which one" family. An account holds several contracts, each with its own renewal date. The one that matters operationally is the *soonest*:
min({Renewal date})One formula on the Company and every account shows its next renewal — the real next-action date for CS, sortable in every view, always pointing at whichever contract is currently closest. No workflow updates it, no one maintains it; when a contract renews and its date rolls forward, the formula simply starts pointing at the new soonest one.
The mirror image is max() on dates: latest contract end across an account (when does this relationship actually run out?), or most recent close date among its deals.
max() on values: the biggest-deal anchor
On amounts, max() answers the anchoring question:
max({Deal value})The largest deal in an account's history — the number that anchors expansion conversations, because "they've bought at $80K before" reframes what a $60K proposal looks like. min() on the same array is the floor: the smallest deal you've done with them, useful for spotting accounts that started tiny and grew (your best expansion story) versus accounts that started big and shrank.
Blank-proofing
The series regular, with a twist worth thinking through. A record with nothing linked — no deals, no contracts — feeds an empty array, and the result is null.
For amounts, guard it with ?? as usual:
max({Deal value}) ?? 0For dates, blank is usually *correct* — an account with no contracts has no next renewal, and inventing one would be worse. Put the guard on the downstream math instead:
(dateDiff(today(), min({Renewal date}), "days") ?? 999) < 60Accounts without contracts behave as "renewal 999 days away" — safely out of the risk queue instead of invisibly absent from it. Same principle as always: decide what empty means, then make the fallback say it.
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">min() returns a date, so it slots into everything the date library does. Countdown to the next renewal, via [dateDiff():
dateDiff(today(), min({Renewal date}), "days")Risk flag on top, via if():
if((dateDiff(today(), min({Renewal date}), "days") ?? 999) < 60, "Renewal soon", "Healthy")And a reporting label, via formatDate(): formatDate(min({Renewal date}), "Q[Q] YYYY") groups accounts by the quarter their next renewal lands in — a renewal-load-by-quarter view in one line. The pattern from the last three articles keeps compounding: one function finds the date, the next measures it, the next labels it.
CRM use cases that earn their keep
- Next renewal per account —
min({Renewal date})across linked contracts — important because CS priorities should be driven by whichever contract is actually closest, not by whichever one someone remembered to flag. - Largest deal in history —
max({Deal value})— important because expansion proposals anchor against the biggest number the account has already said yes to. - Renewal countdown —
dateDiff(today(), min({Renewal date}), "days")— important because "days to next renewal" as a sortable column turns the renewals meeting into a sorted view. - Latest contract end —
max({Contract end})— important because "when does this relationship fully run out?" is the churn-exposure date, and it's the latest end, not the earliest. - Smallest-deal floor —
min({Deal value})— important because start-small-grow-big accounts are your expansion playbook, and the floor-vs-ceiling gap is how you find them. - Renewal load by quarter —
formatDate(min({Renewal date}), "Q[Q] YYYY")— important because capacity planning wants accounts bucketed by when their next renewal lands.
Copy-paste formulas
Swap in your attribute names and these work as-is:
Next renewal across linked contracts (Date output):
min({Renewal date})Largest deal ever, blank-safe (Currency or Number output):
max({Deal value}) ?? 0Days to next renewal (Number output):
dateDiff(today(), min({Renewal date}), "days")Renewal risk flag, blank-safe (Text output):
if((dateDiff(today(), min({Renewal date}), "days") ?? 999) < 60, "Renewal soon", "Healthy")Next-renewal quarter label (Text output):
formatDate(min({Renewal date}), "Q[Q] YYYY")min()/max() vs. sum()/avg()/median()
Five array functions, two kinds of questions:
| Question | Function | Returns |
|---|---|---|
| "Which renewal comes first?" | min({Renewal date}) | One value — the earliest date |
| "Which deal was biggest?" | max({Deal value}) | One value — the largest amount |
| "What's the total in play?" | sum({Deal value}) | The aggregate — everything added up |
| "What's a typical deal here?" | avg({Deal value}) or median({Deal value}) | The aggregate — the center of the pile |
The mental model: sum() and avg() describe the pile; min() and max() pick from it. Aggregate questions ("how much", "how typical") go to the describers; selection questions ("which one", "what's next", "what's the record") go to the pickers. Account views usually end up wanting one of each — total pipeline *and* next renewal.
Final thoughts
13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">min() and max() are the "which one" functions, and the date form is the one to ship first: min({Renewal date}) puts a self-maintaining next-action date on every account, and the countdown and risk flag from the [dateAdd() playbook stack straight on top. The value form follows — every expansion conversation is better anchored to max({Deal value}) than to memory.
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 renewal radar, account rollups, and expansion signals designed and shipped for you, that's literally what we do. Get a free workspace audit or see the AI-native Attio sprint.
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 callReady when you are.
Two ways in. Pick the friction that fits.