Skip to content
notionsolutions

A practical library for building,
organizing and working in Notion.

Formula guides

Notion If Formula: if() and ifs() Syntax, Order and Examples

if() returns one value when a condition is true and another when it's false; ifs() checks conditions in order and returns the first match. Conditions for each property type, and/or/not, why line order decides the label, empty() and return-type traps, list properties, color and common error fixes.

Published
A row of five brushed-steel gates with glass panels on a steel rail; the nearest gate stands open with a blue glass ball stopped inside it, and the four gates behind it stay closed
ifs() checks its conditions in order and stops at the first one that's true.

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:

Conditions by property type for if() and ifs()
Property typeConditionTrue when
Checkboxprop("Invoice sent")The box is checked. not prop("Invoice sent") flips it.
Select or Statusprop("Status") == "Done"The option name matches the quoted text.
Numberprop("Hours") > 40The comparison holds.
Dateprop("Due") < today()Due falls before today's date.
Multi-selectprop("Tags").includes("Urgent")The list holds that tag.
Textcontains(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".

The same five-line Notion ifs() status formula in two orders for a finished task due April 12 with today April 19: with the past-due check on line 2 the formula stops there and returns Overdue; with the Done check on line 1 it stops at line 1 and returns Done
A finished task stops at whichever line is true first, so the Done check has to sit above the date checks.

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.

Two ways to color an overdue label in Notion: style("Overdue", "red", "b") turns only the word Overdue red in a table cell, while a Conditional color rule set under View settings, Conditional color, New color setting gives the whole card a red background in that view
style() colors only the text the formula returns; a Conditional color rule colors the whole page in one view.

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.

Status labels from the wrong and right ifs() order, with today as April 19, 2024
RowStatusDueWrong orderRight order
Finished, past dueDoneApril 12, 2024OverdueDone
Finished, no dateDoneBlankNo due dateDone
Finished, due aheadDoneApril 22, 2024DoneDone
Open, no dateIn progressBlankNo due dateNo due date
Open, past dueIn progressApril 15, 2024OverdueOverdue
Due todayIn progressApril 19, 2024Due soonDue soon
Due in 3 daysIn progressApril 22, 2024Due soonDue soon
Due in 7 daysNot startedApril 26, 2024Due soonDue soon
Due in 20 daysNot startedMay 9, 2024LaterLater

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

empty() and == empty() results from Notion's reference and Thomas Frank's documentation
ExpressionResultWhat it showsSource
empty(0)trueZero counts as empty.Notion syntax reference
empty([])trueAn empty list counts as empty.Notion syntax reference
empty("")trueBlank text counts as empty.Thomas Frank, empty(); Notion lists "" as empty
empty(false)trueAn unchecked Boolean counts as empty.Thomas Frank, empty()
empty([""])falseA list holding one blank item isn't empty.Thomas Frank, empty()
prop("Number").empty() with Number at 0trueA real zero looks blank to empty().Thomas Frank, empty()
prop("Number") == empty() with Number at 0falseThe equality form catches only a blank Number.Thomas Frank, empty()
prop("Name").empty() with a blank NametrueA blank title counts as empty.Thomas Frank, empty()
A list compared with == empty()Never trueFor 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

if() and ifs() symptoms, causes and fixes
What you seeCauseFix
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 nothingAn 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 saveOne 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 propertyThe name differs in case or spelling.Select the property from the menu.
A depth error on a formula that reads other formulasThe chain passed 15 layers.Merge formulas and rollups that do similar work.
A formula in a button or automation can't computeNo 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.

  1. Write the labels you need in the order they should win: finished, missing data, late, soon, everything else.
  2. Turn each one into a condition from the property table above, with an empty check before any date or number comparison.
  3. Put them in one ifs(), one pair per line, with a fallback at the end.
  4. 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.

Continue with formula logic