The eomonth() function in Attio: getting your CRM onto the calendar finance actually uses
Your pipeline thinks in deals. Your P&L thinks in months. Almost every reporting argument between sales and finance is really an argument about that mismatch — a deal that closed on the 29th, an invoice dated the 3rd, a renewal that "belongs" to March according to one system and April according to another.
eomonth() is a small function that fixes a surprisingly large part of this. It takes a date and returns the last day of the month that date lands in. That's it. But it is the only clean way to move a CRM record onto the calendar the rest of the business already runs on, and it does the one thing manual date arithmetic reliably gets wrong: it knows how long every month actually 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(), and today()/now().
Table of contents
- What eomonth() does
- Why month-end is the unit finance counts in
- The live month-end clock
- End of next month, and the missing second argument
- Billing dates that survive February
- Composing with the rest of the library
- CRM use cases that earn their keep
- Copy-paste formulas
- Gotchas
- Final thoughts
What eomonth() does
The signature is as short as they come:
eomonth(date)It returns the last day of the month containing that date. Attio's own example is the one worth memorising, because it's the edge case:
eomonth(date("2024-02-10")) → 2024-02-29February 2024 had 29 days. February 2023 had 28. April has 30, May has 31. A human writing "end of month" logic by hand has to encode all of that, and will get it wrong in a leap year that arrives three years after they wrote the formula and left the company. eomonth() is the function whose entire value proposition is not making you think about it.
Note what it returns: a date, not a number and not a label. That matters, and it's the source of the most common misuse — covered below.
Why month-end is the unit finance counts in
Sales operates in continuous time. A deal closes when it closes. Finance operates in discrete buckets, and every one of those buckets has a hard edge on the last day of a month: revenue recognition periods, invoicing runs, commission cut-offs, cohort definitions, board reporting.
When those two views disagree, it's almost never because someone is wrong about the deal. It's because the CRM stored a raw event date and the finance system stored a period, and nobody built the bridge between them. eomonth() is that bridge, in one function call:
eomonth({Close date})Every deal now carries the period it belongs to, as a real date. Two deals closing on 3 March and 28 March both return 2024-03-31. You can group on it, filter ranges on it, sort on it, and compare it against a target date — none of which you can do with a text label.
The live month-end clock
Pair eomonth() with today() and you get something no static field can give you: a number that counts itself down.
dateDiff(today(), eomonth(today()), "days")today() carries a daily recalculation, so this field shows days remaining in the current month on every record, every morning, without anyone touching anything. Turn it into a state and it becomes a view your team can actually work:
if(dateDiff(today(), eomonth(today()), "days") <= 5, "Month-end crunch", "Normal")The more useful version puts the deal's own close date against the current period boundary — which deals are supposed to land *this* month:
if({Close date} <= eomonth(today()), "Lands this month", "Next month or later")Combine that with a stage check and you have the forecast call that usually happens on a spreadsheet:
and({Close date} <= eomonth(today()), {Stage} == "Negotiation")Everything that flag catches is a deal your team believes closes this period and hasn't yet. That's the list, generated on its own, refreshed nightly.
End of next month, and the missing second argument
If you've used Excel, your fingers will want to type eomonth({Date}, 1) for "end of next month". Attio's documented signature takes one argument. There is no month-offset parameter.
The temptation is to reach for dateAdd() first:
eomonth(dateAdd({Invoice date}, 1, "months"))That reads well, and for most dates it does what you want. But adding a month to the 31st is exactly the class of arithmetic that has ambiguous answers across date libraries, and it's not a question you want your billing logic quietly resolving on your behalf.
The composition that avoids the question entirely:
eomonth(dateAdd(eomonth({Invoice date}), 1, "days"))Read it inside out. eomonth() lands on the last day of the month — always a real, unambiguous date. Add one day and you're on the first of the next month, which is also always real, whatever the month length was. Take eomonth() of that and you have the end of next month, with no day-of-month arithmetic anywhere in the chain. Same trick, one day at a time, works for the first day of the next period on its own:
dateAdd(eomonth({Contract start}), 1, "days")That's your billing anchor: the first day of the next full period.
Billing dates that survive February
The classic subscription problem: a contract starts on 31 January and bills monthly. What's the second billing date? Different systems answer 28 February, 2 March, or 3 March, and the customer notices.
Snapping to month-end removes the question, because month-end is the one day of the month that exists in every month:
eomonth({Contract start})For a customer whose contract began 31 January, that's 31 January. For one who began on the 3rd, it's also the 31st. Both are now on the same billing rhythm, and every subsequent period boundary is derivable with the +1-day trick above rather than by adding months to a day number that may not exist.
The same logic aligns renewals. If your contracts nominally run twelve months but your finance team wants them landing on period boundaries:
eomonth(dateAdd({Contract start}, 12, "months"))And the gap between the nominal date and the aligned one — the days you're giving away or clawing back — is worth surfacing rather than hiding:
dateDiff(dateAdd({Contract start}, 12, "months"), eomonth(dateAdd({Contract start}, 12, "months")), "days")Composing with the rest of the library
eomonth() returns a date, which means it slots into every date-shaped thing in the library:
| Composition | What you get |
|---|---|
formatDate(eomonth({Close date}), "MMMM yyyy") | A clean period label — "March 2026" |
dateDiff(today(), eomonth(today()), "days") | Days left in the current month, recalculating daily |
dateAdd(eomonth({Date}), 1, "days") | First day of the next month |
day(eomonth({Date})) | How many days that month has — 28, 29, 30 or 31 |
eomonth(dateAdd({Contract start}, 12, "months")) | A renewal date snapped to a period boundary |
day(eomonth({Date})) is the sleeper. It gives you the length of the month as a number, which is what you need for any per-day proration: divide a monthly amount by it and you have a daily rate that's correct in February and correct in July, without a lookup table.
{MRR} / day(eomonth({Contract start}))CRM use cases that earn their keep
Period assignment on every deal. eomonth({Close date}) gives sales and finance the same bucket for the same deal. Most "our numbers don't match" conversations end here.
Month-end deal watch. Days remaining in the month, on every open deal, counting down on its own. Filter to under five days and a Negotiation stage — that's the daily standup list, and nobody built it by hand.
Billing alignment. Snap contract starts to month-end so every customer bills on the same rhythm, and derive each next period with the +1-day composition rather than month arithmetic.
Renewal alignment. Twelve months from start, then snapped to the period boundary, so CS and finance are looking at the same renewal month.
Cohort dates that hold up. Signup month as a real date, not a string, so you can filter ranges and compute tenure against it later.
Proration rates. day(eomonth({Date})) as the denominator, so mid-month starts and stops are calculated against the actual length of the actual month.
Copy-paste formulas
eomonth({Close date})
eomonth(today())
dateDiff(today(), eomonth(today()), "days")
if(dateDiff(today(), eomonth(today()), "days") <= 5, "Month-end crunch", "Normal")
if({Close date} <= eomonth(today()), "Lands this month", "Next month or later")
and({Close date} <= eomonth(today()), {Stage} == "Negotiation")
eomonth(dateAdd(eomonth({Invoice date}), 1, "days"))
dateAdd(eomonth({Contract start}), 1, "days")
eomonth(dateAdd({Contract start}, 12, "months"))
formatDate(eomonth({Close date}), "MMMM yyyy")
day(eomonth({Contract start}))
{MRR} / day(eomonth({Contract start}))Gotchas
If you only need a label, you don't need this function. formatDate({Close date}, "yyyy-MM") gives you a groupable month string in one call. Reach for eomonth() when you need a real date — one you can compare, sort, filter by range, or hand to dateDiff(). Using it purely to produce a label you then format is a step you didn't need.
There is no second argument. Excel's EOMONTH(start, months) has no equivalent here. Compose with dateAdd() — and prefer stepping a single day off a known month-end over adding months to an arbitrary day number.
It doesn't know your fiscal calendar. eomonth() returns the last *calendar* day. If finance runs 4-4-5 periods, a fiscal year offset from January, or period-ends pinned to a weekday, this function will disagree with the ledger every single period — and it will look right while doing it. Check what "month-end" means in your finance system before you build on this.
Month-end is a date, not a deadline. A deal dated the 31st and a deal dated the 3rd both return the same value, which is the point — but it also means the field tells you nothing about urgency on its own. Pair it with dateDiff(today(), ...) when you want the pressure, not just the bucket.
The daily rollover is UTC. Any month-end countdown built on today() flips at midnight UTC, not in your workspace timezone. On the last day of a month, that boundary lands mid-afternoon for some of your team. Not broken, but explain it before someone files it as a bug.
Blank dates need handling. eomonth() on an empty date can't produce a period, and a deal with no close date is precisely the one worth chasing. Test for the null explicitly rather than letting the field sit quietly empty.
Force the output type. Date for the period fields, Number for day(eomonth(...)) and the countdowns, Text for the labels. Auto guesses, and a date field guessed as text sorts alphabetically — which is how "10 March" ends up before "3 March".
Three formula attributes deep is the ceiling. The nested compositions above are each a single formula, and nesting inside one editor is unconstrained. The limit is on formula attributes referencing other formula attributes.
Final thoughts
eomonth() is a one-line function with no options, and it's easy to read past it in the function list as trivia. It isn't. It's the piece that lets a CRM speak in periods instead of instants — and once records carry a real period date, the reconciliation work that eats the first week of every month mostly stops being work.
Add eomonth({Close date}) to your deals object, and dateDiff(today(), eomonth(today()), "days") beside it. One tells finance which bucket a deal belongs to. The other tells sales how long they have. They're the same function, pointed in two directions.
For the rest of the library — every math, logic, date, text, and history function with CRM use cases — see the complete guide to Attio formula attributes.
And if you'd rather have month-end alignment, billing anchors, and period reporting 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 callReady when you are.
Two ways in. Pick the friction that fits.