> For the complete documentation index, see [llms.txt](https://docs.autopilotmonitor.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.autopilotmonitor.com/rules/analyze-rules/concepts.md).

# Concepts

The anatomy of an analyze rule — conditions, sources, operators, preconditions, suppression, and the confidence model that decides whether a finding is shown.

An analyze rule is a JSON document evaluated against a session's **entire event stream**. This page explains every building block; the [Cookbook](/rules/analyze-rules/cookbook.md) shows them in action.

## Anatomy of a rule

```json
{
  "ruleId": "ANALYZE-APP-004",
  "title": "Insufficient Disk Space",
  "description": "Detects when disk space is critically low during enrollment...",
  "severity": "critical",
  "category": "apps",
  "version": "1.1.0",
  "enabled": true,
  "preconditions": [],
  "conditions": [
    {
      "signal": "low_disk",
      "source": "event_type",
      "eventType": "performance_snapshot",
      "dataField": "disk_free_gb",
      "operator": "lt",
      "value": "5",
      "required": true
    }
  ],
  "baseConfidence": 75,
  "confidenceFactors": [
    { "signal": "app_failed", "condition": "exists", "weight": 15 }
  ],
  "confidenceThreshold": 40,
  "explanation": "Disk free space dropped below 5 GB...",
  "remediation": [ { "title": "Free disk space", "steps": ["..."] } ],
  "tags": ["apps", "disk-space"]
}
```

| Field                                                          | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ruleId`                                                       | Unique ID matching `ANALYZE-<AREA>-<NNN>`. Custom rules created from templates get a `-CUSTOM` suffix.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `severity`                                                     | `info` · `warning` · `high` · `critical` — drives the finding's color and the session's severity statistics.                                                                                                                                                                                                                                                                                                                                                                                               |
| `category`                                                     | `network` · `identity` · `apps` · `device` · `esp` · `enrollment` · `security` — used for filtering.                                                                                                                                                                                                                                                                                                                                                                                                       |
| `preconditions`                                                | Device-fact gates checked **before** anything else (see below).                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `conditions`                                                   | The matching logic — the heart of the rule.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `baseConfidence` / `confidenceFactors` / `confidenceThreshold` | The confidence model (see below).                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `explanation`                                                  | Markdown shown on the finding. Supports `{{field}}` interpolation with evidence from the matched events.                                                                                                                                                                                                                                                                                                                                                                                                   |
| `remediation`                                                  | One or more titled step lists telling the reader what to do.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `presentation`                                                 | How the finding shows on the [diagnosis page](/portal-guide/session-details-and-diagnosis.md#guided-diagnosis) and to AI assistants: its `role` (`outcome` restates the result, `cause` can explain a failure or a hang, `context` is a circumstance such as sleep or a restart, `advisory` is hygiene unrelated to the result), a one-line `summary`, the `subject` it is about (an app, a policy, an account …) and up to six key `facts` (label, value, format). Optional; every built-in rule has one. |
| `markSessionAsFailedDefault`                                   | If `true`, a firing rule marks the entire session as **Failed** — a knock-out criterion. Tenant admins can override this per rule.                                                                                                                                                                                                                                                                                                                                                                         |

## How a rule is evaluated

```mermaid
flowchart LR
    A[Preconditions] -->|any fails → silent skip| X1[No finding]
    A -->|all pass| B[Conditions]
    B -->|a required condition<br/>does not match| X2[No finding]
    B -->|all required match,<br/>≥1 condition matched| C[Confidence]
    C -->|base + matched factor weights,<br/>capped at 100| D{≥ threshold?}
    D -->|no| X3[No finding]
    D -->|yes| E[Finding 🎯]
```

1. **Preconditions** — every precondition must pass, otherwise the rule is skipped **silently** (no finding, no card). Use them to scope a rule to devices where it makes sense at all — e.g. *skip virtual machines* for a firmware check. Preconditions are pure gates; they never contribute evidence or confidence.
2. **Conditions** — all conditions marked `"required": true` must match. Optional conditions (`"required": false`) don't block the rule; they collect additional evidence and can feed confidence factors. At least one condition must match overall — a rule with only optional conditions never fires vacuously.
3. **Confidence** — the score starts at `baseConfidence`, each matched confidence factor adds its `weight`, the total is capped at 100. The finding is only shown if the score reaches `confidenceThreshold`.

## Evaluation triggers — when a rule runs

By default, every rule is evaluated once, **at enrollment end** — when the session completes, fails, or times out. That is the right moment for almost every rule: the full evidence is in, and suppressions like "don't fire when the enrollment completed" work as intended.

Some problems deserve attention *before* the session is over — a device looping in Account Setup for hours, or a finding a technician should see while still standing at the WhiteGlove bench. For those, a rule can declare additional **evaluation triggers** (`evaluateOn` in the JSON, the **Evaluation Triggers** section in the rule editor):

| Trigger                | The rule is evaluated…                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `enrollment_end`       | at session end (complete / failed / timed out) — the default, recommended for almost every rule                                 |
| `whiteglove_sealed`    | when WhiteGlove pre-provisioning completes and the session is sealed for the user phase — the technician is still at the device |
| `on_event:<eventType>` | whenever the agent reports an event of that type while the enrollment is running (e.g. `entra_user_affinity_pending`)           |

### How interim findings behave

A finding produced before enrollment end is **preliminary**: it appears live on the session page with a *Preliminary* badge and is re-checked by the final analysis at enrollment end. If the condition still holds, the finding is confirmed; if the session healed in the meantime, it is marked *Resolved* — kept for reference, but no longer counted as an open issue.

Interim evaluation is deliberately conservative:

* **Notifications fire once.** Each finding notifies its channels at most once per session, no matter how often it is re-evaluated — an interim alert followed by the final analysis never produces a duplicate.
* **"Mark session as failed" never applies mid-run.** A knock-out rule only fails a session at enrollment end; an interim pass can never terminate a running enrollment.
* **Statistics stay clean.** Fire statistics, hit rates, and regression detection count each finding once, at enrollment end.

### Writing interim-safe rules

A rule that runs mid-enrollment sees an *incomplete* session, so its logic must not depend on the session being over:

* A precondition like `enrollment_complete` *not\_exists* passes trivially while the enrollment is still running — it only suppresses at enrollment end. Don't rely on it as the only guard.
* Prefer conditions that stay true once they became true, and gate on **repetition**: require a signal to occur more than once (confidence factors with `count >= N` and a threshold above the base confidence) so a single transient event doesn't raise a preliminary finding on a healthy session.
* High-frequency telemetry event types (performance snapshots, download progress, network state changes, …) are **blocked** as `on_event` triggers — the editor and the API reject them, because they would re-run analysis on effectively every data batch.

The built-in rule **ANALYZE-ID-004** (hybrid sign-in never establishes Entra user affinity) is a worked example: it triggers on `entra_user_affinity_pending` events, but its confidence threshold requires the pending-affinity signal to repeat across reboots before a preliminary finding appears.

## Condition sources

The `source` field selects *how* a condition looks at the event stream:

| Source                 | What it does                                                                                                                                                                                                                                                                                                                                                                                                       | Typical use                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `event_type`           | Matches events of a type; with `dataField`, additionally tests one field on those events                                                                                                                                                                                                                                                                                                                           | "an `enrollment_failed` event exists", "`disk_free_gb < 5` on any snapshot"                       |
| `event_data`           | Tests a field in an event's data payload; supports dot notation for nested fields (`scan_summary.critical_cves`)                                                                                                                                                                                                                                                                                                   | Inspecting structured payloads, gather-rule outputs                                               |
| `event_data_array`     | Iterates an **array** field (`dataField`) and tests a sub-field (`itemField`) on each element — matches if **any** element satisfies the operator                                                                                                                                                                                                                                                                  | Allow-lists: "any provisioning package whose `identity` does *not* match my regex"                |
| `event_count`          | Counts events: `count_gte` (total) or `count_per_group_gte` (per group key in `dataField`, e.g. per `appId`). An optional **value filter** (`filterField` / `filterOperator` / `filterValue`) restricts which events are counted                                                                                                                                                                                   | "3+ install starts of the same app", "≥3 snapshots with `memory_used_percent > 90`"               |
| `phase_duration`       | The duration of an ESP phase in seconds (phase selected via `dataField`/`value` on `esp_phase_changed`)                                                                                                                                                                                                                                                                                                            | "DeviceSetup took too long"                                                                       |
| `app_install_duration` | Seconds between `app_install_started` and its completion for the same app                                                                                                                                                                                                                                                                                                                                          | "an app took more than 10 minutes to install"                                                     |
| `event_correlation`    | Joins two event types (A then B) on a shared field, optionally within a time window                                                                                                                                                                                                                                                                                                                                | "install completed, then later reported failed — for the *same* app"                              |
| `clock_skew`           | Measures the device clock against the server receive time across the whole session. `skewMetric` selects the metric: `clock_jump` (persistent mid-session step) or `sustained_offset` (whole session off by at least the threshold). `value` is the threshold in seconds; operator is limited to `gt`/`gte` on the magnitude. No `eventType`/`dataField`; IME-log-derived events are excluded from the measurement | "the clock jumped 30 minutes mid-enrollment", "the clock ran 30 minutes behind the whole session" |

## Operators

All comparisons are case-insensitive.

| Operator                            | Meaning                                                                                              |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `equals` / `not_equals`             | Exact match                                                                                          |
| `contains` / `not_contains`         | Substring                                                                                            |
| `regex` / `not_regex`               | Regular expression (.NET syntax, 1-second evaluation timeout; an invalid regex simply never matches) |
| `gt` / `lt` / `gte` / `lte`         | Numeric comparison                                                                                   |
| `exists` / `not_exists`             | Field present and non-empty / absent or empty                                                        |
| `in` / `not_in`                     | Membership in a comma-separated list                                                                 |
| `count_gte` / `count_per_group_gte` | Count thresholds — only with `source: event_count`                                                   |

## Correlation conditions

With `source: event_correlation` a single condition spans **two** events:

```json
{
  "signal": "reverted_after_success",
  "source": "event_correlation",
  "eventType": "app_install_completed",
  "correlateEventType": "app_install_failed",
  "joinField": "appId",
  "timeWindowSeconds": 3600,
  "required": true
}
```

* `eventType` is event **A**, `correlateEventType` is event **B**; the rule matches when B follows A and both share the same `joinField` value.
* `timeWindowSeconds` (optional) caps how far apart A and B may be.
* `eventAFilterField` / `eventAFilterOperator` / `eventAFilterValue` (optional) pre-filter which A events qualify.

## Suppression — don't alert on solved problems

Any condition can carry a `suppressByEvent` block:

```json
"suppressByEvent": { "eventType": "app_install_completed", "joinField": "appId" }
```

If a *resolving* event of that type exists with the same `joinField` value as the matched event, the match is discarded. This is how built-in rules avoid alerting on an app failure that a later retry fixed.

## Absence is not evidence

The agent sees an enrollment only from the moment the Intune Management Extension started it, and only as well as its collectors work. A missing event can mean *it never happened* — or *it was not seen*. Keep that in mind when a rule reasons about something that did **not** occur:

* **Prefer positive signals.** The agent emits dedicated events for many "nothing happened" situations — `app_install_starved` (a required app never started), `entra_user_affinity_pending` (no user token after the desktop), `session_stalled`. A rule built on such an event is monotonic and interim-safe; a rule built on `not_exists` is neither.
* **Use `not_exists` as a suppression, rarely as a claim.** As a precondition ("skip when `enrollment_complete` exists") it removes false alarms. As a *required* condition it asserts that the agent proved a negative.
* **Gate absence on coverage.** Events derived from the IME logs (`app_install_*`, `script_*`, `ime_*`, `esp_phase_changed`, …) are only as reliable as the log tracker that matched them. When the tracker skipped work it reports `ime_tracker_degraded` once per session; a rule asserting the absence of such an event should carry the precondition `{ "eventType": "ime_tracker_degraded", "operator": "not_exists" }` so it stays silent on those sessions. `collector_degraded` plays the same role for event-log and kernel watchers. `validate_rule` warns when a required absence lacks the gate.
* **Read the coverage before the findings.** In the AI integration, `get_session_summary` returns a `coverage` block — from when the agent actually watched, tracker and collector health, upload and diagnostics-package state, and a `gaps` list with one line per blind spot. A finding that rests on an absence is only as strong as an empty `gaps` list.

## The confidence model

Confidence expresses *how sure* the rule is, and gates whether the finding is shown at all:

> **score = min(baseConfidence + Σ weights of matched factors, 100)** — shown only if **score ≥ confidenceThreshold**

Each confidence factor has a `signal`, a `condition` expression, and a `weight`. Three expressions are supported:

| Factor `condition`     | Matches when…                                                                    | `signal` must be…                                               |
| ---------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `"exists"`             | a **condition with the same signal name** matched                                | the `signal` of one of the rule's (usually optional) conditions |
| `"count >= N"`         | the session contains at least N events **of the event type named by the signal** | an event type, e.g. `app_install_failed`                        |
| `"phase_duration > N"` | the matched phase lasted longer than N seconds                                   | the signal of a `phase_duration` condition                      |

{% hint style="warning" %}
The `signal` linkage is exact: an `"exists"` factor whose signal doesn't match any condition's signal never fires. The typical pattern is: add an **optional** condition (e.g. `app_failed` testing for `app_install_failed` events), then a factor with the **same signal** to reward its presence with extra confidence.
{% endhint %}

**Tuning guide:** `baseConfidence` is what a bare match is worth (50 = plausible, 75+ = strong signal). Use a threshold **above** the base when the rule should only fire with corroborating factors — e.g. base 50, factor +40, threshold 70 means "phase exists" alone never fires, "phase exists *and* is long" does.

## Evidence and `{{interpolation}}`

Every matched condition records evidence: the matched event, the field, its value and — depending on the condition source — more keys such as a count, a threshold, a duration or the app it grouped by. Placeholders in the `explanation`, the `remediation` steps and the `presentation` read that evidence in two forms:

* `{{fieldName}}` resolves by **data field name** — `{{appName}}`, `{{errorCode}}`, `{{reason}}`. A common pattern is adding optional `exists` conditions purely to *harvest* fields for the text.
* `{{signal.key}}` reads one key of one condition's evidence, named by the condition's `signal` — `{{install_retry_loop.count}}`, `{{install_retry_loop.appName}}`. It reaches the counts, thresholds and durations a field name cannot. Which keys a condition records depends on its source; [`validate_rule`](/rules/ai-assisted-rule-authoring.md) checks every placeholder against them.

Readers never see a placeholder: a list item or remediation step that needs a value the session did not record is left out, and such a value in running text reads –. The rule list shows your text with its placeholders; **Test against a session** shows it filled in, including how the finding shows on the diagnosis page.

## Custom rules, versions, and IDs

* Custom rules are created on the Analyze Rules page (**Create Custom Rule**), in form mode or raw JSON — advanced constructs like `event_correlation` require JSON mode. **Test against a session** dry-runs the draft before you save it.
* The `version` is bumped automatically when you edit a rule in the portal, and `author` is set to the editing user.
* Built-in and community rules are read-only; **Export** any of them as JSON to use as a starting point for your own rule.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.autopilotmonitor.com/rules/analyze-rules/concepts.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
