In a Notion formula, if(condition, value_if_true, value_if_false) returns the first value when the condition is true and the second when it's false. ifs(condition1, value1, condition2, value2, …, fallback) checks its conditions in order and returns the value for the first one that's true. Use if() for a two-way result such as "Overdue" or blank, and switch to ifs() once a row can land on three or more labels. I write every multi-outcome label as ifs() with the finished case on the first line, because the first true condition wins and a nested if() hides that order inside brackets.
For a single yes-or-no flag on a task, an invoice or a project, choose if(). For a status column with Done, Overdue, Due soon and Later, choose ifs(). Notion's intro to formulas marks a project as overdue with this one:
if(and(now() > Due Date, Status != "Done"), "Overdue", "")
The help page writes properties by name, as the editor displays them. Notion's formula syntax reference writes them as prop("Due Date"), and that's the form used below so you can type every example. Here is the same check for a task database with a date-only Due property, followed by an ifs() version that gives every row a label:
if(prop("Status") != "Done" and prop("Due") < today(), "Overdue", "")
ifs(
prop("Status") == "Done", "Done",
empty(prop("Due")), "No due date",
prop("Due") < today(), "Overdue",
"Open"
)
The formula labels the row. Which Status option counts as finished, and which situations deserve a label of their own, stays your decision; the formula only checks the text you type between the quotes. For everything else that changed in the 2.0 language, from tokens to lists, the Formulas 2.0 overview has the full picture.
Write an if() condition that returns true or false
if() takes three arguments in a fixed order. Notion's reference says it "returns the first value if the condition is true; otherwise, returns the second value", so if(true, 1, 2) gives 1 and if(false, 1, 2) gives 2. Thomas Frank's if() reference states the rule that matters most when a condition won't work: the first argument must always return a Boolean, a true or false value.
Each property type reaches true or false its own way. These condition shapes come from the property examples in Notion's syntax reference, with names from a client or task database:
| Property type | Condition | True when |
|---|---|---|
| Checkbox | prop("Invoice sent") | The box is checked. not prop("Invoice sent") flips it. |
| Select or Status | prop("Status") == "Done" | The option name matches the quoted text. |
| Number | prop("Hours") > 40 | The comparison holds. |
| Date | prop("Due") < today() | Due falls before today's date. |
| Multi-select | prop("Tags").includes("Urgent") | The list holds that tag. |
| Text | contains(prop("Notes"), "retainer") | The search text appears in the value. |
| Phone, Email, URL | !empty(prop("Phone")) | The property has a value. |
A Checkbox is already true or false, so it goes into the condition as it is; Notion's syntax table writes prop("Checkbox") on its own. A Select reads as text, which is why Frank compares one with == and a quoted option name. The editor is case sensitive about property names; Notion's beginner guide to formulas recommends typing a name exactly as it is in the database or selecting it from the menu. The same guide says contains() only accepts text. When a fixed word won't do, test() checks a regular expression: test("Notion", "Not") returns true.
Notion's reference lists five comparison operators: ==, >, >=, < and <=. For "not equal", write !=, as Notion's overdue example does; the reference shows "a" != "a" returning false. Every example on this page uses those spellings.
Notion also accepts X ? Y : Z as the equivalent of if(X, Y, Z), as in prop("Paid") ? "Paid" : "Unpaid". I keep it for short two-outcome lines, because a question mark is easy to miss inside a longer formula.
Combine conditions with and, or and not
Each logical operator has three spellings in Notion's reference: a word, a symbol and a function. and, && and and() run the same test, as do or, || and or(). not and ! flip a single condition. Frank's and reference adds that capitalized AND and And work too. I stick to the lowercase words, because they match Notion's own reference and read like the rule you'd say out loud.
The overdue rule is an and: the date has passed and the work isn't finished. Both lines below return the same result:
if(prop("Due") < today() and prop("Status") != "Done", "Overdue", "")
if(and(prop("Due") < today(), prop("Status") != "Done"), "Overdue", "")
Notion's example compares with now(), which returns the current date and time. For a Due property that holds only a date, I compare with today(), which the reference describes as the current date without the time, so neither side of the comparison carries a clock time. If your Due dates include a time, the dateBetween off-by-one guide covers where the day boundary falls.
Mixing and with or calls for parentheses. Frank's or reference notes that or has a lower operator precedence than and, so a or b and c is read as a or (b and c). To flag an invoice that's unpaid and either past due or disputed, group the either-or part yourself:
if((prop("Due") < today() or prop("Disputed")) and not prop("Paid"), "Follow up", "")
Without the brackets, every past-due invoice would read "Follow up", paid or not.
Switch to ifs() at three outcomes
ifs() takes pairs of condition and value, then a last value for when no condition is true. Notion's reference describes it as returning "the value that corresponds to the first true condition" and as an alternative to multiple nested if() statements. Its two examples show both halves of that rule: ifs(true, 1, true, 2, 3) returns 1 even though the second condition is also true, and ifs(false, 1, false, 2, 3) falls through to 3.
Frank's ifs() reference adds two details. You can chain any number of condition and value pairs, as long as there's at least one of each. The fallback is optional: leave it off and the result is blank when nothing matches, as in ifs(now() > prop("Due Date"), "This is overdue!").
Here's an invoice status written both ways, for a database with a Paid checkbox, a Sent checkbox and a Due date:
if(prop("Paid"), "Paid",
if(not prop("Sent"), "Draft",
if(empty(prop("Due")), "Sent, no due date",
if(prop("Due") < today(), "Overdue", "Sent"))))
ifs(
prop("Paid"), "Paid",
not prop("Sent"), "Draft",
empty(prop("Due")), "Sent, no due date",
prop("Due") < today(), "Overdue",
"Sent"
)
Both versions give each invoice the same label. The nested version ends with four closing brackets, and reading its third check means holding the first two in your head. In the ifs() version each line is one rule, and the order you read is the order Notion checks. Frank's if() page confirms that nested if() statements are still supported, so an old formula can wait until you next need to change it.
Put the finished case first: the order is the logic
When an ifs() status column shows the wrong label, check the order of its lines before anything else. Notion only reaches a line when every line above it was false. notion.vip's walkthrough of the if() function says the same about a nested grade formula: "At each step, we know that all previous conditions are false."
Here are two versions of a task status with five outcomes. They use the same conditions in a different order:
/* Wrong order */
ifs(
empty(prop("Due")), "No due date",
prop("Due") < today(), "Overdue",
prop("Status") == "Done", "Done",
prop("Due") <= dateAdd(today(), 7, "days"), "Due soon",
"Later"
)
/* Right order */
ifs(
prop("Status") == "Done", "Done",
empty(prop("Due")), "No due date",
prop("Due") < today(), "Overdue",
prop("Due") <= dateAdd(today(), 7, "days"), "Due soon",
"Later"
)
In the first version, a task finished last week still has a Due date before today, so the second line is true and the row reads "Overdue". A finished task with no date stops at the first line and reads "No due date". The Done check never gets its turn for either row. Moving it to the top fixes both, and the remaining lines can stay as they are: by the time Notion reaches the date checks, every row left is unfinished.
The same reasoning orders the date lines. "Overdue" sits above "Due soon" because a date before today also passes the <= dateAdd(today(), 7, "days") test. I order the lines from the most final fact to the least: finished, missing data, late, soon, everything else. A row should stop at the fact that matters most about it.
The seven-day window is a rule I chose for this example. dateAdd() accepts days, weeks, months and other units, so set the window your review uses; the weekly review dashboard guide explains why a defined window beats a vague "this week".

Guard blank values and keep both branches one type
Blank properties are the next source of wrong labels. Notion's reference says empty() "returns true if the value is empty" and that 0, "" and [] all count as empty. Frank's empty() reference explains why: empty() checks for falsiness, so false counts as well, and a Number holding 0 passes the test the same way a blank one does.
That matters wherever 0 is a real value, such as hours logged or an amount due. Frank's examples show prop("Number") == empty() returning false for 0, so the equality form catches only a blank field:
ifs(
prop("Hours") == empty(), "Hours missing",
prop("Hours") == 0, "No hours billed",
"Billed"
)
Guard a property before you operate on it. Notion's common formula errors page says an automation hitting an unexpected empty value errors and pauses, and its advice is to "use if or ifs to check that a property being operated on has a value". I apply the same guard in formula properties, which is why the status formula checks empty(prop("Due")) before any comparison with today. A date can also be the condition on its own: the errors page writes if(Date, …), and Frank's ifs() example uses not prop("Due") to catch a task with no date.
Return types are the second trap. In Formulas 2.0 the two results of if() can have different types, and Frank's if() page gives if(Boolean, string, number) as valid. The cost shows when one branch is a date and the other is "". Notion's errors page says formulas in automations "expect a specific return type" and that the automation will otherwise fail to save. Its example, if(Date, Date.dateAdd(1, "day"), ""), has an unknown return type "because it could either be date or text", and the documented fix is empty():
if(Date, Date.dateAdd(1, "day"), empty())
Frank reports the same for a formula property: if(prop("Number") == empty(), empty(), today()) keeps a return type of Date, and swapping empty() for "" turns it into Unknown. My rule is that both branches return the same type. For a text label, "" is fine, and Notion's own overdue example uses it. For a date, a number or a list, the blank branch is empty(). When a date output still won't behave, the guide to Notion formula date errors sorts out Text dates, lists and empty inputs.
Test Multi-select and Relation properties as lists
Notion's syntax reference lists the formula type of a Multi-select as Text (list), of a Relation as Page (list) and of a Person property as Person (list). Comparing a whole list with one word is the wrong test. For array properties such as Multi-select, the errors page says to "use .includes instead of == in your if() statement":
if(prop("Tags").includes("Urgent"), "Urgent", "")
On a relation, the condition usually runs over the related pages. The errors page lists .filter(), .every(), .some() and .find() for working through a list of pages, and the formulas help checks that every sub-item is finished with every(current.Status == "Done"). A project label built from a Tasks relation looks like this:
ifs(
prop("Tasks").length() == 0, "No tasks",
prop("Tasks").every(current.prop("Status") == "Done"), "All tasks done",
prop("Tasks").some(current.prop("Status") == "Blocked"), "Blocked task",
"In progress"
)
The empty relation gets its own first line, so the label never depends on what every() returns for a project with no tasks. "Blocked" stands for whatever Status option your team uses for stuck work. A rollup can return a number, a date or a list depending on its configuration, so check which one yours gives before choosing between a comparison and .includes(). The relations and rollups guide covers the setup these conditions read from.
Keep a long ifs() readable
Notion's guide to the Formulas 2.0 changes says formulas "can be written multi-line in the editor", with tabs and comments, and the editor type-checks and highlights errors as you write. Frank's ifs() examples put a comment above each pair. One condition per line with a short note above it lets a teammate check the rule without asking you:
ifs(
/* Finished work stops here, whatever its date */
prop("Status") == "Done", "Done",
/* No date means nothing to compare yet */
empty(prop("Due")), "No due date",
prop("Due") < today(), "Overdue",
/* "Soon" is a 7-day window */
prop("Due") <= dateAdd(today(), 7, "days"), "Due soon",
"Later"
)
When several conditions test the same calculation, work it out once with let() and test the name. The 2.0 guide builds a progress label exactly this way from a Tasks relation. Here it is laid out on lines, with one change: I moved the % sign inside the parenthesis, where Notion's version prints it after the closing bracket, as "(50)%".
let(
percentComplete, round(prop("Tasks").map(current.prop("Status")).filter(current == "Done").length() / prop("Tasks").map(current.prop("Status")).length() * 100),
ifs(
percentComplete == 100, "✅ All tasks complete",
percentComplete > 0, "⚠️ In progress (" + percentComplete + "%)",
"❌ Not started"
)
)
The let() and lets() guide covers naming and scope. While you build, open the formula editor from a database row: Notion's formulas help says you'll then see a live preview of the result for that row. Notion's beginner guide also suggests testing a formula in smaller bits, and I'd add one line at a time, checking the preview after each.
Color the label with style() or Conditional color
Inside the formula, style() formats the text it returns. Notion's formulas help colors its overdue label red and bold:
if(and(now() > Due Date, Status != "Done"), style("Overdue", "red", "b"), "")
The reference lists the formatting styles "b", "u", "i", "c" and "s" for bold, underline, italics, code and strikethrough. The colors are gray, brown, orange, yellow, green, blue, purple, pink and red, and adding "_background" to a color sets a background instead.
Conditional color works at the view level, without changing the formula. Notion's database properties guide gives the path: select the slider icon at the top of the view, then under View settings select Conditional color, then New color setting. Pick the property, set a rule and choose a page background color. The rule can use Formulas, Rollups, Status, Date and other property types. It applies to one view only, it colors a page's entire background, and only a table view can color just the property. Changing it requires Can edit content, Can edit or Full access to the database.
I'd keep the logic in one formula and decide on color separately. For a red word in a table column, style() inside the formula is enough. For a board or a calendar where the whole card should stand out, I'd leave the label plain and add a Conditional color rule on the formula property, because that rule paints the card and stays with the view where it's needed. The database views guide for client work covers which view answers which question.

Limits for formulas in chains, automations and Notion AI
Chains of formulas have a ceiling. Notion's errors page says formulas "can only be 15 layers deep", every reference to another formula or rollup adds a layer, and that holds even when the referenced formula sits in a different database. When the limit is reached, Notion shows an error message. The fix is to combine formulas and rollups that do similar work, and the rollup troubleshooting guide walks through simplifying a dependency chain.
Buttons and automations are stricter than a formula property. A formula there must return the type its target expects, a person for a Person property and so on, and it needs access to every page and database it references; without that access, the errors page says, formulas "won't be able to compute reliably". The automation trigger and action guide and the buttons vs automations comparison cover where those formulas run.
On Business and Enterprise plans, Notion AI can write or fix a formula from the editor. Notion's own sample prompts include "Make this formula handle edge cases." and "What does this formula do?" The AI drafts the conditions. Which status means finished, and which rows you check before trusting the column, stay with you.
Test results: the status formula worked through, row by row
The check that matters for a condition is the documented rule applied to concrete rows. Notion's syntax reference shows today() returning @April 19, 2024, so this table takes April 19, 2024 as today, with Due dates that don't include a time. The seven-day window then ends on April 26, 2024. Each output follows the first-true-condition rule for the wrong and right versions in the order section.
| Row | Status | Due | Wrong order | Right order |
|---|---|---|---|---|
| Finished, past due | Done | April 12, 2024 | Overdue | Done |
| Finished, no date | Done | Blank | No due date | Done |
| Finished, due ahead | Done | April 22, 2024 | Done | Done |
| Open, no date | In progress | Blank | No due date | No due date |
| Open, past due | In progress | April 15, 2024 | Overdue | Overdue |
| Due today | In progress | April 19, 2024 | Due soon | Due soon |
| Due in 3 days | In progress | April 22, 2024 | Due soon | Due soon |
| Due in 7 days | Not started | April 26, 2024 | Due soon | Due soon |
| Due in 20 days | Not started | May 9, 2024 | Later | Later |
The wrong order labels seven of the nine rows correctly. Both misses are finished tasks, so a quick look at a few open rows won't reveal the problem. A task due today reads "Due soon" in both versions, because April 19 isn't before April 19, and the task due on April 26 still passes the <= test on the window's last day. Notion's beginner guide suggests changing property values or adding entries to check a formula. Before trusting a status column, I'd add one finished task with a past date and one with no date, because those two rows expose an order mistake straight away.
What empty() treats as empty
| Expression | Result | What it shows | Source |
|---|---|---|---|
empty(0) | true | Zero counts as empty. | Notion syntax reference |
empty([]) | true | An empty list counts as empty. | Notion syntax reference |
empty("") | true | Blank text counts as empty. | Thomas Frank, empty(); Notion lists "" as empty |
empty(false) | true | An unchecked Boolean counts as empty. | Thomas Frank, empty() |
empty([""]) | false | A list holding one blank item isn't empty. | Thomas Frank, empty() |
prop("Number").empty() with Number at 0 | true | A real zero looks blank to empty(). | Thomas Frank, empty() |
prop("Number") == empty() with Number at 0 | false | The equality form catches only a blank Number. | Thomas Frank, empty() |
prop("Name").empty() with a blank Name | true | A blank title counts as empty. | Thomas Frank, empty() |
A list compared with == empty() | Never true | For a blank Multi-select, test length() == 0. | Thomas Frank, empty() |
Frank's page also says a Boolean compared with == empty() is never true, so a Checkbox needs no blank check at all: it's checked or it isn't.
When the formula errors, goes blank or shows the wrong label
| What you see | Cause | Fix |
|---|---|---|
| A finished task reads "Overdue" | The past-due line sits above the Done line. | Move the finished case to the first line. |
| Some rows show nothing | An ifs() without a fallback, or an if() whose else is "", for rows no condition matches. | Add a last value that names the remaining case. |
| A Multi-select condition never matches | == compares the whole list. | Use .includes("Tag"), as Notion's errors page advises. |
| Return type is unknown, or an automation won't save | One branch returns a date and the other returns "". | Return empty() for the blank branch. |
| "Type mismatch" | Mixed data types in one calculation, per Notion's beginner guide. | Convert the value first, for example with format() for text or toNumber() for a number. |
| The editor doesn't recognize a property | The name differs in case or spelling. | Select the property from the menu. |
| A depth error on a formula that reads other formulas | The chain passed 15 layers. | Merge formulas and rollups that do similar work. |
| A formula in a button or automation can't compute | No access to a referenced page or database. | Get access to every page and database the formula references. |
When a formula is the wrong tool
A formula earns its place when a label depends on several properties at once: a date and a status, a checkbox and a relation. If a person sets the stage by hand, a Status property already is the label. If you only need to see late work, a filtered view does it, though its own and/or groups can hide rows; the guide to filtered views missing tasks covers that. If you only need color by an existing property, Conditional color does it without a formula.
- Write the labels you need in the order they should win: finished, missing data, late, soon, everything else.
- Turn each one into a condition from the property table above, with an empty check before any date or number comparison.
- Put them in one
ifs(), one pair per line, with a fallback at the end. - Open the editor from a finished past-due task and from a task with no date, and compare the preview with the label you expect.
For a freelancer with one task database, I'd start with a single ifs() status of three or four lines and stop there, because one column that reads correctly on every row already answers "What needs my attention?". For a small team sharing project and invoice databases, I'd write every label formula as a commented, multi-line ifs() with the finished case first, because that's the version someone else can check line by line without asking you what it does.
More questions about if and ifs
Does Notion have an else-if?
Yes, through ifs(). Frank's if() page says the introduction of ifs() gives Notion else-if statements, and his ifs() page counts each condition and value pair after the first as an else-if.
Can if() return a number, a date or a checkbox instead of text?
Yes. Notion's 2.0 guide says formula outputs now include pages, dates, people and lists, and Frank's if() page shows if() returning numbers, Booleans and dates. Keep both branches the same type, with empty() as the blank one.
How do I check a Status against two options?
Join two comparisons with or: prop("Status") == "Done" or prop("Status") == "Archived". Put that pair in brackets if it sits next to an and, because or is evaluated after and.