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

# Workflow Conditions

> How Rootly workflows evaluate run conditions — all-of / any-of / none-of join operators, per-field operators, recipes, and troubleshooting misfires.

export const ConditionEvaluator = () => {
  const focus = "focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5446a2] focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-[#292523]";
  const chipBase = `inline-flex items-center gap-1 px-2 py-0.5 rounded-[16px] text-[12px] leading-4 font-medium ring-1 ring-inset cursor-pointer transition-colors ${focus}`;
  const chipNeutral = "bg-[#f7f7f6] dark:bg-[#3b3733] ring-[#e7e5e4] dark:ring-[#57534d] text-[#4a4741] dark:text-[#e7e5e4] hover:bg-[#f0f0ef] dark:hover:bg-[#4a4741]";
  const chipSelected = "bg-[#7748f6] dark:bg-[#a88bee] ring-[#7748f6] dark:ring-[#a88bee] text-white hover:bg-[#6734bd] dark:hover:bg-[#bca8f3]";
  const btnBase = `inline-flex items-center justify-center gap-1 whitespace-nowrap cursor-pointer transition-colors font-medium ${focus}`;
  const btnGhostSm = `${btnBase} h-6 px-2 text-[12px] leading-4 rounded-[4px] text-[#57534d] dark:text-[#c6c2bf] hover:bg-[#f7f7f6] dark:hover:bg-[#3b3733]`;
  const selectCls = `h-6 px-2 rounded-[6px] border border-[#dcd9d7] dark:border-[#4a4741] bg-white dark:bg-[#3b3733] text-[12px] leading-4 text-[#292523] dark:text-[#e7e5e4] cursor-pointer ${focus}`;
  const FIELDS = {
    severity: {
      label: "Severity",
      type: "single",
      options: ["SEV0", "SEV1", "SEV2", "SEV3"]
    },
    kind: {
      label: "Kind",
      type: "single",
      options: ["Incident", "Sub Incident", "Test Incident", "Sub Test Incident", "Backfill Incident", "Scheduled Maintenance", "Sub Scheduled Maintenance"]
    },
    status: {
      label: "Status",
      type: "single",
      options: ["Triage", "Started", "Mitigated", "Resolved", "Closed", "Cancelled"]
    },
    environments: {
      label: "Environments",
      type: "multi",
      options: ["Production", "Staging", "Development"]
    },
    services: {
      label: "Services",
      type: "multi",
      options: ["payments-api", "checkout-web", "database"]
    },
    teams: {
      label: "Teams",
      type: "multi",
      options: ["Platform", "Frontend", "Data"]
    },
    incident_types: {
      label: "Incident Types",
      type: "multi",
      options: ["UI Bug", "Infrastructure", "Security Event"]
    }
  };
  const OPERATORS_SINGLE = [{
    code: "IS",
    label: "is"
  }, {
    code: "ANY",
    label: "is one of"
  }, {
    code: "NONE",
    label: "is none of"
  }, {
    code: "SET",
    label: "is set",
    noValue: true
  }, {
    code: "UNSET",
    label: "is unset",
    noValue: true
  }];
  const OPERATORS_MULTI = [{
    code: "CONTAINS",
    label: "contains any of"
  }, {
    code: "CONTAINS_ALL",
    label: "contains all of"
  }, {
    code: "CONTAINS_NONE",
    label: "contains none of"
  }, {
    code: "SET",
    label: "is set",
    noValue: true
  }, {
    code: "UNSET",
    label: "is unset",
    noValue: true
  }];
  const PRESETS = {
    "sev0-prod": {
      label: "Fire only on SEV0 production incidents",
      incident: {
        severity: "SEV0",
        kind: "Incident",
        environments: ["Production"],
        services: ["payments-api"],
        teams: ["Platform"],
        status: "Started"
      },
      join: "ALL",
      rows: [{
        field: "severity",
        op: "ANY",
        values: ["SEV0"]
      }, {
        field: "environments",
        op: "CONTAINS",
        values: ["Production"]
      }, {
        field: "kind",
        op: "IS",
        values: ["Incident"]
      }]
    },
    "team-owned": {
      label: "Fire only on incidents owned by Platform team",
      incident: {
        severity: "SEV1",
        kind: "Incident",
        environments: ["Production"],
        teams: ["Platform", "Frontend"],
        status: "Started"
      },
      join: "ALL",
      rows: [{
        field: "teams",
        op: "CONTAINS",
        values: ["Platform"]
      }]
    },
    "exclude-tests": {
      label: "Exclude test incidents from a paging workflow",
      incident: {
        severity: "SEV0",
        kind: "Test Incident",
        environments: ["Production"],
        status: "Started"
      },
      join: "ALL",
      rows: [{
        field: "kind",
        op: "IS",
        values: ["Incident"]
      }, {
        field: "severity",
        op: "ANY",
        values: ["SEV0", "SEV1"]
      }]
    },
    "not-cancelled": {
      label: "Fire on status updates except cancellation",
      incident: {
        severity: "SEV1",
        kind: "Incident",
        status: "Mitigated"
      },
      join: "NONE",
      rows: [{
        field: "status",
        op: "ANY",
        values: ["Cancelled"]
      }]
    }
  };
  const [presetKey, setPresetKey] = useState("sev0-prod");
  const [incident, setIncident] = useState(PRESETS["sev0-prod"].incident);
  const [join, setJoin] = useState(PRESETS["sev0-prod"].join);
  const [rows, setRows] = useState(PRESETS["sev0-prod"].rows);
  const applyPreset = key => {
    setPresetKey(key);
    setIncident(PRESETS[key].incident);
    setJoin(PRESETS[key].join);
    setRows(PRESETS[key].rows);
  };
  const VALUELESS_OPS = new Set(["SET", "UNSET"]);
  const SINGLE_VALUE_OPS = new Set(["IS", "NONE"]);
  const evaluateRow = (row, ctx) => {
    const raw = ctx[row.field];
    const fieldMeta = FIELDS[row.field];
    const values = row.values || [];
    const isBlank = fieldMeta?.type === "multi" ? !Array.isArray(raw) || raw.length === 0 : raw == null || raw === "";
    const present = !isBlank;
    if (!VALUELESS_OPS.has(row.op) && values.length === 0) return false;
    switch (row.op) {
      case "IS":
        return present && raw === values[0];
      case "ANY":
        return present && values.some(v => v === raw);
      case "CONTAINS":
        return present && values.some(v => raw.includes(v));
      case "CONTAINS_ALL":
        return present && values.every(v => raw.includes(v));
      case "CONTAINS_NONE":
        return isBlank || values.every(v => !raw.includes(v));
      case "NONE":
        return isBlank || raw !== values[0];
      case "SET":
        return present;
      case "UNSET":
        return isBlank;
      default:
        return false;
    }
  };
  const rowResults = rows.map(r => ({
    ...r,
    matched: evaluateRow(r, incident)
  }));
  const fires = (() => {
    const bools = rowResults.map(r => r.matched);
    if (bools.length === 0) return true;
    if (join === "ALL") return bools.every(Boolean);
    if (join === "ANY") return bools.some(Boolean);
    if (join === "NONE") return bools.every(b => !b);
    return false;
  })();
  const updateRow = (i, patch) => {
    const next = [...rows];
    next[i] = {
      ...next[i],
      ...patch
    };
    if (patch.field !== undefined || patch.op !== undefined) next[i].values = [];
    setRows(next);
  };
  const addRow = () => setRows([...rows, {
    field: "severity",
    op: "ANY",
    values: []
  }]);
  const removeRow = i => setRows(rows.filter((_, idx) => idx !== i));
  const toggleIncidentMulti = (field, opt) => {
    const current = incident[field] || [];
    const next = current.includes(opt) ? current.filter(o => o !== opt) : [...current, opt];
    setIncident({
      ...incident,
      [field]: next
    });
  };
  const setIncidentSingle = (field, value) => {
    setIncident({
      ...incident,
      [field]: incident[field] === value ? null : value
    });
  };
  const fmtValue = v => {
    if (v == null || v === "") return "—";
    if (Array.isArray(v)) return v.length === 0 ? "—" : `[${v.join(", ")}]`;
    return v;
  };
  return <div className="my-4 rounded-[8px] border border-[#dcd9d7] dark:border-[#4a4741] overflow-hidden">
      {}
      <div className="flex items-center justify-between px-4 py-2 bg-[#fcfbfa] dark:bg-[#3b3733] border-b border-[#dcd9d7] dark:border-[#4a4741] flex-wrap gap-2">
        <div className="text-[14px] leading-[18px] font-semibold text-[#292523] dark:text-[#e7e5e4]">
          Condition Evaluator
        </div>
        <select value={presetKey} onChange={e => applyPreset(e.target.value)} className={selectCls} aria-label="Load preset">
          {Object.entries(PRESETS).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
        </select>
      </div>

      {}
      <div className="px-4 pt-4 bg-white dark:bg-[#292523]">
        <div className={`rounded-[8px] px-4 py-3 flex items-center gap-3 ring-1 ring-inset ${fires ? "bg-[#e8faef] dark:bg-[#2a553a] ring-[#8ee1ac] dark:ring-[#5dc580] text-[#2e6140] dark:text-[#daf6e5]" : "bg-[#fde8e8] dark:bg-[#85221e] ring-[#fbb3b1] dark:ring-[#f76f6b] text-[#c4231c] dark:text-[#fcdbda]"}`} role="status" aria-live="polite">
          <span className="text-[18px]">{fires ? "✅" : "❌"}</span>
          <div className="flex-1 min-w-0">
            <div className="text-[14px] leading-[18px] font-semibold">
              {fires ? "Would fire" : "Wouldn't fire"}
            </div>
            <div className="text-[12px] leading-4 opacity-80">
              Join <code className="font-mono">{join.toLowerCase()}</code> over {rows.length} condition{rows.length === 1 ? "" : "s"} —{" "}
              {join === "ALL" && `${rowResults.filter(r => r.matched).length}/${rows.length} matched (all required)`}
              {join === "ANY" && `${rowResults.filter(r => r.matched).length}/${rows.length} matched (at least one required)`}
              {join === "NONE" && `${rowResults.filter(r => r.matched).length}/${rows.length} matched (all must be false)`}
            </div>
          </div>
        </div>
      </div>

      {}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-white dark:bg-[#292523]">
        {}
        <div className="rounded-[8px] border border-[#dcd9d7] dark:border-[#4a4741] overflow-hidden">
          <div className="px-3 py-1.5 bg-[#fcfbfa] dark:bg-[#3b3733] border-b border-[#dcd9d7] dark:border-[#4a4741]">
            <div className="text-[12px] leading-4 font-semibold text-[#292523] dark:text-[#e7e5e4] uppercase tracking-wider">
              Incident context
            </div>
          </div>
          <div className="p-3 space-y-3 bg-white dark:bg-[#292523]">
            {Object.entries(FIELDS).map(([key, meta]) => <div key={key}>
                <div className="text-[12px] leading-4 mb-1 text-[#57534d] dark:text-[#a6a09b]">
                  {meta.label}
                </div>
                <div className="flex flex-wrap gap-1">
                  {meta.type === "single" ? <>
                      {meta.options.map(opt => {
    const selected = incident[key] === opt;
    return <button key={opt} type="button" onClick={() => setIncidentSingle(key, opt)} className={`${chipBase} ${selected ? chipSelected : chipNeutral}`}>
                            {opt}
                          </button>;
  })}
                      {incident[key] && <button type="button" onClick={() => setIncidentSingle(key, incident[key])} className={`${chipBase} ${chipNeutral}`} title="Unset" aria-label="Unset">
                          ×
                        </button>}
                    </> : meta.options.map(opt => {
    const checked = (incident[key] || []).includes(opt);
    return <button key={opt} type="button" onClick={() => toggleIncidentMulti(key, opt)} className={`${chipBase} ${checked ? chipSelected : chipNeutral}`}>
                          {checked ? "✓ " : ""}{opt}
                        </button>;
  })}
                </div>
              </div>)}
          </div>
        </div>

        {}
        <div className="rounded-[8px] border border-[#dcd9d7] dark:border-[#4a4741] overflow-hidden">
          <div className="px-3 py-1.5 bg-[#fcfbfa] dark:bg-[#3b3733] border-b border-[#dcd9d7] dark:border-[#4a4741]">
            <div className="text-[12px] leading-4 font-semibold text-[#292523] dark:text-[#e7e5e4] uppercase tracking-wider">
              Condition group
            </div>
          </div>
          <div className="p-3 bg-white dark:bg-[#292523]">
            {}
            <div className="mb-3">
              <div className="text-[12px] leading-4 mb-1 text-[#57534d] dark:text-[#a6a09b]">
                Join operator
              </div>
              <div className="inline-flex rounded-[6px] border border-[#dcd9d7] dark:border-[#4a4741] overflow-hidden">
                {["ALL", "ANY", "NONE"].map((op, i) => <button key={op} type="button" onClick={() => setJoin(op)} className={`h-6 px-3 text-[12px] leading-4 font-medium cursor-pointer transition-colors ${focus} ${i > 0 ? "border-l border-[#dcd9d7] dark:border-[#4a4741]" : ""} ${join === op ? "bg-[#7748f6] dark:bg-[#a88bee] text-white" : "bg-[#f7f7f6] dark:bg-[#292523] text-[#57534d] dark:text-[#c6c2bf] hover:bg-[#f0f0ef] dark:hover:bg-[#3b3733]"}`}>
                    {op.toLowerCase()} of
                  </button>)}
              </div>
            </div>

            {}
            <div className="space-y-2">
              {rowResults.map((r, i) => {
    const fieldMeta = FIELDS[r.field];
    const ops = fieldMeta?.type === "multi" ? OPERATORS_MULTI : OPERATORS_SINGLE;
    const opMeta = ops.find(o => o.code === r.op);
    return <div key={i} className={`rounded-[6px] border p-2 ${r.matched ? "border-[#8ee1ac] dark:border-[#5dc580] bg-[#f3fcf6] dark:bg-[#2a553a]/40" : "border-[#fbb3b1] dark:border-[#f76f6b] bg-[#fdf3f2] dark:bg-[#85221e]/40"}`}>
                    <div className="flex items-center gap-1 flex-wrap">
                      <span className={`inline-flex items-center justify-center w-4 h-4 font-mono text-[12px] leading-4 font-bold ${r.matched ? "text-[#2e6140] dark:text-[#daf6e5]" : "text-[#c4231c] dark:text-[#fcdbda]"}`}>
                        {r.matched ? "✓" : "✗"}
                      </span>
                      <select value={r.field} onChange={e => updateRow(i, {
      field: e.target.value,
      op: FIELDS[e.target.value].type === "multi" ? "CONTAINS" : "ANY"
    })} className={selectCls}>
                        {Object.entries(FIELDS).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
                      </select>
                      <select value={r.op} onChange={e => updateRow(i, {
      op: e.target.value
    })} className={selectCls}>
                        {ops.map(o => <option key={o.code} value={o.code}>{o.label}</option>)}
                      </select>
                      <button type="button" onClick={() => removeRow(i)} className={`ml-auto h-6 w-6 inline-flex items-center justify-center rounded-[4px] text-[#87817b] hover:bg-[#f7f7f6] dark:hover:bg-[#3b3733] hover:text-[#c4231c] cursor-pointer ${focus}`} aria-label="Remove condition" title="Remove">
                        ×
                      </button>
                    </div>

                    {!opMeta?.noValue && <div className="mt-1.5 flex flex-wrap gap-1">
                        {fieldMeta?.options.map(opt => {
      const selected = (r.values || []).includes(opt);
      const isSingleValueOp = SINGLE_VALUE_OPS.has(r.op);
      return <button key={opt} type="button" onClick={() => {
        let nextValues;
        if (isSingleValueOp) {
          nextValues = selected ? [] : [opt];
        } else {
          nextValues = selected ? r.values.filter(v => v !== opt) : [...r.values || [], opt];
        }
        updateRow(i, {
          values: nextValues
        });
      }} className={`${chipBase} ${selected ? chipSelected : chipNeutral}`}>
                              {opt}
                            </button>;
    })}
                      </div>}

                    <div className="mt-1.5 text-[11px] leading-4 text-[#87817b] dark:text-[#a6a09b] font-mono">
                      incident.{r.field} = {fmtValue(incident[r.field])} · {opMeta?.label}
                      {!opMeta?.noValue && ` [${(r.values || []).join(", ") || "—"}]`}
                    </div>
                  </div>;
  })}
              <button type="button" onClick={addRow} className={`${btnGhostSm} w-full border border-dashed border-[#dcd9d7] dark:border-[#4a4741] justify-center hover:border-[#5446a2] hover:text-[#5446a2] dark:hover:text-[#c3c3ea]`}>
                + Add condition
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>;
};

## Overview

After a workflow's trigger fires, Rootly evaluates **run conditions** to decide whether the workflow's actions should execute. Conditions are the difference between a workflow that fires once a quarter on real SEV0s and one that fires hundreds of times a day on every minor update.

Every condition has two layers:

1. **A join operator** — how the individual conditions combine (**all of**, **any of**, **none of**).
2. **A per-condition operator** — how each condition compares its target field to the value(s) you configured (`is`, `is one of`, `contains any of`, `is set`, etc.).

Getting the join operator wrong is the most common cause of workflows that run more often than expected — the default is **all of**, but a slip to **any of** turns every condition into an OR.

***

## Join Operators

Rootly supports three operators for joining multiple run conditions:

| Operator    | Meaning                         | When to Use                                                                                                |
| ----------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **all of**  | Every condition must be true.   | The default and most common choice. Use when you want the workflow to fire only on a specific combination. |
| **any of**  | At least one condition is true. | Use when any of several conditions independently warrants firing (e.g., SEV0 *or* Security incident type). |
| **none of** | Every condition must be false.  | Use to exclude — e.g., "fire on all incidents except Test Kind".                                           |

<Note>
  If a workflow is running more often than expected, verify the join operator. Defaulting to **all of** is the safe choice; **any of** turns N conditions into an OR and dramatically widens the match set.
</Note>

***

## Per-Condition Operators

Each individual condition picks an operator that determines how Rootly compares the field's current value to the value(s) you configured.

| Operator           | Returns true when…                                                 | Common Field Types     |
| ------------------ | ------------------------------------------------------------------ | ---------------------- |
| `is`               | The field's value matches exactly.                                 | Single-select, boolean |
| `is one of`        | A single-select field matches any of the provided values.          | Single-select          |
| `none of`          | A single-select field matches none of the provided values.         | Single-select          |
| `contains any of`  | A multi-select field includes at least one of the provided values. | Multi-select           |
| `contains all of`  | A multi-select field includes all of the provided values.          | Multi-select           |
| `contains none of` | A multi-select field includes none of the provided values.         | Multi-select           |
| `is set`           | The field has any value.                                           | Any                    |
| `is unset`         | The field has no value.                                            | Any                    |

### Operator Selection by Field Type

The right operator depends on whether the field stores a single value, multiple values, or a boolean:

* **Boolean fields** (e.g., `Is Private`) — use `is set` / `is unset`, or `is true` / `is false` patterns.
* **Single-select fields** (e.g., `Severity`, `Kind`, `Status`) — use `is`, `is one of`, `none of`.
* **Multi-select fields** (e.g., `Services`, `Teams`, `Functionalities`) — use `contains any of`, `contains all of`, `contains none of`.
* **Presence checks** — `is set` and `is unset` work on any field type and are useful for "fire only when an optional field has been filled in" patterns.

<Frame>
  <img src="https://mintcdn.com/rootly/JFQ1ZeVNi4nNRKF5/images/workflows-overview/4.webp?fit=max&auto=format&n=JFQ1ZeVNi4nNRKF5&q=85&s=5f1e292a72d31bce72ca99087c9cdc87" alt="Per-condition operator picker" width="899" height="618" data-path="images/workflows-overview/4.webp" />
</Frame>

***

## Try It: Condition Evaluator

The widget below evaluates a condition group against a sample incident context so you can see the operator semantics in action before you build the real thing. Pick a preset, tweak the incident fields on the left, adjust the condition rows on the right, and watch the verdict flip. Each row shows whether it individually matched, and the top-level verdict applies the join operator across all rows.

<ConditionEvaluator />

<Note>
  This evaluator mirrors the operator semantics from Rootly's condition engine (`is`, `is one of`, `contains any of`, `contains all of`, `contains none of`, `is none of`, `is set`, `is unset`) exactly. The available fields are a representative subset — production workflows expose more fields including all your custom fields.
</Note>

***

## Common Condition Recipes

The patterns below cover the majority of real-world **incident workflow** conditions — each is a starting point you can extend with additional conditions joined by **all of** to scope further. Recipes for alert, action item, retrospective, or pulse workflows follow the same operator patterns but with different available fields.

<Note>
  Field examples like **Services**, **Teams**, **Environments**, **Functionalities**, and **Incident Types** can be configured as either single-select or multi-select per workspace (see [Built-in Fields](/configuration/built-in-fields)). The recipes below assume the default multi-select configuration; if your workspace has one of these set to single-select, use `is` / `is one of` / `none of` in place of the contains-family operators.

  All values shown in the recipes are the **user-facing labels** visible in the workflow editor. In the raw API and audit-log payloads, some of these correspond to different internal identifiers (for example, the Kind label **Sub Test Incident** is `test_sub`); use the labels shown in the UI when authoring conditions.
</Note>

### Fire only on SEV0 production incidents

```
all of
  Severity is one of: SEV0
  Environments contains any of: Production
  Kind is: Incident
```

The `Kind is: Incident` clause is the easiest miss — without it, a Test Incident at SEV0 will fire the workflow. This example assumes Environments is multi-select (the default); if it's configured as single-select, use `Environment is: Production` instead.

### Fire only on incidents owned by a specific team

```
all of
  Teams contains any of: Platform
```

`contains any of` (not `is`) because Teams is multi-select by default. `Teams is one of` would silently fail to match incidents tagged with multiple teams that include Platform. If your workspace has Teams configured as single-select, use `Teams is one of: Platform`.

### Exclude test incidents from a paging workflow

```
all of
  Kind is: Incident
  Severity is one of: SEV0, SEV1
```

Or, equivalently, list the kinds to exclude explicitly:

```
none of
  Kind is one of: Test Incident, Sub Test Incident, Scheduled Maintenance, Sub Scheduled Maintenance, Backfill Incident
```

The positive form (`Kind is: Incident`) is usually clearer and survives the addition of new Kind values better than an exclusion list. See [Incident Kind](/configuration/incident-kind) for the full list of kinds and what each one triggers.

### Fire only when a custom field is filled in

```
all of
  Custom Field "Customer Tier" is set
```

Combine with another condition like `Severity is one of: SEV0` to scope further.

### Fire on any status update except cancellation

```
none of
  Status is one of: Cancelled
```

Paired with a Status Updated trigger.

***

## How Conditions Interact with Triggers

Triggers are **OR-joined** — if any selected trigger fires, the workflow initiates. Conditions are then evaluated against the incident's state at the moment the trigger fired.

This has two consequences worth knowing:

1. **Triggers do not contribute to conditions.** A workflow with a Status Updated trigger and a `Status is mitigated` condition will fire only when status changes *to* mitigated — but `Status` in the condition refers to the *new* status, not the trigger event.
2. **Conditions evaluate post-trigger.** If a workflow's trigger is Incident Created and a condition references `Mitigated At`, the condition evaluates against the just-created incident — which has no `mitigated_at` yet. Use `is unset` deliberately if you want to fire only on freshly-created incidents.

***

## Best Practices

* **Default to `all of` and verify the operator before saving.** A workflow that fires too often is almost always an `any of` slip.
* **Always include a `Kind is: Incident` condition on any workflow that pages or notifies people.** Without it, Test Incidents will trigger the workflow during training and demos.
* **Prefer positive conditions over exclusion lists.** `Kind is: Incident` survives the introduction of a new kind value; a `none of` exclusion list (`Test Incident, Sub Test Incident, Scheduled Maintenance, Sub Scheduled Maintenance, Backfill Incident`) breaks the next time a kind is added.
* **Use `contains any of` for multi-select fields.** `is` and `is one of` silently fail to match when the field stores multiple values that include your target — use the contains-family operators instead.
* **Test new conditions on Test Incidents first.** Run a `/rootly test` and confirm the workflow fires (or doesn't) as expected before relying on it in production.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="A workflow runs when not all conditions are met" icon="circle-xmark">
    Check the join operator. The default is **all of** — if it's been switched to **any of**, the workflow fires whenever a single condition matches. Open the workflow's run conditions block and verify the operator is **all of** (or the value you actually intended).
  </Accordion>

  <Accordion title="The workflow doesn't fire even though it should" icon="bell">
    Three common causes:

    1. **Trigger mismatch.** Workflows fire only on the triggers you selected. If you expect a workflow to fire on Severity Updated but only Status Updated is selected, it won't run.
    2. **Multi-select condition using `is`.** A condition like `Teams is Platform` evaluates to false when Teams contains \[Platform, Database]. Switch to `Teams contains any of: Platform`.
    3. **Kind filter exclusion.** If the workflow has `Kind is: Incident` and you're testing with a `/rootly test`, the condition correctly filters out the test incident. Drop the Kind filter temporarily or run a real `/rootly new` for the test.
  </Accordion>

  <Accordion title="A workflow fires for test incidents" icon="flask">
    Add a `Kind is: Incident` condition to scope the workflow to real production incidents only. Test incidents trigger workflows by default, which is desirable when you want to validate workflow logic — but undesirable for workflows that page humans or notify customers.
  </Accordion>

  <Accordion title="A condition referencing a custom field doesn't evaluate" icon="code">
    Custom field conditions evaluate against the field's current value at the time the trigger fires. If the field is populated later in the incident's lifecycle, an Incident Created trigger will see the field as unset. Either change the trigger to a later event (e.g., Status Updated) or add a separate workflow keyed off the field being populated.
  </Accordion>

  <Accordion title="A `none of` condition is matching when I expected it to fail" icon="circle-half-stroke">
    `none of` returns true when **none** of the listed values match — so an empty/unset field returns true (none of the values match because there's no value to match). If you want the condition to require the field to be set, combine `none of` with `is set` under an **all of** join.
  </Accordion>
</AccordionGroup>

***

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Can I nest condition groups?" icon="object-group">
    No. Rootly's condition editor is flat — every condition in a workflow uses the same join operator (**all of**, **any of**, or **none of**). To express compound boolean logic (e.g., `(A AND B) OR (C AND D)`), split into two workflows or restructure as a single workflow with conditions that can be joined under one operator.
  </Accordion>

  <Accordion title="Are conditions evaluated against the incident's pre-update or post-update state?" icon="pen">
    Post-update. When a Status Updated trigger fires after an incident moves from Started to Mitigated, conditions reference the post-mitigation state — `Status is mitigated` returns true.
  </Accordion>

  <Accordion title="Do conditions support Liquid expressions?" icon="code">
    Run conditions use a structured comparison interface (field + operator + value), not Liquid. For dynamic logic, use Liquid in workflow actions instead. Custom field values can still be compared structurally.
  </Accordion>

  <Accordion title="Can a workflow with no conditions fire?" icon="filter">
    Yes. A workflow with zero conditions fires every time any of its triggers fires. This is usually unintended — add at least one condition to scope when the workflow runs.
  </Accordion>
</AccordionGroup>

***

## Related Pages

<CardGroup cols={3}>
  <Card title="Workflows Overview" icon="diagram-project" href="/workflows/workflows">
    The umbrella page covering the full workflow execution model.
  </Card>

  <Card title="Incident Workflows" icon="triangle-exclamation" href="/workflows/incident-workflows">
    Workflow type for standard incident lifecycle automation.
  </Card>

  <Card title="Actions Reference" icon="bolt" href="/workflows/actions-reference">
    The full catalog of actions a workflow can execute once conditions pass.
  </Card>
</CardGroup>
