> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zudo.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Indicators

> Named, typed predicates over product-usage events — power user, active in last 7 days, ever exported. Reusable in segments, smart traits, and health scoring.

Indicators are named predicates over the product-usage events flowing into Zudo from [Segment](/integrations/segment) or [PostHog](/integrations/posthog). Build them once and reuse them everywhere — segments, smart traits, account health scores, and the indicator chips on every account page.

Examples:

* **Active in last 7 days** — `count(any_event, last 7d) ≥ 1`
* **Power user** — `count(feature_used, last 30d) ≥ 50`
* **Has ever exported** — `count(export_run, all_time) ≥ 1`
* **Logins last 30 days** — `count(login, last 30d)` *(returns a number, not yes/no)*
* **Unique users last 7 days** — `unique_users(any_event, last 7d)`

## Result types

Every indicator returns one of two shapes:

| Type         | Returns                     | Use when                                                                    |
| ------------ | --------------------------- | --------------------------------------------------------------------------- |
| **Yes / no** | A boolean (predicate match) | The signal is binary — power user vs. not, churned-likely vs. not.          |
| **Number**   | A raw count or sum          | You want to graph it, threshold it later, or compose it into a smart trait. |

Number indicators feed into health-score thresholds (excellent / good / fair / poor); boolean indicators feed in as a true / false score.

## Build an indicator

<Steps>
  <Step title="Open the builder">
    Go to **Settings → Indicators** and click **New Indicator**. (Org owners and admins only.)
  </Step>

  <Step title="Name and describe">
    Give it a clear name like *Power user* or *Logins last 30 days*. Description is optional but helps teammates know
    what it captures.
  </Step>

  <Step title="Pick a result type">
    **Yes / no (predicate)** for binary signals, or **Number (count)** for raw values you want to graph or threshold
    separately.
  </Step>

  <Step title="Configure the aggregation">
    Each event row is an aggregation:

    * **Function**: `Count of events` (default), `Unique users`, or `Sum of property` (requires a property dot-path).
    * **Event name**: the exact event name as it appears in your Segment / PostHog data (e.g. `login`, `Item Viewed`).
      This dropdown lists allowlisted events from your connected sources.
    * **Time window**: `Rolling N days` (most common), `All time`, or `Since first seen` (uses the account's creation
      date as the lower bound).
  </Step>

  <Step title="Add filters (optional)">
    Refine which events count by filtering on payload properties — for example, only `feature_used` events where
    `properties.feature == "export"`. Filters are AND-only and use a dot-path into the original event payload.

    <Note>
      Filtered indicators read from the event staging table, which is bounded by a 7-day TTL. If you set a window longer
      than 7 days on a filtered indicator, only the most recent 7 days of payload-level filtering will be exact —
      earlier days fall back to the unfiltered rollup. For most use cases this is fine; if exact long-window filtered
      counts matter, prefer indicators without filters.
    </Note>
  </Step>

  <Step title="Set a threshold (yes/no only)">
    For boolean indicators, pick a comparator (`≥`, `>`, `≤`, `<`, `=`, `≠`, `between`) and a value per event row. The
    indicator returns `true` when the aggregation passes the comparator.
  </Step>

  <Step title="Combine multiple events (yes/no only)">
    Boolean indicators can combine more than one event with **AND** / **OR**. Click **+ Add event** to add another
    aggregation row at the top level — they're combined with the operator pill (`AND` or `OR`) shown above the rows.

    For nested logic, click **+ Add group** to start a sub-group with its own operator. One level of nesting is supported,
    mirroring how segments work. Example: *logged in **AND** (used export ≥ 5x **OR** ran a report ≥ 10x)* — a top-level
    AND with a single OR group inside.

    Each event row carries its own threshold, so different events can require different counts (e.g. *login ≥ 1* AND
    *export ≥ 5*).
  </Step>

  <Step title="Preview">
    Click **Preview match** to see how many of your active accounts currently match the indicator before saving. Useful to
    sanity-check the threshold.
  </Step>

  <Step title="Save">
    The indicator becomes available immediately. Values are computed on a 15-minute job, so within \~15 minutes every
    account in your org has a fresh value.
  </Step>
</Steps>

## Where indicators show up

### On every account page

The **Indicators** panel sits at the top of the account activity timeline. Each indicator renders as a chip:

* **Yes / no indicators** — green chip with a ✓ when true, gray with ✗ when false, yellow when no value has been computed yet.
* **Number indicators** — labeled badge with the current numeric value.

Hovering shows when the value was last computed.

### In segments

When building a [segment](/accounts/segments), you can use indicator values as condition fields:

* **Indicator** field type — references a saved indicator by name. The condition reads the indicator's current value (boolean or number) and applies an operator.
* **Event aggregate** field type — inline aggregation defined directly on the condition, no saved indicator required. Useful for one-off filters.

### In smart traits

Smart trait formulas can reference indicators by their **slug** (auto-generated from the name — `power_user`, `active_last_7_days`):

```js theme={null}
ifelse(indicators.power_user && traits.vitally.mrr > 1000, "expansion_target", "monitor");
```

Boolean indicators expose their boolean value; number indicators expose their numeric value. Indicators that don't exist or haven't been computed yet evaluate to `undefined` and won't crash the formula.

### In account health scores

Indicators can contribute to the [health score](/accounts/health-scoring) as a single weighted **Indicators** factor. See [health-score config](/settings/health-score-config#indicators) for the setup flow.

## Slugs

Each indicator gets a stable slug derived from its name (lowercase, snake\_case, max 60 chars). The slug is what smart-trait formulas reference — `indicators.power_user`. You can override the auto-generated slug from the builder.

Renaming an indicator does **not** change its slug. This is intentional: existing formulas keep working when you rewrite the human-readable name.

## How values get computed

Indicators are computed in two paths depending on whether your indicator has filters:

1. **Fast path** (no filters, function = count or unique\_users) — a single SQL aggregation over the daily rollup table. Fast even for orgs with thousands of accounts.
2. **Slow path** (any filter clauses, or function = sum) — scans the event staging table for the time window. Bounded by the 7-day staging TTL.

The compute job runs every 15 minutes. Newly created or edited indicators get values within that window.

## Disable vs. delete

The **Enabled** toggle on each indicator stops the compute job from updating its value. Existing values stay in place but get older. Indicators that participate in the health score are silently skipped while disabled — no need to remove them from the health score config.

**Delete** removes the indicator and all its computed values. Smart-trait formulas referencing the slug start returning `undefined` after delete; segments referencing it stop matching.

## Volume notes

* Indicators read from per-account daily rollups. The cost of a single indicator is roughly proportional to (accounts × days in window) but most orgs see this measured in hundreds of milliseconds.
* The `unique_users` function is approximate — it sums each day's distinct-user count without cross-day deduplication. For a strict per-account "unique users in the last 7 days" you'll see a small over-count when the same user appears on multiple days. Trends are still meaningful; if exact counts matter, file a support request and we can plug in a HyperLogLog sketch.
* The **All time** window doesn't actually scan all of history when filters are present — it's bounded by the 7-day staging TTL. Use unfiltered all-time counts (which read directly from the rollup) for true lifetime totals.
