> ## Content Index
> Fetch the complete content index at: https://notionsolutions.net/llms.txt
> Use this file to discover other available public pages before exploring further.

# Notion map() and filter() Formulas: Build the Filtered Rollup
- URL: https://notionsolutions.net/notion-formula-map-filter/
- Published: 2026-10-30T10:17:23.000Z
- Updated: 2026-10-30T10:17:26.000Z
- Description: map() turns each related page into a value and filter() keeps only the pages that pass a rule: the filtered rollup Notion's rollup menu doesn't offer. How current and index work, find(), some() and every(), nine recipes on one project, nested current, automations and the errors long chains hit.
- Author: Kevin Turner
- Tags: guide, databases

**In a Notion formula, map() turns each item in a list into something else, and filter() keeps only the items that pass a condition.** A Relation property is a list of pages, so `prop("Tasks").filter(current.prop("Status") != "Done")` returns a project's open tasks, and adding `.map(current.prop("Due"))` turns them into their due dates. That's the filtered rollup people go looking for: a rollup works on every related page, while the formula works only on the pages that match. I'd write filter() first and map() second, because the condition can still read any property on the page, and you pull out the one value you need after the list is narrowed.

For a plain total of every related page, such as all minutes logged on a project, keep a rollup. For a question with "only" in it, such as open tasks, unbilled hours or the next due date among unfinished work, use a formula with filter(). For one page or a yes/no answer, use find(), some() or every(). The basic one-line relation patterns are listed in the [guide to prop() and current.prop()](https://notionsolutions.net/notion-formula-prop/); this guide picks up where those stop, with chains, conditions on several properties and the cases where a rollup is still the better property.

## How current and index work inside a list function

A list function runs its expression once per item. Inside that expression, `current` is the item being processed and `index` is its position, counted from zero. Notion Mastery's [overview of everything new in Formulas 2.0](https://notionmastery.com/everything-new-in-notion-formulas-2-0/?ref=notionsolutions.net) describes them as "the provided values current and index" that list functions such as map hand to their expression. Notion's [formula syntax reference](https://www.notion.com/help/formula-syntax?ref=notionsolutions.net) shows both in one example:

```
map([1, 2, 3], current + index)
```

The result is \[1, 3, 5\]: 1 + 0, 2 + 1 and 3 + 2\. Thomas Frank's [map() reference](https://thomasjfrank.com/formulas/functions/map/?ref=notionsolutions.net) isolates the position with `[1, 2, 3].map(index)`, which returns \[0, 1, 2\], because "the first element has an index of 0."

Three things follow once the list is a relation:

- **current is a page.** Each item in a Relation is a related page, so `current.prop("Status")` reads that page's Status. Frank's map() page explains that Relation properties "always contain a list of pages (even when there's only one element)", which is why you need a list function or a picker such as first() to reach their properties.
- **Both call styles work.** Frank's reference gives the syntax as `map(list, current)` and as `list.map(current)`. The dot form chains left to right, and Notion Mastery notes that "Whitespace is also ignored in a lot of cases", so you can give each step its own line starting with a dot. The recipes below use that layout.
- **filter() leaves the relation alone.** Frank's [filter() page](https://thomasjfrank.com/formulas/functions/filter/?ref=notionsolutions.net) says it "doesn't modify the original list. It simply creates a new list" of the items that pass. The next step in the chain works on that new list, so `index` after a filter() counts positions in the filtered list, not in the relation.

Order matters once the condition and the output are different properties. Notion's [guide to what changed in the new formulas](https://www.notion.com/help/guides/new-formulas-whats-changed?ref=notionsolutions.net) counts completed tasks with `prop("Tasks").map(current.prop("Status")).filter(current == "Done").length()`, which works because it filters the same Status values it just mapped. To get the due dates of open tasks, map() can't come first: after `.map(current.prop("Due"))` the list holds dates, and a date has no Status left to check. Filter the pages, then map. For the wider picture of what 2.0 changed, the [Formulas 2.0 guide](https://notionsolutions.net/notion-formulas-2-0/) covers it.

## Which list function answers which question

Six functions do most of the work on a relation. Each answers a different question and returns something different when items fail the condition. The examples are the ones in Notion's syntax reference.

__map, filter, find, findIndex, some and every side by side__
| Function    | Returns                                    | Question it answers                         | Notion's example                                  | When items fail the condition                                                |
| ----------- | ------------------------------------------ | ------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------- |
| map()       | A new list, one result per item            | What is this value for each related page?   | map(\[1, 2, 3\], current + 1) \= \[2, 3, 4\]      | No test: every item is transformed                                           |
| filter()    | A list of the items that pass              | Which pages meet the rule?                  | filter(\[1, 2, 3\], current > 1) \= \[2, 3\]      | Those items are left out of the new list                                     |
| find()      | The first item that passes                 | Which page comes first that meets the rule? | find(\["a", "b", "c"\], current == "b") \= "b"    | Empty, as in find(\[1, 2, 3\], current > 100)                                |
| findIndex() | The zero-based position of the first match | Where in the list is that page?             | findIndex(\["a", "b", "c"\], current == "b") \= 1 | \-1 when nothing matches                                                     |
| some()      | true or false                              | Does any page meet the rule?                | some(\[1, 2, 3\], current == 2) \= true           | false when no item passes                                                    |
| every()     | true or false                              | Do all pages meet the rule?                 | every(\[1, 2, 3\], current > 0) \= true           | false when any item fails: every(\["a", "b", "c"\], current == "b") \= false |

After one of these, a helper usually finishes the chain. Each has a one-line example in the same reference:

- **Pick one item:** `first()`, `last()` and `at()`, where `at([1, 2, 3], 1)` \= 2.
- **Order or tidy the list:** `sort([3, 1, 2])` \= \[1, 2, 3\], `unique([1, 1, 2])` \= \[1, 2\], `reverse()`, and `flat([[1, 2], [3, 4]])` \= \[1, 2, 3, 4\] for a list of lists.
- **Reduce to one number:** `sum()`, `mean()`, `median()`, `min()`, `max()` and `length()`.
- **Turn it into text:** `join(["a", "b", "c"], ", ")` \= "a, b, c".
- **Check membership:** `includes(["a", "b", "c"], "b")` \= true.

## The filtered rollup, rebuilt as a formula

A rollup asks you for the relation, the property on the related pages, the calculation and the number formatting, according to Notion's [relations and rollups help page](https://www.notion.com/help/relations-and-rollups?ref=notionsolutions.net). The calculation menu runs from Show original and Show unique values through the Count and Percent options, then Sum, Average, Median, Min, Max and Range for numbers and Earliest date, Latest date and Date range for dates. None of those calculations takes a condition. Each one works on all related pages, so no rollup setting gives you "minutes on unbilled sessions" or "the earliest due date among open tasks". A formula fills that gap.

Two different things get called a rollup filter. Filtering a view by a rollup column decides which rows the view shows, and each row's rollup still covers all of its related pages. Changing what the rollup itself includes takes filter() in a formula.

Without filter(), getting there took real upkeep. David Ntumba's [Medium walkthrough of filtered rollups](https://medium.com/@davidthe7th/filtered-rollups-in-notion-this-is-as-close-as-we-get-4348e8a0eaa2?ref=notionsolutions.net), from July 2023, related each matching sale to a summary record, used buttons to make those links, and concluded that "the filtering was done manually." With filter(), the condition lives inside the formula, so there are no extra links to keep current.

Alex, who writes as aNotioneer, shows in a guide to [recreating every rollup type with formulas](https://anotioneer.substack.com/p/how-to-create-every-type-of-rollup) that each calculation has a formula twin. Show original is `map(prop("Tasks"), current.prop("Name"))`, Sum ends in `.sum()`, and Earliest date ends in `.sort() .first()`. Once a rollup is written as a formula, the condition is one more step at the front of the chain:

__Rollup calculations and their versions with a condition__
| Rollup calculation         | Formula that adds a condition                                                                                             |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Show original              | prop("Tasks").filter(current.prop("Status") != "Done")                                                                    |
| Show unique values         | The same filter, then .map(current.prop("Phase")).unique()                                                                |
| Sum, Min, Max              | prop("Sessions").filter(not current.prop("Billed")).map(current.prop("Minutes")).sum(), or end with .min() or .max()      |
| Average, Median            | mean(prop("Sessions").filter(not current.prop("Billed")).map(current.prop("Minutes"))), or median(…) around the same list |
| Earliest date, Latest date | Filter, then .map(current.prop("Due")).sort().first(), or .last()                                                         |
| Count all                  | Filter, then .length()                                                                                                    |

### When I'd keep the plain rollup

I'd keep a rollup in two cases. The first is when the whole related set is the answer. Total minutes across every session come out the same either way, and a rollup shows its relation, property and calculation as settings that whoever maintains the database can check without reading formula code. The second is when the subset is a person's choice rather than a rule. If you pick by hand which sessions go on an invoice by relating them to an Invoice page, a Sum rollup on that relation is already filtered by the person who chose them. A formula is the better tool when the rule can be written down, like "Billed is unchecked" or "Status isn't Done".

A formula also goes where a rollup can't. Notion's help page answers "Can I rollup a rollup?" with "Unfortunately not". A formula on a Clients database can read two relations deep instead: `prop("Projects").map(current.prop("Tasks")).flat()` returns every task across the client's projects as one list, ready for filter(). A task related to two of those projects appears once for each, and the [length() counting guide](https://notionsolutions.net/notion-formula-length-count/) deals with that when you count across two relations. If the relation itself isn't set up yet, start with the [relations and rollups setup guide](https://notionsolutions.net/notion-relations-and-rollups/). If a plain Sum rollup already disagrees with the linked records, the [rollup sum guide](https://notionsolutions.net/notion-rollup-sum-wrong/) reconciles it before you add a condition on top.

## Recipes for a Projects and Tasks setup

Every recipe runs on one sample project, Brand guide, in a Projects database with two relations: Tasks and Sessions. Name is the tasks' title, Status is a Status property, Approval and Phase are selects, and Due is a date without a time. Assume today() returns November 12 and every date falls in the same year. Each recipe gives the question, the formula and what it returns for this sample; the test results section lays all of them next to what a rollup would show.

__The seven tasks related to Brand guide, in relation order__
| Task            | Status | Due    | Approval | Phase      |
| --------------- | ------ | ------ | -------- | ---------- |
| Discovery brief | Done   | Nov 3  | Approved | Strategy   |
| Usage rules     | To Do  | Nov 17 | Empty    | Copy       |
| Color palette   | Done   | Nov 10 | Waiting  | Design     |
| Type pairings   | Doing  | Nov 10 | Empty    | Design     |
| Final PDF       | To Do  | Nov 21 | Empty    | Production |
| Voice notes     | To Do  | Nov 14 | Empty    | Copy       |
| Client sign-off | To Do  | Empty  | Empty    | Production |

### 1\. Which tasks are still open?

Filter on Status, then turn the remaining pages into one line of names:

```
prop("Tasks")
  .filter(current.prop("Status") != "Done")
  .map(current.prop("Name"))
  .join(", ")
```

Returns "Usage rules, Type pairings, Final PDF, Voice notes, Client sign-off". Stop after the filter() line and the formula returns the five pages themselves; Notion's guide to the new formulas says formulas now display pages, dates, people and lists as well as text, numbers and checkboxes. End with `.length()` instead of map() and join() and it returns 5\. Notion's syntax reference writes the same condition as `prop("Tasks").filter(current.prop("Status") !== "Done")`; its unequal() entry uses `!=`, which is the spelling these recipes keep.

### 2\. A numbered list of open tasks

```
prop("Tasks")
  .filter(current.prop("Status") != "Done")
  .map(format(index + 1) + ". " + current.prop("Name"))
  .join(", ")
```

Returns "1\. Usage rules, 2\. Type pairings, 3\. Final PDF, 4\. Voice notes, 5\. Client sign-off". Final PDF is the fifth page in the relation but number 3 here, because map() receives the filtered list and `index` counts positions in that list. format() turns the number into text, as in Notion's `format(1234)` \= "1234", so it can be joined to the name with +. Without it you're joining a number to text, which Notion's writing guide names as a cause of a "Type mismatch" error.

### 3\. Done but not approved

A condition can test more than one property of the same page. This one finds work that's finished but still waiting on the client:

```
prop("Tasks")
  .filter(current.prop("Status") == "Done" and current.prop("Approval") != "Approved")
  .map(current.prop("Name"))
  .join(", ")
```

Returns "Color palette". Discovery brief is Done too, but it's already Approved. Notion's reference lists `and`, `&&` and `and(true, false)` as forms of the same operator, so pick one spelling and stay with it. A task with an empty Approval would also pass the second test, because an empty value isn't equal to "Approved". That's the behavior you want here: no approval recorded means not approved.

### 4\. The next due date among open tasks

```
prop("Tasks")
  .filter(current.prop("Status") != "Done" and !empty(current.prop("Due")))
  .map(current.prop("Due"))
  .sort()
  .first()
```

Step by step: filter() keeps four open tasks with a date (Client sign-off has none), map() gives \[Nov 17, Nov 10, Nov 21, Nov 14\], sort() orders them as \[Nov 10, Nov 14, Nov 17, Nov 21\], and first() returns Nov 10\. Notion's reference shows sort() only with numbers. Sorting dates and taking the first one is the pattern Matthias Frank uses in his [Notion Formulas 2.0 tutorial](https://matthiasfrank.de/en/notion-formulas-tutorial/?ref=notionsolutions.net), and aNotioneer recreates the Earliest date rollup the same way. The `!empty()` check keeps undated tasks out of the sort, so the answer never depends on where an empty date would land. An Earliest date rollup on Due returns Nov 3, Discovery brief's date, because it can't skip finished work. When related dates come back as text or the wrong type, the [date formula errors guide](https://notionsolutions.net/notion-formula-date-errors/) resolves them with an explicit rule.

[![Chain diagram for one project's tasks: seven related task pages narrowed by filter() to four open tasks with a due date, map() turning them into Nov 17, Nov 10, Nov 21 and Nov 14, sort() ordering them and first() returning Nov 10](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-map-filter-chain-steps.webp)](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-map-filter-chain-steps.webp?ref=notionsolutions.net)

Filter the pages first, while they still carry Status, then map them to dates and pick the earliest.

### 5\. The next task itself, with find()

A date tells you when, not what. To return the page, store the date with let() and look for the open task that carries it:

```
let(
  nextDue,
  prop("Tasks")
    .filter(current.prop("Status") != "Done" and !empty(current.prop("Due")))
    .map(current.prop("Due"))
    .sort()
    .first(),
  prop("Tasks").find(current.prop("Status") != "Done" and current.prop("Due") == nextDue)
)
```

Returns the Type pairings page. This is the shape of Matthias Frank's next-task formula with one change. His find() matches on the date alone, and find() returns the first item that passes. In the Brand guide sample, Color palette is also due Nov 10 and sits earlier in the relation, so a date-only match would return a task that's already Done. The Status check belongs in both places. If no task is open, nothing passes and find() returns Empty, as in Notion's `find([1, 2, 3], current > 100)` example. Swap find() for findIndex() with the same condition and you get 3, Type pairings' zero-based position in the relation, or -1 when nothing matches.

### 6\. Which phases still have open work?

```
prop("Tasks")
  .filter(current.prop("Status") != "Done")
  .map(current.prop("Phase"))
  .unique()
```

map() returns \[Copy, Design, Production, Copy, Production\], and unique() drops the repeats, leaving Copy, Design and Production. A Show unique values rollup on Phase lists four phases, Strategy included, because Discovery brief is still part of the related set even though it's done.

### 7\. Is anything overdue?

```
prop("Tasks").some(
  !empty(current.prop("Due")) and
  current.prop("Due") < today() and
  current.prop("Status") != "Done"
)
```

Returns true: Type pairings was due Nov 10 and is still Doing. Color palette shares the date but is Done, so it doesn't count. Dates compare with `<` and `>` the same way the property table compares `prop("Due Date") > now()`, and Notion's reference says today() "Returns the current date without the time", so with date-only Due values a task due today isn't overdue yet, since `<` is strict. Frank's [some() page](https://thomasjfrank.com/formulas/functions/some/?ref=notionsolutions.net) points out that once a true/false value sits in a formula property, you can filter a database view by it; here, that gives a view of only the projects with something late. For the number of late tasks rather than a yes or no, replace some() with filter() and finish with length().

### 8\. Is every deliverable approved?

```
if(
  prop("Tasks").length() > 0,
  prop("Tasks").every(current.prop("Approval") == "Approved"),
  false
)
```

Returns false, since only Discovery brief is Approved. The guard copies the shape of Notion's task automation in its [formulas help article](https://www.notion.com/help/formulas?ref=notionsolutions.net), which checks that a parent task has sub-items before running every() and returns false when it has none. Notion writes it with the ternary operator, which its reference treats as the same thing as if(). A project with no tasks shouldn't show up as fully approved, so the empty case gets an explicit answer instead of whatever every() would make of an empty list.

### 9\. How many unbilled hours?

Frank's map() page totals the time on a task across many work sessions with `prop("Sessions").map(current.prop("Duration")).sum()`, sample output 225\. Put a filter on a Billed checkbox in front and the same chain shows what hasn't been invoiced. Brand guide's Sessions relation:

__The five sessions related to Brand guide__
| Session           | Minutes | Billed    |
| ----------------- | ------- | --------- |
| Kickoff call      | 90      | Checked   |
| Logo concepts     | 120     | Checked   |
| Palette revisions | 50      | Unchecked |
| Font tests        | 75      | Unchecked |
| Review prep       | 40      | Unchecked |

```
prop("Sessions")
  .filter(not current.prop("Billed"))
  .map(current.prop("Minutes"))
  .sum() / 60
```

filter() keeps Palette revisions, Font tests and Review prep, map() gives \[50, 75, 40\], sum() returns 165, and dividing by 60 gives 2.75 hours. A Sum rollup on Minutes shows 375, which includes the 210 minutes already billed. `not` on a checkbox comes from Notion's property table, which lists `not prop("Checkbox")`; Matthias Frank writes the same kind of test as `== false`. The formula only reads the checkbox. Deciding when a session counts as billed, and ticking it when the invoice goes out, is still your job.

[![Two paths over the same five work sessions: a Sum rollup on Minutes returns 375, while filter() on unchecked Billed keeps 50, 75 and 40, sum() returns 165 and dividing by 60 gives 2.75 hours](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-map-filter-unbilled-sessions.webp)](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-map-filter-unbilled-sessions.webp?ref=notionsolutions.net)

The rollup adds every session; the formula adds only the ones not yet billed.

## When a nested list function needs the outer page

Put one list function inside another and `current` changes meaning. Say a Clients database relates to Projects, each project has a Deadline date and a Tasks relation, and you want the client's projects where any task is due after the project's deadline. Inside the inner some(), `current` is a task, so the project needs a name of its own:

```
prop("Projects").filter(let(
  project, current,
  project.prop("Tasks").some(
    !empty(current.prop("Due")) and
    current.prop("Due") > project.prop("Deadline")
  )
))
```

let() stores the outer `current` as `project` before some() reuses the word for each task. The [let() and lets() guide](https://notionsolutions.net/notion-formula-let/) walks through the same move with a list of lists, along with the scope rules that decide where a variable can be read. If you only need the inner pages and not which project they came from, map() and flat() give you one list and no variable is needed.

## map() and filter() in automations and buttons

The same functions work in database automations and buttons, with stricter rules. Notion's [page on common formula errors](https://www.notion.com/help/common-formula-errors?ref=notionsolutions.net) says formulas in automations "expect a specific return type", and that relation, rollup and person properties return a list of pages or people. To use a property inside the relation, take the first element with `.first()` or `.at(0)`, or loop through the pages with `.map()`, `.filter()`, `.every()`, `.some()` or `.find()`; otherwise the automation pauses. In practice, end the chain in the type the field expects:

- a text field: map(), then join();
- a number field: sum() or length();
- one page: find() or first();
- a yes/no condition: some() or every().

Notion's formulas help article builds exactly this kind of step. Its task automation defines a Parent task variable with `Trigger page.Parent item.first()`, then an All subtasks done? variable with `Parent Task.Sub-item ? Parent Task.Sub-item.every(current.Status == "Done") : false`. They sit in separate actions for a reason: the errors page says variables defined within the same action "cannot reference each other". Two more rules from that page matter for long chains. Formulas need access to every page and database they reference, and an unexpected empty value, most often a date or a person, makes the automation pause. Its fix for a date is to guard it with `if(Date, Date.dateAdd(1, "day"), empty())` instead of calling `Date.dateAdd(1, "day")` directly. To choose between the two triggers, see [buttons vs. database automations](https://notionsolutions.net/notion-buttons-vs-automations/); when a rule stops running, the [trigger, action and exception guide](https://notionsolutions.net/notion-automations-trigger-action-exception/) starts with the event.

## Limits and errors in long chains

__What goes wrong with list formulas on relations, and the fix__
| What you see                             | Cause                                                                                                            | Fix                                                                                                           |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| A list where you expected one value      | A relation is always a list of pages, even with one page in it                                                   | Finish with first(), at(0), find() or a reducer such as sum()                                                 |
| "Type mismatch"                          | Numbers joined to text, or mixed types in one result                                                             | Convert with format() before joining                                                                          |
| A Multi-select condition never matches   | A Multi-select is a list of text, not one value                                                                  | Use includes(): current.prop("Tags").includes("Finance"), after Notion's prop("Tags").includes("Finance")     |
| find() shows Empty, findIndex() shows -1 | No item passed the condition                                                                                     | Check the condition against one page, including the exact option name                                         |
| A formula depth error                    | Notion allows 15 layers, and every reference to another formula or rollup adds one, even in a different database | Read source properties with current.prop() instead of through helper rollups, and merge helper formulas       |
| A rollup the formula reads is blank      | The rollup, not the formula                                                                                      | Work through the [rollup not working checklist](https://notionsolutions.net/notion-rollup-not-working/) first |
| The same page twice after flat()         | It's related to more than one of the outer pages                                                                 | Expected; decide whether it should count once or once per project                                             |
| An automation pauses                     | A list or an empty value where the field expects one value                                                       | first(), at(0) or a loop function, plus an if(…, …, empty()) guard                                            |

Build a long chain one step at a time. Notion's [guide to writing database formulas](https://www.notion.com/help/guides/write-formulas-that-extend-capabilities-of-databases?ref=notionsolutions.net) suggests you "test your formula in smaller bits to make sure you're getting the desired output at each step", and the formula editor's live preview, which Notion Mastery describes as a real-time result for the page you opened the editor from, shows what each step returns. I'd start on one project small enough to check by eye: stop after filter(), confirm the pages, then add map() and the helper.

## Test results: documented outputs and one project worked through

The first table collects the outputs Notion and Thomas Frank publish for each function, so you can confirm the behavior before building on it. Both define find() as the first matching item, some() as at least one match and every() as all items matching, and where their examples cover the same function they agree.

__Documented examples and their outputs__
| Example                                                                                  | Output                                    | Published by                                                                                 |
| ---------------------------------------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- |
| map(\[1, 2, 3\], current + index)                                                        | \[1, 3, 5\]                               | Notion syntax reference                                                                      |
| \[1, 2, 3\].map(index)                                                                   | \[0, 1, 2\]                               | Frank, map()                                                                                 |
| \[1, 2, 3\].map(current \* index)                                                        | \[0, 2, 6\]                               | Frank, map()                                                                                 |
| \[1, 2, -3, 0, 5\].filter(current > 0)                                                   | \[1, 2, 5\]                               | Frank, filter()                                                                              |
| filter(\[1, 2, 3\], current > 1)                                                         | \[2, 3\]                                  | Notion syntax reference                                                                      |
| find(\[1, 2, 3\], current > 100)                                                         | Empty                                     | Notion syntax reference                                                                      |
| \[1, "Monkey D. Luffy", true, "2025-01-01", 37, "1999-12-23"\].find(current.parseDate()) | "2025-01-01"                              | Frank, [find()](https://thomasjfrank.com/formulas/functions/find/?ref=notionsolutions.net)   |
| findIndex(\[1, 2, 3\], current > 100)                                                    | \-1                                       | Notion syntax reference                                                                      |
| \[1, 0, 3, -12, 5\].some(current > 0)                                                    | true                                      | Frank, some()                                                                                |
| some(\["a", "b", "c"\], current.length > 2)                                              | false                                     | Notion syntax reference                                                                      |
| \["1999-04-01", "2023-01-25", "2025-13-32"\].every(current.parseDate())                  | false                                     | Frank, [every()](https://thomasjfrank.com/formulas/functions/every/?ref=notionsolutions.net) |
| unique(\[1, 1, 2\]), sort(\[3, 1, 2\])                                                   | \[1, 2\] and \[1, 2, 3\]                  | Notion syntax reference                                                                      |
| prop("Sessions").map(current.prop("Duration")).sum()                                     | 225 (sample output)                       | Frank, map()                                                                                 |
| prop("Tasks").filter(current.prop("Due").day() > 5).length()                             | 2 (sample output: tasks due on a weekend) | Frank, filter()                                                                              |

The last two rows are sample outputs from Frank's example databases, so they show the shape of a result rather than a number to expect in yours. The second table applies every recipe to the Brand guide sample, with the steps that produce each result and what the closest rollup calculation returns for the same project.

__Brand guide: each formula result next to the closest rollup__
| Question                       | Formula result                                                      | How it gets there                                                      | Closest rollup gives                                              |
| ------------------------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
| How many open tasks?           | 5                                                                   | Every task except Discovery brief and Color palette                    | Count all: 7                                                      |
| Open task names                | Usage rules, Type pairings, Final PDF, Voice notes, Client sign-off | The same five, in relation order                                       | Show original: all 7 pages                                        |
| Numbered open tasks            | 1\. Usage rules … 5\. Client sign-off                               | index counts in the filtered list                                      | No matching calculation                                           |
| Done but not approved          | Color palette                                                       | Discovery brief is Done and Approved                                   | No matching calculation                                           |
| Next due date among open tasks | Nov 10                                                              | \[Nov 17, Nov 10, Nov 21, Nov 14\] sorted; Client sign-off has no date | Earliest date: Nov 3                                              |
| Next open task                 | Type pairings                                                       | Color palette is also due Nov 10 but Done                              | No matching calculation; a date-only find() returns Color palette |
| Its position                   | 3                                                                   | Zero-based position in the relation                                    | No matching calculation                                           |
| Phases with open work          | Copy, Design, Production                                            | Five values, two repeats removed                                       | Show unique values: Strategy, Copy, Design, Production            |
| Anything overdue?              | true                                                                | Type pairings, due Nov 10, still Doing                                 | No matching calculation                                           |
| Every task approved?           | false                                                               | 1 of 7 tasks is Approved                                               | No matching calculation                                           |
| All session minutes            | 375                                                                 | 90 + 120 + 50 + 75 + 40                                                | Sum: 375                                                          |
| Unbilled minutes               | 165, or 2.75 hours                                                  | 50 + 75 + 40                                                           | Sum: 375                                                          |
| Average unbilled session       | 55 minutes                                                          | 165 divided by 3 sessions                                              | Average: 75                                                       |

Of the thirteen questions, the rollup menu answers one as asked: total session minutes, where Sum and the formula both give 375\. For the other twelve, the closest rollup either takes in finished or billed work or has no calculation for the question at all. That's the case for filter() in one table.

## Verdict: filter first, then pick the value

Keep a rollup for the plain whole-set number people read on its own, like total minutes on a project. As soon as the question has "only" in it, write a formula: filter() for the rule, map() for the property you want, then sort().first(), sum(), join() or length() to finish. Use find() when you need the page itself, and give it the same condition you used to pick the date. Use some() and every() for yes/no columns, with a guard for projects that have no tasks yet. The formula applies the rule; you still decide what the rule is, which Status counts as open and when a session becomes billed. Start by rewriting the one rollup you've been wishing had a filter.

## Questions that come up next

### Can map() and filter() work on a rollup instead of the relation?

Only when the rollup returns a list. Notion's syntax reference gives a rollup's formula type as "Number, date, or list of any type. Depends on rollup configuration." A Show original rollup shows the related pages, the same as the relation itself, while a Sum rollup is a single number that list functions can't loop over. I'd read the relation directly anyway: it already has every property you need, and each extra rollup in the path adds a layer toward the 15-layer limit.

### Is map() the same thing as Notion's Map view?

No. map() is a formula function that transforms a list. Map view is a database layout: Notion's [help article on map view](https://www.notion.com/help/maps?ref=notionsolutions.net) describes it as a way to "visualize database items on an interactive map", with each location coming from a Place property.