> For the complete documentation index, see [llms.txt](https://docs.membercounter.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.membercounter.app/using-the-dashboard/nested-counters.md).

# Nested counters

Feed the result of one counter into another to build numbers no single counter can produce.

Most counter options are boxes you type into, and almost all of them take a counter instead. The inner counter runs first, and its result becomes the value of that option. That is a nested counter, and it is how you get numbers no single counter offers: members minus bots, or a value pulled from your own API and then formatted.

A nested counter is not a second chip in the template. It lives inside its parent, and the whole thing still shows as one chip.

## Which options accept a nested counter

Any option that takes free text or a number. Each counter's own page says which of its options qualify.

Options that are a fixed choice (**Operation type**, **Filter by status**, **Display**) take one of their listed values only. The digit boxes under **Override default digits** in **Format settings** take text and emoji, not counters.

## Adding one

1. Open the chip's edit panel and find the option you want to fill.
2. Click the option's box. The dropdown lists the same counters as **Add counter**, under the literal values the option already offers.
3. Pick a counter. The option now shows a row with the counter's name instead of a typed value.

Its gear opens the nested counter's own options, and its X takes it back out.

Where the gear has a tooltip, it says what the nested counter has to return there, for example a role ID on **Filter by role**. Nothing is checked while you edit, so a nested counter that returns the wrong kind of value only fails when the bot runs the template, see [Troubleshooting](/using-the-dashboard/troubleshooting.md).

{% hint style="warning" %}
The **Timezone** box on [Clock](/counters-reference/clock.md) and [Date](/counters-reference/date.md) also lists **Unknown**, an internal placeholder that makes the template fail with **The counter provided is unknown or not recognized.**
{% endhint %}

<figure><img src="https://1582170640-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lu2S5r7lKtu4IK5ucu9%2Fuploads%2Fgit-blob-676d7e3f39c2a3bbfb018b0a12b5bd4c594b0136%2Feditor-nested-counter-row.png?alt=media" alt="A nested counter inside an option"><figcaption><p>A counter used as the value of an option, with its Edit counter and Remove counter buttons</p></figcaption></figure>

## The breadcrumb

Pressing the gear opens the nested counter, and a breadcrumb shows where you are. Each entry is the counter's generated chip name, not the counter's type: a **Math** counter with one nested counter reads `Add and 1 more`.

* Click an earlier entry to jump straight back to it.
* The back arrow goes up one level.
* The X closes the panel entirely.

<figure><img src="https://1582170640-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lu2S5r7lKtu4IK5ucu9%2Fuploads%2Fgit-blob-ee4a3ddb81f81006a7f2bfc3edbb3b9d740665d8%2Feditor-nested-breadcrumb.png?alt=media" alt="The nesting breadcrumb"><figcaption><p>The breadcrumb shows where you are; earlier entries are clickable</p></figcaption></figure>

## Formatting comes from the outermost counter

A nested counter hands its raw value to its parent. Compact notation, the locale and custom digits are applied **once**, to the final value of the outermost counter in the chip. That is why the **Format settings** block keeps editing the outermost counter even while you are drilled several levels down: a nested counter has no formatting of its own.

So to get `1.2M` instead of `1234567`, set compact notation on the counter at the top of the stack, not on the one that produces the number. See [Formatting counters](/using-the-dashboard/customizing-counters.md).

## Examples

{% tabs %}
{% tab title="Math over Members" %}
**Goal:** show how many members are *not* staff.

There is no "members without a role" filter, so subtract one **Members** count from another.

1. Press **Add counter** and pick [Math](/counters-reference/math.md).
2. Set **Operation type** to **Subtraction**.
3. In **Number list**, add a counter and pick **Members**. Leave every filter empty, so it counts the whole server.
4. Add a second counter to the list and pick **Members** again. Press its gear, set **Filter by role** to @Staff, and go back with the breadcrumb.
5. Type `non-staff` after the chip.

The channel shows the number of members without the role, followed by `non-staff`.

**Order matters.** Subtraction and division fold the list from first to last with no starting value, so the first entry is the one everything else is taken away from. Addition and multiplication do not care about order.

Give each **Members** chip a [display label](/using-the-dashboard/template-editor.md#display-label) such as `All` and `Staff`, or both rows will show almost the same generated name.

<figure><img src="https://1582170640-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lu2S5r7lKtu4IK5ucu9%2Fuploads%2Fgit-blob-5fcb42cdb5d03e34d5b347bc3d9bc4d1d6e971de%2Fnested-example-math-members.png?alt=media" alt="A Math counter subtracting two Members counters"><figcaption><p>The Math counter with its two Members rows, and the second one filtered by role</p></figcaption></figure>
{% endtab %}

{% tab title="Replace over HTTP" %}
**Goal:** turn a status word from your own API into something readable.

Your endpoint returns `{"status":"ok"}` and you want the channel to say `🟢 Online`.

1. Press **Add counter** and pick [Replace](/counters-reference/replace.md).
2. Click **Input text** and pick **HTTP** from the list.
3. Press the gear on the HTTP row. Set **URL (GET)** to your endpoint and **Data path** to `status`. Set **Lifetime (in seconds)** to `60` so the bot does not hit your API on every run. Go back with the breadcrumb.
4. Press **Add replacement**. Put `ok` in **Search for** and `🟢 Online` in **Replace with**.

The channel reads `🟢 Online`.

<figure><img src="https://1582170640-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lu2S5r7lKtu4IK5ucu9%2Fuploads%2Fgit-blob-da9fba540e8fb1fe5f97864333f4bda9af0b4be4%2Fnested-example-replace-http.png?alt=media" alt="A Replace counter wrapping an HTTP counter"><figcaption><p>The Replace counter with the HTTP counter as its input, and one replacement</p></figcaption></figure>

{% hint style="warning" %}
A replacement with an empty **Replace with** is ignored, and so is a single space, so Replace cannot delete text.
{% endhint %}
{% endtab %}

{% tab title="Number over HTTP" %}
**Goal:** make a value from your own API obey your formatting.

The [HTTP counter](/counters-reference/http.md) always returns text, even when the text happens to be digits. Text is passed through untouched, so compact notation, the locale and your custom digits never apply to it.

Wrap it in [Number](/counters-reference/number.md) to turn it back into a number:

1. Press **Add counter** and pick **Number**.
2. Click the **Number** box and pick **HTTP** from the list.
3. Press the gear, configure the URL and data path, then go back.
4. Open **Format settings** on the **Number** chip and set **Use compact notation** to **Yes**.

The channel now shows the value in compact notation.

The same trick works for any counter that produces text, and it is the reason the **Number** counter exists.

<figure><img src="https://1582170640-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lu2S5r7lKtu4IK5ucu9%2Fuploads%2Fgit-blob-80d7692ba31ee8a5cbcf1ccb7432bda1ec7d498f%2Fnested-example-number-http.png?alt=media" alt="A Number counter wrapping an HTTP counter"><figcaption><p>Number turns the HTTP counter's text into a number, so Format settings apply</p></figcaption></figure>
{% endtab %}
{% endtabs %}

## Limits

Nothing caps nesting depth or the number of counters directly, but a very deep tree eventually trips the explorer's step limit and fails with **An unknown error occurred.**

A chip and everything nested inside it must finish within 60 seconds. The usual culprit is a [Game](/counters-reference/game.md) counter pointing at a server that does not answer, or a long chain of nested requests. [HTTP](/counters-reference/http.md) requests give up after 5 seconds on their own.

<details>

<summary>Technical details</summary>

* The evaluator (`packages/services/src/DataSource/index.ts`) walks the chip's object tree and runs a node once it has no unexplored object children, replacing it with its result. The walk stops after 50,000 steps and throws a plain `Error`, which the dashboard shows as **An unknown error occurred.** Exploring one nested counter costs several steps.
* The 60-second timeout is set per delimited counter in the template, not per nested counter, so the whole tree shares it. A timeout raises `EVALUATION_TIMEOUT`.
* Format settings are resolved once, from the chip's root counter, and the same `formatSettings` object is handed to every nested evaluation: per-counter override, then the server setting, then the built-in default, per field. Digit substitution runs on the final value only, and only for a text or announcement channel.
* The counter list offered inside an option has the internal `UNKNOWN` entry filtered out. Clock's and Date's **Timezone** box is the exception: it is built from the unfiltered list, which is why **Unknown** appears there and nowhere else. Picking it fails with `UNKNOWN_DATA_SOURCE`.
* Gear tooltips are per-option locale strings; Replace, Concat and Members' **Filter by playing a game** pass none, so their gear shows no tooltip.
* Nothing bounds the template: neither the API input nor the database column sets a maximum length. An unset template is stored as an empty string, not null.

</details>
