
Notion Formulas 2.0: Syntax, Real Formulas & Fixes (2026)
Formulas 2.0 replaced Notion’s old single-line, text-flattening formula language with a multi-line editor that supports local variables (let/lets), returns rich types instead of forcing everything into text, and lets you pull properties off related pages directly instead of routing everything through a rollup first. If you wrote formulas before the update, the syntax mostly still runs — but several patterns you relied on now behave differently, and that’s where most migration headaches start.
What Actually Changed From 1.0
The old formula engine had one job: take property values and squash them into a single text, number, or checkbox output. That’s why every 1.0 formula referencing a person or relation property ended up wrapped in join() or string concatenation — there was no other option.
Two things broke that constraint. First, relation and people properties now return actual lists instead of comma-separated text. Second, formulas can output pages, dates, people, and lists directly, not just flattened strings. Notion auto-converted every existing formula to preserve its old output — so a formula that used to read prop("Person") and return “Sarah, Mike” as text got rewritten behind the scenes to something like prop("Person").map(current.format()).join(", "). Functionally identical output, but now it’s explicit about the list-to-text conversion instead of doing it silently.
This matters because if you go back and simplify that converted formula to just prop("Person"), you’ll get a list of person objects, not text — and anything downstream expecting a string (a filter, another formula, a linked database’s text search) will break. I’ve seen this exact failure in a client workspace: someone “cleaned up” a legacy formula, the property type looked fine in the column, but a dependent formula three properties over started throwing type errors because it was doing string operations on what was now a list.
Why prop() References Work Differently Now
prop() didn’t disappear — it’s still how you pull any property into a formula. What changed is what you can do with the result once you have it, and how deep you can reach.
Under 1.0, referencing a relation property gave you a flattened text value. There was no way to reach into a related database’s fields without first building a rollup, then referencing that rollup in your formula. Under 2.0, a relation property returns a list of pages, and you can chain methods directly onto it: .map(), .filter(), .length(), .at(). For single-value lookups like “Created By,” you get dedicated methods — prop("Created By").name() and prop("Created By").email() — that pull workspace-level user data you previously couldn’t touch without a mention or manual entry.
The practical shift: rollups aren’t dead, but they’re no longer mandatory just to read a related property. You still need a rollup when you’re aggregating across many related records with Notion’s built-in aggregation math (sum, average, percent). You use direct prop() chaining when you need custom logic on top of that related data that a rollup’s dropdown menu can’t express.
Syntax Essentials You Need Before Writing Anything Complex
A few building blocks show up in almost every real-world formula:
- let / lets —
let(name, value, expression)defines one variable and uses it in the third argument.lets(a, val1, b, val2, expression)lets you chain multiple variables in one block. This is the single biggest readability upgrade in 2.0 — you no longer repeat the sameprop()chain five times in one formula. - current — inside
.map(),.filter(),.find(),.some(), and.every(),currentrefers to whatever list item is being evaluated at that step. You’ll writecurrent.prop("Status")constantly when working with relation lists. - .map() and .filter() — both work as standalone functions (
filter([1,2,3], current > 1)) or chained onto a property (prop("Tasks").filter(current.prop("Status") != "Done")). Chained syntax reads better once formulas get longer. - List helpers —
.length(),.first(),.last(),.at(index),.join(separator)operate on any list, including the lists that relation and people properties now return natively. - ifs() — evaluates condition/value pairs in order and returns the first match, with a final fallback value. Cleaner than nesting multiple
if()calls when you have three or more branches.
All of the syntax below is checked against Notion’s own formula syntax reference and its Formulas 2.0 migration guide — worth bookmarking both, because Notion updates function behavior without much fanfare.
Seven Formulas Worth Copying Into Your Workspace
1. Traffic-light status from a relation’s completion rate
Why you’d want this: gives a project record a single visual signal instead of making someone scan a task list to gauge progress.
let(
percentComplete,
round(
prop("Tasks").filter(current.prop("Status") == "Done").length()
/ prop("Tasks").length() * 100
),
ifs(
percentComplete == 100, "🟢 Complete",
percentComplete >= 50, "🟡 On Track",
percentComplete > 0, "🟠 Behind",
"🔴 Not Started"
)
)2. Date-bucket formula for a deadline view
Why you’d want this: groups tasks into “Overdue / Today / This Week / Later” buckets so a board view can sort by urgency instead of raw date.
let(
daysLeft, dateBetween(prop("Due Date"), now(), "days"),
ifs(
prop("Status") == "Done", "Done",
daysLeft < 0, "Overdue",
daysLeft == 0, "Due Today",
daysLeft <= 7, "This Week",
"Later"
)
)Note the argument order on dateBetween() — it's dateBetween(laterDate, earlierDate, unit). Reverse the two dates and every “days left” number comes out negative when it shouldn't. This is the single most common bug I see in migrated date formulas.
3. Text progress bar without a bar chart property
Why you'd want this: a visual percentage indicator inside a table view, no separate chart or rollup graphic needed.
let(
pct, round(prop("Tasks").filter(current.prop("Status") == "Done").length() / prop("Tasks").length() * 10),
repeat("▓", pct) + repeat("░", 10 - pct) + " " + format(pct * 10) + "%"
)4. Pull the assignee's name and email without a rollup
Why you'd want this: personalize a notification-ready text field (for an automation or a formatted view) using workspace data you'd otherwise have to type manually.
prop("Owner").name() + " (" + prop("Owner").email() + ")"5. Flag tasks assigned to someone no longer active on the project
Why you'd want this: catches orphaned assignments after a team member rolls off, before they turn into a missed deadline.
let(
activeTeam, prop("Project").at(0).prop("Active Members"),
isOrphaned, not activeTeam.some(current.name() == prop("Assignee").name()),
if(isOrphaned, "⚠️ Reassign", "")
)Relation properties return a list even when a task only ever links to one project, so you need .at(0) to pull that single related page before reaching into its “Active Members” field. Skip the .at(0) and you'll get a type error instead of the page you expected — this is one of the more common mistakes once people get comfortable with direct property access and forget relations are still lists under the hood.
6. Weighted priority score from three number fields
Why you'd want this: replaces a subjective “High/Medium/Low” select with a number you can actually sort and filter on.
lets(
impact, prop("Impact") * 0.5,
urgency, prop("Urgency") * 0.3,
effort, (10 - prop("Effort")) * 0.2,
round(impact + urgency + effort, 1)
)7. First unresolved comment-style flag using find()
Why you'd want this: surfaces the first blocking task in a project instead of a full list, which is more useful on a dashboard card.
let(
blocker, prop("Tasks").find(current.prop("Status") == "Blocked"),
if(empty(blocker), "No blockers", blocker.prop("Task Name") + " is blocked")
)Migrating Old Formulas and Debugging the New Editor
If you're converting a 1.0 formula rather than writing from scratch:
- Open the database and click the property name at the top of the column you want to edit.
- Select Edit property, then click into the formula field to open the multi-line editor.
- Read the auto-converted formula before touching it. Look for
.map(current.format()).join(...)patterns — that's Notion's compatibility shim for a property that now returns a list. - Decide whether you actually want the list behavior. If a downstream formula or filter expects text, leave the conversion in place. If you want the new list methods, strip the
.join()wrapper and adjust anything that reads this property. - Use the editor's inline type hints — hover over any sub-expression and it shows the resolved type. This is the fastest way to catch a formula that's silently returning a list where you expected a single value, which is the most common post-migration error.
- Test against an edge case first: an empty relation, a person property with nobody assigned, a date field left blank. Formulas that assume a value exists will throw on the first blank record someone creates.
On the type-hint point specifically: the editor now flags mismatches inline rather than waiting until you save, which catches most of the “expected number, got list” errors before they hit your data. It won’t catch logic errors — a working formula that returns the wrong answer for the wrong reason still needs a human to spot it.
On performance: formulas that chain .filter() or .map() over a relation property get slower as that relation grows, because Notion re-evaluates the chain against every related record on every recalculation. On a database with a few hundred related tasks this is invisible. On a database with several thousand, especially if multiple formulas on the same page each run their own .filter() pass over the same relation, view load time noticeably degrades. If you're hitting that wall, push the aggregation into a rollup (which Notion computes more efficiently at the database level) and reference the rollup's output in your formula instead of re-filtering the raw relation every time.
What Formulas Still Can't Do
Formulas operate on the current page's own properties and whatever they can reach through relations — that's the boundary, and it hasn't moved with 2.0. Specifically:
- No cross-database queries without a relation. You cannot write a formula that searches an unrelated database for matching records. If two databases aren't linked, a formula can't bridge them — you need an actual relation property first.
- No custom reusable functions. Every formula lives on its own property, in its own database. There's no shared function library you can define once and call from multiple databases — if five databases need the same logic, you're pasting (and maintaining) that logic five times.
- No side effects. Formulas can't create, update, or delete records, send notifications, or call an external API. That's what Notion's automations are for. When a request is “notify someone when X happens” rather than “compute a value from existing data,” you want an automation, not a formula.
- No loops beyond list methods.
.map()and.filter()cover most iteration needs, but there's no general-purpose loop construct for arbitrary repeated logic.
When a request outgrows these limits, the two usual fallbacks are: use an automation for anything that needs to write data or trigger externally, or restructure the database (add a relation, split a database, standardize a select property) so the query becomes possible again rather than trying to formula your way around a structural gap.
Frequently Asked Questions
Do my old Notion formulas still work after the 2.0 update?
Yes. Notion automatically converted existing formulas to preserve their original output, usually by wrapping list-returning properties in .map().join() patterns. They'll keep running as-is; you only need to touch them if you want the new list-based behavior or you're debugging an error.
Why does my formula return a list instead of text now?
Relation and people properties changed from returning flattened text to returning actual lists of pages or people. If you reference one of these properties directly without .map(), .join(), or a similar conversion, you'll get a list object back, not a string — which breaks anything downstream expecting text.
Can Notion formulas reference a property in a completely separate database?
Only if the two databases are connected by a relation. Formulas 2.0 lets you reach directly into a related page's properties without a rollup step first, but the relation itself still has to exist. There's no way to query an unrelated database from a formula.
What's the difference between using a rollup and chaining prop() with .filter()?
A rollup uses Notion's built-in aggregation math (sum, count, average, etc.) and is generally faster on large relations because Notion optimizes it at the database level. Chaining prop().filter() gives you custom logic a rollup's dropdown can't express, but it re-evaluates against every related record each time and gets slower as that relation scales into the thousands.
How do I see what type a piece of my formula is returning?
Open the formula in the multi-line editor and hover over any sub-expression — the editor shows the resolved type inline. This catches most type-mismatch errors (expecting a number, getting a list) before you save, though it won't catch a formula that returns the wrong value for the right type.
For the underlying database structure these formulas usually sit on top of, see our guide to building relational Notion databases with views, relations, and rollups. If you're specifically deciding between a rollup and a direct formula reference, our linked databases, relations, and rollups guide covers that tradeoff in more depth. And if a formula keeps failing silently because the trigger you actually need is event-based rather than computed, check our Notion automations troubleshooting guide before you keep fighting the formula editor.