The formatDate() function in Attio: quarter labels, cohorts, and dates people can read
Every date in your CRM is precise to the day, and that precision is exactly why you can't report on it: group a pipeline by close date and you get one bucket per day, which is no report at all. The questions that matter — how did Q1 do, which signup cohort retains, what's landing in March — live at a coarser grain. formatDate() is the regrouping tool: it collapses dates into the quarter, month, and cohort labels reporting actually runs on.
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(), and dateAdd(). This one covers formatDate(): the tokens, the quarter-label unlock, the sort trap, and the formulas worth copying.
Table of contents
- What the formatDate() function does
- The tokens
- Quarter labels: the reporting unlock
- The sort trap: it's text now
- Composing with the date library
- Blank-proofing
- CRM use cases that earn their keep
- Copy-paste formulas
- formatDate() vs. the date extractors
- Final thoughts
What the formatDate() function does
formatDate() turns a date into text, shaped by a format string:
formatDate(date, format)So:
formatDate({Close date}, "MMMM YYYY")returns 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">March 2026 — the same information as the raw date, at the grain a human (or a report) actually wants. The input can be any date: a native attribute, a custom one, or a date another function computed — including one manufactured by [dateAdd().
The output is a text attribute. That's the point — text is what views group on and labels display as — and it's also the source of the one trap in this function, which we'll get to. Force the output type to Text rather than leaving it on Auto.
The tokens
The format string is built from tokens; each token becomes a piece of the date:
YYYY— four-digit year:2026MM— two-digit month:03DD— day of month:14MMMM— full month name:MarchQ[Q] YYYY— the quarter pattern:Q1 2026
Combine them into whatever label the report needs: "MMMM YYYY" for readable month labels, "YYYY-MM" for sortable cohorts, "Q[Q] YYYY" for quarters. If you'd rather not think in tokens at all, type *"close date as quarter, like Q1 2026"* into the AI prompt box under the formula editor — Attio writes the format string for you, without consuming workspace AI credits.
Quarter labels: the reporting unlock
The headline use case. Revenue runs on quarters; date attributes don't. The bridge is one line:
formatDate({Close date}, "Q[Q] YYYY")Every deal now carries a 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">Q1 2026-style label, and any view or report grouped by that attribute becomes a per-quarter breakdown: pipeline by quarter, closed-won by quarter, [sum() of deal values per quarter. The analysis that used to mean exporting to a spreadsheet and building a pivot table is now just... a grouped view, always current, visible to the whole team.
The month version is the same move at a finer grain — formatDate({Created at}, "YYYY-MM") stamps every record with its signup cohort, and cohort-grouped views fall out for free.
The sort trap: it's text now
The one thing to internalize before shipping: formatDate() returns text, and text sorts alphabetically. Sort a column of "MMMM YYYY" labels and April comes before January, August before February — a timeline scrambled into dictionary order.
The fix is choosing tokens whose alphabetical order *is* chronological order:
formatDate({Created at}, "YYYY-MM")2026-03 sorts before 2026-08 as text, so the cohort column orders itself correctly. The rule of thumb: pretty labels (MMMM YYYY, Q[Q] YYYY) for grouping, where buckets matter more than sequence; numeric labels (YYYY-MM) whenever the column will be sorted. And keep the raw date attribute around — the formatted label is for humans and grouping, the original date is still what filters and date math should run on.
Composing with the date library
Because the input can be any date expression, formatDate() labels dates that don't exist as attributes. Yesterday's article built a renewal date nobody typed; today's gives it a reporting grain:
formatDate(dateAdd({Close date}, 12, "months"), "MMMM YYYY")That's the renewal *month* on every closed deal — group by it and you're looking at renewal load per month, the capacity-planning view every CS team wants. The same pattern labels any computed date: quarter of a projected contract end, cohort month of a timestamp pulled from history with valueSetAt().
Blank-proofing
The series regular applies: an empty date in means an empty label out. If 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">{Close date} is blank — every open deal — the quarter label is blank too, and those records cluster into an unnamed group at the bottom of your report. Give the gap a name with [??:
formatDate({Close date}, "Q[Q] YYYY") ?? "Not closed"Now the grouped view reads Q1, Q2, and an honest "Not closed" bucket, instead of a mystery blank that half the team misreads as an error. As always: decide what a missing date means, then make the fallback say it.
CRM use cases that earn their keep
- Pipeline by quarter —
formatDate({Close date}, "Q[Q] YYYY")— important because quarter-grouped revenue views replace the export-and-pivot ritual that goes stale the day it's built. - Signup cohorts —
formatDate({Created at}, "YYYY-MM")— important because cohort analysis starts with a cohort label on every record, and this one sorts chronologically by construction. - Renewal load per month —
formatDate(dateAdd({Close date}, 12, "months"), "MMMM YYYY")— important because CS capacity planning needs renewals bucketed by month, not scattered across precise dates. - Readable date labels —
formatDate({Close date}, "MMMM YYYY")— important because views shown to executives and clients read better as "March 2026" than as a raw timestamp. - Fiscal reporting lines — quarter and month labels feeding grouped reports — important because "how did Q1 do" should be a saved view, not a quarterly data-pull request to whoever owns the spreadsheet.
- color:var(--color-text-heading)]">Cohort-vs-cohort comparison — group by the cohort label, then compare [count() and sum() rollups across groups — important because trend questions are cohort questions in disguise.
Copy-paste formulas
Swap in your attribute names and these work as-is:
Quarter label (Text output):
formatDate({Close date}, "Q[Q] YYYY")Quarter label, blank-safe for grouped views (Text output):
formatDate({Close date}, "Q[Q] YYYY") ?? "Not closed"Sortable month cohort (Text output):
formatDate({Created at}, "YYYY-MM")Readable month label (Text output):
formatDate({Close date}, "MMMM YYYY")Renewal month from a computed renewal date (Text output):
formatDate(dateAdd({Close date}, 12, "months"), "MMMM YYYY")formatDate() vs. the date extractors
Three ways to pull something out of a date, for three kinds of questions:
| Question | Function | Returns |
|---|---|---|
| "What label should this date group under?" | formatDate({Close date}, "Q[Q] YYYY") | Text — Q1 2026 |
| "What day of the month is this?" | day({Contract start}) | A number (1–31) — for billing-day math |
| "What hour did this arrive?" | hour({Created at}) | A number (0–23) — for routing and coverage analysis |
The mental model: 13px] bg-[color:var(--color-bg-muted)] border border-[color:var(--color-border)] px-1.5 py-0.5 rounded">formatDate() makes labels for grouping and display; day() and hour() extract numbers for arithmetic and comparisons. If the next step is a report grouping, you want text from formatDate(); if the next step is math or an [if() threshold, you want a number from an extractor.
Final thoughts
formatDate() is the reporting-grain function: your dates already know which quarter and cohort they belong to, and one format string makes that knowledge groupable. Ship the quarter label first — it's one line, and it retires a spreadsheet pivot the moment the first grouped view exists. Just remember the trap: the output is text, so reach for YYYY-MM when a column needs to sort.
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 quarter views, cohort reports, and renewal capacity planning 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.