> ## 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 repeat() Formula: Progress Bars, Ratings and the Loop Trick
- URL: https://notionsolutions.net/notion-formula-repeat-progress-bar/
- Published: 2026-10-30T10:17:29.000Z
- Updated: 2026-11-02T14:10:11.000Z
- Description: repeat() repeats a piece of text a set number of times, which makes it the cleanest way to draw a text progress bar in Notion. One formula that stays ten symbols long at every percentage, a color by state, native Bar versus formula bar, star ratings, zero padding and the repeat() loop trick.
- Author: Kevin Turner
- Tags: guide, databases

**Notion's repeat() function repeats a piece of text a set number of times, so `repeat("●", 3)` and `"●".repeat(3)` both give "●●●".** Its most useful job is a text progress bar: one filled symbol for each tenth of the work that's done and an empty symbol for the rest. I'd build every text bar with repeat() and compute the filled count once, because then the bar is always exactly ten symbols long and changing its look means editing one character. Notion's own reference shows the function at its smallest: `repeat("0", 4)` returns "0000".

For a plain one-color bar in a table, Notion's built-in Bar display needs no formula at all. For a bar that carries a label, turns green when the work is finished or uses your own symbols, write a repeat() formula. If you searched for a way to repeat a task every week, that's a different feature: repeat() repeats text, while a weekly task comes from a repeating database template or an automation, which the [recurring tasks guide](https://notionsolutions.net/notion-recurring-tasks/) sets up.

## repeat() syntax and documented behavior

repeat() takes two inputs: the text and the number of times to repeat it. Thomas Frank's [repeat() reference](https://thomasjfrank.com/formulas/functions/repeat/?ref=notionsolutions.net) gives both call styles, `repeat(string, number)` and `string.repeat(number)`, and sums the function up as one that "will repeat a string the specified number of times." Notion's [formula syntax reference](https://www.notion.com/help/formula-syntax?ref=notionsolutions.net) describes it as "Repeats the text a given number of times." Between them, the two references document four results:

__Documented repeat() results__
| Formula           | Result                           | What it shows                                                         | Documented by |
| ----------------- | -------------------------------- | --------------------------------------------------------------------- | ------------- |
| repeat("0", 4)    | "0000"                           | The function-call style                                               | Notion        |
| repeat("\~=", 10) | "\~=\~=\~=\~=\~=\~=\~=\~=\~=\~=" | A two-character unit repeats whole, so ten repeats make 20 characters | Notion        |
| 123.repeat(3)     | "123123123"                      | A number is turned into text before it repeats                        | Thomas Frank  |
| false.repeat(3)   | "falsefalsefalse"                | So is a true/false value                                              | Thomas Frank  |

Frank states the conversion rule directly: "If you specify a non-string data type as the first argument, it will be converted to a string before being repeated." The count is the input to watch. Every documented example passes a whole number, and neither reference says what a decimal, a negative number or an empty property does to the result. Pass a whole number from 0 up. That's why the bar formulas below run the count through floor() or round() first and clamp it to the bar's length.

Longer formulas are easier to read spread over several lines. Notion's [guide to the new formulas](https://www.notion.com/help/guides/new-formulas-whats-changed?ref=notionsolutions.net) says they "can be written multi-line in the editor", so the longer code blocks here keep that layout.

## The progress bar formula

The formula reads a Progress property holding a number between 0 and 1, where 0.75 means 75% done. That can be a Number formatted as a percent, a rollup that calculates a percentage of related pages, or a formula that divides finished tasks by all tasks. The [guide to counting related pages](https://notionsolutions.net/notion-formula-length-count/) builds that last one with a guard for projects that have no tasks yet, and the [relations and rollups guide](https://notionsolutions.net/notion-relations-and-rollups/) covers the rollup route. With Progress in place, this is the formula I'd paste:

```
let(
  filled, min(10, max(0, floor(prop("Progress") * 10))),
  "●".repeat(filled) + "○".repeat(10 - filled) + " " + format(round(prop("Progress") * 100)) + "%"
)
```

At a Progress of 0.75 it returns "●●●●●●●○○○ 75%". Each part has one job:

- **let() names the filled count once.** Both halves of the bar read the same number, so they can't disagree about how long the bar is. The [let() and lets() guide](https://notionsolutions.net/notion-formula-let/) explains the variable syntax.
- **floor() rounds down,** so the last symbol fills only at 100%. Notion's reference gives `floor(0.4) = 0`. A full bar should mean finished work, and at 95% this formula shows nine filled symbols and one empty one.
- **min() and max() keep the count between 0 and 10.** Progress can pass 1 when hours logged in a [time-tracking database](https://notionsolutions.net/notion-time-tracking/) overshoot the estimate, or drop below 0 after a typo. Without the clamp, 120% would ask repeat() for 12 filled symbols and minus 2 empty ones.
- **`10 - filled` sets the empty part,** so the two halves always add up to ten symbols.
- **The label uses round(), not floor().** 29 tasks done out of 100 should read 29%, and the worked-through section below shows how floor() can print 28% for exactly that case.
- **format() turns the number into text** so it can join the bar. Notion's reference shows `format(1234) = "1234"`.

[![Flow diagram: a Progress of 0.75 times 10 gives 7.5, floor() makes it 7, the clamp between 0 and 10 keeps 7, and the result is seven filled circles and three empty circles with the label 75% from round(0.75 × 100)](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-repeat-progress-bar-bar-flow.webp)](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-repeat-progress-bar-bar-flow.webp?ref=notionsolutions.net)

floor() picks the filled count, the clamp keeps it between 0 and 10, and the empty part is whatever's left of ten.

### If you'd rather round the bar

Some people want the bar to snap to the nearest tenth. That's a one-word change, floor to round, in the filled line:

```
filled, min(10, max(0, round(prop("Progress") * 10))),
```

The trade-off shows at 95%. 9.5 is an exact midpoint, and Thomas Frank's [round() reference](https://thomasjfrank.com/formulas/functions/round/?ref=notionsolutions.net) says "The exact midpoint between two whole numbers will round up towards positive infinity", with `round(4.5)` giving 5\. So the bar looks complete while one task in twenty is still open, and 25% shows three filled symbols instead of two. I'd keep floor(): someone scanning a column of bars reads a full one as done.

### If your Progress runs from 0 to 100

A Number holding 75 instead of 0.75, or a formula that already multiplies by 100, needs one change: divide by 10 for the bar and drop the `* 100` from the label.

```
let(
  filled, min(10, max(0, floor(prop("Percent done") / 10))),
  "●".repeat(filled) + "○".repeat(10 - filled) + " " + format(round(prop("Percent done"))) + "%"
)
```

Watch for a value that was rounded before it got here. A percentage built with `round(done / total * 100)` turns 199 finished tasks out of 200 into 100, because 99.5 rounds up, and the bar fills. Point the bar at the unrounded value when you can.

### Other symbols, a longer bar, no label

- **Different symbols:** swap the two characters for ▓ and ░, ■ and □, or ★ and ☆. The formula doesn't care which characters you use, and Clarity Mastery's [progress bar formula generator](https://www.claritymastery.co/tools/notion-progress-bar-formula-generator?ref=notionsolutions.net) lists more pairs, such as ⬤ with ◯ and ✓ with □.
- **A 20-step bar:** use `min(20, max(0, floor(prop("Progress") * 20)))` for filled and `"○".repeat(20 - filled)` for the rest. Each symbol is then 5%, so 25% shows five filled and fifteen empty.
- **No label:** delete `+ " " + format(round(prop("Progress") * 100)) + "%"`. I'd keep the label whenever the bar is the only progress signal in a view, because ten symbols can't tell 41% from 49%.

## Color the bar by state with style()

style() wraps text in a color or text style. Notion's reference lists the valid colors as gray, brown, orange, yellow, green, blue, purple, pink and red, and says to "Add "\_background" to colors to set background colors", as in `style("Notion", "blue", "gray_background")`. To turn the bar green once the work is finished and keep it blue until then, wrap the whole bar in style() and let if() pick the color:

```
let(
  filled, min(10, max(0, floor(prop("Progress") * 10))),
  style(
    "●".repeat(filled) + "○".repeat(10 - filled) + " " + format(round(prop("Progress") * 100)) + "%",
    if(prop("Progress") >= 1, "green", "blue")
  )
)
```

For three states, swap the if() for an ifs(), which takes condition and value pairs plus a fallback: `ifs(prop("Progress") >= 1, "green", prop("Progress") >= 0.5, "blue", "gray")` keeps early work gray. The [if() and ifs() guide](https://notionsolutions.net/notion-formula-if-ifs/) covers the branching. The formula applies the color; which threshold counts as on track is still your call, so pick it before you pick the colors.

style() colors only the formula's own text. To color a whole row or card by progress, use the view settings described in [conditional color in views](https://notionsolutions.net/notion-conditional-color-in-views/).

## Native Bar or Ring versus a repeat() bar

Notion can draw a progress bar without any formula. Its [database properties help](https://www.notion.com/help/database-properties?ref=notionsolutions.net) says only that Number values "can also be formatted as currency or progress bars", so the menu details come from walkthroughs. Bullet.so's [progress bar tutorial](https://bullet.so/blog/how-to-create-progress-bar-notion/?ref=notionsolutions.net) sets a formula's Number Format to Percentage, then chooses Bar or Ring under "Show as" along with a color. Easlo's [large progress bar walkthrough](https://www.easlo.co/blog/notion-formulas-create-a-large-progress-bar-in-notion?ref=notionsolutions.net) gets the same display from a rollup set to Percent checked, and notes that the larger display in page layout is "only available for Number and Formula properties".

Krista Lamen's [walkthrough of native progress bars](https://kristalamen.medium.com/native-progress-bars-in-notion-use-cases-and-limitations-e71368906a31?ref=notionsolutions.net) adds the limits. In her walkthrough, the Divide by value is the number "considered as 100%", and you type a fixed number there because it can't take its value from a property. She also found no way to change the bar's color with the value, and suggests the Ring for "views configured for mobile screen". Her point about scale matters for setup: the bar reads 0 to 1, so "0.7 will be shown as 70% while 70 will be shown as 7000%". When the total differs from row to row, compute the fraction in a formula, such as done divided by total, and set Divide by to 1.

__Native Bar or Ring versus a repeat() formula bar__
| What you need                                 | Native Bar or Ring                                                           | repeat() bar                                    |
| --------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------- |
| A plain one-color bar in a table              | Yes, with no formula to maintain                                             | Works, but adds a formula for no gain           |
| A bar that changes color when work is done    | Not in Lamen's walkthrough: one color per property                           | style() with if() or ifs()                      |
| A label on board or gallery cards             | Lamen found no way to add a title; she adds a separate formula above the bar | The label is part of the same text              |
| Custom symbols or a fixed length              | Bar or Ring, sized to the column per Lamen                                   | Any characters, any length                      |
| A compact view on a phone                     | Ring, which Lamen suggests for mobile views                                  | Ten characters of text                          |
| The value for other formulas and calculations | Stays a number, per Lamen                                                    | The output is text; keep Progress as the number |

I'd use the native Bar for a plain one-color bar in a table, because there's nothing to maintain and the value stays a number that other formulas and calculations can use. I'd switch to a repeat() bar when the bar has to say something, such as a green finish or a label on a board card. The two can sit side by side: keep Progress as a number with a native Bar in the table view, and show the text bar on cards.

## More uses: ratings, dividers and zero padding

### A star rating

The rating row you'll find copied around is two repeats:

```
"★".repeat(prop("Rating")) + "☆".repeat(5 - prop("Rating"))
```

A Rating of 4 gives "★★★★☆". It relies on Rating always holding a whole number from 0 to 5; a 3.5 or a 6 hands repeat() a count the documentation doesn't cover. The clamped version rounds first and keeps the count in range:

```
let(
  stars, min(5, max(0, round(prop("Rating")))),
  "★".repeat(stars) + "☆".repeat(5 - stars)
)
```

That gives "★★★★☆" for 4, "★★★★☆" for 3.5 (the midpoint rounds up), "★★★★★" for 6 and "☆☆☆☆☆" for -1\. An unrated row is a separate decision. If a blank should read "Not rated" rather than five empty stars, test for it with empty() first, and remember that Notion's reference counts 0 as empty too (`empty(0) = true`), so a genuine 0 would read "Not rated" as well.

### A divider line

`"─".repeat(20)` returns a line of 20 box-drawing characters. Lamen's walkthrough uses static formulas to add titles and separators on cards, since a formula fills itself on every page while a Text property would need typing on each one.

### Leading zeros

To show a whole number at a fixed width of three digits, repeat "0" for the missing places:

```
"0".repeat(max(0, 3 - format(prop("Number")).length())) + format(prop("Number"))
```

7 becomes "007", 42 becomes "042", and 1234 stays "1234", because the max() stops the count going negative once the number has more than three digits. Use it for whole numbers from 0 up only, since a decimal point or a minus sign counts as a character too.

## The repeat() loop trick

Notion's formula language has no loop function. Frank's repeat() page says so plainly, "Notion's formula language doesn't include actual looping functions, unlike most programming languages", and Notion's [intro to formulas](https://www.notion.com/help/formulas?ref=notionsolutions.net) lists "Iterative logic (like for loops)" among the things Notion AI can't write for you. Frank's workaround builds a list with repeat() and split(), then runs map() over it:

```
".".repeat(10).split(".").map(index * 2)
```

In his description, ten dots are split on the same character "to create a List of length 10", and map() then multiplies each item's index by 2\. Frank's [map() reference](https://thomasjfrank.com/formulas/functions/map/?ref=notionsolutions.net) notes that "the first element has an index of 0", so a ten-item list maps to 0, 2, 4 and on up to 18.

Before you build on it, check the count. Put `".".repeat(10).split(".").length()` in a scratch formula and confirm it returns the number you expect. The trick depends on the list having the right number of items, and the documentation doesn't spell out how split() treats the empty text between two dots. When your data is already a list, such as a relation, map() works on it directly, and the [map() and filter() guide](https://notionsolutions.net/notion-formula-map-filter/) covers that.

## Progress bar problems and fixes

The usual suspects are a copied formula that rounds two halves on its own, a missing clamp or the wrong rounding function. Here's how each symptom traces back:

[![Two rows at 25% progress: rounding each half separately gives three filled and eight empty circles, 11 symbols; computing the filled count once with floor() gives two filled and eight empty circles, 10 symbols](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-repeat-progress-bar-rounding-compare.webp)](https://storage.ghost.io/c/5c/30/5c30c46b-86a3-4d20-984f-4f6ae8ad1ed2/content/images/2026/10/notion-formula-repeat-progress-bar-rounding-compare.webp?ref=notionsolutions.net)

At 25% both halves land on a midpoint, so rounding them separately adds an eleventh symbol.

__Progress bar symptoms, causes and fixes__
| Symptom                                               | Cause                                                                                                                                                                                                     | Fix                                                                                                                                                     |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 11 symbols at 25%, 75% or 95%                         | The filled and empty parts are rounded separately, and both halves round up: 2.5 and 7.5 at 25% and 75%, 9.5 and just over 0.5 at 95%                                                                     | Compute filled once and make the empty part 10 - filled                                                                                                 |
| The bar looks full at 95%                             | round() on the filled count sends 9.5 up to 10                                                                                                                                                            | Use floor() for the bar                                                                                                                                 |
| Progress goes above 100% or below 0                   | Without a clamp, the formula asks for more than ten filled symbols and a negative empty count                                                                                                             | Wrap the count in min(10, max(0, …))                                                                                                                    |
| At 0% the bar shows only empty symbols                | That's correct: zero filled, ten empty                                                                                                                                                                    | Leave it. Don't hide it with empty(): empty(0) = true, so a real 0% would vanish                                                                        |
| An old formula calls slice() on a string of stars     | Notion's reference documents slice() for lists and substring() for text; Frank's [slice() reference](https://thomasjfrank.com/formulas/functions/slice/?ref=notionsolutions.net) calls it a list function | Rebuild the bar with repeat(), which needs neither; the [Formulas 2.0 guide](https://notionsolutions.net/notion-formulas-2-0/) covers what else changed |
| The label reads 28% for 29 done out of 100            | floor() on a value the arithmetic stores a hair under 29                                                                                                                                                  | Use round() for the label                                                                                                                               |
| Every row shows 0%, even projects with finished tasks | The rollup or formula feeding Progress returns nothing or 0                                                                                                                                               | Trace one row with the [rollup troubleshooting steps](https://notionsolutions.net/notion-rollup-not-working/) before touching the bar                   |

## Every percentage, worked through

A text bar is easy to check by arithmetic, so this section does exactly that. It starts with the documented results every bar formula rests on, then counts the symbols four published patterns produce at the percentages where bars usually go wrong.

__Documented results the bar formulas rely on__
| Formula                     | Documented result                | Source                          |
| --------------------------- | -------------------------------- | ------------------------------- |
| repeat("0", 4)              | "0000"                           | Notion formula syntax reference |
| repeat("\~=", 10)           | "\~=\~=\~=\~=\~=\~=\~=\~=\~=\~=" | Notion formula syntax reference |
| 123.repeat(3)               | "123123123"                      | Thomas Frank, repeat()          |
| false.repeat(3)             | "falsefalsefalse"                | Thomas Frank, repeat()          |
| round(4.5)                  | 5                                | Thomas Frank, round()           |
| \-4.5.round()               | \-4                              | Thomas Frank, round()           |
| floor(0.4)                  | 0                                | Notion formula syntax reference |
| ceil(0.4)                   | 1                                | Notion formula syntax reference |
| min(1, 2, 3) / max(1, 2, 3) | 1 / 3                            | Notion formula syntax reference |
| empty(0)                    | true                             | Notion formula syntax reference |

Three published bars make useful comparisons, each built on slice() and a ten-symbol string. Notion VIP's [star progress bar](https://uno.notion.vip/notion-formulas-create-a-progress-bar/?ref=notionsolutions.net) rounds both halves: `slice("★★★★★★★★★★", 0, round(prop("Progress (Percent)") * 10)) + slice("☆☆☆☆☆☆☆☆☆☆", 0, round((1 - prop("Progress (Percent)")) * 10))`, followed by a percent label. NotionSender's [emoji bar](https://www.notionsender.com/blog/notion-progress-bar?ref=notionsolutions.net) rounds once and subtracts: `slice("★★★★★★★★★★", 0, round(prop("Progress") * 10)) + slice("☆☆☆☆☆☆☆☆☆☆", 0, 10 - round(prop("Progress") * 10))`. IntelliTect's [variable-formula bar](https://intellitect.com/blog/creating-a-notion-progress-bar-with-variable-formulas/?ref=notionsolutions.net) pairs floor() with ceil(): `format(slice("▓▓▓▓▓▓▓▓▓▓", 0, floor(prop("_progress") * 10)) + format(slice("░░░░░░░░░░", 0, ceil(10 - prop("_progress") * 10))))`.

__Filled + empty symbols at each percentage__
| Progress | Round both halves (Notion VIP) | Round once, empty = 10 - filled (NotionSender) | floor + ceil (IntelliTect) | floor + clamp + repeat (this guide) |
| -------- | ------------------------------ | ---------------------------------------------- | -------------------------- | ----------------------------------- |
| 0%       | 0 + 10 = 10                    | 0 + 10 = 10                                    | 0 + 10 = 10                | 0 + 10 = 10                         |
| 25%      | 3 + 8 = 11                     | 3 + 7 = 10                                     | 2 + 8 = 10                 | 2 + 8 = 10                          |
| 50%      | 5 + 5 = 10                     | 5 + 5 = 10                                     | 5 + 5 = 10                 | 5 + 5 = 10                          |
| 75%      | 8 + 3 = 11                     | 8 + 2 = 10                                     | 7 + 3 = 10                 | 7 + 3 = 10                          |
| 95%      | 10 + 1 = 11                    | 10 + 0 = 10 (full bar)                         | 9 + 1 = 10                 | 9 + 1 = 10                          |
| 100%     | 10 + 0 = 10                    | 10 + 0 = 10                                    | 10 + 0 = 10                | 10 + 0 = 10                         |

The counts are arithmetic on the documented functions, with Frank's midpoint rule settling 2.5, 7.5 and 9.5\. They show what each formula asks for, not how Notion draws an older slice()-on-text formula. Rounding both halves separately is the only pattern that reaches 11 symbols, because at 25% and 75% both halves land on a midpoint and both round up. At 95% the filled half (9.5) rounds up, and the empty half, a hair above 0.5 in binary arithmetic, rounds up too. Rounding once fixes the length but fills the bar at 95%. IntelliTect's floor-and-ceil pairing matches this guide's counts at every row in the table.

The difference shows above 100%. At 120%, this guide's formula works out `min(10, max(0, floor(12)))` \= 10 filled, 0 empty and the label "120%". The other three don't clamp, so they would ask for 12 filled symbols and a negative empty count, an input the documentation doesn't define.

__Output of the recommended formula__
| Progress | Output            |
| -------- | ----------------- |
| 0%       | "○○○○○○○○○○ 0%"   |
| 25%      | "●●○○○○○○○○ 25%"  |
| 50%      | "●●●●●○○○○○ 50%"  |
| 75%      | "●●●●●●●○○○ 75%"  |
| 95%      | "●●●●●●●●●○ 95%"  |
| 100%     | "●●●●●●●●●● 100%" |
| 120%     | "●●●●●●●●●● 120%" |

Three of those rows rest on a count of zero. At 0% the filled part repeats ● zero times, and at 100% and 120% the empty part does the same with ○, which follows from "Repeats the text a given number of times". Check the 0% and 100% rows once after you paste the formula into your own database.

The label has one trap of its own. Computers store most decimals in binary, so 0.29 × 100 comes out as 28.999999999999996 rather than 29: floor() on that prints 28%, and round() prints 29%. Across every fraction with a total up to 100, floor() on the label goes wrong for 29/100, 57/100 and 29/50 (58/100 is the same fraction), while floor(Progress × 10) for the bar matches exact arithmetic every time. round() has one miss of its own in the same set, 23/40 (46/80 is the same fraction), where 57.5% comes out as 57.49999999999999 and prints 57%. So round() goes on the label, where it misses less often than floor(), and floor() stays on the bar, where it never misses and keeps 95% from looking finished.

## Verdict

Build text bars with repeat(), compute the filled count once with floor() and a min/max clamp, and derive the empty part from it. That gives ten symbols at every percentage, a full bar only when the work is done and no surprises above 100%. Keep Notion's native Bar for a plain one-color bar in a table, where a formula adds nothing, and reach for the repeat() bar when it needs a label or a color that changes.

Start small: pick the one database where your bar looks a symbol too long, replace the copied star formula with the version above and check the 0%, 25% and 100% rows. The formula draws the bar. Deciding what counts as done, and whether 95% deserves green, stays with you.