diff --git a/redirects.mjs b/redirects.mjs
index f4cb0f5e..6edf0151 100644
--- a/redirects.mjs
+++ b/redirects.mjs
@@ -23,7 +23,24 @@ export const redirects = {
'/azure-monitoring': '/azure-integration/',
'/configuration': '/kloudmate-agent/management/',
'/database-integrations': '/database-monitoring/',
- '/direct-database-monitoring': '/database-monitoring/direct-database-monitoring/',
+ '/database-monitoring/database-activity-monitoring-dam': '/kloudmate-agent/ebpf-observability/',
+ '/database-monitoring/database-activity-monitoring-dam/ml-classifier-deployment-guide':
+ '/database-monitoring/sensitive-data/',
+ '/database-monitoring/database-integrations/mongodb-monitoring':
+ '/kloudmate-agent/database-monitoring/engines-and-coverages/',
+ '/database-monitoring/database-integrations/mssql-monitoring':
+ '/kloudmate-agent/database-monitoring/engines-and-coverages/',
+ '/database-monitoring/database-integrations/mysql-monitoring':
+ '/kloudmate-agent/database-monitoring/engines-and-coverages/',
+ '/database-monitoring/database-integrations/oracledb-monitoring':
+ '/kloudmate-agent/database-monitoring/engines-and-coverages/',
+ '/database-monitoring/database-integrations/postgresql-monitoring':
+ '/kloudmate-agent/database-monitoring/engines-and-coverages/',
+ '/database-monitoring/database-integrations/redis-monitoring':
+ '/kloudmate-agent/database-monitoring/engines-and-coverages/',
+ '/database-monitoring/direct-database-monitoring': '/kloudmate-agent/database-monitoring/overview/',
+ '/database-monitoring/opentelemetry-database-monitoring': '/apm-and-tracing/auto-instrumentation/',
+ '/direct-database-monitoring': '/kloudmate-agent/database-monitoring/overview/',
'/enable-service-principal-azure': '/azure-integration/enable-service-principal-azure/',
'/get-help': '/getting-started/get-help/',
'/go-instrumentation-running-on-kubernetes': '/kloudmate-agent/auto-instrumentation/go/',
@@ -35,8 +52,7 @@ export const redirects = {
'/managing-kloudmate-agents': '/kloudmate-agent/management/',
'/managing-panels': '/visualize-data/dashboards/managing-panels/',
'/mcp-integrations': '/kloudmate-assistant/mcp-server/',
- '/ml-classifier-deployment-guide':
- '/database-monitoring/database-activity-monitoring-dam/ml-classifier-deployment-guide/',
+ '/ml-classifier-deployment-guide': '/database-monitoring/sensitive-data/',
'/net-instrumentation-running-on-kubernetes': '/kloudmate-agent/auto-instrumentation/dotnet/',
'/setting-up-kloudmate': '/getting-started/setting-up-kloudmate/',
'/sns': '/incident-management/integrations/integrating-with-aws-cloudwatch/',
diff --git a/src/content/docs/docs/alerts/alert-groups.mdx b/src/content/docs/docs/alerts/alert-groups.mdx
index 3785d7e8..621b929a 100644
--- a/src/content/docs/docs/alerts/alert-groups.mdx
+++ b/src/content/docs/docs/alerts/alert-groups.mdx
@@ -7,8 +7,6 @@ sidebar:
When related alerts fire close together, KloudMate groups them into a single **Alert Group**. Instead of notifying on every individual alert, you get one notification for the group, and new alerts append to it rather than fan out into separate notifications.
-A group survives restarts and deduplicates against the same incident over time, so a recurring issue keeps showing up on the same group rather than spawning a fresh one each time.
-
## How groups form
An Alert Group forms by static grouping or by the AI correlation engine. Both produce the same kind of group:
@@ -20,28 +18,32 @@ You choose the mode per routing rule; see [Routing Rules](../routing-rules/#aler
## Where to find groups
-Open **Alerts → Alert groups** in the left navigation. This is the workspace-wide view of every group, open or resolved.
+Open **Alerts**, which starts on the **Groups** tab: the workspace-wide view of every group, firing or resolved.
+
+
-
+Above the list, a summary shows how many groups are firing and how many are resolved, such as **6 Firing, 130 Resolved**.
-Columns: **Group** (title), **State** (Open / Resolved), **Instances** (the firing-instance count), **Opened**, **Last update**, and an RCA indicator when an investigation is attached (a manual **Run RCA** or [Auto-RCA](../auto-rca/)).
+Columns: **Group** (title), **State** (**Firing** or **Resolved**), **Severity**, **Instances** (the number of unique instances in the group), **Opened**, **Last update**, and **RCA** when an investigation is attached (a manual **Run RCA** or [Auto-RCA](../auto-rca/)).
-Expand any row to preview that group's alerts inline: alert name, state, severity, received time, and labels, without leaving the list. The group title links straight to the full detail page.
+Expand any row to preview that group's alerts inline, with each alert's state, instance counts, and when it was last seen. The group title links straight to the full detail page.
Filter the list by:
-- **State:** All / Open / Resolved toggle. Defaults to **Open**.
-- **Routing rule:** dropdown filtered to rules in the workspace.
-- **Label matchers:** filter by `key=value`.
+- **State:** **All**, **Firing**, or **Resolved**. Defaults to **Firing**.
+- **Routing rule:** a single rule, or **All routing rules**.
+
+To find a group by title or by a label such as `service=api`, use the search box. On the **Resolved** and **All** filters, the search matches titles only.
-Severity isn't a filter on the list. It lives on the per-instance alerts inside a group, not on the group itself.
+Severity isn't a filter on the list.
## Anatomy of a group
A group carries:
-- **Title:** set by the first alert that opened the group, and **immutable** afterward, even if a higher-severity alert joins later.
-- **State:** `Open` while at least one underlying instance is firing; `Resolved` once everything quiets. A firing group whose instances are **all silenced** stays `Open` but carries a **Muted** badge. Muting gates notifications; it doesn't resolve the group.
+- **Title:** for a **Static** group, the rendered **Title** annotation when every instance that opened the group renders the same text, and otherwise the name of the alert that opened it. The correlation engine names an **Auto (AI)** group. See [Set a custom notification title](../annotations-and-severity/#set-a-custom-notification-title).
+- **Severity:** the most severe **Severity** annotation among the instances that opened the group.
+- **State:** **Firing** while at least one underlying instance is firing, and **Resolved** as soon as none is. A firing group whose firing instances are **all silenced** stays **Firing** and is marked **Muted**. Muting gates notifications; it doesn't resolve the group.
- **Labels:** the labels that define the group, read from the alerts that joined it (the group-by keys, in Static mode).
- **Alerts and instances:** how many distinct alerts and firing instances the group holds so far.
- **Routing rule:** the rule that opened the group.
@@ -49,34 +51,32 @@ A group carries:
- **Correlation reason:** for an Auto-correlated group, why these alerts were grouped. See [Why these were grouped](#why-these-were-grouped).
- **Investigation:** a root-cause investigation, when one is attached. Start it manually with **Run RCA**, or automatically with [Auto-RCA](../auto-rca/); see [Run RCA and View RCA](#run-rca-and-view-rca).
-Severity isn't a single group-level field. Each underlying instance carries its own severity.
-
## Group detail
-Click any row to open the group detail page.
+Click a group's title to open its detail page.
-
+
### Header
-- **Title:** set by the first alert and can't be changed.
+- **Title:** named from the alerts that opened the group. See [Anatomy of a group](#anatomy-of-a-group).
- **Run RCA / View RCA:** starts or opens a root-cause investigation for the group. See [Run RCA and View RCA](#run-rca-and-view-rca).
-- **Ask KloudMate Assistant:** opens the assistant chat panel with the group's labels, state, alert and instance counts, and routing rule pre-loaded into the prompt, so you can start investigating without retyping context.
+- **Ask Assistant:** opens the assistant chat panel with the group's labels, state, alert and instance counts, and routing rule pre-loaded into the prompt, so you can start investigating without retyping context.
- **Silence this group:** opens the silence creator with matchers derived from the group, bound to the group so the silence lifts when the group resolves. The binding sets when the silence ends, not what it mutes: the silence covers any alert in the workspace whose labels match its matchers, group member or not. See [From an Alert Group](../silences/#from-an-alert-group).
-### Firing instances
+### Instances
-The **Firing instances** panel is a table of the unique alert instances that joined this group, deduplicated by the per-instance labels the grouping engine uses.
+The **Instances** panel lists the unique alert instances in this group. Filter it by **All**, **Firing**, or **Resolved**; it shows **Firing** instances by default.
- **Common labels:** labels shared by every instance, so the per-row labels column only shows what varies.
- **Columns:**
- - **State:** the instance's current state: **Firing**, **No Data**, **Error**, or **Resolved**. Silenced instances are flagged as muted, so you can see at a glance which ones are firing but suppressed.
+ - **State:** **Firing** or **Resolved**. Instances muted by a silence or maintenance window are also marked **Muted**, so you can see at a glance which ones are firing but suppressed.
+ - **Value:** the last evaluated value behind the instance's state.
- **Alert:** the alert rule name. Links to the rule when the alert is KloudMate-native (carries an `alarm_id` label).
- **Instance labels:** the labels that distinguish this instance from its siblings (common labels are stripped out).
- - **Severity:** the per-instance severity, if the alert carries one.
- **Since:** how long this instance has been in its current state.
-Use this panel to see what's firing inside the group at a glance. For example, service A's `prod` and `staging` are firing while `dev` has already recovered.
+Use this panel to see what's firing inside the group at a glance. Switch to **All** to see, for example, that service A's `prod` and `staging` are firing while `dev` has already recovered.
### Run RCA and View RCA
@@ -91,10 +91,12 @@ This is the manual trigger. To investigate every group a rule opens automaticall
### Tabs
-- **Alerts:** every underlying alert as a row. Columns: Alert (linked to the rule when available), State, Severity, Received at, Labels. Use this when you need the raw event-by-event stream rather than the instance-level rollup.
+- **Alerts:** one row per alert rule in the group, with its state, how many of its instances are firing, and when it was last seen. The alert name links to its rule when the alert is KloudMate-native.
- **Blast radius:** what the group's firing resources connect to. See [Blast radius](#blast-radius).
-- **Audit:** a timeline of everything that happened to the group: opened, alerts appended, silence applied, notification dispatched, resolved. For Auto-correlated groups, it also logs when the engine opened the group and when it correlated each alert.
-- **Notifications:** per-channel dispatch outcomes (ok / failed / suppressed-by-silence), with deep links to where the notification landed: Slack thread URL, Jira ticket, KloudMate Incidents incident, and so on.
+- **Audit:** a timeline of the group's history, from when it opened to when it resolved. It records each notification and reminder sent, notifications suppressed by a silence or skipped, and investigations. For Auto-correlated groups, it also logs when the engine opened the group and when it correlated each alert.
+- **Notifications:** every notification sent for the group, reminders included, with each channel's result and a link to where it landed: the Slack thread, the Jira ticket, the KloudMate Incidents incident, and so on.
+
+
### Blast radius
@@ -110,7 +112,7 @@ The same graph appears on a single alert's **Blast radius** tab and on a resourc
**Why these were grouped** gives the reason in plain English, the shared labels, and supporting detail.
-This reason appears only when the engine correlated more than one alert. A lone alert the engine left on its own has none, which is expected (see [Cold start](#cold-start)).
+This reason appears only when the engine grouped more than one alert instance, either several alerts or the same alert firing on multiple targets. A lone instance the engine left on its own has none, which is expected (see [Cold start](#cold-start)).
### Cold start
@@ -124,7 +126,7 @@ Choosing **Wrong** lets you point at the specific alert that doesn't belong, or
## When a group resolves
-KloudMate marks a group `Resolved` once every underlying instance has resolved and a short grace period for flapping has elapsed (a few minutes, long enough to absorb instances that flap rapidly without closing the group prematurely). Resolved groups stay in the list under the **Resolved** filter and remain searchable indefinitely.
+KloudMate marks a group **Resolved** as soon as none of its instances is firing. If a matching alert fires again after that, it opens a new group. Resolved groups stay in the list under the **Resolved** filter.
## Related
diff --git a/src/content/docs/docs/alerts/alert-lifecycle.mdx b/src/content/docs/docs/alerts/alert-lifecycle.mdx
index 4ca5baea..282a37b5 100644
--- a/src/content/docs/docs/alerts/alert-lifecycle.mdx
+++ b/src/content/docs/docs/alerts/alert-lifecycle.mdx
@@ -8,7 +8,7 @@ sidebar:
import { LinkCard, CardGrid } from '@astrojs/starlight/components';
-Once you've [created an alert](../create-alerts/), KloudMate evaluates it on a schedule and moves it through a series of states. The control knobs in **Configure evaluation settings** (**Evaluate every**, **Pending duration**, **Recovery period**, **Alert state if No data**, and **Alert state if Error**) each shape *when* and *how* those transitions happen.
+After you've [created an alert](../create-alerts/), KloudMate evaluates it on a schedule and moves it through a series of states. The control knobs in the rule's evaluation settings (**Evaluate every**, **Pending duration**, **Recovery period**, **Alert state if No data**, and **Alert state if Error**) each shape *when* and *how* those transitions happen.
This page explains the full lifecycle: why an alert can fire instantly, and why No Data and Error don't wait out the pending duration.
@@ -76,7 +76,7 @@ A **re-breach during Recovering** snaps the instance back to **Firing** without
## The control knobs at a glance
-| Knob (form label) | What it controls | Default | Inherits from [folder](../folders/)? |
+| Knob (form label) | What it controls | When blank | Inherits from [folder](../folders/)? |
|---|---|---|---|
| **Evaluate every** | How often the rule runs. | `60s` | Yes |
| **Pending duration** | How long a breach must hold before **Firing**. | Fire immediately | No (per rule) |
@@ -84,13 +84,15 @@ A **re-breach during Recovering** snaps the instance back to **Firing** without
| **Alert state if No data** | Which state a no-data result becomes. | No Data | Yes |
| **Alert state if Error** | Which state an error result becomes. | Error | Yes |
+The **When blank** values apply when neither the rule nor its folder sets a value. New rules are pre-filled with **Evaluate every** set to `1m`, **Alert state if No data** set to **No Data**, and **Alert state if Error** set to **Firing**.
+
## How No Data and Error fit in
No Data and Error behave differently from a threshold breach. A common question:
> *I configured the same query, pending duration, and recovery period. Only the **Firing** state goes through the pending and recovery windows; **No Data** and **Error** don't. Is that expected?*
-**Yes, that's expected.** The **Pending duration** and **Recovery period** are properties of the *firing episode*. They gate the ramp **into** and **out of** Firing. **No Data** and **Error** are separate evaluation outcomes. By default they aren't part of a firing episode, so there is nothing for those windows to debounce. An instance switches into (and out of) No Data or Error **immediately**.
+**Yes, that's expected.** The **Pending duration** and **Recovery period** are properties of the *firing episode*. They gate the ramp **into** and **out of** Firing. **No Data** and **Error** are separate evaluation outcomes. Unless you map them to **Firing**, they aren't part of a firing episode, so there is nothing for those windows to debounce. An instance switches into (and out of) No Data or Error **immediately**.
What each setting does on a no-data or error result:
@@ -105,14 +107,16 @@ What each setting does on a no-data or error result:
**Alert state if Error**
+New rules start with this set to **Firing**. **Error** applies when the field is blank and the folder doesn't set it.
+
| Target | Behavior when the evaluation errors |
|---|---|
-| **Error** (default) | Instance switches to **Error** immediately. No pending wait, no recovery wait. |
+| **Error** | Instance switches to **Error** immediately. No pending wait, no recovery wait. |
| **Normal** | Instance switches to **Normal** immediately, treated as a clear (so a firing instance enters the Recovery period). |
| **Firing** | Treated exactly like a threshold breach: it ramps through the **Pending duration**, and honors the **Recovery period** when the query recovers. |
:::note[The takeaway]
-If you want a no-data or error condition to be **debounced by the same Pending duration and Recovery period as a threshold breach**, set **Alert state if No data** / **Alert state if Error** to **Firing**. Left on their default (**No Data** / **Error**), they're immediate and deliberate: a gap or a broken query shows up right away rather than waiting out a timer.
+To debounce a no-data or error condition with the same **Pending duration** and **Recovery period** as a threshold breach, set **Alert state if No data** or **Alert state if Error** to **Firing**. Set to **No Data** or **Error**, they switch immediately, so a gap or a broken query shows up right away instead of waiting out a timer.
:::
:::tip
@@ -121,7 +125,7 @@ Infrastructure failures (a query timeout, an unreachable source, or any error th
## Other behaviors you might notice
-- **Transient blips on a firing alert.** A firing instance only *rides through* a momentary No Data or Error gap when that state is mapped to **Firing**. If No Data / Error are left on their defaults, a firing instance switches to **No Data** / **Error** on the next bad evaluation; it does not stay "Firing" through the gap.
+- **Transient blips on a firing alert.** A firing instance only *rides through* a momentary No Data or Error gap when that state is mapped to **Firing**. If No Data or Error is mapped to its own state, a firing instance switches to **No Data** or **Error** on the next bad evaluation; it does not stay "Firing" through the gap.
- **A series that disappears entirely.** When a whole series stops appearing in the query results (a host is decommissioned, a metric is dropped), KloudMate waits a short grace window (about **two evaluation intervals**) and then treats the vanished instance as resolved and closes it. This is distinct from **No Data**, which is the query *running* and returning nothing. To alert on the disappearance instead of closing it quietly, turn on [Instance Absence Detection](../instance-absence-detection/).
- **Multi-instance rules.** A rule's overall state reflects its instances: if any instance is firing (or recovering), the rule reads as **Firing**; otherwise it surfaces the most significant instance state. Each instance still runs its own lifecycle independently.
- **Silences and maintenance windows don't change state.** They gate the outbound *notification*, not the lifecycle. A silenced instance keeps evaluating and transitioning, and its state changes still land in history; only the notification is withheld. See [Silences](../silences/) and [Maintenance Windows](../maintenance-windows/).
diff --git a/src/content/docs/docs/alerts/annotations-and-severity.mdx b/src/content/docs/docs/alerts/annotations-and-severity.mdx
index 08ee3614..4360bd62 100644
--- a/src/content/docs/docs/alerts/annotations-and-severity.mdx
+++ b/src/content/docs/docs/alerts/annotations-and-severity.mdx
@@ -17,21 +17,22 @@ Annotation values support **Liquid templating**, so you can pull live data from
Annotations live in the **Responder context** section of the alert rule editor. The pre-defined annotation keys are:
- **Severity:** free-form severity string, for example `sev1`, `critical`, or `p1`. Plain text or a Liquid template.
-- **Summary:** the alert headline in notifications. Supports templates.
- **Dashboard:** pick a workspace dashboard to link from the notification.
- **Panel:** when a dashboard is picked, narrows the link to a specific panel.
+- **Title:** the notification headline, in place of the automatic title. Plain text or a Liquid template. See [Set a custom notification title](#set-a-custom-notification-title).
+- **Summary:** body text included in notifications. Supports templates.
- **Playbook URL:** link to the runbook responders should follow. Supports templates.
-Any other key is a **custom annotation**. Click **Add Annotation** to add one.
+Any other key is a **custom annotation**. Custom annotations are available only in Advanced mode, where you add them with **Add annotation**.
## Add a custom annotation
-1. **Open the rule editor.** Scroll to **Responder context** and expand **Custom annotations**.
+1. **Open the rule in Advanced mode.** Editing an existing rule opens it in Advanced mode. For a new rule, click **Advanced mode** first. In **Add alert details**, expand **Custom annotations**.
-2. **Click Add Annotation.**
+2. **Click Add annotation.**
-3. **Provide a key.** Use any string, for example `service_owner`, `region`, or `runbook_section`.
+3. **Provide a key.** Use any key except the pre-defined ones, for example `service_owner`, `region`, or `runbook_section`.
4. **Provide a value.** This can be static text (`payments-team`) or a Liquid template (`{{ labels.service }} is degraded`).
@@ -47,7 +48,7 @@ You can interpolate live alert data into any annotation value with the `{{ }}` s
"labels": {
"service": "checkout-api",
"env": "prod"
- // ...plus any user-defined label keys from query dimensions or folders
+ // ...the rule's labels plus the series labels from the query
},
"state": {
"current_state": "Alerting",
@@ -64,6 +65,8 @@ You can interpolate live alert data into any annotation value with the `{{ }}` s
}
```
+`labels` holds the rule's own labels merged with the labels on the series. If both use the same key, the series value wins.
+
### Common references
- `{{ labels.service }}`: the value of the `service` label on the firing instance.
@@ -84,22 +87,49 @@ When the alert fires for `web-server-01` at 95%, the notification reads: *Host w
## Testing a template
-Click **Test template** on any templated value field to preview how it renders:
+To preview how a template renders, click the edit icon in any templated field. The template editor opens with your current template loaded.
+
+1. Edit the sample labels, state, and rule JSON the template renders against.
+2. Click **Test** to preview the rendered output. Any parse errors are shown so you can fix them before saving.
+3. Click **Save** to put the template back in the field, then save the rule.
-- It opens with your current template loaded.
-- Edit the sample labels, state, and rule JSON the template renders against.
-- Click **Test** to preview the rendered output. Any parse errors are shown so you can fix them before saving.
+
-Testing is optional; KloudMate doesn't validate templates on save. If a template can't render at notification time, KloudMate falls back to the raw template string rather than dropping the notification.
+Testing is optional; KloudMate doesn't validate templates on save. If a template can't render at notification time, KloudMate uses the raw template string instead, so the notification still goes out. **Title** is the exception, as described below.
### Failure mode
- A broken template stays in the value field as-is when you save.
- At render time, a parse failure makes that single annotation surface the raw template text instead of an evaluated value.
- Other annotations on the same alert keep rendering normally.
+- **Title** doesn't fall back to raw text. If it renders empty, or still contains `{{` or `{%`, the notification uses the automatic title.
This fail-soft behavior is intentional: a typo shouldn't silence a real incident.
+## Set a custom notification title
+
+A custom title gives the alert a readable headline in every channel. **Title** is an optional field in **Responder context**, in both Simple and Advanced mode, and it takes plain text or a Liquid template.
+
+
+
+The title replaces the automatic title in the email subject, the Slack and Microsoft Teams message, the Jira summary and heading, the SNS subject, the KloudMate Incidents incident title, and the webhook payload's `group.title`. It also names the group on **Alerts → Groups**, set when the group opens. The instance count is added to the end, for example `- 3 instances`.
+
+The title is used only when it renders the same for every instance in the group, and every rule in the group has the same title. Build it from labels that are the same across the group, such as a service or a namespace. A value that differs between instances, such as a pod name, a host name, or `state.value`, gives each instance a different title. Notifications then use the automatic title, and the group on **Alerts → Groups** keeps the alert's name.
+
+For example, with a [routing rule](../routing-rules/) that groups by `k8s_namespace_name`, set **Title** to:
+
+```liquid
+Memory pressure in {{ labels.k8s_namespace_name }}
+```
+
+When three instances in the `payments` namespace are firing, the title reads `Memory pressure in payments - 3 instances`.
+
+
+
+The template can use `labels.*` (the rule's labels plus the series labels), `state.value`, `state.values.*`, `rule.name`, and `rule.description`. If it renders empty, or still contains `{{` or `{%` because the template didn't render, the automatic title is used.
+
+The automatic title for a single rule is the alert name with its shared label values. For several rules grouped by labels, it's the group-by values with alert and instance counts. For an auto-correlated group, it's the correlation title with the instance count.
+
## Dynamic severity
Severity flows out of the reserved `severity` annotation. It's a string that downstream notification channels and incident-management tools use to prioritize the alert. Combine Liquid with `if` / `elsif` to compute severity from the firing value.
@@ -124,7 +154,7 @@ At notification time, KloudMate evaluates the template against the firing alert'
Earlier versions of KloudMate routed by **notification tags** on the rule. Tags are gone now, replaced by:
-- **Labels:** auto-derived from query dimensions and folders. These are what [Routing Rules](../routing-rules/) match against.
+- **Labels:** the labels you add to the rule with **Add label**, plus labels from query dimensions and folders. These are what [Routing Rules](../routing-rules/) match against.
- **Annotations:** human-readable context that flows into the notification.
- **Severity:** the reserved annotation key, free-form, optionally Liquid-templated.
diff --git a/src/content/docs/docs/alerts/auto-rca.mdx b/src/content/docs/docs/alerts/auto-rca.mdx
index 552abb9e..c9d6c8fb 100644
--- a/src/content/docs/docs/alerts/auto-rca.mdx
+++ b/src/content/docs/docs/alerts/auto-rca.mdx
@@ -26,11 +26,11 @@ Auto-RCA runs only once per alert group, when the group is first opened. Alerts
## Enable Auto-RCA on a routing rule
-1. Open **Alerts → Routing Rules** and edit the rule you want to enrich.
+1. Open **Alerts → Routing rules** and edit the rule you want to enrich.
2. Scroll to the **Auto-RCA** section.
-3. Toggle **Auto-RCA** on.
+3. Turn on **Enable Auto-RCA**.
4. Set the **Delay** if the default 5 minutes isn't right. A shorter delay surfaces answers faster; a longer one captures more alerts before the investigation runs.
-5. Save.
+5. Click **Save rule**.
Every group this rule opens from now on triggers an investigation after the configured delay.
diff --git a/src/content/docs/docs/alerts/create-alerts.mdx b/src/content/docs/docs/alerts/create-alerts.mdx
index 144fcce0..c4cd2f6a 100644
--- a/src/content/docs/docs/alerts/create-alerts.mdx
+++ b/src/content/docs/docs/alerts/create-alerts.mdx
@@ -9,23 +9,23 @@ An alert watches a query, evaluates a condition, and when the condition holds lo
## Getting started
-Open the **Alerts** section from the left navigation.
+Open **Alerts → Rules**.
-
+
-The **Alerts** screen lists your alert rules with their current state, name, and description. You can group rules into [Folders](../folders/).
+The **Rules** tab lists your alert rules with their current state, name, and description. You can group rules into [Folders](../folders/). Select a rule's name to open it.
From the **more options (⋯)** icon on any rule, you can:
- **Edit** the rule configuration
- **Duplicate** the rule
-- **Move to folder** to move the rule into a different [folder](../folders/)
+- **Move to folder…** to move the rule into a different [folder](../folders/)
- **Export as JSON** to copy the rule into another workspace. See [Export and Import Alerts](../export-and-import/)
-- **View state history** for the rule
+- **State history** to see the rule's past state changes
- **Pause Evaluation** or **Pause Notifications**
- **Delete** the rule
-
+
**Pause Notifications** creates a temporary [silence](../silences/) scoped to this alert: the rule keeps evaluating, but KloudMate suppresses its notifications until the silence expires. While it's paused, the rule is marked **Silenced**; view or end the pause from **Alerts → Silences**. Add matchers to limit it to specific instances.
@@ -33,7 +33,7 @@ For the key concepts, see the [Alerts Overview](../).
## Creating a new alert
-Click **Create Alert**. A dialog appears with three ways to create an alert:
+Click **Create alert**, then choose how to start:

@@ -46,51 +46,96 @@ Click **Create Alert**. A dialog appears with three ways to create an alert:
Instead of building an alert from scratch, start from a pre-configured template that covers a common monitoring scenario.
1. In the **Create Alert** dialog, select **From Template**.
-2. Click the **Select a template** dropdown and choose a template that matches your monitoring needs.
+2. Open the **Template** dropdown and choose a template that matches your monitoring needs.

-3. Click **Create Alert**. The alert appears in the **Alerts** list.
-4. To open and configure it, click the menu next to the alert and select **Edit**.
+3. Click **Create Alert**. The rule is saved right away and appears on the **Rules** tab.
+4. To configure it, click the menu next to the rule and select **Edit**.
5. The alert opens pre-configured with the query, aggregation, and threshold from the template. Review it and adjust any filters to match your environment.
6. Click **Save** or **Save & Close** when you're done.
### Using AI
-The KloudMate assistant generates the queries and thresholds from a plain-English prompt.
+**KloudMate Builder** drafts an alert from a plain-English description and fills in the alert editor for you. Nothing is saved until you click **Save**.
1. In the **Create Alert** dialog, select **Using AI**.
-2. A text box appears. Describe the alert you want to create.
+2. Describe the alert you want to create.

-3. Click **Create Alert**. KloudMate generates the alert configuration from your description.
-4. To review or adjust the settings, click the menu next to the alert and select **Edit**.
+3. Click **Create Alert**. KloudMate Builder opens with your description as a draft message.
+
+
+
+4. Send the message. The Builder may ask follow-up questions before it fills in the rule in the alert editor. A draft that Simple mode can't represent, such as one with several queries or expressions, opens in Advanced mode.
+5. Review the query, condition, and evaluation settings, and change anything that doesn't match your environment.
+6. Click **Save**.
### From Scratch
-To build a fully custom alert, select **From Scratch** in the **Create Alert** dialog and click **Create Alert**.
+Select **From Scratch** in the **Create Alert** dialog and click **Create Alert**. The alert editor opens in Simple mode.
-This opens the alert creation form, where you choose a data source, configure the metric or query to monitor, and define the alert condition on a single page.
+- **Simple mode** builds the rule from one query and one condition, on a single page. It's the only mode with the **Detection method** toggle for Anomaly and Forecast alerts. See [Build an alert in Simple mode](#build-an-alert-in-simple-mode).
+- **Advanced mode** lets you add more queries and combine them with math, reduce, and condition expressions. It's also the only mode with custom annotations. See [Build an alert in Advanced mode](#build-an-alert-in-advanced-mode).
-Create more queries and expressions with the **Add Query** and **Add Expression** buttons. KloudMate assigns each one a letter, such as **A**, **B**, or **C**, and you can duplicate any block with its copy icon.
+To switch, click **Advanced mode**. You can't switch back to Simple mode afterward. Editing or duplicating an existing rule always opens it in Advanced mode.
+
+## Build an alert in Simple mode
+
+Simple mode puts the whole rule on one page.
+
+
+
+1. In **What to monitor**, choose a data source and set up the query. The query fields are the same as in [Advanced mode](#1-setup-query-conditions-and-expressions). Click **Run query** to chart the result.
+2. In **Alert when**, set the condition. For a **Threshold**, pick a reduction, a comparison, and a threshold value. To alert on unusual values or a trend instead, [choose a detection method](#choose-a-detection-method). Click **Preview alerts** to check the result.
+3. In **Evaluation settings**, pick a folder and set how the rule is evaluated. The fields are described in [Configure evaluation settings](#2-configure-evaluation-settings).
+4. In **Alert details**, enter an **Alert name** and a description. Expand **Responder context** to add annotations such as a severity, a title, or a summary. See [Add alert details](#3-add-alert-details).
+5. Expand **Labels** and add the labels you want routing rules to match on. See [Notifications](#4-notifications).
+6. Click **Save**, or **Save & Close** to save and return to the **Rules** tab.
+
+### Choose a detection method
+
+The **Alert when** section sets the firing condition with a **Detection method** toggle:
+
+- **Threshold:** the classic fixed rule. Pick a reduction (such as `mean` or `max`), a comparison, and a value; the alert fires when the reduced value crosses it.
+- **Anomaly:** KloudMate learns the metric's normal range for each entity and alerts when a value moves well outside it, with no threshold to set. Choose a **Sensitivity** (High, Medium, or Low) and a **Direction** (Above, Below, or Both).
+- **Forecast:** KloudMate follows the recent trend and alerts before the value reaches a **Limit**, within an **Alert horizon** of 1, 4, or 7 days. Set the **Direction** toward the max or the min. Good for disks, memory, and quotas.
+
+
+
+Add a **Group by** key (such as host or service) so each entity is scored or forecast on its own baseline. The preview shows the learned range or the projected trend; click **Preview live data** to pull this workspace's own data.
+
+
+
+Anomaly and Forecast are the same detection [Smart Alerts](../smart-alerts/) provisions and maintains automatically for common infrastructure. Both need a plan with anomaly detection; Threshold works on any plan.
+
+Anomaly and Forecast rules evaluate every 5 minutes, so **Evaluate every** is fixed for them. If you leave **Pending duration** empty, it's saved as `30m`. Set **Sensitivity**, **Direction**, **Limit**, and **Alert horizon** before you save. Editing the rule opens it in Advanced mode, which doesn't have these controls.
+
+:::note
+The **Detection method** toggle appears only in Simple mode, and only for KloudMate metric queries. Anomaly and Forecast rely on the learned baselines KloudMate builds from your metrics, which aren't available for AWS (CloudWatch) or log and trace queries. Those sources use a **Threshold** condition.
+:::
-To reach advanced options such as **Math expressions**, **Reduce**, and **Condition expressions**, click **Advanced mode**.
+## Build an alert in Advanced mode
-
+Advanced mode is for rules that need more than one query, or expressions that transform query results. The editor is split into steps: **Setup query conditions and expressions**, **Configure evaluation settings**, **Add alert details**, and **Notifications**, followed by a save.
-The rule editor walks you through **Setup query conditions and expressions**, **Configure evaluation settings**, **Add alarm details**, and **Notifications**, then a final save.
+
### 1. Setup query conditions and expressions
+Create more queries and expressions with the **Add Query** and **Add Expression** buttons. KloudMate assigns each one a letter, such as **A**, **B**, or **C**, and you can duplicate any block with its copy icon.
+
#### Setting up query conditions for OpenTelemetry / KloudMate

- **Data Set:** select the dataset you want to retrieve from your data source.
- **Metric to Aggregate:** select the metric you want to monitor from that dataset.
+- **Aggregation:** choose the function applied to the metric, such as `Avg` or `Rate`. Click **Customize aggregation** for temporal and spatial aggregation controls. See [Metrics Aggregations](../../visualize-data/explore/metrics-aggregations/).
- **Group By:** enter the attributes used to group the data points.
- **Filters:** add filters to narrow the retrieved data points.
+- **Ranking:** control how the series are ordered.
OpenTelemetry users can also use **Prometheus query language** to retrieve data and configure alerts.
@@ -115,35 +160,15 @@ Query time ranges support the following:
- **Supported values:** the same units and keywords used in dashboards
- **Examples:** `now`, `now-5m`
-#### Choose a detection method
-
-In Simple mode, the **Alert when** section sets the firing condition with a **Detection method** toggle:
-
-- **Threshold:** the classic fixed rule. Pick a reduction (such as `mean` or `max`), a comparison, and a value; the alert fires when the reduced value crosses it.
-- **Anomaly:** KloudMate learns the metric's normal range for each entity and alerts when a value moves well outside it, with no threshold to set. Choose a **Sensitivity** (High, Medium, or Low) and a **Direction** (Above, Below, or Both).
-- **Forecast:** KloudMate follows the recent trend and alerts before the value reaches a **Limit**, within an **Alert horizon** of 1, 4, or 7 days. Set the **Direction** toward the max or the min. Good for disks, memory, and quotas.
-
-
-
-Add a **Group by** key (such as host or service) so each entity is scored or forecast on its own baseline. The preview shows the learned range or the projected trend; click **Preview live data** to pull this workspace's own data.
-
-
-
-Anomaly and Forecast are the same detection [Smart Alerts](../smart-alerts/) provisions and maintains automatically for common infrastructure. Both need a plan with anomaly detection; Threshold works on any plan.
-
-:::note
-The **Detection method** toggle appears only for KloudMate metric queries. Anomaly and Forecast rely on the learned baselines KloudMate builds from your metrics, which aren't available for AWS (CloudWatch) or log and trace queries. Those sources use a **Threshold** condition.
-:::
-
#### Setting up evaluation expressions
Expressions let you apply logic to query results. Reference any query or expression by its letter, such as **A**, **B**, or **C**. An expression can be passed as a parameter only when multiple expressions are configured.
Choose from these expression types:
-- **Math Expression:** enter a mathematical expression to apply to a query or expression, such as `$A+1`, `$A<$B`, or `$A && $C`. See [Alert Expressions](../expressions/).
+- **Math expression:** enter a mathematical expression to apply to a query or expression, such as `$A+1`, `$A<$B`, or `$A && $C`. See [Alert Expressions](../expressions/).
- **Reduce:** aggregate a query or expression into a single number with a function, then pick the target from the **Input** dropdown. Available functions include `mean()`, `max()`, `min()`, `sum()`, `last()`, and `count()`.
-- **Condition Expression:** pick a function and a query or expression, then choose a condition and a threshold to evaluate against. You can add multiple conditions and combine them with **AND** or **OR**.
+- **Condition expression:** pick a function and a query or expression, then choose a condition and a threshold to evaluate against. You can add multiple conditions and combine them with **AND** or **OR**.
Click **Run Queries** to execute everything you've configured.
@@ -153,9 +178,11 @@ To avoid the **NoData** issue when a single alert uses multiple queries, use the
### 2. Configure evaluation settings
-Pick a **Folder** to organize the rule and inherit shared defaults: select an existing folder, or type a new name to create one inline. The folder's **Evaluate every**, **No data state**, and **Eval error state** flow into the rule as defaults you can override per field. See [Folders](../folders/).
+In Simple mode, these fields are in **Evaluation settings**, without **Alert condition**.
+
+Pick a **Folder** to organize the rule: select an existing folder, or type a new name to create one inline. A folder can set defaults for **Evaluate every**, **Alert state if No data**, **Alert state if Error**, **Alert when an instance stops reporting**, and **Auto-close after**. See [Folders](../folders/).
-
+
- **Alert condition:** the query or expression that triggers the alert, such as **A**, **B**, or **C**.
- **Evaluate every:** how often the alert condition is evaluated (for example, `60s` or `1m`).
@@ -163,7 +190,7 @@ Pick a **Folder** to organize the rule and inherit shared defaults: select an ex
- **Recovery period:** how long the condition must stay within threshold before the rule resolves (for example, `5m`). This stops a flapping metric from resolving and immediately re-firing. While the rule waits out this window, the instance shows as **Recovering**: still firing, not yet back to Normal. Leave empty to resolve as soon as the condition clears.
- **Alert state if No data:** which state the alert enters when the query returns no data points. Options: **Firing**, **No Data**, **Normal**, or **Error**.
- **Alert state if Error:** which state the alert enters when a query returns an error. Options: **Firing**, **Error**, or **Normal**.
-- **Alert when an instance stops reporting:** track each instance the query returns, and alert when one goes silent while the others keep reporting. Turning this on also sets **Alert state if No data** to **Firing**. An **Auto-close after** field appears alongside it, controlling how long a silent instance stays tracked. See [Instance Absence Detection](../instance-absence-detection/).
+- **Alert when an instance stops reporting:** track each instance the query returns, and alert when one goes silent while the others keep reporting. Turning this on also sets **Alert state if No data** to **Firing**. Set **Auto-close after** to control how long a silent instance stays tracked. See [Instance Absence Detection](../instance-absence-detection/).
:::note[How “Firing” interacts with the pending duration]
When **Alert state if No data** or **Alert state if Error** is set to **Firing**, the alert doesn't fire the instant data goes missing or a query errors. It fires only after the condition has held for the **Pending duration** above, so a brief gap or a transient error won't notify anyone, and a series that's already firing rides through the blip without resetting. Set the **Pending duration** to `0` to fire immediately instead.
@@ -175,15 +202,15 @@ For the full lifecycle, and how **Pending duration**, **Recovery period**, and t
Click **Preview alerts** to run the query immediately and check the result.
-:::tip
-If the rule belongs to a folder with shared defaults, **Evaluate every**, **Alert state if No data**, and **Alert state if Error** show placeholder values inherited from the folder. Leave them blank to keep inheriting, or type a value to override. **Reset to folder default** returns an overridden field to inheriting.
+:::tip[Folder defaults on a new rule]
+A new rule starts with **Evaluate every** set to `1m`, **Alert state if No data** set to **No Data**, and **Alert state if Error** set to **Firing**. These values override the folder's defaults until you click **Reset to folder default** on each field. On an existing or duplicated rule, a blank field inherits from the folder. **Alert when an instance stops reporting** and **Auto-close after** inherit from the folder on every rule until you change them.
:::
-### 3. Add alarm details
+### 3. Add alert details
-
+
-Set the alert's name and description, then fill in the responder context:
+Set the alert's name and description, then fill in the responder context. In Simple mode, these fields are in the **Alert details** section.
- **Alert name:** a name for the alert.
- **Description:** a short description of the alert's purpose.
@@ -191,34 +218,42 @@ Set the alert's name and description, then fill in the responder context:
**Responder context** holds the annotations responders see when the notification lands.
- **Severity:** free-form severity (for example, `sev1`, `critical`, or `p1`). Supports Liquid templates, so severity can depend on the firing value.
+- **Dashboard:** optional dashboard link included with the notification.
+- **Panel:** after you pick a dashboard, narrows the link to a specific panel.
+- **Title:** replaces the automatic notification title. Supports templates. See [Set a custom notification title](../annotations-and-severity/#set-a-custom-notification-title).
- **Summary:** multiline message included in notifications. Supports templates.
-- **Dashboard:** optional dashboard link surfaced with the notification.
-- **Panel:** when you pick a dashboard, narrows the link to a specific panel.
-- **Playbook URL:** optional runbook URL.
+- **Playbook URL:** optional runbook URL. Supports templates.
-Open **Custom annotations** to add your own key-value pairs (for example, `service_owner` or `region`). Values support Liquid templates. See [Annotations & Severity](../annotations-and-severity/).
+In Advanced mode, open **Custom annotations** and click **Add annotation** to add your own key-value pairs (for example, `service_owner` or `region`). Values support Liquid templates. Simple mode doesn't have custom annotations. See [Annotations & Severity](../annotations-and-severity/).
### 4. Notifications
-The Notifications step controls how this alert flows into the grouping engine. Routing rules match on **labels**, not free-form tags:
+The Notifications step controls how this alert flows into the grouping engine. In Simple mode, add labels in the **Labels** section. Routing rules match on **labels**, not free-form tags:
- **Labels:** key/value pairs attached to each alert this rule fires. Add them with **Add label**, or leave the list empty. Routing rules match on these labels, plus the reserved `alarm_id`, `alarm_rule_folder_id`, and per-instance query labels, to decide which channels notify and how alerts group.
- **Routing:** [Routing Rules](../routing-rules/) in **Alerts → Routing rules** match alerts by label and send them to one or more notification channels.
- **Severity:** flows through the reserved `severity` annotation from the previous step; downstream tools use it to prioritize.
+
+
If you're migrating a rule that used notification tags, it keeps working; routing now matches on labels instead.
### 5. Save the alert
-Click **Save** to save the alert, or **Save & Close** to save and return to the **Alerts** screen. A confirmation appears once the alert is created.
+Click **Save** to save the alert, or **Save & Close** to save it and return to the **Rules** tab.
## Viewing an alert
-To open an alert, click the menu next to it and select **View**. The detail page has these tabs:
+To open an alert, select its name on the **Rules** tab. The detail page has these tabs:
-
+
- **Overview:** instance states, breaching instances with their labels, reason, and duration, plus recent state transitions.
- **Instances:** the full list of alert instances and their current states.
+- **Blast radius:** the resources the alert's instances map to, and what those resources depend on.
- **History:** the state-change history over time.
- **Rule:** the alert configuration and query definition.
+
+
+
+The instance tables on **Overview** and **Instances** also show each instance's last evaluated value and when it was **Last seen**.
diff --git a/src/content/docs/docs/alerts/export-and-import.mdx b/src/content/docs/docs/alerts/export-and-import.mdx
index 3b92f4ee..247f7e94 100644
--- a/src/content/docs/docs/alerts/export-and-import.mdx
+++ b/src/content/docs/docs/alerts/export-and-import.mdx
@@ -11,9 +11,9 @@ Importing creates new rules, so it requires the same permission as creating an a
## Export a rule or a folder
-To export a single rule, open the **more options (⋯)** menu on its row and select **Export as JSON**.
+To export a single rule, go to **Alerts → Rules**, open the **more options (⋯)** menu on the rule's row, and select **Export as JSON**.
-To export an entire folder, switch the Alerts list to **Folder view**, open the folder header's kebab menu, and select **Export alerts as JSON**. The export includes every rule in the folder, including any that the search box or state filter is hiding.
+To export an entire folder, switch the **Rules** tab to **Folder view**, open the folder header's kebab menu, and select **Export alerts as JSON**. The export includes every rule in the folder, including any that the search box or state filter is hiding.
## What is not copied
diff --git a/src/content/docs/docs/alerts/folders.mdx b/src/content/docs/docs/alerts/folders.mdx
index 522286f2..c9a026f7 100644
--- a/src/content/docs/docs/alerts/folders.mdx
+++ b/src/content/docs/docs/alerts/folders.mdx
@@ -7,21 +7,22 @@ sidebar:
Folders organize related alert rules and let member rules inherit shared evaluation defaults, the same grouping pattern dashboards use.
-On the **Alerts** list, switch between a flat **List view** and a grouped **Folder view**. In Folder view, rules are grouped under their folder; rules without one appear under **Ungrouped**.
+On **Alerts → Rules**, switch between a flat **List view** and a grouped **Folder view**. In Folder view, rules are grouped under their folder, and rules without one appear under **Ungrouped**. Rules that [Smart Alerts](../smart-alerts/) manages appear under **Smart Alerts** until you move them into a folder.
-
+
-There's no separate Folders page or sidebar entry; everything happens from the Alerts list.
+There's no separate Folders page. You manage folders from the **Rules** tab and the rule editor.
## Create a folder
You create folders inline from the alert rule editor's **Folder** picker; there's no separate "New folder" button.
1. Open an alert rule (create a new one or edit an existing one).
-2. In the **Configure evaluation settings** section, click the **Folder** dropdown.
+2. In **Configure evaluation settings** (**Evaluation settings** in Simple mode), click the **Folder** dropdown.
3. Type a new folder name, then select the `+ Create ""` option that appears.
4. KloudMate creates the folder and assigns the current rule to it.
-5. To set folder-level defaults (interval, no-data state, eval-error state), switch to **Folder view** on the Alerts list, open the folder's kebab menu, and select **Edit folder**.
+5. Save the rule. A folder appears in Folder view only after it contains at least one saved rule.
+6. To set folder defaults, switch to **Folder view** on the **Rules** tab, open the folder's kebab menu, and select **Edit folder**.
## Edit folder defaults
@@ -29,22 +30,28 @@ In Folder view, each folder header row has a kebab menu with **Edit folder**, **
**Edit folder** opens a dialog with the folder's settings:
+
+
| Field | What it controls |
|---|---|
| **Name** | Required, unique within the workspace. |
| **Description** | Optional. |
| **Evaluate every** | Default evaluation interval (for example `60s`, `1m`, or `2h`). Members inherit when blank. |
-| **No data state** | Default state when queries return no data. Options: `Normal`, `Alerting`, `NoData`. |
-| **Eval error state** | Default state when evaluation errors out. Options: `Normal`, `Alerting`, `Error`. |
+| **No data state** | Default state when queries return no data. Options: **Alerting**, **Normal**, **No Data**, or **Error**. |
+| **Eval error state** | Default state when evaluation errors out. Options: **Alerting**, **Normal**, **No Data**, or **Error**. |
+| **Instance stops reporting** | Whether members alert when an instance stops reporting. Options: **Alert** or **Don't alert**. **Alert** also needs **No data state** set to **Alerting**, on the folder or on each member rule. See [Instance Absence Detection](../instance-absence-detection/). |
+| **Auto-close after** | How long a silent instance stays tracked before it closes on its own, from `5m` to `72h` (for example `30m` or `24h`). |
Member alerts inherit these defaults unless they set their own value. Leave a folder's fields blank to make it organizational-only.
## Move rules into a folder
-Move rules from the rule editor, or in bulk from the list:
+You move rules one at a time, from the **Rules** tab or from the rule editor:
+
+- **From the Rules tab:** open the rule's more options (⋯) menu and select **Move to folder…**. Choose a folder or **Ungrouped**, then click **Move**.
+- **From the rule editor:** open the rule, pick the folder from the **Folder** dropdown in the **Configure evaluation settings** section, and save the rule.
-- **From the rule editor:** open the rule and pick the folder from the **Folder** dropdown in the **Configure evaluation settings** section.
-- **Bulk-move from the list:** select rules with the checkboxes (a toolbar appears with the selection count). Click **Move to folder…**, select a destination from the radio list (existing folders or **Ungrouped**), and confirm.
+
Moving a rule does **not** rewrite its own **Evaluate every**, **No data state**, or **Eval error state** values. Whatever the rule had before the move stays. The folder defaults only apply where the rule's field is blank.
@@ -61,8 +68,10 @@ A rule's evaluation fields resolve like this:
**Reset to folder default** clears an overridden field so it inherits again. A blank field keeps inheriting, so when the folder default later changes, the rule picks up the new value automatically.
+New rules start with **Evaluate every** set to `1m`, **Alert state if No data** set to **No Data**, and **Alert state if Error** set to **Firing**, and those values override the folder's defaults. To inherit the folder's value instead, click **Reset to folder default** on the field. Existing and duplicated rules keep inheriting wherever a field is blank. **Alert when an instance stops reporting** and **Auto-close after** inherit from the folder on every rule, new ones included, until you set them.
+
:::tip
-To detach a rule from its folder entirely, set its **Folder** to the ungrouped option. The rule keeps its stored evaluation values and stops inheriting.
+To detach a rule from its folder entirely, set its **Folder** to **None (ungrouped)**. The rule keeps its stored evaluation values and stops inheriting.
:::
## Delete a folder
diff --git a/src/content/docs/docs/alerts/images/absence-detection-settings.png b/src/content/docs/docs/alerts/images/absence-detection-settings.png
new file mode 100644
index 00000000..b8e3afe7
Binary files /dev/null and b/src/content/docs/docs/alerts/images/absence-detection-settings.png differ
diff --git a/src/content/docs/docs/alerts/images/advanced-mode-options.png b/src/content/docs/docs/alerts/images/advanced-mode-options.png
index c21c6d55..52d66d78 100644
Binary files a/src/content/docs/docs/alerts/images/advanced-mode-options.png and b/src/content/docs/docs/alerts/images/advanced-mode-options.png differ
diff --git a/src/content/docs/docs/alerts/images/alarm-detail-blast-radius.png b/src/content/docs/docs/alerts/images/alarm-detail-blast-radius.png
new file mode 100644
index 00000000..91f16985
Binary files /dev/null and b/src/content/docs/docs/alerts/images/alarm-detail-blast-radius.png differ
diff --git a/src/content/docs/docs/alerts/images/alarm-detail-overview.png b/src/content/docs/docs/alerts/images/alarm-detail-overview.png
index 0c0e89d3..c368c4cb 100644
Binary files a/src/content/docs/docs/alerts/images/alarm-detail-overview.png and b/src/content/docs/docs/alerts/images/alarm-detail-overview.png differ
diff --git a/src/content/docs/docs/alerts/images/alert-group-audit-reminder.png b/src/content/docs/docs/alerts/images/alert-group-audit-reminder.png
new file mode 100644
index 00000000..c87f21f5
Binary files /dev/null and b/src/content/docs/docs/alerts/images/alert-group-audit-reminder.png differ
diff --git a/src/content/docs/docs/alerts/images/alert-group-detail.png b/src/content/docs/docs/alerts/images/alert-group-detail.png
index 2870afac..50f2ce68 100644
Binary files a/src/content/docs/docs/alerts/images/alert-group-detail.png and b/src/content/docs/docs/alerts/images/alert-group-detail.png differ
diff --git a/src/content/docs/docs/alerts/images/alert-groups-list.png b/src/content/docs/docs/alerts/images/alert-groups-list.png
index 11c53983..554a20d6 100644
Binary files a/src/content/docs/docs/alerts/images/alert-groups-list.png and b/src/content/docs/docs/alerts/images/alert-groups-list.png differ
diff --git a/src/content/docs/docs/alerts/images/alerts-folder-view.png b/src/content/docs/docs/alerts/images/alerts-folder-view.png
index 06f85d3b..5a7ff6bd 100644
Binary files a/src/content/docs/docs/alerts/images/alerts-folder-view.png and b/src/content/docs/docs/alerts/images/alerts-folder-view.png differ
diff --git a/src/content/docs/docs/alerts/images/alerts-history-tab.png b/src/content/docs/docs/alerts/images/alerts-history-tab.png
new file mode 100644
index 00000000..860e7623
Binary files /dev/null and b/src/content/docs/docs/alerts/images/alerts-history-tab.png differ
diff --git a/src/content/docs/docs/alerts/images/annotation-template-editor.png b/src/content/docs/docs/alerts/images/annotation-template-editor.png
new file mode 100644
index 00000000..56509ed1
Binary files /dev/null and b/src/content/docs/docs/alerts/images/annotation-template-editor.png differ
diff --git a/src/content/docs/docs/alerts/images/create-alarm-dialog.png b/src/content/docs/docs/alerts/images/create-alarm-dialog.png
index 781082a2..ffdcc192 100644
Binary files a/src/content/docs/docs/alerts/images/create-alarm-dialog.png and b/src/content/docs/docs/alerts/images/create-alarm-dialog.png differ
diff --git a/src/content/docs/docs/alerts/images/custom-notification-title.png b/src/content/docs/docs/alerts/images/custom-notification-title.png
new file mode 100644
index 00000000..163dd70a
Binary files /dev/null and b/src/content/docs/docs/alerts/images/custom-notification-title.png differ
diff --git a/src/content/docs/docs/alerts/images/custom-title-notification.png b/src/content/docs/docs/alerts/images/custom-title-notification.png
new file mode 100644
index 00000000..b550de9d
Binary files /dev/null and b/src/content/docs/docs/alerts/images/custom-title-notification.png differ
diff --git a/src/content/docs/docs/alerts/images/folder-edit-dialog.png b/src/content/docs/docs/alerts/images/folder-edit-dialog.png
new file mode 100644
index 00000000..889baa7c
Binary files /dev/null and b/src/content/docs/docs/alerts/images/folder-edit-dialog.png differ
diff --git a/src/content/docs/docs/alerts/images/from-template-dropdown.png b/src/content/docs/docs/alerts/images/from-template-dropdown.png
index e7a0fe1b..662c6c48 100644
Binary files a/src/content/docs/docs/alerts/images/from-template-dropdown.png and b/src/content/docs/docs/alerts/images/from-template-dropdown.png differ
diff --git a/src/content/docs/docs/alerts/images/instances-tab-dismiss.png b/src/content/docs/docs/alerts/images/instances-tab-dismiss.png
new file mode 100644
index 00000000..4333b203
Binary files /dev/null and b/src/content/docs/docs/alerts/images/instances-tab-dismiss.png differ
diff --git a/src/content/docs/docs/alerts/images/maintenance-window-1.png b/src/content/docs/docs/alerts/images/maintenance-window-1.png
index 997fd5c0..0005bc68 100644
Binary files a/src/content/docs/docs/alerts/images/maintenance-window-1.png and b/src/content/docs/docs/alerts/images/maintenance-window-1.png differ
diff --git a/src/content/docs/docs/alerts/images/maintenance-window-2.png b/src/content/docs/docs/alerts/images/maintenance-window-2.png
index b33aa5f7..0e765daf 100644
Binary files a/src/content/docs/docs/alerts/images/maintenance-window-2.png and b/src/content/docs/docs/alerts/images/maintenance-window-2.png differ
diff --git a/src/content/docs/docs/alerts/images/maintenance-window-3.png b/src/content/docs/docs/alerts/images/maintenance-window-3.png
index 34d73982..d98c1239 100644
Binary files a/src/content/docs/docs/alerts/images/maintenance-window-3.png and b/src/content/docs/docs/alerts/images/maintenance-window-3.png differ
diff --git a/src/content/docs/docs/alerts/images/move-to-folder-dialog.png b/src/content/docs/docs/alerts/images/move-to-folder-dialog.png
new file mode 100644
index 00000000..5133508d
Binary files /dev/null and b/src/content/docs/docs/alerts/images/move-to-folder-dialog.png differ
diff --git a/src/content/docs/docs/alerts/images/routing-rules-alert-grouping.png b/src/content/docs/docs/alerts/images/routing-rules-alert-grouping.png
index c1cab2db..3ad18e0e 100644
Binary files a/src/content/docs/docs/alerts/images/routing-rules-alert-grouping.png and b/src/content/docs/docs/alerts/images/routing-rules-alert-grouping.png differ
diff --git a/src/content/docs/docs/alerts/images/routing-rules-list.png b/src/content/docs/docs/alerts/images/routing-rules-list.png
index 5acb219d..63012571 100644
Binary files a/src/content/docs/docs/alerts/images/routing-rules-list.png and b/src/content/docs/docs/alerts/images/routing-rules-list.png differ
diff --git a/src/content/docs/docs/alerts/images/routing-rules-suggest.png b/src/content/docs/docs/alerts/images/routing-rules-suggest.png
index 0df4448a..55310dd3 100644
Binary files a/src/content/docs/docs/alerts/images/routing-rules-suggest.png and b/src/content/docs/docs/alerts/images/routing-rules-suggest.png differ
diff --git a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.jpeg b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.jpeg
index e3b77572..98e7fc68 100644
Binary files a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.jpeg and b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.jpeg differ
diff --git a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.png b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.png
index f7d3b5b2..2d5eb6e1 100644
Binary files a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.png and b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-1.png differ
diff --git a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-2.png b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-2.png
index 7fcb331c..ced7a9be 100644
Binary files a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-2.png and b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-2.png differ
diff --git a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-4.png b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-4.png
index 6cb0f520..34d29a22 100644
Binary files a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-4.png and b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-4.png differ
diff --git a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-5.png b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-5.png
index 14350afa..cf6b452a 100644
Binary files a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-5.png and b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-5.png differ
diff --git a/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-6.png b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-6.png
new file mode 100644
index 00000000..61d09568
Binary files /dev/null and b/src/content/docs/docs/alerts/images/setting-up-kloudmate-alarms-6.png differ
diff --git a/src/content/docs/docs/alerts/images/silence-from-group.png b/src/content/docs/docs/alerts/images/silence-from-group.png
new file mode 100644
index 00000000..fc8f1d17
Binary files /dev/null and b/src/content/docs/docs/alerts/images/silence-from-group.png differ
diff --git a/src/content/docs/docs/alerts/images/silenced-by-rule.png b/src/content/docs/docs/alerts/images/silenced-by-rule.png
new file mode 100644
index 00000000..6407ce56
Binary files /dev/null and b/src/content/docs/docs/alerts/images/silenced-by-rule.png differ
diff --git a/src/content/docs/docs/alerts/images/silences-list.png b/src/content/docs/docs/alerts/images/silences-list.png
index de1b533c..4e13dfae 100644
Binary files a/src/content/docs/docs/alerts/images/silences-list.png and b/src/content/docs/docs/alerts/images/silences-list.png differ
diff --git a/src/content/docs/docs/alerts/images/simple-mode-editor.png b/src/content/docs/docs/alerts/images/simple-mode-editor.png
new file mode 100644
index 00000000..91f442f2
Binary files /dev/null and b/src/content/docs/docs/alerts/images/simple-mode-editor.png differ
diff --git a/src/content/docs/docs/alerts/images/smart-alerts-detectors.png b/src/content/docs/docs/alerts/images/smart-alerts-detectors.png
index bdff3f82..5adb7d1b 100644
Binary files a/src/content/docs/docs/alerts/images/smart-alerts-detectors.png and b/src/content/docs/docs/alerts/images/smart-alerts-detectors.png differ
diff --git a/src/content/docs/docs/alerts/images/smart-alerts-settings.png b/src/content/docs/docs/alerts/images/smart-alerts-settings.png
index 467ebe7f..5a667c0b 100644
Binary files a/src/content/docs/docs/alerts/images/smart-alerts-settings.png and b/src/content/docs/docs/alerts/images/smart-alerts-settings.png differ
diff --git a/src/content/docs/docs/alerts/images/using-ai-builder-draft.png b/src/content/docs/docs/alerts/images/using-ai-builder-draft.png
new file mode 100644
index 00000000..5f89a62a
Binary files /dev/null and b/src/content/docs/docs/alerts/images/using-ai-builder-draft.png differ
diff --git a/src/content/docs/docs/alerts/images/using-ai-prompt.png b/src/content/docs/docs/alerts/images/using-ai-prompt.png
index 63b58162..4b217dd4 100644
Binary files a/src/content/docs/docs/alerts/images/using-ai-prompt.png and b/src/content/docs/docs/alerts/images/using-ai-prompt.png differ
diff --git a/src/content/docs/docs/alerts/index.mdx b/src/content/docs/docs/alerts/index.mdx
index 510f495e..d0c9bfab 100644
--- a/src/content/docs/docs/alerts/index.mdx
+++ b/src/content/docs/docs/alerts/index.mdx
@@ -12,6 +12,10 @@ Alerts tell you when something needs attention: an error-rate spike, a service g
This page covers the concepts behind the Alerts module. The rest of this section covers each feature in detail.
+## Navigate the Alerts module
+
+**Alerts** opens on the **Groups** tab, where related alerts are collected into [alert groups](./alert-groups/). The **Rules** tab lists your alert rules and their [folders](./folders/), and **History** records every alert state change in the workspace. The other tabs control how alerting behaves: [Routing rules](./routing-rules/) decide where notifications go, [Silences](./silences/) and [Maintenance](./maintenance-windows/) windows suppress notifications for a set period, and [Smart Alerts](./smart-alerts/) creates and maintains rules for common infrastructure.
+
## Key concepts
### Alert rule
@@ -28,15 +32,15 @@ Expressions transform query output with math, reductions, or conditions. They re
### Alert labels
-Labels are key-value pairs attached to each firing alert. They come from query dimensions and any folder the rule lives in, and they're what [Routing Rules](./routing-rules/) match against to decide where notifications go. KloudMate generates labels automatically. You don't enter them by hand.
+Labels are key-value pairs attached to each firing alert. They come from query dimensions, the labels you add to the rule with **Add label**, and any folder the rule lives in. [Routing Rules](./routing-rules/) match against them to decide where notifications go.
### Annotations and severity
-Annotations carry human-readable context (summary, runbook URL, dashboard link) into each notification. Values support Liquid templates, so you can interpolate live label and state data into the message. The reserved `severity` annotation key drives downstream routing and incident severity. See [Annotations & Severity](./annotations-and-severity/).
+Annotations carry human-readable context (title, summary, runbook URL, dashboard link) into each notification. Values support Liquid templates, so you can interpolate live label and state data into the message. The reserved `severity` annotation key drives downstream routing and incident severity. See [Annotations & Severity](./annotations-and-severity/).
### Folders
-Folders group related alert rules and let member rules inherit shared defaults: evaluation interval, no-data state, and eval-error state. See [Folders](./folders/).
+Folders group related alert rules and let member rules inherit shared defaults: evaluation interval, no-data state, eval-error state, whether to alert when an instance stops reporting, and how long a silent instance stays tracked. See [Folders](./folders/).
### Alert groups
@@ -65,7 +69,7 @@ Instead of hand-authoring thresholds, turn on curated detectors and let KloudMat
3. The condition is evaluated; if it holds for the pending duration, the rule fires.
4. The firing alert is tagged with labels and annotations and sent into the grouping engine.
5. The grouping engine matches the alert against [Routing Rules](./routing-rules/), opens or appends to an [Alert Group](./alert-groups/), and dispatches notifications to the rule's destination channels.
-6. Active [Silences](./silences/) and [Maintenance Windows](./maintenance-windows/) can suppress the outbound notification at this step. The rule keeps evaluating in the background and the state change still lands in history.
+6. Active [Silences](./silences/) and [Maintenance Windows](./maintenance-windows/) can suppress the outbound notification at this step. The rule keeps evaluating in the background, and the state change is still recorded on the **History** tab.
## Alert states
@@ -78,6 +82,12 @@ Instead of hand-authoring thresholds, turn on curated detectors and let KloudMat
For how these states transition, and how the pending duration, recovery period, and no-data or error settings shape them, see [Alert Lifecycle & States](./alert-lifecycle/).
+## Review alert history
+
+The **History** tab lists every alert state change in the workspace, newest first, with the reason for each change and the query and expression **Values** at that moment. The total number of transitions is shown above the list. Filter by state or search by alert name to narrow the list. For example, select **Firing** and search for `checkout` to see each time a checkout alert moved to **Firing**. Select an alert's name to open its rule.
+
+
+
## Next steps
diff --git a/src/content/docs/docs/alerts/instance-absence-detection.mdx b/src/content/docs/docs/alerts/instance-absence-detection.mdx
index d458d68e..9d679558 100644
--- a/src/content/docs/docs/alerts/instance-absence-detection.mdx
+++ b/src/content/docs/docs/alerts/instance-absence-detection.mdx
@@ -12,11 +12,11 @@ A threshold alert only evaluates the datapoints that arrive. When the datapoints
An **instance** is a unique series identified by its distinct labels. A heartbeat metric grouped by `host_name` produces one instance per host; group by `serviceName` and `pod_name` and you get one per pod. Absence detection tracks each instance individually: it has its own state, its own history, and its own dismissal.
-The setting is off by default and opt-in per rule, so existing rules don't change until you enable it. It works with any datasource, whether the data comes from the KloudMate agent or not.
+The setting is off by default and opt-in, per rule or per folder, so existing rules don't change until you enable it. It works with any datasource, whether the data comes from the KloudMate agent or not.
## How it works
-On every evaluation, KloudMate compares the query results against the set of instances the rule has seen. An instance missing from the results is evaluated as if the query had returned no data for it, and it moves through the normal [lifecycle](../alert-lifecycle/): **Pending** for the pending duration, then **Firing**, with the reason `instance stopped reporting; last seen `. The moment the instance reports again, it recovers through the usual path, including any recovery period.
+On every evaluation, KloudMate compares the query results against the set of instances the rule has seen. An instance missing from the results is evaluated as if the query had returned no data for it, and it moves through the normal [lifecycle](../alert-lifecycle/): **Pending** for the pending duration, then **Firing**. From **Pending** onward, its reason is `instance stopped reporting; last seen `. The moment the instance reports again, it recovers through the usual path, including any recovery period.
Absence detection changes what fires, not how notifications are threaded. Threading comes from the [routing rule](../routing-rules/) that matches, through its **Group-by** keys, exactly as it does for threshold alerts:
@@ -27,14 +27,16 @@ Routing itself works as usual, since each instance carries its own labels.
## Turn it on
-1. Create or edit an alert rule and go to **Configure evaluation settings**.
+1. Create or edit an alert rule and go to **Configure evaluation settings** (**Evaluation settings** in Simple mode).
2. Turn on **Alert when an instance stops reporting**.
3. Optionally set **Auto-close after**: how long a silent instance stays tracked, and firing, before it closes on its own, which happens only while other instances are still reporting. Leave it blank to inherit the folder default, or the system default of 24 hours. It accepts durations from `5m` to `72h`, for example `30m` or `48h`.
4. Save the rule.
-Turning the toggle on also sets **Alert state if No data** to **Firing**, since absence detection needs both. Turning the toggle off leaves the No-data state alone, in case you still want the rule to fire when the whole query returns nothing.
+
-Both settings inherit from [folders](../folders/) exactly like the other evaluation defaults: the rule's own value wins, a blank field falls back to the folder default, and the system default applies last. Folders expose the same two settings, **Instance stops reporting** (**Alert** / **Don't alert**) and **Auto-close after**.
+Absence detection runs only while **Alert state if No data** is **Firing**, so turning the toggle on sets that field to **Firing** as well. If you later change **Alert state if No data** to another state, absence detection stops. Turning the toggle off leaves the No-data state alone, in case you still want the rule to fire when the whole query returns nothing.
+
+Both settings inherit from [folders](../folders/): the rule's own value wins, a blank field falls back to the folder default, and the system default applies last. Folders expose the same two settings, **Instance stops reporting** (**Alert** / **Don't alert**) and **Auto-close after**. A folder set to **Alert** also needs **No data state** set to **Alerting**, either on the folder or on each rule. A new rule starts with **Alert state if No data** set to **No Data**, which overrides the folder, so click **Reset to folder default** on that field to use the folder's setting.
## Make the query window longer than the reporting interval
@@ -58,8 +60,8 @@ Say every host emits a heartbeat metric once a minute, and you want to know when
Here's what happens when `web-7` drops off the network at 10:03, right after its last heartbeat:
- Until about 10:08, the 5-minute window still contains datapoints from `web-7`, so it counts as reporting.
-- At 10:09 the window is empty for `web-7`, and the instance enters **Pending**.
-- At 10:11, after the 2-minute pending duration, `web-7` starts **Firing** with the reason `instance stopped reporting; last seen 2026-08-03T10:08:12Z`. With a routing rule that doesn't group, the rule's notification thread lists `host_name=web-7`.
+- At 10:09 the window is empty for `web-7`, and the instance enters **Pending** with the reason `instance stopped reporting; last seen 2026-08-03T10:08:12Z`.
+- At 10:11, after the 2-minute pending duration, `web-7` starts **Firing**. With a routing rule that doesn't group, the rule's notification thread lists `host_name=web-7`.
- If `web-9` goes quiet at 10:20, it fires the same way. It joins the same thread under that ungrouped rule, or opens its own under a rule grouped by `host_name`.
- When `web-7` comes back, its instance resolves automatically. Nothing to clean up.
@@ -67,13 +69,15 @@ That's about eight minutes after the last datapoint: the 5-minute window plus th
## The Instances tab
-The **Instances** tab lists every tracked instance and when each was **Last seen**. Last seen records when the instance last appeared in query results, so with a lookback window it can read up to one window later than the instance's final datapoint. While an instance is firing for absence, its reason is `instance stopped reporting; last seen `.
+The **Instances** tab lists every tracked instance and when each was **Last seen**. Last seen records when the instance last appeared in query results, so with a lookback window it can read up to one window later than the instance's final datapoint. While an instance is pending or firing for absence, its reason is `instance stopped reporting; last seen `.
+
+
### Dismiss an instance that was terminated on purpose
-Autoscalers scale in and hosts get decommissioned. When an instance went away on purpose, close its alert instead of waiting out the auto-close window. Admins can **Dismiss** any instance that stopped reporting.
+Autoscalers scale in and hosts get decommissioned. When an instance went away on purpose, close its alert instead of waiting out the auto-close window. With the **Developer** role, you can **Dismiss** any instance that stopped reporting, including one that's still **Pending**.
-Dismissal forgets the instance; it isn't a mute. If the instance ever reports again, tracking resumes automatically, so a dismissal can't permanently hide a live host. The instance's alert closes right away, and the alert group and any linked ticket close within one evaluation interval. Dismissing an instance that's already been dismissed changes nothing.
+Dismissal forgets the instance; it isn't a mute. If the instance ever reports again, tracking resumes automatically, so a dismissal can't permanently hide an instance that comes back. Dismiss removes the instance right away, and the instance leaves its alert group within one evaluation interval. The group and any linked ticket resolve only if nothing else in the group is still firing. Dismissing an instance that's already been dismissed changes nothing.
If the instance is coming back later, after planned maintenance or a reboot, use a [silence](../silences/) or [maintenance window](../maintenance-windows/) instead. Both apply to absence alerts like any other: the instance keeps its state, notifications are withheld, and it recovers on its own when it reports again.
@@ -89,9 +93,9 @@ Every transition lands in the alert's **History** tab with a reason:
| Reason | What happened |
|---|---|
-| `instance stopped reporting; last seen ` | The instance went silent and started firing. |
+| `instance stopped reporting; last seen ` | The instance went silent and moved to **Pending** or **Firing**. |
| `auto-closed: instance silent past absence retention` | The instance stayed silent past the **Auto-close after** window while other instances kept reporting, and closed on its own. |
-| `dismissed: instance deregistered by user` | An admin dismissed the instance. |
+| `dismissed: instance deregistered by user` | Someone dismissed the instance. |
## Other behaviors you might notice
diff --git a/src/content/docs/docs/alerts/maintenance-windows.mdx b/src/content/docs/docs/alerts/maintenance-windows.mdx
index 2ddd30af..a0967dba 100644
--- a/src/content/docs/docs/alerts/maintenance-windows.mdx
+++ b/src/content/docs/docs/alerts/maintenance-windows.mdx
@@ -24,33 +24,34 @@ Each maintenance window shows:
- **Alert rule targeted:** the alert rule the maintenance window applies to.
- **Matching labels:** labels assigned to the maintenance window.
- **Schedule:** start and end time for the maintenance window.
-- **Status:** whether the maintenance window is active or paused.
+- **Status:** **Active** when the window is enabled, or **Disabled**.
-
+
+
+With the **Developer** role, you can **Disable** or **Enable**, **Edit**, or **Delete** a window from its row menu.
## Creating a maintenance window
-1. Open the **Maintenance Windows** page in the Alerts module.
-2. Click **Create Maintenance Window**.
+1. Open **Alerts → Maintenance**.
+2. Click **Create window**. You need the **Developer** role to create maintenance windows.
3. Configure these details:
- **Name:** a name for the maintenance window.
- **Description:** a short description of the maintenance window.
- **Timezone:** the timezone for the scheduled window.
-- **One time:** schedules the maintenance period once, for a specific date and time.
+- **One Time:** schedules the maintenance period once, for a specific date and time.
- **Recurring:** repeats automatically on a regular cycle, so you don't reconfigure it each time.
-
+
Additional settings:
-- **Start time:** date and time when the maintenance window begins.
-- **End time:** date and time when the maintenance window ends.
-- **Frequency:** available only for recurring windows; select a daily, weekly, or monthly schedule.
-- **Label Matchers:** target the window to specific alerts by their labels (for example, service name, host, or region). Each matcher is a label key, an operator, and a value, combined with AND. The operators are the same four used across the alert module (**Equals**, **Not equals**, **Matches regex**, **Doesn't match regex**), matched against alert rule labels and alert instance labels.
+- For a **One Time** window, pick the **From** and **To** date and time, or type a **Duration** such as `2h` and press Enter to start the window now and run it for that long.
+- For a **Recurring** window, set a **Start Date**, an optional **End Date**, a **Start Time**, an **End Time**, and a **Frequency** of **Daily**, **Weekly**, or **Monthly**. **Weekly** adds **Days of Week**, and **Monthly** lets you choose by **Day of Month** or **Day of Week**. The **End Time** must be later than the **Start Time**, so a recurring window can't run past midnight.
+- **Label Matchers:** target the window to specific alerts by their labels (for example, service name, host, or region). Each matcher is a label key, an operator, and a value, combined with AND. The operators are the same four used across the alert module (**Equals**, **Not equals**, **Matches regex**, **Doesn't match regex**), matched against alert rule labels and alert instance labels. A window needs at least one matcher unless it targets specific alert rules.
As you add matchers, an **Affected Alert Instances** preview lists the alert rules and instances the window currently matches, so you can confirm its scope before saving.
4. Click **Save** to create the new maintenance window.
-
+
diff --git a/src/content/docs/docs/alerts/routing-rules.mdx b/src/content/docs/docs/alerts/routing-rules.mdx
index da4a2d79..21d0bd55 100644
--- a/src/content/docs/docs/alerts/routing-rules.mdx
+++ b/src/content/docs/docs/alerts/routing-rules.mdx
@@ -29,12 +29,12 @@ A routing rule has these fields:
| Field | What it does |
|---|---|
-| **Name** | How the rule appears in the list and audit log. |
+| **Rule name** | How the rule appears in the list and audit log. |
| **Priority** | Lower numbers win. When an alert matches multiple rules, the highest-priority (lowest-numbered) rule routes it. |
| **Matchers** | Label conditions that pick which alerts this rule applies to. |
-| **Group-by** | In **Static** grouping, whether and how matched alerts correlate. Leave it empty (or set only `alarm_id`) for **one notification per alert rule**, with no grouping. Add a real label key (for example `service`) to **correlate** matching alerts that share that value into one [Alert Group](../alert-groups/). `severity` can't be used here. This field is ignored when the rule uses **Auto (AI)** grouping. |
+| **Group by** | In **Static** grouping, whether and how matched alerts correlate. Leave it empty (or set only `alarm_id`) for **one notification per alert rule**, with no grouping. Add a real label key (for example `service`) to **correlate** matching alerts that share that value into one [Alert Group](../alert-groups/). `severity` can't be used here. This field is ignored when the rule uses **Auto (AI)** grouping. |
| **Notification timing** | When the first notification goes out (**Group wait**), the minimum gap between notifications when the group changes (**Group interval**), and how often to re-send while the group stays open and unchanged (**Repeat interval**). |
-| **Destination channels** | One or more notification channels that receive the dispatched payload. |
+| **Destinations** | One or more notification channels that receive the dispatched payload. |
Optional:
@@ -55,27 +55,29 @@ The available operators:
A matcher value is always a single string. To match any of several values, use a regex alternation. For example, `service` **Matches regex** `api|web` matches either service. (There's no `in`/`not_in` operator; the regex form replaces it.)
-As you add or edit matchers, the editor shows a live **Affected Alert Instances** preview: the alert instances this rule currently matches, with a running count, so you can confirm the rule's scope before saving.
+In **Static** mode, the editor shows a live **Alerts this rule matches** preview as you add or edit matchers: the alert instances this rule currently matches, with a running count, so you can confirm the rule's scope before saving. A higher-priority rule may handle some of those alerts first.
+
+Above the preview, **What this rule does** describes the rule in plain language: which alerts it handles, how it groups them, when it notifies and reminds, and when Auto-RCA starts. Read it before saving to check that the rule does what you intended.
### Alert grouping: Static or Auto (AI)
-Each rule has an **Alert grouping** control, and it defaults to **Static**.
+Each rule has an **Alert grouping** control. New rules start in **Auto (AI)**, and a saved rule keeps the mode it was saved with.
- **Static** groups matching alerts by the label keys you choose, the deterministic behavior described under [Group-by keys](#group-by-keys) below. Alerts that share those key values fold into one group.
- **Auto (AI)** hands grouping to the correlation engine. It correlates related alerts from this rule into a single incident on its own, and learns which alerts tend to fire together over time. In this mode you don't choose grouping keys at all.
-Leave a rule on **Static** when you want predictable grouping along keys you control. Switch it to **Auto (AI)** when related alerts don't share a clean label to group on, or when you'd rather let the engine find the connections. Either mode produces the same [Alert Group](../alert-groups/).
+Switch a rule to **Static** when you want predictable grouping along keys you control. Keep it on **Auto (AI)** when related alerts don't share a clean label to group on, or when you'd rather let the engine find the connections. Either mode produces the same [Alert Group](../alert-groups/).
For how each mode decides what belongs together, including why one rule firing on many hosts stays a single Auto group, see [How Alert Grouping Works](../how-alert-grouping-works/).
-
+
### Group-by keys
Group-by keys apply in **Static** mode. They decide **whether** matched alerts correlate and, if so, along **which** dimension:
- **Leave it empty, or set only `alarm_id`,** and the rule doesn't correlate anything: each alert rule notifies on its own, one notification per rule. These notifications surface through the normal alerts UI, not the [Alert Groups](../alert-groups/) page.
-- **Add a real label key** and matching alerts that share that key's value fold into a single alert group. The editor suggests common keys (`service`, `env`, `host`, `region`, `team`, `issue_id`, `check_id`, `alarm_rule_folder_id`), and you can type any other label key.
+- **Add a real label key** and matching alerts that share that key's value fold into a single alert group. The picker offers the label keys seen on your recent alerts, plus **Alert folder**, which groups alerts from rules in the same folder. You can also type any other label key.
The editor enforces these constraints:
@@ -102,6 +104,8 @@ Set a **Repeat interval** and the rule re-sends the notification at that cadence
Reminders stop on their own when the group resolves. A [silenced](../silences/) group doesn't get them, and it doesn't build up a backlog to deliver once the silence ends. A group that opened while it was already silenced was never announced, so it gets no reminders at all.
+Reminders count only firing instances that aren't muted, so a [maintenance window](../maintenance-windows/) that mutes all of a group's firing instances stops them too. Turning off the rule's **Enabled** toggle stops them as well. There's no acknowledge step for a group, so to stop reminders on a group that's still firing, silence it. Any other notification the group sends, such as one for a newly firing instance, restarts the reminder clock.
+
Slack, email, Microsoft Teams, [webhook](../../platform/settings/notification-channels/#webhooks), and [SNS](../../platform/settings/notification-channels/#sns) deliver reminders. [Jira](../../platform/settings/notification-channels/#jira) and [KloudMate Incidents](../../platform/settings/notification-channels/#kloudmate-incidents) don't. The ticket or the incident is already a standing record that the alert is open, so a comment on it every few hours would only add noise. A rule that routes only to those two sends nothing extra when a reminder is due.
In Slack, the reminder posts as a reply in the alert's existing thread and is broadcast to the channel, so you see it in the channel instead of buried under an hours-old parent message.
@@ -112,7 +116,7 @@ A reminder is delivered with `"event": "repeat"` in the payload. Everything else
## The default passthrough rule
-Every workspace has a default passthrough rule. It matches everything that didn't match a higher-priority rule. You can edit its destination channels and notification timing, but you can't delete it or change its matchers.
+Every workspace has a default passthrough rule. It matches everything that didn't match a higher-priority rule. Its matchers and priority are locked, and it can't be deleted. You can edit everything else, including its grouping, notification timing, and destinations.
For a clean "everything else goes to Slack" path, point the default passthrough at your fallback Slack channel.
@@ -125,15 +129,15 @@ For a clean "everything else goes to Slack" path, point the default passthrough
3. **Add matchers.** Pick a label key, an operator, and a value. Add as many rows as needed; all matchers combine with AND.
-4. **Choose how alerts group.** Leave **Alert grouping** on **Static** and set group-by keys: empty to notify once per alert rule, or a real label key (start with `service`) to correlate matching alerts into one group (`severity` isn't allowed here). Or switch to **Auto (AI)** to let the correlation engine group related alerts for you, with no keys to set.
+4. **Choose how alerts group.** New rules start in **Auto (AI)**, which lets the correlation engine group related alerts for you, with no keys to set. To group by label keys instead, switch **Alert grouping** to **Static**, then set the **Group by** keys: leave them empty to notify once per alert rule, or add a real label key (start with `service`) to correlate matching alerts into one group (`severity` isn't allowed here). To get keys based on your alert history, click **Suggest keys**. See [Get suggested group-by keys](#get-suggested-group-by-keys).
5. **Set the notification timing.** Leave the defaults (`30s` group wait, `5m` group interval) for most cases. Tighten them for time-sensitive notifications, or loosen them for digest-style summaries. Set a **Repeat interval** such as `1h` to be reminded while the alert stays open, or leave it at `0` for no reminders.
-6. **Pick destination channels.** Add one or more channels. If you don't have a [KloudMate Incidents](../../platform/settings/notification-channels/#kloudmate-incidents) channel yet, use the inline **+ Add KloudMate Incidents channel** link in the picker.
+6. **Pick destination channels.** Add one or more channels under **Destinations**. If the workspace doesn't have a [KloudMate Incidents](../../platform/settings/notification-channels/#kloudmate-incidents) channel yet, click **Add a KloudMate Incidents channel** to create one. That link leaves the rule editor, and anything you haven't saved is lost.
-7. **Optionally enable Auto-RCA.** Toggle Auto-RCA on and set the delay (default 5 minutes after group open). See [Auto-RCA](../auto-rca/).
+7. **Optionally enable Auto-RCA.** Turn on **Enable Auto-RCA** and set the **Delay** (default 5 minutes after the group opens). See [Auto-RCA](../auto-rca/).
-8. **Save.**
+8. **Save the rule.** Click **Create rule**, or **Save rule** when you're editing an existing rule.
## Row actions
@@ -146,22 +150,22 @@ The kebab menu on each row offers:
The **Enabled** toggle disables a rule without deleting it. Use it to stage a rule before it goes live.
-## Suggest rules
+## Get suggested group-by keys
-Admins can open **Suggest rules** to get up to five AI-suggested rules based on your workspace's recent alert traffic.
+In **Static** mode, click **Suggest keys** in the rule editor's **Group by** section. KloudMate looks at which alerts fired together in your workspace over the **Lookback** window and proposes up to five groupings. The lookback is 7, 14, or 30 days, and it starts at 14.
-
+
Each suggestion shows:
-- A title describing the proposed grouping (for example, "Group by service + env").
-- A short rationale: how much notification noise the rule would have collapsed over the lookback window.
-- A **preview matches** count for that window.
-- **Accept** opens the create-rule form pre-filled with the suggestion's matchers, group-by keys, and notification timing, under a name derived from what the rule targets. **Dismiss** drops the card.
+- A title naming the proposed keys (for example, "Group by service + env").
+- A short rationale: how many alert instances it would have collapsed into how few groups.
+- The proposed **Group by** keys.
+- How many instances it would have matched over the lookback window.
-Some suggestions are grouping-only: they propose group-by keys with no matchers. Accepting one starts the rule as a catch-all that groups everything by those keys. Add matchers before saving to scope it to a subset of alerts.
+**Accept** fills in the rule you're editing with the suggested group-by keys and notification timing. It replaces your matchers only when the suggestion has matchers of its own, and it names the rule only if the name is still blank. **Dismiss** hides the suggestion.
-Suggestions don't persist; closing the drawer discards them. Re-run the suggester whenever you want fresh proposals.
+Suggestions aren't saved. Closing the drawer discards them, and reopening it runs the analysis again.
## Related
@@ -169,3 +173,4 @@ Suggestions don't persist; closing the drawer discards them. Re-run the suggeste
- [Silences](../silences/): temporarily suppress matching alerts.
- [Notification Channels](../../platform/settings/notification-channels/): set up Slack, KloudMate Incidents, email, and more.
- [Auto-RCA](../auto-rca/): automatic investigations on group open.
+- [Workflows](../../workflows/): run an automated response when an alert fires. Add a webhook channel that points at the workflow's URL to the rule. See [Start a workflow from an alert](../../workflows/triggers/#start-a-workflow-from-an-alert).
diff --git a/src/content/docs/docs/alerts/silences.mdx b/src/content/docs/docs/alerts/silences.mdx
index 7af12fa3..a745f0ba 100644
--- a/src/content/docs/docs/alerts/silences.mdx
+++ b/src/content/docs/docs/alerts/silences.mdx
@@ -15,7 +15,7 @@ Open **Alerts → Silences**. Silences are ad-hoc; for recurring quiet windows,

-Silences are grouped into **Active**, **Expired**, and **All** tabs.
+Silences are grouped into **Active**, **Expired**, and **All** tabs. The **Created by** column shows who created each silence.
## Anatomy of a silence
@@ -36,12 +36,14 @@ Create a silence from the Silences page, an alert group, or an alert's Pause Not
2. Add one or more matcher rows. For each, pick a label key, an operator, and a value.
3. Pick an **Expires at** date and time, up to 30 days out.
4. Add a **Reason** describing why this silence exists.
-5. Click **Save**.
+5. Click **Create silence**.
### From an Alert Group
On any [Alert Group](../alert-groups/) detail page, click **Silence this group**. The silence creator opens with matchers derived from the group and a binding to it.
+
+
:::caution[A group binding sets when the silence ends, not what it mutes]
The binding does one thing: when the bound group resolves, the silence's expiry moves to that moment and it lifts early. Matching never looks at it, so the silence mutes every alert in the workspace whose labels match its matchers, group member or not.
:::
@@ -67,14 +69,18 @@ On an alert rule's **more options (⋯) → Pause Notifications**, KloudMate cre
The Silences list carries an **Alerts muted** column: how many alert rules each active silence is muting. Expand a row to list those rules, each marked with a chip such as `3 firing` when its instances are firing. The silence detail page shows the same list, along with a link to the group the silence is bound to.
-In the other direction, the **Silenced** badge links to the silence or maintenance window muting that alert. When more than one is muting it, the badge opens a menu listing all of them.
+To find what's muting a particular alert, check the alert's row on the **Rules** tab, its expanded instance rows, or its detail page. Each links to the silence or maintenance window that's muting the alert. When more than one applies, you can pick which one to open. A group's **Instances** panel marks muted instances but doesn't name the silence.
+
+
+
+A new silence shows up in the **Alerts muted** count and on the alerts it mutes after those alerts are next evaluated. An expired silence clears from them the same way.
## Silence vs. maintenance window
-Both features are a **notification gate, not a state change**: the alert still evaluates on its schedule and its state transitions are still recorded in history; only the notification is withheld. Silencing every firing instance of an alert group keeps the group **Open** (shown as **Muted**); it does **not** resolve the group, and muting never emits a *resolved* notification. The differences between the two are operational, not behavioral:
+Both features are a **notification gate, not a state change**: the alert still evaluates on its schedule and its state transitions are still recorded in history; only the notification is withheld. Silencing every firing instance of an alert group keeps the group **Firing** (marked **Muted**); it does **not** resolve the group, and muting never emits a *resolved* notification. The differences between the two are operational, not behavioral:
- **Silences** are ad-hoc, label-matcher driven, capped at 30 days, and can be auto-bound to a specific alert group so they expire when the group resolves.
-- **Maintenance Windows** are scheduled: one-time (RFC3339 start/end) or recurring (`DAILY` / `WEEKLY` / `MONTHLY` with a timezone-aware time-of-day).
+- **Maintenance Windows** are scheduled: **One Time**, with a start and end, or **Recurring**, repeating **Daily**, **Weekly**, or **Monthly** at a set time of day in the window's timezone.
Both use the same label-matcher operators (**Equals**, **Not equals**, **Matches regex**, **Doesn't match regex**), matched against alert rule labels and alert instance labels.
@@ -86,6 +92,8 @@ Pick a silence for a reactive, one-off suppression you'll re-evaluate within 30
From a row's kebab menu, select **End silence** (or **View**). Ending an active silence removes suppression for matching alerts immediately; new firings notify as normal.
+Silences can't be edited. To change the matchers or extend the expiry, end the silence and create a new one.
+
## Related
- [Alert Groups](../alert-groups/): the Silence-this-group action.
diff --git a/src/content/docs/docs/alerts/smart-alerts.mdx b/src/content/docs/docs/alerts/smart-alerts.mdx
index a28b5ee1..797aa96b 100644
--- a/src/content/docs/docs/alerts/smart-alerts.mdx
+++ b/src/content/docs/docs/alerts/smart-alerts.mdx
@@ -53,10 +53,11 @@ A threshold detector is a plain ceiling for a state that's wrong at any scale. I
Detectors are grouped by pack:
-- **Hosts & VMs:** CPU, memory, and network anomalies; a disk-fill forecast; disk-full and load-saturation thresholds.
+- **Hosts & VMs:** CPU, memory, and network anomalies; a disk-fill forecast; CPU-critically-high, memory-critically-high, disk-full, and load-saturation thresholds.
- **Kubernetes & Containers:** pod CPU and memory anomalies; container memory-and-CPU-limit detectors; node, deployment, statefulset, and pod-health thresholds.
- **Services (RED):** request latency and throughput anomalies.
- **Kafka & Queues:** a consumer-lag anomaly; under-replicated, under-min-ISR, and offline-partition thresholds.
+- **Browser & Mobile (RUM):** detectors for real user monitoring data from browser and mobile apps.

@@ -76,7 +77,7 @@ Only detectors with matching data run. Enable a detector and KloudMate creates a
Set a workspace-wide **default sensitivity** and **default forecast horizon** under **Defaults for new managed alerts**. New detectors inherit these unless you override them per detector.
:::note
-Only workspace admins can change Smart Alerts settings.
+You need the **Developer** role to change Smart Alerts settings.
:::
## What a responder sees
@@ -103,7 +104,7 @@ Changing how often the rule runs keeps it managed too. Edit **Evaluate every**,
Editing the rule's **query**, **threshold**, or **condition** takes it over. The rule detaches from Smart Alerts, is labeled **Customized**, and leaves the **Smart Alerts** folder. From then on it behaves like any alert you wrote by hand: KloudMate stops sending it template updates and no longer manages its lifecycle. Its anomaly or forecast detection keeps running; taking a rule over doesn't turn detection off.
-To quiet a managed rule without taking it over, pause it from its row menu with **Pause Evaluation**. The pause holds: saving another Smart Alert won't resume it. To remove a managed rule for good, turn its detector off on the Smart Alerts page. You can't delete a managed rule directly, since Smart Alerts would recreate it. KloudMate disables it instead and points you to the detector toggle.
+To quiet a managed rule without taking it over, pause it from its row menu with **Pause Evaluation**. The pause holds: saving another Smart Alert won't resume it. To remove a managed rule for good, turn its detector off on the Smart Alerts page. Deleting a managed rule that's still evaluating disables it instead, the same as **Pause Evaluation**, and a disabled managed rule can then be deleted.
## Plan availability
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/api-monitoring.mdx b/src/content/docs/docs/apm-and-tracing/apm-views/api-monitoring.mdx
index c5346d43..29549d7e 100644
--- a/src/content/docs/docs/apm-and-tracing/apm-views/api-monitoring.mdx
+++ b/src/content/docs/docs/apm-and-tracing/apm-views/api-monitoring.mdx
@@ -11,11 +11,11 @@ Where the [Service Map](../service-map/) shows caller-to-callee dependencies and
## Overview
-In the left navigation, open **APM & Tracing → API Monitoring**.
+Open **API Monitoring**.

-Use the **time range** picker in the top-right to set the observation window. The view defaults to the last **24 hours** so the per-endpoint trend sparklines have enough history to draw.
+Use the **time range** picker to set the observation window. The view defaults to the last **24 hours** so the per-endpoint trend sparklines have enough history to draw.
Endpoints are derived from your services' server spans. If the table is empty, confirm your services are instrumented and sending traces, or widen the time range — see [Getting Accurate Data](#getting-accurate-data).
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/apm-and-services.mdx b/src/content/docs/docs/apm-and-tracing/apm-views/apm-and-services.mdx
index 07ed8942..61f76675 100644
--- a/src/content/docs/docs/apm-and-tracing/apm-views/apm-and-services.mdx
+++ b/src/content/docs/docs/apm-and-tracing/apm-views/apm-and-services.mdx
@@ -5,13 +5,13 @@ sidebar:
label: "Services"
order: 2
---
-The **Services** view in APM is where you compare application and service performance at a glance.
+Open **APM & Services** to compare application and service performance at a glance. It opens in the **List** view. Select the **Service map** view to see how your services connect, as described in [Service Map](../service-map/).
## Services Overview
-The Services Overview page lists all instrumented applications and services in a tabular view, making it easy to compare their health across the selected time range.
+The **List** view shows all instrumented applications and services in a table, so you can compare their health across the selected time range.
-
+
Each row represents one service and includes:
@@ -20,18 +20,18 @@ Each row represents one service and includes:
- **Throughput:** request rate, such as requests per minute
- **P99, P95, and P50 latency:** percentile-based latency views for worst-case, upper-percentile, and median performance
- **Error rate:** percentage of failed requests
-- **Version:** reported deployment version for release correlation
+- **Dep. failures:** outbound calls this service made that failed, with their share of all its outbound calls. These failures don't count toward **Error rate**.
## Service APM Dashboard
-Clicking any service in the Services Overview opens its APM dashboard, which is a preconfigured view focused on that specific service.
+Clicking any service in the **List** view opens its APM dashboard, which is a preconfigured view focused on that specific service.
-
+
The dashboard includes:
- **Requests, errors, and latency over time:** timeline charts that help identify traffic spikes, error bursts, and latency regressions
-- **Destinations table:** a breakdown of downstream dependencies, including request rate, error percentage, and dependency latency
+- **Service dependencies:** tables of downstream dependencies, outbound calls, and upstream callers, including request rate, error percentage, and latency
- **Related issues, alerts, and dashboards:** links to broader operational context for that service
## Traces and Logs
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-1.png b/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-1.png
index d0265cd9..33ced11d 100644
Binary files a/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-1.png and b/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-1.png differ
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-2.png b/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-2.png
index 0049e6f5..0a104f03 100644
Binary files a/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-2.png and b/src/content/docs/docs/apm-and-tracing/apm-views/images/apm-and-services-2.png differ
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.jpeg b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.jpeg
index 156d4b82..ee81856e 100644
Binary files a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.jpeg and b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.jpeg differ
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.png b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.png
index 47a9656f..d5df8e37 100644
Binary files a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.png and b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-1.png differ
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.jpeg b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.jpeg
index 367e2503..856da23f 100644
Binary files a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.jpeg and b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.jpeg differ
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.png b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.png
index 285c428c..31287b36 100644
Binary files a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.png and b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-2.png differ
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-3.png b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-3.png
index cc3ab7c1..4eaa8de6 100644
Binary files a/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-3.png and b/src/content/docs/docs/apm-and-tracing/apm-views/images/service-map-3.png differ
diff --git a/src/content/docs/docs/apm-and-tracing/apm-views/service-map.mdx b/src/content/docs/docs/apm-and-tracing/apm-views/service-map.mdx
index a66e350b..f50055fa 100644
--- a/src/content/docs/docs/apm-and-tracing/apm-views/service-map.mdx
+++ b/src/content/docs/docs/apm-and-tracing/apm-views/service-map.mdx
@@ -19,13 +19,13 @@ Each service becomes a node under its `service.name`, and an edge is drawn where
## Overview
-Click **Service Map** in the left navigation to open the map.
+Open **APM & Services** and select the **Service map** view. You can also open the map from a service's page: click **View in service map** in the **Service dependencies** panel.
-
+
-At the top of the page, a summary bar shows the total number of services, dependencies, services with errors, and erroring edges in the selected time range.
+Summary chips count the services, external dependencies, dependencies, services with errors, and erroring edges in the selected time range. A service or dependency counts as erroring when its error rate is 0.05% or higher.
-Use the **time range** selector to adjust the observation window, and click **Refresh** to reload the map with the latest data.
+The **Service map** view uses the same time range as the **List** view. Click **Refresh** to reload the map with the latest data, and turn on **Show external dependencies** to add the uninstrumented dependencies your services call, such as databases, queues, and third-party hosts.
The main area displays the **Service Dependency Graph**. Each service is represented as a node showing its name, protocol, request rate, and average latency. The directed edges between nodes represent the call flow and dependencies between services.
diff --git a/src/content/docs/docs/apm-and-tracing/index.mdx b/src/content/docs/docs/apm-and-tracing/index.mdx
index 7b336332..9b68ff0a 100644
--- a/src/content/docs/docs/apm-and-tracing/index.mdx
+++ b/src/content/docs/docs/apm-and-tracing/index.mdx
@@ -8,7 +8,7 @@ sidebar:

-The `APM & Tracing` section in KloudMate gives you end-to-end visibility into how requests travel across your services, along with service-level health and dependency analysis.
+The **APM & Services**, **API Monitoring**, and **Traces** pages in KloudMate give you end-to-end visibility into how requests travel across your services, along with service-level health and dependency analysis.
## Core Concepts
diff --git a/src/content/docs/docs/aws-integration/images/lambda-functions-2.png b/src/content/docs/docs/aws-integration/images/lambda-functions-2.png
index 3b0170fc..62bf530f 100644
Binary files a/src/content/docs/docs/aws-integration/images/lambda-functions-2.png and b/src/content/docs/docs/aws-integration/images/lambda-functions-2.png differ
diff --git a/src/content/docs/docs/aws-integration/images/lambda-functions-3.png b/src/content/docs/docs/aws-integration/images/lambda-functions-3.png
index 83ea7955..25d6db99 100644
Binary files a/src/content/docs/docs/aws-integration/images/lambda-functions-3.png and b/src/content/docs/docs/aws-integration/images/lambda-functions-3.png differ
diff --git a/src/content/docs/docs/aws-integration/images/lambda-functions-4.png b/src/content/docs/docs/aws-integration/images/lambda-functions-4.png
index c5cc5098..8f7f4c1c 100644
Binary files a/src/content/docs/docs/aws-integration/images/lambda-functions-4.png and b/src/content/docs/docs/aws-integration/images/lambda-functions-4.png differ
diff --git a/src/content/docs/docs/aws-integration/images/step-functions-overview.png b/src/content/docs/docs/aws-integration/images/step-functions-overview.png
index 89d6a903..ad91503d 100644
Binary files a/src/content/docs/docs/aws-integration/images/step-functions-overview.png and b/src/content/docs/docs/aws-integration/images/step-functions-overview.png differ
diff --git a/src/content/docs/docs/aws-integration/images/step-functions-state-machines.png b/src/content/docs/docs/aws-integration/images/step-functions-state-machines.png
index 32220e50..bf1ba7ef 100644
Binary files a/src/content/docs/docs/aws-integration/images/step-functions-state-machines.png and b/src/content/docs/docs/aws-integration/images/step-functions-state-machines.png differ
diff --git a/src/content/docs/docs/aws-integration/index.mdx b/src/content/docs/docs/aws-integration/index.mdx
index dd89e278..82683ec0 100644
--- a/src/content/docs/docs/aws-integration/index.mdx
+++ b/src/content/docs/docs/aws-integration/index.mdx
@@ -30,3 +30,7 @@ Explore the following guides to set up and configure your AWS integrations:
- **[AWS Lambda Monitoring](./lambda-monitoring/)**: Explore how KloudMate aggregates Lambda metrics, invocations, and issues in a unified console.
- **[Lambda Telemetry Extension](./lambda-telemetry-extension/)**: Send Lambda logs, metrics, and traces to KloudMate with a layer, skipping the cross-account CloudFormation setup.
- **[AWS Step Functions Monitoring](./step-functions-monitoring/)**: Watch state-machine executions, spot failures, and replay the workflow graph to see which state failed.
+
+## Acting on AWS, not just reading it
+
+Everything above is read-only telemetry. To let a [workflow](../workflows/) call AWS APIs, for example to reboot an EC2 instance or redrive a Step Functions execution, connect AWS separately under [Settings → Connections](/platform/settings/connections/). That connection has its own IAM role, so a policy you write decides what automation can do, not the read-only role used for monitoring.
diff --git a/src/content/docs/docs/aws-integration/lambda-monitoring.mdx b/src/content/docs/docs/aws-integration/lambda-monitoring.mdx
index 1869355e..b9365d05 100644
--- a/src/content/docs/docs/aws-integration/lambda-monitoring.mdx
+++ b/src/content/docs/docs/aws-integration/lambda-monitoring.mdx
@@ -5,7 +5,7 @@ sidebar:
label: "Lambda Monitoring"
order: 4
---
-Open the Lambda section from the left navigation menu to monitor Lambda functions connected through your AWS integration.
+Open **Serverless → Lambda** to monitor Lambda functions connected through your AWS integration.
KloudMate collects and aggregates Lambda metrics such as:
@@ -17,9 +17,9 @@ KloudMate collects and aggregates Lambda metrics such as:
## Lambda Console Walkthrough
-The Lambda console provides a list of all the [Lambda Functions](https://app.kloudmate.com/lambda) in the associated AWS account. You can use the search bar located at the top right corner of the list to find a specific function.
+The **Lambda** tab lists the Lambda functions in the selected AWS account. Search by name to find a specific function.
-
+
Each row in the list shows basic information such as the function name, region, executions, error rate, response time, memory usage, and log ingestion.
diff --git a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-log-explorer.png b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-log-explorer.png
index 7c1fed92..d973baef 100644
Binary files a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-log-explorer.png and b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-log-explorer.png differ
diff --git a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-metrics-dashboard.png b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-metrics-dashboard.png
index b9296917..17d609bc 100644
Binary files a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-metrics-dashboard.png and b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-metrics-dashboard.png differ
diff --git a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-detail.png b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-detail.png
index e4299e32..0711ac35 100644
Binary files a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-detail.png and b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-detail.png differ
diff --git a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-list.png b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-list.png
index 0019da00..054a49fb 100644
Binary files a/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-list.png and b/src/content/docs/docs/aws-integration/lambda-telemetry-extension/images/telemetry-traces-list.png differ
diff --git a/src/content/docs/docs/aws-integration/step-functions-monitoring.mdx b/src/content/docs/docs/aws-integration/step-functions-monitoring.mdx
index 17d28c97..c7ca1ebf 100644
--- a/src/content/docs/docs/aws-integration/step-functions-monitoring.mdx
+++ b/src/content/docs/docs/aws-integration/step-functions-monitoring.mdx
@@ -8,7 +8,7 @@ sidebar:
Step Functions monitoring shows your state-machine executions, which ones failed, and where they failed. Open a failed run to read the error and follow the path it took through the workflow.
-Open **Step Functions** from the sidebar, under **Serverless**.
+Open **Serverless → Step Functions**.
Some of this works the moment you connect an AWS account. The rest needs execution logging turned on:
@@ -29,13 +29,13 @@ Some of this works the moment you connect an AWS account. The rest needs executi
## The Overview dashboard
-Start on the **Overview** tab to see whether your executions are healthy. It summarizes the last 24 hours across the workspace.
+The **Step Functions** tab opens on the **Overview** view, which summarizes the last 24 hours across the workspace. Start here to see whether your executions are healthy. Use the **Overview** / **State machines** toggle to switch views.
-
+
-A workspace can span several AWS accounts, so set the scope with the **Account** and **Region** menus at the top left. KloudMate reads CloudWatch for that account and region, and the page remembers your choice next time. Use **Refresh** to update the numbers.
+A workspace can span several AWS accounts, so set the scope with the **Account** and **Region** menus. KloudMate reads CloudWatch for that account and region, and the page remembers your choice next time. Use **Refresh** to update the numbers.
-The tab shows:
+The view shows:
- **Outcome tiles**: **Started**, **Succeeded**, **Failed**, **Aborted**, **Timed out**, and **Throttled** counts for the selected scope.
- **Executions chart**: Started, Succeeded, and Failed plotted across the day.
@@ -45,9 +45,9 @@ The tab shows:
## The State machines table
-Use the **State machines** tab to find a specific machine or spot the ones that are failing. It lists every state machine KloudMate has discovered in the workspace. Search by name from the box at the top right.
+Select **State machines** to find a specific machine or spot the ones that are failing. The view lists every state machine KloudMate has discovered in the workspace. Search by name to narrow the list.
-
+
Each row shows the machine's **Name** and **Region**, with its **Executions**, **Errors**, and **Error Rate** over the last 24 hours. Sort by **Error Rate** to bring the machines with the most errors to the top, then open one to see which runs failed.
diff --git a/src/content/docs/docs/azure-integration/auto-integration-with-azure-monitor.mdx b/src/content/docs/docs/azure-integration/auto-integration-with-azure-monitor.mdx
index e7b6f902..ae805e70 100644
--- a/src/content/docs/docs/azure-integration/auto-integration-with-azure-monitor.mdx
+++ b/src/content/docs/docs/azure-integration/auto-integration-with-azure-monitor.mdx
@@ -201,7 +201,7 @@ az vm create --resource-group "$RESOURCE_GROUP" --name "$VM_NAME" --image "$IMAG
After completing the setup, follow these steps to confirm that KloudMate is receiving Azure metrics:
1. Log into your **KloudMate account.**
-2. Navigate to **Explorer** or **Dashboards.**
+2. Navigate to **Explore** or **Dashboards.**
3. Filter for Azure-specific metrics.
- If data is flowing correctly, you should see the metrics appearing in real-time.
4. Troubleshoot if no data appears:
diff --git a/src/content/docs/docs/azure-integration/images/mongodb-monitoring-1.png b/src/content/docs/docs/azure-integration/images/mongodb-monitoring-1.png
index 8c1a4a4d..1efe3cdd 100644
Binary files a/src/content/docs/docs/azure-integration/images/mongodb-monitoring-1.png and b/src/content/docs/docs/azure-integration/images/mongodb-monitoring-1.png differ
diff --git a/src/content/docs/docs/azure-integration/images/save-collector-config.png b/src/content/docs/docs/azure-integration/images/save-collector-config.png
new file mode 100644
index 00000000..bb96d7f3
Binary files /dev/null and b/src/content/docs/docs/azure-integration/images/save-collector-config.png differ
diff --git a/src/content/docs/docs/azure-integration/index.mdx b/src/content/docs/docs/azure-integration/index.mdx
index cf9e6bd7..11b29769 100644
--- a/src/content/docs/docs/azure-integration/index.mdx
+++ b/src/content/docs/docs/azure-integration/index.mdx
@@ -27,3 +27,7 @@ Explore the following guides to set up and configure your Azure integrations:
- **[Manual Integration of Azure Monitor](./manual-integration-of-azure-monitor/)**: Instructions for manually configuring a KloudMate Agent to scrape Azure Monitor when you need full control.
- **[Enable Service Principal Azure](./enable-service-principal-azure/)**: A prerequisite guide on how to create the Service Principal needed for authenticated Azure Monitor collection.
- **[Azure Application Gateway Log Monitoring](./azure-application-gateway-log-monitoring/)**: A guide for streaming Azure logs into KloudMate through Azure Event Hubs.
+
+## Acting on Azure, not just reading it
+
+Everything above is read-only telemetry. To let a [workflow](../workflows/) act on an Azure resource or run a script on a VM, connect Azure separately under [Settings → Connections](/platform/settings/connections/). That connection uses its own service principal, so you can grant it the RBAC roles automation needs without giving more access to the read-only service principal used for monitoring.
diff --git a/src/content/docs/docs/azure-integration/manual-integration-of-azure-monitor.mdx b/src/content/docs/docs/azure-integration/manual-integration-of-azure-monitor.mdx
index dd760952..22000056 100644
--- a/src/content/docs/docs/azure-integration/manual-integration-of-azure-monitor.mdx
+++ b/src/content/docs/docs/azure-integration/manual-integration-of-azure-monitor.mdx
@@ -101,7 +101,7 @@ service:
After updating the configuration, click "**Save Configuration** " in the UI
-
+
or, from your server’s console, run:
diff --git a/src/content/docs/docs/database-monitoring/agent-jobs-and-awr.mdx b/src/content/docs/docs/database-monitoring/agent-jobs-and-awr.mdx
new file mode 100644
index 00000000..fe4db218
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/agent-jobs-and-awr.mdx
@@ -0,0 +1,48 @@
+---
+title: "SQL Server Agent jobs and Oracle AWR"
+description: "Check SQL Server Agent job runs and failures, and read Oracle's Automatic Workload Repository report in KloudMate."
+sidebar:
+ order: 11
+---
+
+SQL Server and Oracle instances each have one extra tab: **Agent jobs** for SQL Server and **AWR** for Oracle.
+
+## SQL Server Agent jobs
+
+The **Agent jobs** tab lists the SQL Server Agent job runs in the time range, so you can catch a failed backup or maintenance job without logging in to the server. The tiles at the top count the runs, and how many succeeded, failed, and are still running.
+
+
+
+Each run shows the job name, its status, when it ran, how long it took, and the message it logged. Expand a run to read the full message.
+
+To collect job runs, turn on the **Agent jobs** coverage for the instance in the agent's database monitoring settings. The monitoring login needs the `SQLAgentReaderRole` role in the `msdb` database:
+
+```sql
+USE msdb;
+CREATE USER [kloudmate] FOR LOGIN [kloudmate];
+ALTER ROLE SQLAgentReaderRole ADD MEMBER [kloudmate];
+```
+
+Replace `kloudmate` with your monitoring login. Skip the `CREATE USER` line if the login already has a user in `msdb`.
+
+:::note
+Older agents send run times without a time zone. For those, times are shown in the instance's own time zone, and runs can reach back up to 48 hours.
+:::
+
+## Oracle AWR
+
+The **AWR** tab shows the sections of Oracle's Automatic Workload Repository report as panels:
+
+- **Report Summary**, with the load profile and instance efficiency percentages
+- **Time Model and Wait Class Statistics**
+- **Foreground Wait Events, Enqueues, and Services**
+- **Load Profile Trends**
+- **SQL Statistics**, such as SQL ordered by elapsed time
+- **I/O Stats and Segment Statistics**
+- **Active Session History**
+
+:::caution
+AWR is part of the Oracle Diagnostics Pack, a separately licensed Enterprise Edition option. Only turn on the AWR coverages if your Oracle license includes it.
+:::
+
+Each row needs its AWR coverage turned on for the instance in the agent's database monitoring settings. The values change once per AWR snapshot, which Oracle takes every hour by default.
diff --git a/src/content/docs/docs/database-monitoring/calling-services.mdx b/src/content/docs/docs/database-monitoring/calling-services.mdx
new file mode 100644
index 00000000..a6bba503
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/calling-services.mdx
@@ -0,0 +1,33 @@
+---
+title: "Calling services"
+description: "See which services call a database or run a specific query, and jump to their traces."
+sidebar:
+ order: 7
+---
+
+The **Calling services** tab lists the services that called the database in the selected time range, with their call and error counts. Use it to find which application is behind a load spike, then open that service in APM or its traces.
+
+
+
+## How services are matched
+
+Calling services are found from traces, not from the database. A service appears when its database calls are recorded as client spans with the database's address and port, either by [eBPF monitoring](../../kloudmate-agent/ebpf-observability/) or by OpenTelemetry instrumentation in the service. See [APM and tracing](../../apm-and-tracing/) to instrument your services.
+
+If the instance has no endpoint recorded, there's nothing to match spans against, and the tab has no data.
+
+## Read the tab
+
+**Query Duration Percentiles** charts the P50, P90, P95, and maximum duration of the database calls over the time range.
+
+The table lists each calling service with its language, the number of calls, and the number of calls that ended in an error. Click a service to open it in APM, or **View traces** to open the Traces explorer filtered to that service's calls to this database.
+
+## Match a query to its traces
+
+The **Calling services** tab in a query's [details](../query-performance/#query-details) narrows the list to the services that ran that one query. Queries are matched to spans in one of two ways, and the tab shows which one it used:
+
+- **Exact match** uses a query ID that the query and its spans both carry.
+- **Approximate match** uses a distinctive part of the query text when spans carry no query ID. It can miss some traces, or include a different query with the same text.
+
+Most instrumentation doesn't record a query ID, so the match is usually approximate. On MySQL, which stores queries in a rewritten form, the match uses a run of words from the query rather than an exact fragment.
+
+If the traces in your workspace carry neither a query ID nor query text, queries can't be matched to traces.
diff --git a/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/_meta.json b/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/_meta.json
deleted file mode 100644
index 505f5742..00000000
--- a/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/_meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Database Activity Monitoring (DAM)",
- "order": 2
-}
diff --git a/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/index.mdx b/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/index.mdx
deleted file mode 100644
index d1e0409c..00000000
--- a/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/index.mdx
+++ /dev/null
@@ -1,63 +0,0 @@
----
-title: "Database Activity Monitoring (DAM)"
-description: "Use eBPF-powered monitoring to inspect database traffic without adding database-side agents."
-sidebar:
- label: "Overview"
- order: 1
----
-
-The eBPF Receiver includes built-in **Database Activity Monitoring** that automatically profiles database traffic at the kernel level. No database-side agents or plugins needed.
-
-### Supported Databases
-
-| **Database** | **Captured Metrics** |
-| ---------------- | ---------------------------------------------------------------------------- |
-| **MySQL** | Query operations, latency (avg + P99), prepared statements, read/write ratio |
-| **PostgreSQL** | Query operations, latency (avg + P99), prepared statements, read/write ratio |
-| **Redis** | Query operations, latency (avg + P99), prepared statements, read/write ratio |
-
-### Supported Messaging Systems
-
-| **System** | **Captured Metrics** |
-| ----------------- | --------------------------------------- |
-| **Kafka** | Producer/Consumer metrics, throughput |
-| **Elasticsearch** | Request operations, latency, throughput |
-
-### Key DAM Capabilities
-
-- **Table-level hotspot detection** — identify your most queried and slowest tables
-- **Read vs. Write ratio analysis** — understand workload characteristics per table
-- **P99 latency tracking** — surface tail latency issues before they impact users
-- **Zero configuration** — automatically detected via eBPF kernel hooks; no database credentials required
-
-### When to Use
-
-- When you need instant database visibility with **zero setup**
-- When you cannot or prefer not to create a monitoring user on the database
-- When running on Linux with kernel ≥ 5.8
-- When you want to monitor traffic across **all** supported databases and messaging systems from a single agent
-
-## Related DAM Workflows
-
-- [ML Classifier Deployment Guide](ml-classifier-deployment-guide/)
-
-:::note
- For deeper query analytics, including execution plans and schema discovery, see [Direct Database Monitoring](../direct-database-monitoring/). For distributed trace context from your application SDKs, see [OpenTelemetry Database Monitoring](../opentelemetry-database-monitoring/).
-:::
-
-### Configuration
-
-Enable heuristic SQL detection and tune caches for high-load environments:
-
-```yaml
-ebpf:
- heuristic_sql_detect: true
- mysql_prepared_statements_cache_size: 1024
- postgres_prepared_statements_cache_size: 1024
-```
-
-### Prerequisites
-
-- Linux kernel ≥ 5.8 (recommended; some features work on 4.18+)
-- Root privileges or `CAP_SYS_ADMIN` + `CAP_BPF` capabilities
-- The eBPF Receiver must be enabled in the agent configuration (see [Configuration](../../kloudmate-agent/management/)).
diff --git a/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/ml-classifier-deployment-guide.mdx b/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/ml-classifier-deployment-guide.mdx
deleted file mode 100644
index 990cf219..00000000
--- a/src/content/docs/docs/database-monitoring/database-activity-monitoring-dam/ml-classifier-deployment-guide.mdx
+++ /dev/null
@@ -1,298 +0,0 @@
----
-title: "ML Classifier Deployment Guide"
-description: "Deploy the KloudMate ML classifier for sensitive data detection in standalone and Kubernetes environments."
-sidebar:
- order: 2
----
-
-The **KloudMate ML Classifier** inspects database queries in your telemetry pipeline to detect sensitive data, such as Personally Identifiable Information (PII) and Protected Health Information (PHI). It combines a machine learning model with pattern-matching rules, and tags your traces with risk fingerprints before the telemetry leaves your infrastructure.
-
-This guide covers two strategies for deploying the **KloudMate ML Classifier**:
-
-1. Standalone deployment via **Docker Compose** on a simple machine.
-2. Enterprise deployment via **Kubernetes (K8s)**.
-
-## Strategy 1: Standalone Machine via Docker Compose
-
-:::note
- **When to use this strategy?**
-
- Use this strategy if you are running the classifier on a single VM or physical host where the KloudMate Agent is already running.
-:::
-
-### Prerequisites
-
-- Docker Engine and Docker Compose are installed.
-- The `KloudMate Agent` is running on the host machine, listening to OTLP HTTP traffic on port `4318`.
-
-
-### Prepare the Compose File
-
-
-Create a `docker-compose.yaml` file on your machine. It is configured to run the classifier with optimized CPU/Memory limits to handle the ML NLP models.
-
-If you need the ML Classifier to reach out to the KloudMate Agent running directly on the host machine, Docker uses the special DNS `host.docker.internal` to route traffic to the host.
-
-`docker-compose.yaml`
-
-```yaml
-version: '3.8'
-
-services:
- ml-classifier:
- image: ghcr.io/kloudmate/dam-classifier:latest
- build:
- context: .
- dockerfile: Dockerfile
- container_name: km-ml-classifier
- ports:
- - "8080:8080"
- environment:
- - HTTP_WORKERS=2
- - LOG_LEVEL=INFO
- volumes:
- - ./config.yaml:/app/config.yaml:ro
- deploy:
- resources:
- limits:
- cpus: '2.0'
- memory: 4G
- reservations:
- memory: 512M
- restart: unless-stopped
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
- interval: 30s
- timeout: 10s
- retries: 3
- networks:
- - km-network
-
-networks:
- km-network:
- driver: bridge
-```
-
-
-### Start the Classifier
-
-
-Navigate to the directory containing `docker-compose.yaml` and run:
-
-```bash
-# Start the classifier in detached mode
-docker compose up -d
-
-# Check the logs to ensure models are loaded
-docker compose logs -f
-```
-
-
-### Configure KloudMate Agent
-
-
-Update your host Agent's configuration to route traces to the classifier container. Since the classifier exposes port `8080` to the host:
-
-```yaml
-processors:
- # ...
- km_classifier:
- endpoint: "http://localhost:8080" # The classifier is bound to localhost:8080
- timeout: 500ms
- batch_timeout: 2s
- max_batch_size: 100
-```
-
-## **Strategy 2: Enterprise Cluster via Kubernetes**
-
-:::note
- **When to use this strategy?**
-
- Use this strategy if you are deploying the classifier to a Kubernetes cluster alongside the KloudMate agent ecosystem.
-:::
-
-### Prerequisites
-
-- Active Kubernetes cluster with `kubectl` access.
-- Helm or standard K8s networking setup.
-
-
-### Deploy the Configuration
-
-
-The classifier relies on a ConfigMap (`configmap.yaml`) to define the Presidio patterns and GLiNER labels.
-
-```bash
-kubectl apply -f k8s/configmap.yaml
-```
-
-
-### Deploy the Service & Application
-
-
-Apply the Kubernetes service and deployment manifests. The deployment automatically allocates `4Gi` of memory and spins up `2` HTTP workers for concurrent classification.
-
-`service.yaml`
-
-```yaml
-apiVersion: v1
-kind: Service
-metadata:
- name: ml-classifier
- namespace: km-agent
- labels:
- app: ml-classifier
- component: dam-classifier
-spec:
- type: ClusterIP
- selector:
- app: ml-classifier
- ports:
- - name: http
- port: 8080
- targetPort: 8080
- protocol: TCP
-```
-
-`deployment.yaml`
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: ml-classifier
- namespace: km-agent
- labels:
- app: ml-classifier
- component: dam-classifier
-spec:
- replicas: 1
- selector:
- matchLabels:
- app: ml-classifier
- template:
- metadata:
- labels:
- app: ml-classifier
- component: dam-classifier
- spec:
- containers:
- - name: classifier
- image: ghcr.io/kloudmate/dam-classifier:latest
- imagePullPolicy: Always
- ports:
- - name: http
- containerPort: 8080
- protocol: TCP
- resources:
- # GLiNER small model (~400MB)
- requests:
- memory: "512Mi"
- cpu: "250m"
- limits:
- memory: "4Gi"
- cpu: "2"
- env:
- - name: KM_AGENT_ENDPOINT
- value: "km-agent-svc.km-agent.svc.cluster.local:4318"
- - name: LOG_LEVEL
- value: "INFO"
- - name: HTTP_WORKERS
- value: "2"
- volumeMounts:
- - name: config
- mountPath: /app/config.yaml
- subPath: config.yaml
- readOnly: true
- volumes:
- - name: config
- configMap:
- name: ml-classifier-config
- # Ensure pods are spread across nodes
- topologySpreadConstraints:
- - maxSkew: 1
- topologyKey: kubernetes.io/hostname
- whenUnsatisfiable: ScheduleAnyway
- labelSelector:
- matchLabels:
- app: ml-classifier
-```
-
-```bash
-kubectl apply -f service.yaml
-kubectl apply -f deployment.yaml
-```
-
-
-### Verify Deployment
-
-
-Verify that the pod has started properly and the GLiNER models have been downloaded into memory.
-
-```bash
-# Check if the pod is Running
-kubectl get pods -n km-agent -l app=ml-classifier
-
-# Verify model loading logs
-kubectl logs -n km-agent -l app=ml-classifier -f
-```
-
-
-### Configure KloudMate Agent in K8s
-
-
-In your KloudMate Agent's config (if deployed as a separate pod in the same namespace), configure the processor to hit the fully qualified internal DNS of the service we just created.
-
-`otel-collector-config.yaml`
-
-```yaml
-# Agent configuration with DAM processor
-receivers:
- otlp:
- protocols:
- grpc:
- endpoint: 0.0.0.0:4317
- http:
- endpoint: 0.0.0.0:4318
-
-processors:
- batch:
- timeout: 1s
- send_batch_size: 1024
-
- memory_limiter:
- check_interval: 1s
- limit_mib: 512
- spike_limit_mib: 128
-
- # DAM Classifier processor (using the correct type name from factory.go)
- km_classifier:
- endpoint: "http://ml-classifier.km-agent.svc.cluster.local:8080" # Fully qualified K8s Service URL
- timeout: 500ms
- batch_timeout: 2s
- max_batch_size: 100
- enable_batching: true
- skip_if_no_query: true
- max_idle_conns: 100
- max_conns_per_host: 100
-
-exporters:
- otlp:
- endpoint: "https://otel.kloudmate.com:4318"
- tls:
- insecure: true
-
- debug:
- verbosity: detailed
-
-service:
- pipelines:
- traces:
- receivers: [otlp]
- processors: [memory_limiter, km_classifier, batch]
- exporters: [otlp]
-```
-
-## Verifying Classification is Working
-
-Regardless of the deployment strategy, you can verify the setup by observing the KloudMate Agent logs (ensure `level: debug` is enabled temporarily). The traced applications will start showing `dam.lineage.fingerprint`, `dam.lineage.risk`, and extracted `PII/PHI` properties dynamically added to the payload before arriving at your backend.
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/_meta.json b/src/content/docs/docs/database-monitoring/database-integrations/_meta.json
deleted file mode 100644
index 3a3e605e..00000000
--- a/src/content/docs/docs/database-monitoring/database-integrations/_meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Database Integrations",
- "order": 5
-}
\ No newline at end of file
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/images/mongodb-monitoring-1.png b/src/content/docs/docs/database-monitoring/database-integrations/images/mongodb-monitoring-1.png
deleted file mode 100644
index 8c1a4a4d..00000000
Binary files a/src/content/docs/docs/database-monitoring/database-integrations/images/mongodb-monitoring-1.png and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/images/mongodb-monitoring-2.png b/src/content/docs/docs/database-monitoring/database-integrations/images/mongodb-monitoring-2.png
deleted file mode 100644
index 5a4bb701..00000000
Binary files a/src/content/docs/docs/database-monitoring/database-integrations/images/mongodb-monitoring-2.png and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/images/mysql-monitoring-1.png b/src/content/docs/docs/database-monitoring/database-integrations/images/mysql-monitoring-1.png
deleted file mode 100644
index 44d90853..00000000
Binary files a/src/content/docs/docs/database-monitoring/database-integrations/images/mysql-monitoring-1.png and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/images/mysql-monitoring-2.png b/src/content/docs/docs/database-monitoring/database-integrations/images/mysql-monitoring-2.png
deleted file mode 100644
index c83f10c2..00000000
Binary files a/src/content/docs/docs/database-monitoring/database-integrations/images/mysql-monitoring-2.png and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/images/postgresql-monitoring-1.png b/src/content/docs/docs/database-monitoring/database-integrations/images/postgresql-monitoring-1.png
deleted file mode 100644
index 24cc14e1..00000000
Binary files a/src/content/docs/docs/database-monitoring/database-integrations/images/postgresql-monitoring-1.png and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/mongodb-monitoring.mdx b/src/content/docs/docs/database-monitoring/database-integrations/mongodb-monitoring.mdx
deleted file mode 100644
index 8995e223..00000000
--- a/src/content/docs/docs/database-monitoring/database-integrations/mongodb-monitoring.mdx
+++ /dev/null
@@ -1,296 +0,0 @@
----
-title: "MongoDB"
-description: "Monitor MongoDB health, performance, and logs with the KloudMate Agent and OpenTelemetry."
----
-This guide sets up MongoDB monitoring with the **KloudMate Agent** and OpenTelemetry. It collects metrics and logs so you can watch the health, performance, and behavior of MongoDB instances running on AWS EC2, Azure Virtual Machines, or on-premise servers.
-
-### **What This Integration Provides**
-
-With MongoDB Monitoring enabled, KloudMate collects telemetry at multiple levels, including:
-
-- Server-level performance metrics
-- Database and collection-level metrics
-- Connection, operation, and lock behavior
-- Network and storage usage
-- MongoDB server logs
-
-This visibility helps identify performance bottlenecks, abnormal query behavior, resource saturation, and availability issues.
-
-### **Pre-requisites**
-
-Before configuring MongoDB Monitoring, ensure the following:
-
-1. MongoDB server is running
-2. Supported MongoDB versions: 4.0+, 5.0, 6.0, 7.0
-3. KloudMate Agent installed on the MongoDB host
- - Refer to Agent installation for [Linux](../../../kloudmate-agent/installation/linux-agent/) or [Windows](../../../kloudmate-agent/installation/windows-agent/)
-4. MongoDB user configured with least-privilege access.
-
-MongoDB recommends creating a user with the [clusterMonitor role](https://www.mongodb.com/docs/v5.0/reference/built-in-roles/#mongodb-authrole-clusterMonitor) to collect metrics. Refer to [lpu.sh](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/mongodbreceiver/testdata/integration/scripts/lpu.sh) for an example of how to configure these permissions.
-
-Example:
-
-```text
-mongosh
-use admin
-db.createUser({
- user: "USER",
- pwd: "PASSWORD",
- roles: [{ role: "clusterMonitor", db: "admin" }]
-})
-exit
-```
-
-### **Step 1: Access Agents and OpenTelemetry Collector Configuration**
-
-1. Log in to the KloudMate platform
-2. Navigate to **Settings → Agents**
-3. Select the agent running on the MongoDB host
-4. Open **Collector Configuration** from the agent actions
-5. The configuration editor opens, allowing you to modify the YAML directly
-
-
-
-### **Step 2: Add Required Extensions and Receivers**
-
-**Extensions**
-
-Add the following extensions to support health checks, debugging, and local storage:
-
-
-```yaml
- extensions:
- file_storage:
- create_directory: true
- ```
-
- ```none
- ```
-
-
-**Receivers**
-
-Two receivers can be used with MongoDB:
-
-- **MongoDB Receiver –** Collects MongoDB metrics
-- **File Log Receiver –** Collects MongoDB logs
-
-If you only need metrics, configure only the MongoDB receiver.
-
-```yaml
-receivers:
- mongodb:
- hosts:
- - endpoint: localhost:27017
- username: otel #Change the correct username
- password: PASSWD #change the correct password
- collection_interval: 60s
- initial_delay: 1s
- tls:
- insecure: true
- insecure_skip_verify: true
- filelog:
- include: [/var/log/mongodb/*.log]
- storage: file_storage
- retry_on_failure:
- enabled: true
-```
-
-### **Step 3: Configure Processors**
-
-Set up the processor component to identify resource information from the host and either append or replace the resource values in the telemetry data with this information.
-
-Choose **one configuration** based on where MongoDB is running.
-
-- **Server (On-premise / Non-cloud / Cloud)**
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection:
- detectors: [system]
- system:
- resource_attributes:
- host.name:
- enabled: true
- host.id:
- enabled: true
- os.type:
- enabled: false
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: host.name
-```
-
-- **AWS EC2:**
-
-(Optional) To collect EC2 tags, attach an IAM role with `EC2:DescribeTags` permission.
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/ec2:
- detectors: ["ec2"]
- ec2:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: ec2.tag.Name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-- **Azure Virtual Machines:**
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/azure:
- detectors: ["azure"]
- azure:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: azure.vm.name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-### **Step 4: Configure Exporter and Pipelines**
-
-Configure the KloudMate backend exporter and define pipelines for metrics and logs.
-
-```yaml
-exporters:
- debug:
- verbosity: detailed
- otlphttp:
- endpoint: 'https://otel.kloudmate.com:4318'
- headers:
- Authorization: xxxxxxxx # Use the auth key
-
-service:
- pipelines:
-
- metrics:
- receivers: [mongodb]
- processors: [batch, resourcedetection, resource] # Apply the correct resourcedetection
- exporters: [otlphttp]
-
- logs:
- receivers: [filelog]
- processors: [batch, resourcedetection, resource] # Apply the correct resourcedetection
- exporters: [otlphttp]
-
- extensions: [health_check, pprof, zpages, file_storage]
-```
-
-### **Step 5: Save Configuration and Restart Agent**
-
-After updating the configuration, click **Save Configuration** in the KloudMate UI.
-
-
-
-You can also restart the agent from your server’s console.
-
-**For Linux:**
-
-1. Execute the following commands:
-
-```none
-systemctl restart kmagent
-systemctl status kmagent
-```
-
-These commands restart the KloudMate Agent and display its current status.
-
-**For Windows**
-
-1. Open the **Services** window
- - Press `Win + R`, type `services.msc`, and press `OK`
- - Or search for **Services** from the Start menu
-2. Locate the **KloudMate Agent** service.
-3. Right-click the service and select **Restart**.
-
-After restarting the agent, verify that MongoDB metrics and logs are visible in the KloudMate dashboard.
-
-## Post‑Integration Data Validation
-
-Verify that metrics are flowing into KloudMate using the **Explore** view.
-
-After the agent restarts:
-
-1. Log in to KloudMate
-2. Navigate to **Explore**
-3. Select **OpenTelemetry → Metrics**
-4. Choose a MongoDB metric and run the query
-
-Seeing time-series data confirms that MongoDB telemetry is flowing successfully.
-
-
-
-## Standard MongoDB Dashboards
-
-KloudMate provides prebuilt MongoDB dashboards through [dashboard templates](https://templates.kloudmate.com/mongo-db/index.html). These dashboards visualize MongoDB server performance, operations, storage, and resource usage.
-
-To import and start using these templates, follow the steps described in [Importing a Dashboard](../../../visualize-data/dashboards/#importing-a-dashboard). Alternatively, use [From Template](../../../visualize-data/dashboards/create-a-dashboard/#from-template) when creating a new dashboard to pick from KloudMate's built-in templates.
-
-## **Default MongoDB Metrics**
-
-MongoDB Monitoring automatically collects commonly used MongoDB metrics when enabled.
-
-Example Metrics
-
-| **Metric Name** | **Description** |
-| --------------------------------- | ------------------------------------------------------------------------ |
-| mongodb\_connection\_count | The number of connections. |
-| mongodb\_operation\_count | The number of operations executed. |
-| mongodb\_operation\_latency\_time | The latency of operations. |
-| mongodb\_data\_size | The size of the collection. Data compression does not affect this value. |
-| mongodb\_storage\_size | The total amount of storage allocated to this collection. |
-| mongodb\_network\_io\_receive | The number of bytes received. |
-| mongodb\_network\_io\_transmit | The number of bytes transmitted. |
-| mongodb\_lock\_acquire\_count | Number of times the lock was acquired in the specified mode. |
-| mongodb\_lock\_acquire\_time | Cumulative wait time for the lock acquisitions. |
-| mongodb\_health | The health status of the server. |
-| mongodb\_uptime | The amount of time that the server has been running. |
-
-For the complete metrics list, refer to the [metrics reference](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/mongodbreceiver/documentation.md).
-
-## Optional Deployment Methods
-
-### VM/EC2 with Fluent Bit
-[Streaming MongoDB Metrics & Logs to KloudMate Using Fluent Bit](https://blog.kloudmate.com/streaming-mongodb-metrics-logs-to-kloudmate-using-fluent-bit-32b54e91fe93?postPublishedType=initial)
-
-### Docker Compose
-[Monitoring MongoDB with KloudMate Using Docker Compose](https://blog.kloudmate.com/monitoring-mongodb-with-kloudmate-using-docker-compose-49a5dc59c660?postPublishedType=initial)
-
-##
-
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/mssql-monitoring.mdx b/src/content/docs/docs/database-monitoring/database-integrations/mssql-monitoring.mdx
deleted file mode 100644
index 866bac15..00000000
--- a/src/content/docs/docs/database-monitoring/database-integrations/mssql-monitoring.mdx
+++ /dev/null
@@ -1,488 +0,0 @@
----
-title: "MSSQL"
-description: "Monitor Microsoft SQL Server health, performance, queries, and logs with the KloudMate Agent and OpenTelemetry."
----
-
-This guide sets up Microsoft SQL Server (MSSQL) monitoring with the KloudMate Agent and OpenTelemetry. It collects metrics and logs so you can watch the health, performance, and query behavior of MSSQL instances running on Windows hosts, AWS EC2, Azure Virtual Machines, or on-premise servers.
-
-# MSSQL Integration using KloudMate Agents
-
-### What This Integration Provides
-
-With MSSQL Monitoring enabled, KloudMate collects telemetry that provides visibility into:
-
-- Database health and performance metrics
-- Lock waits, blocking processes, and transaction behavior
-- Buffer pool efficiency and I/O performance
-- Transaction log usage and growth patterns
-- Resource pool throttling and disk I/O latency
-- Server-level resource utilization
-
-This visibility helps identify performance bottlenecks, blocking issues, transaction log problems, buffer pool inefficiencies, and availability concerns.
-
-### Prerequisites
-
-- MSSQL Server must be installed and running
-- KloudMate Agent installed on the MSSQL host (see Agent Installation for Linux and Windows)
-- Run agents as Administrator to collect all Windows Performance Counters
-
-### Required Permissions
-
-1. **Windows Performance Counters**
- Run the KloudMate Agent as Administrator to collect all performance counter-based metrics.
-2. **Direct Database Connection**
- When configured for direct SQL Server connection, the monitoring user must have:
-
-- **Database Permissions** (at least one of):
- • `CREATE DATABASE`
- • `ALTER ANY DATABASE`
- • `VIEW ANY DATABASE`
-- **Server State Permissions:**
- • SQL Server pre-2022: `VIEW SERVER STATE`
- • SQL Server 2022+: `VIEW SERVER PERFORMANCE STATE`
-
-## Configuration Overview
-
-### Step 1: Configure the KloudMate Agent with
-• `hostmetrics` receiver (system metrics)
-• `sqlserver` receiver (MSSQL performance metrics)
-• `sqlquery` receiver (custom SQL metrics)
-• `filelog` receiver (MSSQL logs)
-
-### **Step 2: Access Agents and OpenTelemetry Collector Configuration**
-
-- Log in to the KloudMate platform
-- Go to **Settings → Agents**
-- Select the agent running on the MSSQL host
-- Click **Actions → Collector Configuration**
-- YAML editor opens for configuration
-
-
-
-### **Step 3: Add Required Extensions and Receivers**
-
-```yaml
-extensions:
- health_check:
- pprof:
- endpoint: 0.0.0.0:1777
- zpages:
- endpoint: 0.0.0.0:55679
- file_storage:
- create_directory: true
-
-receivers:
- otlp:
- protocols:
- grpc:
- endpoint: 0.0.0.0:4317
- http:
- endpoint: 0.0.0.0:4318
-
- hostmetrics:
- collection_interval: 30s
- scrapers:
- cpu:
- metrics:
- system.cpu.utilization:
- enabled: true
- load:
- cpu_average: true
- memory:
- metrics:
- system.memory.utilization:
- enabled: true
- filesystem:
- metrics:
- system.filesystem.usage:
- enabled: true
- system.filesystem.utilization:
- enabled: true
- disk: {}
- network: {}
- processes: {}
-
- sqlserver:
- collection_interval: 30s
- username: #use the username
- password: #use the secure password
- server: 0.0.0.0
- port: 1433
- metrics:
- sqlserver.database.count:
- enabled: true
- sqlserver.database.io:
- enabled: true
- sqlserver.database.latency:
- enabled: true
- sqlserver.database.operations:
- enabled: true
- sqlserver.processes.blocked:
- enabled: true
- sqlserver.resource_pool.disk.throttled.read.rate:
- enabled: true
- sqlserver.resource_pool.disk.throttled.write.rate:
- enabled: true
-
- sqlquery/upmetrics:
- driver: mssql
- datasource: "sqlserver://:@localhost:1433?database=Kloudmate-test&encrypt=disable" #modify the username and password
- queries:
- # Query: Collect SQL Server Uptime as a Metric
- - sql: |
- SELECT
- sqlserver_start_time,
- DATEDIFF(MINUTE, sqlserver_start_time, GETDATE()) AS Minutes_Uptime
- FROM
- sys.dm_os_sys_info;
- metrics:
- - metric_name: sqlserver.uptime.minutes
- value_column: Minutes_Uptime
- attribute_columns: ["sqlserver_start_time"]
- static_attributes:
- dbinstance: mydbinstance
- filelog:
- include:
- # Ensure the path is appropriate for your server's operating system. Uncomment the line below if the server is running Windows.
- - /var/opt/mssql/log/errorlog*#- 'C:\Program Files\Microsoft SQL Server\MSSQL\LOG\ERRORLOG*'
- storage: file_storage
- multiline:
- line_start_pattern: ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{2}
- retry_on_failure:
- enabled: true
-# query to show client connections and IP
- sqlquery/connection:
- driver: mssql
- datasource: "sqlserver://:@localhost:1433?database=Kloudmate-test&encrypt=disable"
- storage: file_storage
- queries:
- - sql: |
- SELECT
- c.session_id,
- c.client_net_address AS client_ip_address,
- s.host_name,
- s.login_name,
- s.program_name,
- s.login_time,
- s.last_request_start_time,
- s.last_request_end_time,
- 'mssql_client_log' AS log_source
- FROM
- sys.dm_exec_connections AS c
- JOIN
- sys.dm_exec_sessions AS s
- ON
- c.session_id = s.session_id
- WHERE
- s.last_request_start_time > $$1
- ORDER BY
- s.last_request_start_time ASC;
- tracking_column: last_request_start_time
- tracking_start_value: "" # add today date
- logs:
- - body_column: client_ip_address
- attribute_columns: ["session_id", "host_name", "login_name", "program_name", "login_time", "last_request_start_time", "last_request_end_time", "log_source"]
-# To show slow query
- sqlquery/slowquery:
- driver: mssql
- datasource: "sqlserver://:@localhost:1433?database=Kloudmate-test&encrypt=disable"
- storage: file_storage
- queries:
- - sql: |
- SELECT
- *,
- SUBSTRING(
- st.text,
- (qs.statement_start_offset / 2) + 1,
- (
- (
- CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset
- ) / 2
- ) + 1
- ) AS statement_text,
- 'mssql_slow_query_log' AS log_source
- FROM
- sys.dm_exec_query_stats AS qs
- CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
- WHERE qs.last_execution_time > $$1
- ORDER BY qs.last_execution_time;
- tracking_column: last_execution_time
- tracking_start_value: " " #add today's date
- logs:
- - body_column: statement_text
- attribute_columns: ["sql_handle", "execution_count", "total_worker_time", "last_execution_time", "creation_time", "total_elapsed_time", "statement_start_offset", "statement_end_offset", "log_source"]
-# To get audit log
- sqlquery/audit:
- driver: mssql
- datasource: "sqlserver://:@localhost:1433?database=Kloudmate-test&encrypt=disable"
- storage: file_storage
- collection_interval: 30s
- queries:
- - sql: "SELECT event_time, action_id, succeeded, session_server_principal_name AS user_name, server_principal_name AS login_name, database_name, schema_name, object_name, statement, additional_information FROM sys.fn_get_audit_file('/var/opt/mssql/log/Audit-*.sqlaudit', DEFAULT, DEFAULT) WHERE event_time > $$1 ORDER BY event_time ASC"
- tracking_column: event_time
- tracking_start_value: " " # modify today date
- logs:
- - body_column: statement
- attribute_columns: ["event_time", "action_id", "succeeded", "user_name", "login_name", "database_name", "schema_name", "object_name", "additional_information", "log_source"]
-
-# Active query that is running
- sqlquery/activequery:
- driver: mssql
- datasource: "sqlserver://:@localhost:1433?database=Kloudmate-test&encrypt=disable"
- queries:
- - sql: |
- SELECT
- r.session_id,
- r.status,
- r.command,
- r.cpu_time,
- r.start_time,
- s.login_name,
- s.host_name,
- s.program_name,
- st.text AS query_text,
- 'mssql_active_query' as log_source
- FROM
- sys.dm_exec_requests r
- INNER JOIN
- sys.dm_exec_sessions s
- ON
- r.session_id = s.session_id
- CROSS APPLY
- sys.dm_exec_sql_text(r.sql_handle) st
- WHERE
- s.is_user_process = 1
- logs:
- - body_column: query_text
- attribute_columns: ["session_id", "status", "command", "cpu_time", "start_time", "login_name", "host_name", "program_name", "log_source"]
-# user session
- sqlquery/usersession:
- driver: mssql
- datasource: "sqlserver://:@localhost:1433?database=Kloudmate-test&encrypt=disable"
- queries:
- - sql: |
- SELECT
- session_id,
- login_name,
- host_name,
- program_name,
- login_time,
- status,
- client_interface_name,
- cpu_time,
- memory_usage,
- reads,
- writes,
- 'mssql_user_sessions' as log_source
- FROM
- sys.dm_exec_sessions
- WHERE
- is_user_process = 1;
- logs:
- - body_column: login_name
- attribute_columns: ["session_id", "host_name", "program_name", "login_time", "status", "client_interface_name", "cpu_time", "memory_usage", "reads", "writes", "log_source"]
-```
-
-### **Step 4: Configure Processors**
-
-Set up the processor component to identify resource information from the host and either append or replace the resource values in the telemetry data with this information.
-
-Choose **one configuration** based on where MSSQL is running.
-
-- **Server (On-premise / Non-cloud / Cloud)**
-
-```yaml
-processors:
- batch:
- send_batch_size: 10000
- timeout: 30s
- resourcedetection:
- detectors: [system]
- system:
- resource_attributes:
- host.name:
- enabled: true
- host.id:
- enabled: true
- os.type:
- enabled: false
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: host.name
-```
-
-- **AWS EC2:**
-
-Optional: To retrieve AWS EC2 instance tags along with logs and metrics, you need to associate an IAM role with the EC2 instance that includes the EC2\:DescribeTags policy. The processor below needs to be added:
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/ec2:
- detectors: ["ec2"]
- ec2:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: ec2.tag.Name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-- **Azure Virtual Machines:**
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/azure:
- detectors: ["azure"]
- azure:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: azure.vm.name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-### **Step 5: Configure Exporter and Pipelines**
-
-Configure the KloudMate backend exporter and define pipelines for metrics and logs.
-
-:::note
- *Change the pipeline as per your correct configuration.*
-:::
-
-```yaml
-exporters:
- debug:
- verbosity: detailed
- otlphttp:
- endpoint: 'https://otel.kloudmate.com:4318'
- headers:
- Authorization: xxxxxxxx # Use the auth key
-
-service:
- pipelines:
- metrics:
- receivers: [sqlserver, hostmetrics, sqlquery/upmetrics]
- processors: [batch, resourcedetection, resource]
- exporters: [debug, otlphttp]
-
- log:
- receivers: [filelog, sqlquery/activequery, sqlquery/connection, sqlquery/audit, sqlquery/usersession, sqlquery/slowquery]
- processors: [batch, resourcedetection, resource]
- exporters: [debug, otlphttp]
- extensions: [health_check, pprof, zpages, file_storage]
-```
-
-### **Step 6: Save Configuration and Restart Agent**
-
-After updating the configuration, click **Save Configuration** in the KloudMate UI.
-
-
-
-You can also restart the agent from your server’s console.
-
-**For Linux:**
-
-1. Execute the following commands:
-
-```none
-systemctl restart kmagent
-systemctl status kmagent
-```
-
-These commands restart the KloudMate Agent and display its current status.
-
-**For Windows**
-
-1. Open the **Services** window
- - Press `Win + R`, type `services.msc`, and press `OK`
- - Or search for **Services** from the Start menu
-2. Locate the **KloudMate Agent** service.
-3. Right-click the service and select **Restart**.
-
-After restarting the agent, verify that MSSQL metrics and logs are visible in the KloudMate dashboard.
-
-## Post‑Integration Data Validation
-
-Verify that metrics are flowing into KloudMate using the **Explore** view.
-
-After the agent restarts:
-
-1. Log in to KloudMate
-2. Navigate to **Explore**
-3. Select **OpenTelemetry → Metrics**
-4. Choose a MSSQL metric and run the query
-
-Seeing time-series data confirms that MSSQL telemetry is flowing successfully.
-
-
-
-## **Standard MSSQL Dashboards**
-
-KloudMate provides prebuilt MSSQL dashboards through [dashboard templates](https://templates.kloudmate.com/mssql/index.html). These dashboards visualize MSSQL database performance, query operations, connections, and resource usage.
-
-To import and start using these templates, follow the steps described in [Importing a Dashboard](../../../visualize-data/dashboards/#importing-a-dashboard). Alternatively, use [From Template](../../../visualize-data/dashboards/create-a-dashboard/#from-template) when creating a new dashboard to pick from KloudMate's built-in templates.
-
-## Default Metrics
-
-| **Metrics** | **Description** | **Type** | **Unit** |
-| ------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------- | ------------ |
-| sqlserver\_user\_connection\_count | Number of users connected to the SQL Server. | Gauge
| number |
-| sqlserver\_lock\_wait\_time\_avg | Average wait time for all lock requests that had to wait. | Gauge
| milliseconds |
-| sqlserver\_lock\_wait\_rate | Number of lock requests resulting in a wait. | Gauge
| number |
-| sqlserver\_batch\_request\_rate | Number of batch requests received by SQL Server. | Gauge
| number |
-| sqlserver\_batch\_sql\_compilation\_rate | Number of SQL compilations needed. | Gauge
| number |
-| sqlserver\_batch\_sql\_recompilation\_rate | Number of SQL recompilations needed. | Gauge
| number |
-| sqlserver\_page\_buffer\_cache\_hit\_ratio | Pages found in the buffer pool without having to read from disk. | Gauge
| number |
-| sqlserver\_page\_life\_expectancy | Time a page will stay in the buffer pool. | Gauge
| seconds |
-| sqlserver\_page\_split\_rate | Number of pages split as a result of overflowing index pages. | Gauge
| number |
-| sqlserver\_page\_lazy\_write\_rate | Number of lazy writes moving dirty pages to disk. | Gauge
| number |
-| sqlserver\_page\_checkpoint\_flush\_rate | Number of pages flushed by operations requiring dirty pages to be flushed. | Gauge
| number |
-| sqlserver\_page\_operation\_rate | Number of physical database page operations issued. | Gauge
| number |
-| sqlserver\_transaction\_log\_growth\_count | Total number of transaction log expansions for a database. | Sum | number |
-| sqlserver\_transaction\_log\_shrink\_count | Total number of transaction logs shrinks for a database. | Sum | number |
-| sqlserver\_transaction\_log\_usage | Percent of transaction log space used. | Gauge
| number |
-| sqlserver\_transaction\_log\_flush\_wait\_rate | Number of commits waiting for a transaction log flush. | Gauge
| number |
-| sqlserver\_transaction\_log\_flush\_rate | Number of log flushes. | Gauge
| number |
-| sqlserver\_transaction\_log\_flush\_data\_rate | Total number of log bytes flushed. | Gauge
| bytes |
-| sqlserver\_transaction\_rate | Number of transactions started for the database (not including XTP-only transactions). | Gauge
| number |
-| sqlserver\_transaction\_write\_rate | Number of transactions that were written to the database and committed. | Gauge
| number |
-| sqlserver\_database\_latency | Total time that the users waited for I/O issued on this file. | Sum | seconds |
-| sqlserver\_database\_operations | The number of operations issued on the file. | Sum | number |
-| sqlserver\_database\_io | The number of bytes of I/O on this file. | Sum | bytes |
-| sqlserver\_resource\_pool\_disk\_throttled\_read\_rate | The number of read operations that were throttled in the last second | Gauge
| number |
-| sqlserver\_resource\_pool\_disk\_throttled\_write\_rate | The number of write operations that were throttled in the last second | Gauge
| number |
-| sqlserver\_processes\_blocked | The number of processes that are currently blocked. | Gauge
| number |
-| sqlserver\_database\_count | The number of databases. | Gauge
| number |
-
-For the complete metrics list, refer to the [metrics reference](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/sqlserverreceiver/documentation.md).
-
-###
-
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/mysql-monitoring.mdx b/src/content/docs/docs/database-monitoring/database-integrations/mysql-monitoring.mdx
deleted file mode 100644
index de9606c0..00000000
--- a/src/content/docs/docs/database-monitoring/database-integrations/mysql-monitoring.mdx
+++ /dev/null
@@ -1,329 +0,0 @@
----
-title: "MySQL"
-description: "Monitor MySQL health, performance, and logs with the KloudMate Agent and OpenTelemetry."
----
-
-
-This guide sets up MySQL monitoring with the **KloudMate Agent** and OpenTelemetry. It collects metrics and logs so you can watch the health, performance, and query behavior of MySQL instances running on AWS EC2, Azure Virtual Machines, or on-premise servers. A built-in dashboard visualizes the metrics, and you can set alerts on key MySQL conditions.
-
-# MySQL Integration using KloudMate Agents
-
-### What This Integration Provides
-
-With MySQL Monitoring enabled, KloudMate collects telemetry that provides visibility into:
-• Buffer pool usage and operations
-• Connection handling and thread states
-• InnoDB row locks, double writes, and log operations
-• Index I/O waits and table operations
-• Query execution, sorts, and temporary resources
-• MySQL server uptime and opened resources
-• Slow query logs and error logs
-
-This visibility helps identify buffer pool inefficiencies, connection bottlenecks, InnoDB performance issues, and slow query problems.
-
-### Pre-requisites
-
-Before configuring MySQL Monitoring, ensure the following:
-
-1. MySQL version 8.0 or MariaDB 10.11 is installed and running.
-2. Refer to the MySQL documentation for supported versions and configuration requirements.
-3. The monitoring user must have permission to execute `SHOW GLOBAL STATUS`.
-4. To collect query samples, `performance_schema` must be enabled and the monitoring user must be granted access:
- `` `GRANT SELECT ON performance_schema.* TO @'%';` ``
-5. KloudMate Agent installed on the MySQL host
- - Refer to Agent installation for [Linux](../../../kloudmate-agent/installation/linux-agent/) or [Windows](../../../kloudmate-agent/installation/windows-agent/).
-6. MySQL username and password for authentication.
-7. MySQL logs must be enabled from configurations.
-
-### Step 1: Access Agents and OpenTelemetry Collector Configuration
-
-- Log in to the KloudMate platform
-- Go to **Settings → Agents**
-- Select the agent running on the MySQL host
-- Click **Actions → Collector Configuration**
-- YAML editor opens for configuration
-
-
-
-### Step 2: Add Required Extensions and Receivers
-
-**Extensions**
-
-```yaml
-extensions:
- file_storage:
- create_directory: true
-```
-
-**Receivers**
-
-Two receivers are used:
-
-- **MySQL Receiver –** collects metrics
-- **File Log Receiver –** collects logs (general, error, slow queries)
-
-If you only need metrics, configure only the MySQL receiver.
-
-```yaml
-receivers:
- mysql:
- endpoint: localhost:3306
- username: root #Change the username
- password: 'YourStr0ngPassword!' #Change the password
- collection_interval: 60s
- initial_delay: 1s
- statement_events:
- digest_text_limit: 120
- time_limit: 24h
- limit: 250
-
- filelog/mysqlerror:
- include: [/var/log/mysql/error.log]
- exclude: [/var/log/mysql/*.gz]
- storage: file_storage
- retry_on_failure:
- enabled: true
- operators:
- - type: regex_parser
- parse_to: body
- regex: '(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z)\s(?P\d+)\s\[(?P[^ ]*)\]\s\[(?P[^ ]*)\]\s\[(?P[^ ]*)\]\s(?P.*)'
- timestamp:
- parse_from: body.timestamp
- layout: '%Y-%m-%d %H:%M:%S.%f %Z'
- severity:
- parse_from: body.severity
- mapping:
- info:
- - STATEMENT
- - LOG
- - SYSTEM
- - type: remove
- field: body.severity
- - type: remove
- field: body.timestamp
-
- filelog/mysqlslowquery:
- include: [/var/log/mysql/*-slow.log]
- exclude: [/var/log/mysql/*.gz]
- storage: file_storage
- multiline:
- line_start_pattern: '^# Time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z'
- retry_on_failure:
- enabled: true
- operators:
- - type: regex_parser
- parse_to: body
- regex: '(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\n#\sUser@Host:\s(?P.*)\s+Id:\s+(?P\d+)\n# Query_time:\s(?P\d+.\d+)\s+Lock_time:\s(?P\d+.\d+)\sRows_sent:\s(?P\d+)\s+Rows_examined:\s(?P\d+)\n(?P[\s\S]+)'
- timestamp:
- parse_from: body.timestamp
- layout: '%Y-%m-%d %H:%M:%S.%f %Z'
- severity:
- parse_from: body.severity
- mapping:
- info:
- - STATEMENT
- - LOG
- - type: remove
- field: body.severity
- - type: remove
- field: body.timestamp
-```
-
-### **Step 3: Configure Processors**
-
-Set up the processor component to identify resource information from the host and either append or replace the resource values in the telemetry data with this information.
-
-Choose **one configuration** based on where MYSQL is running.
-
-- **Server (On-premise / Non-cloud / Cloud)**
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection:
- detectors: [system]
- system:
- resource_attributes:
- host.name:
- enabled: true
- host.id:
- enabled: true
- os.type:
- enabled: false
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: host.name
-```
-
-- **AWS EC2:**
-
-(Optional) To collect EC2 tags, attach an IAM role with `EC2:DescribeTags` permission.
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/ec2:
- detectors: ["ec2"]
- ec2:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: ec2.tag.Name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-- **Azure Virtual Machines:**
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/azure:
- detectors: ["azure"]
- azure:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: azure.vm.name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-### **Step 4: Configure Exporter and Pipelines**
-
-Configure the KloudMate backend exporter and define pipelines for metrics and logs.
-
-```yaml
-exporters:
- debug:
- verbosity: detailed
- otlphttp:
- endpoint: 'https://otel.kloudmate.com:4318'
- headers:
- Authorization: xxxxxxxx # Use the auth key
-
-service:
- pipelines:
-
- metrics:
- receivers: [mysql]
- processors: [batch, resourcedetection, resource] #use the correct resourcedetection
- exporters: [otlphttp]
- logs:
- receivers: [filelog/mysqlslowquery, filelog/mysqlerror]
- processors: [batch, resourcedetection, resource] #use the correct resourcedetection
- exporters: [otlphttp]
- extensions: [health_check, pprof, zpages, file_storage]
-```
-
-### **Step 5: Save Configuration and Restart Agent**
-
-After updating the configuration, click **Save Configuration** in the KloudMate UI.
-
-
-
-You can also restart the agent from your server’s console.
-
-**For Linux:**
-
-1. Execute the following commands:
-
-```none
-systemctl restart kmagent
-systemctl status kmagent
-```
-
-These commands restart the KloudMate Agent and display its current status.
-
-**For Windows**
-
-1. Open the **Services** window
- - Press `Win + R`, type `services.msc`, and press `OK`
- - Or search for **Services** from the Start menu
-2. Locate the **KloudMate Agent** service.
-3. Right-click the service and select **Restart**.
-
-After restarting the agent, verify that MySQL metrics and logs are visible in the KloudMate dashboard.
-
-## Post‑Integration Data Validation
-
-Verify that metrics are flowing into KloudMate using the **Explore** view.
-
-After the agent restarts:
-
-1. Log in to KloudMate
-2. Navigate to **Explore**
-3. Select **OpenTelemetry → Metrics**
-4. Choose a MySQL metric and run the query
-
-Seeing time-series data confirms that MySQL telemetry is flowing successfully.
-
-
-
-## **Standard MySQL Dashboards**
-
-KloudMate provides prebuilt MYSQL dashboards through [dashboard templates](https://templates.kloudmate.com/MySql/index.html). These dashboards visualize MYSQL database performance, query operations, connections, and resource usage.
-
-To import and start using these templates, follow the steps described in [Importing a Dashboard](../../../visualize-data/dashboards/#importing-a-dashboard). Alternatively, use [From Template](../../../visualize-data/dashboards/create-a-dashboard/#from-template) when creating a new dashboard to pick from KloudMate's built-in templates.
-
-## Default Metrics
-
-| Metric Name | Description |
-| ---------------------------------- | ------------------------------------------------------------------- |
-| mysql\_operations | Total operations including fsync, reads and writes |
-| mysql\_buffer\_pool\_data\_pages | Number of data pages for an InnoDB buffer pool |
-| mysql\_buffer\_pool\_limit | Configured size of the InnoDB buffer pool |
-| mysql\_buffer\_pool\_operations | Number of operations on the InnoDB buffer pool |
-| mysql\_buffer\_pool\_page\_flushes | Sum of Requests to flush pages for the InnoDB buffer pool |
-| mysql\_mysqlx\_connections | Total MySQLx connections |
-| mysql\_tmp\_resources | Number of temporary resources created |
-| mysql\_uptime | Number of seconds since the server has been up |
-| mysql\_index\_io\_wait\_count | Total time of I/O wait events for a particular index |
-| mysql\_handlers | Number of requests to various MySQL handlers |
-| mysql\_locks | Total MySQL locks |
-| mysql\_log\_operations | Number of InnoDB log operations |
-| mysql\_index\_io\_wait\_time | Total time of I/O wait events for a particular index |
-| mysql\_row\_locks | Total InnoDB row locks present |
-| mysql\_buffer\_pool\_usage | Number of bytes in the InnoDB buffer pool |
-| mysql.page\_operations | Total operation on InnoDB pages |
-| mysql\_double\_writes | Number of writes to the InnoDB doublewrite buffer pool |
-| mysql\_table\_io\_wait\_count | Total I/O wait events for a specific table |
-| mysql\_table\_io\_wait\_time | Total wait time for I/O events for a table |
-| mysql\_prepared\_statements | Number of times each type of Prepared statement command got issued |
-| mysql\_sorts | Total MySQL sort execution |
-| mysql\_row\_operations | Total row operations executed |
-| mysql\_threads | Current state of MySQL threads |
-| mysql\_opened\_resources | Total opened resources |
-| mysql\_buffer\_pool\_pages | Sum of pages in the InnoDB buffer pool |
-
-For the complete metrics list, refer to the [metrics reference](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/mysqlreceiver/documentation.md).
-
-##
-
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/oracledb-monitoring.mdx b/src/content/docs/docs/database-monitoring/database-integrations/oracledb-monitoring.mdx
deleted file mode 100644
index e0ceab27..00000000
--- a/src/content/docs/docs/database-monitoring/database-integrations/oracledb-monitoring.mdx
+++ /dev/null
@@ -1,745 +0,0 @@
----
-title: "OracleDB"
-description: "Monitor Oracle Database sessions, performance, tablespaces, and logs with the KloudMate Agent and OpenTelemetry."
----
-
-This guide sets up Oracle Database (OracleDB) monitoring with the KloudMate Agent and OpenTelemetry. It collects logs and metrics so you can watch sessions, system statistics, tablespace usage, and performance across OracleDB instances running on AWS EC2, Azure Virtual Machines, or on-premise servers.
-
-# OracleDB Integration using KloudMate Agents
-
-### What This Integration Provides
-
-With OracleDB Monitoring enabled, KloudMate collects telemetry that provides visibility into:
-
-- Database session and system statistics
-- Tablespace utilization and data file metrics
-- Resource limits and consumption patterns
-- Performance bottlenecks and resource contention
-- Server logs and operational events
-
-This visibility helps identify capacity issues, performance degradation, storage constraints, and availability problems.
-
-### Prerequisites
-
-- Oracle Database must be installed and running
-- KloudMate Agent installed on the OracleDB host (see agent installation for [Linux](../../../kloudmate-agent/installation/linux-agent/) and [Windows](../../../kloudmate-agent/installation/windows-agent/))
-
-### Required Permissions
-
-Monitoring database user requires:
-
-```sql
-GRANT SELECT ON V_$SESSION TO ;
-GRANT SELECT ON V_$SYSSTAT TO ;
-GRANT SELECT ON V_$RESOURCE_LIMIT TO ;
-GRANT SELECT ON DBA_TABLESPACES TO ;
-GRANT SELECT ON DBA_DATA_FILES TO ;
-GRANT SELECT ON DBA_TABLESPACE_USAGE_METRICS TO ;
-```
-
-## Receiver Configuration
-
-Two named receiver instances are used to logically separate SQL performance queries from session queries.
-
-| Receiver | Purpose |
-| --- | --- |
-| `sqlquery/high_cpu` | SQL performance queries for top CPU, top elapsed time, top executions, and deadlock logs |
-| `sqlquery/sessions` | Active session and wait event queries |
-
-## Metric Naming Convention
-
-| Metric Prefix | Description |
-| --- | --- |
-| `oracle.sql.*` | Top SQL by CPU time |
-| `oracle.el.*` | Top SQL by elapsed time |
-| `oracle.ex.*` | Top SQL by executions |
-| `oracle.sess.*` | Active session wait events |
-
-## Configuration Overview
-
-### **Step 1: Access Agents and OpenTelemetry Collector Configuration**
-
-- Log in to the KloudMate platform
-- Go to **Settings → Agents**
-- Select the agent running on the OracleDB host
-- Click **Actions → Collector Configuration**
-- YAML editor opens for configuration
-
-
-
-### **Step 2: Configure the OracleDB Receiver**
-
-:::note
- *This receiver collects server-level metrics and system resource usage.*
-:::
-
-```yaml
-receivers:
- oracledb:
- datasource: "oracle://otel:passwd@/XEPDB1"
-```
-
-Connection string format: `oracle://username:password@host:port/service_name`
-
-Replace:
-
-- `otel` → monitoring username
-- `passwd` → monitoring user password
-- `` → OracleDB host IP
-- `XEPDB1` → PDB/service name (adjust for your database)
-
-### **Step 3: Configure the SQL Query Receiver**
-
-:::note
- *Use this receiver to monitor and collect SQL query performance metrics.*
-:::
-
-Two named receiver instances are used to logically separate SQL performance queries from session queries.
-
-```yaml
-sqlquery/high_cpu:
- driver: oracle
- host:
- database:
- port: 1521
- collection_interval: 10s
- username: sys
- password: password
- storage: file_storage
- queries:
- - sql: >
- SELECT * FROM (
- SELECT
- sql_id AS "sql_id",
- sql_text AS "sql_text",
- parsing_schema_name AS "parsing_schema_name",
- executions AS "executions",
- cpu_time/1000000 AS "cpu_seconds",
- elapsed_time/1000000 AS "elapsed_seconds",
- buffer_gets AS "buffer_gets",
- disk_reads AS "disk_reads",
- ROUND(cpu_time/DECODE(executions,0,1,executions)/1000000,2) AS "cpu_per_exec"
- FROM v$sql
- ORDER BY cpu_time DESC
- ) WHERE ROWNUM <= 10
- metrics:
- - metric_name: oracle.sql.cpuseconds
- value_column: cpu_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_text
- - parsing_schema_name
- - sql_id
- - metric_name: oracle.sql.elapseseconds
- value_column: elapsed_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.executions
- value_column: executions
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.buffergets
- value_column: buffer_gets
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.diskreads
- value_column: disk_reads
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.cpuexec
- value_column: cpu_per_exec
- value_type: double
- data_type: gauge
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - sql: >
- SELECT * FROM (
- SELECT
- sql_id AS "sql_id",
- sql_text AS "sql_text",
- parsing_schema_name AS "parsing_schema_name",
- executions AS "executions",
- elapsed_time/1000000 AS "elapsed_seconds",
- cpu_time/1000000 AS "cpu_seconds",
- buffer_gets AS "buffer_gets",
- disk_reads AS "disk_reads",
- ROUND(elapsed_time/DECODE(executions,0,1,executions)/1000000,2) AS "elapsed_per_exec"
- FROM v$sql
- ORDER BY elapsed_time DESC
- ) WHERE ROWNUM <= 10
- metrics:
- - metric_name: oracle.el.elapseseconds
- value_column: elapsed_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.cpuseconds
- value_column: cpu_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.executions
- value_column: executions
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.buffergets
- value_column: buffer_gets
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.diskreads
- value_column: disk_reads
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.elapseexec
- value_column: elapsed_per_exec
- value_type: double
- data_type: gauge
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - sql: |
- SELECT * FROM (
- SELECT
- sql_id AS "sql_id",
- sql_text AS "sql_text",
- parsing_schema_name AS "parsing_schema_name",
- executions AS "executions",
- cpu_time/1000000 AS "cpu_seconds",
- elapsed_time/1000000 AS "elapsed_seconds"
- FROM v$sql
- WHERE executions > 0
- ORDER BY executions DESC
- ) WHERE ROWNUM <= 10
- metrics:
- - metric_name: oracle.ex.executions
- value_column: executions
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.ex.cpuseconds
- value_column: cpu_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.ex.elapseseconds
- value_column: elapsed_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - sql: >
- SELECT
- originating_timestamp AS "originating_timestamp",
- to_char(originating_timestamp, 'DD-MON-YYYY HH24:MI:SS') AS "time",
- message_text AS "message_text"
- FROM v$diag_alert_ext WHERE message_text LIKE '%deadlock%' ORDER BY
- originating_timestamp DESC
- tracking_column: originating_timestamp
- tracking_start_value: 2024-01-01T00:00:00Z
- logs:
- - body_column: message_text
- attribute_columns:
- - time
- - originating_timestamp
-sqlquery/sessions:
- driver: oracle
- host:
- database:
- port: 1521
- collection_interval: 30s
- username: sys
- password: password
- storage: file_storage
- queries:
- - sql: |
- SELECT
- s.sid AS "sid",
- s.username AS "username",
- s.status AS "status",
- s.osuser AS "osuser",
- s.machine AS "machine",
- s.program AS "program",
- s.sql_id AS "sql_id",
- s.event AS "event",
- s.wait_class AS "wait_class",
- s.seconds_in_wait AS "seconds_in_wait",
- s.state AS "state"
- FROM v$session s
- WHERE s.status = 'ACTIVE'
- AND s.username IS NOT NULL
- ORDER BY s.seconds_in_wait DESC
- metrics:
- - metric_name: oracle.sess.secinwait
- value_column: seconds_in_wait
- value_type: int
- data_type: gauge
- attribute_columns:
- - sid
- - username
- - machine
- - program
- - sql_id
- - event
- - wait_class
- - state
- - status
-```
-
-### **Step 4: Export Data to KloudMate**
-
-```yaml
-exporters:
- otlphttp:
- endpoint: 'https://otel.kloudmate.com:4318'
- headers:
- Authorization:
-```
-
-This enables forwarding telemetry to KloudMate for analysis and visualization.
-
-### **Sample Configuration:**
-
-```yaml
-extensions:
- file_storage:
- create_directory: true
-
-receivers:
- oracledb:
- datasource: "oracle://otel:passwd@/XEPDB1"
-
- sqlquery/high_cpu:
- driver: oracle
- host:
- database: XEPDB1
- port: 1521
- collection_interval: 10s
- username: sys
- password:
- storage: file_storage
- queries:
- - sql: >
- SELECT * FROM (
- SELECT
- sql_id AS "sql_id",
- sql_text AS "sql_text",
- parsing_schema_name AS "parsing_schema_name",
- executions AS "executions",
- cpu_time/1000000 AS "cpu_seconds",
- elapsed_time/1000000 AS "elapsed_seconds",
- buffer_gets AS "buffer_gets",
- disk_reads AS "disk_reads",
- ROUND(cpu_time/DECODE(executions,0,1,executions)/1000000,2) AS "cpu_per_exec"
- FROM v$sql
- ORDER BY cpu_time DESC
- ) WHERE ROWNUM <= 10
- metrics:
- - metric_name: oracle.sql.cpuseconds
- value_column: cpu_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_text
- - parsing_schema_name
- - sql_id
- - metric_name: oracle.sql.elapseseconds
- value_column: elapsed_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.executions
- value_column: executions
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.buffergets
- value_column: buffer_gets
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.diskreads
- value_column: disk_reads
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - metric_name: oracle.sql.cpuexec
- value_column: cpu_per_exec
- value_type: double
- data_type: gauge
- attribute_columns:
- - sql_id
- - parsing_schema_name
- - sql_text
- - sql: >
- SELECT * FROM (
- SELECT
- sql_id AS "sql_id",
- sql_text AS "sql_text",
- parsing_schema_name AS "parsing_schema_name",
- executions AS "executions",
- elapsed_time/1000000 AS "elapsed_seconds",
- cpu_time/1000000 AS "cpu_seconds",
- buffer_gets AS "buffer_gets",
- disk_reads AS "disk_reads",
- ROUND(elapsed_time/DECODE(executions,0,1,executions)/1000000,2) AS "elapsed_per_exec"
- FROM v$sql
- ORDER BY elapsed_time DESC
- ) WHERE ROWNUM <= 10
- metrics:
- - metric_name: oracle.el.elapseseconds
- value_column: elapsed_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.cpuseconds
- value_column: cpu_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.executions
- value_column: executions
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.buffergets
- value_column: buffer_gets
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.diskreads
- value_column: disk_reads
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.el.elapseexec
- value_column: elapsed_per_exec
- value_type: double
- data_type: gauge
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - sql: |
- SELECT * FROM (
- SELECT
- sql_id AS "sql_id",
- sql_text AS "sql_text",
- parsing_schema_name AS "parsing_schema_name",
- executions AS "executions",
- cpu_time/1000000 AS "cpu_seconds",
- elapsed_time/1000000 AS "elapsed_seconds"
- FROM v$sql
- WHERE executions > 0
- ORDER BY executions DESC
- ) WHERE ROWNUM <= 10
- metrics:
- - metric_name: oracle.ex.executions
- value_column: executions
- value_type: int
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.ex.cpuseconds
- value_column: cpu_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - metric_name: oracle.ex.elapseseconds
- value_column: elapsed_seconds
- value_type: double
- data_type: sum
- aggregation: cumulative
- monotonic: true
- attribute_columns:
- - sql_id
- - sql_text
- - parsing_schema_name
- - sql: >
- SELECT
- originating_timestamp AS "originating_timestamp",
- to_char(originating_timestamp, 'DD-MON-YYYY HH24:MI:SS') AS "time",
- message_text AS "message_text"
- FROM v$diag_alert_ext WHERE message_text LIKE '%deadlock%' ORDER BY
- originating_timestamp DESC
- tracking_column: originating_timestamp
- tracking_start_value: 2024-01-01T00:00:00Z
- logs:
- - body_column: message_text
- attribute_columns:
- - time
- - originating_timestamp
- sqlquery/sessions:
- driver: oracle
- host:
- database: XEPDB1
- port: 1521
- collection_interval: 30s
- username: sys
- password: password
- storage: file_storage
- queries:
- - sql: |
- SELECT
- s.sid AS "sid",
- s.username AS "username",
- s.status AS "status",
- s.osuser AS "osuser",
- s.machine AS "machine",
- s.program AS "program",
- s.sql_id AS "sql_id",
- s.event AS "event",
- s.wait_class AS "wait_class",
- s.seconds_in_wait AS "seconds_in_wait",
- s.state AS "state"
- FROM v$session s
- WHERE s.status = 'ACTIVE'
- AND s.username IS NOT NULL
- ORDER BY s.seconds_in_wait DESC
- metrics:
- - metric_name: oracle.sess.secinwait
- value_column: seconds_in_wait
- value_type: int
- data_type: gauge
- attribute_columns:
- - sid
- - username
- - machine
- - program
- - sql_id
- - event
- - wait_class
- - state
- - status
-
-processors:
- memory_limiter:
- check_interval: 1s
- limit_mib: 1000
- spike_limit_mib: 800
- batch:
- send_batch_size: 1000
- timeout: 10s
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: host.name
- resourcedetection:
- detectors:
- - system
- system:
- resource_attributes:
- host.name:
- enabled: true
- host.id:
- enabled: true
- os.type:
- enabled: false
-
-exporters:
- debug:
- verbosity: detailed
- otlphttp:
- endpoint: 'https://otel.kloudmate.com:4318'
- headers:
- Authorization:
-
-service:
- pipelines:
- metrics:
- receivers: [oracledb, sqlquery/high_cpu, sqlquery/sessions]
- processors: [resourcedetection, resource, memory_limiter, batch]
- exporters: [debug, otlphttp]
- logs:
- receivers: [oracledb, sqlquery/high_cpu]
- processors: [resourcedetection, resource, memory_limiter, batch]
- exporters: [debug, otlphttp]
- extensions:
- - file_storage
-```
-
-## Post-Integration Data Validation
-
-Verify that metrics are flowing into KloudMate using the **Explore** view.
-
-After the agent restarts:
-
-1. Log in to KloudMate
-2. Navigate to **Explore**
-3. Select **OpenTelemetry → Metrics**
-4. Choose a OracleDB metric and run the query
-
-Seeing time-series data confirms that OracleDB telemetry is flowing successfully.
-
-## Set Up KloudMate Dashboards and Alerts
-
-Access KloudMate and create dashboards to visualize OracleDB metrics and logs. Configure alerting rules for critical thresholds and unusual activity.
-
-
-
-## Default Metrics
-
-The following metrics are collected through the `oracledb` receiver:
-
-| **Default Metrics** |
-| --- |
-| oracledb_cpu_time |
-| oracledb_dml_locks_limit |
-| oraclb_dml_locks_usage |
-| oracledb_enqueue_deadlocks |
-| oracledb_enqueue_locks_limit |
-| oracledb_enqueue_locks_usage |
-| oracledb_enqueue_resources_limit |
-| oracledb_enqueue_resources_usage |
-| oracledb_exchange_deadlocks |
-| oracledb_executions |
-| oracledb_hard_parses |
-| Oracledb_logical_reads |
-| oracledb_parse_calls |
-| oracledb_pga_memory |
-| oracledb_physical_reads |
-| oracledb_processes_limit |
-| oracledb_processes_usage |
-| oracledb_sessions_limit |
-| oracledb_sessions_usage |
-| oracledb_tablespace_size_limit |
-| Oracledb_tablespace_size_usage |
-| oracledb_transactions_limit |
-| oracledb_transactions_usage |
-| oracledb_user_commits |
-| oracledb_user_rollbacks |
-
-| **Optional Metrics** |
-| --- |
-| oracledb_consistent_gets |
-| oracledb_db_block_gets |
-
-For the complete metrics list, refer to the [metrics reference](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/oracledbreceiver/documentation.md).
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/postgresql-monitoring.mdx b/src/content/docs/docs/database-monitoring/database-integrations/postgresql-monitoring.mdx
deleted file mode 100644
index 60f9792c..00000000
--- a/src/content/docs/docs/database-monitoring/database-integrations/postgresql-monitoring.mdx
+++ /dev/null
@@ -1,301 +0,0 @@
----
-title: "PostgreSQL"
-description: "Monitor PostgreSQL health, performance, queries, and logs with the KloudMate Agent and OpenTelemetry."
----
-
-
-This guide sets up PostgreSQL monitoring with the **KloudMate Agent** and OpenTelemetry. It collects metrics and logs so you can watch the health, performance, and query behavior of PostgreSQL instances running on AWS EC2, Azure Virtual Machines, or on-premise servers.
-
-# PostgreSQL Integration using KloudMate Agents
-
-### **What This Integration Provides**
-
-With PostgreSQL Monitoring enabled, KloudMate collects telemetry that provides visibility into:
-
-- Database health and performance metrics
-- Locks, deadlocks, and query behavior
-- Slow queries through log analysis
-- Resource usage at host level
-- PostgreSQL server logs
-
-This visibility helps identify performance bottlenecks, long-running queries, locking issues, and availability problems.
-
-### **Pre-requisites**
-
-Before configuring PostgreSQL Monitoring, ensure the following:
-
-1. Refer to PostgreSQL documentation for [supported versions](https://www.postgresql.org/support/versioning/)
-2. Monitoring user must have SELECT permission on pg\_stat\_database
-3. PostgreSQL is installed and running
-4. KloudMate Agent installed on the PostgreSQL host
- - Refer to Agent installation for [Linux](../../../kloudmate-agent/installation/linux-agent/) or [Windows](../../../kloudmate-agent/installation/windows-agent/)
-5. PostgreSQL username and password
-6. PostgreSQL logging enabled in the server configuration
-
-### **Step 1: Access Agents and OpenTelemetry Collector Configuration**
-
-- Log in to the KloudMate platform
-- Go to **Settings → Agents**
-- Select the agent running on the PostgreSQL host
-- Click **Actions → Collector Configuration**
-- YAML editor opens for configuration
-
-
-
-### **Step 2: Add Required Extensions and Receivers**
-
-**Extensions**
-
-```yaml
-extensions:
- file_storage:
- create_directory: true
-```
-
-**Receivers**
-
-Two receivers are used:
-
-- **PostgreSQL Receiver –** collects metrics
-- **File Log Receiver –** collects logs (general, error, slow queries)
-
-If you only need metrics, configure only the PostgreSQL receiver.
-
-```yaml
-receivers:
- postgresql:
- endpoint: localhost:5432 #update the endpoint
- transport: tcp
- username: #update postgres username
- password: #Update postgres password
- databases: []
- collection_interval: 30s
- tls:
- insecure: true
- metrics:
- postgresql.deadlocks:
- enabled: true
- postgresql.database.locks:
- enabled: true
-
- filelog/pgsql_json: #use this file log if postgres version >= 15
- include: [/var/lib/postgresql/*/main/log/postgresql*]
- storage: file_storage
- retry_on_failure:
- enabled: true
- operators:
- - type: json_parser
- parse_to: body
- severity:
- parse_from: body.error_severity
- mapping:
- info: LOG
-
- filelog/pgsql_log: #use this file log if postgres version < 15
- include: [/var/lib/postgresql/*/main/log/postgresql*]
- storage: file_storage
- multiline:
- line_start_pattern: ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}
- retry_on_failure:
- enabled: true
- operators:
- - type: regex_parser
- parse_to: body
- regex: '^(?P[0-9\-]+\s[0-9:\.]+\s\w+)\s+\[(?P\d+)\](?:\s+(?P\w+)@(?P\w+))?\s+(?P\w+):(?:\s+duration:\s+(?P\d+\.\d+)\s+[^ ]*\s+(statement:\s+)?)?\s+(?P.+)$'
- timestamp:
- parse_from: body.timestamp
- layout: '%Y-%m-%d %H:%M:%S.%f %Z'
- severity:
- parse_from: body.severity
- mapping:
- info:
- - STATEMENT
- - LOG
- - type: remove
- field: body.severity
- - type: remove
- field: body.timestamp
-```
-
-### **Step 3: Configure Processors**
-
-Set up the processor component to identify resource information from the host and either append or replace the resource values in the telemetry data with this information.
-
-Choose **one configuration** based on where PostgresSQL is running.
-
-- **Server (On-premise / Non-cloud / Cloud)**
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection:
- detectors: [system]
- system:
- resource_attributes:
- host.name:
- enabled: true
- host.id:
- enabled: true
- os.type:
- enabled: false
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: host.name
-```
-
-- **AWS EC2:**
-
-(Optional) To collect EC2 tags, attach an IAM role with `EC2:DescribeTags` permission.
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/ec2:
- detectors: ["ec2"]
- ec2:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: ec2.tag.Name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-- **Azure Virtual Machines:**
-
-```yaml
-processors:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
- resourcedetection/azure:
- detectors: ["azure"]
- azure:
- tags:
- - ^tag1$
- - ^tag2$
- - ^label.*$
- - ^Name$
-
- resource:
- attributes:
- - key: service.name
- action: insert
- from_attribute: azure.vm.name
- - key: service.instance.id
- action: insert
- from_attribute: host.id
-```
-
-### **Step 4: Configure Exporter and Pipelines**
-
-Configure the KloudMate backend exporter and define pipelines for metrics and logs.
-
-```yaml
-exporters:
- debug:
- verbosity: detailed
- otlphttp:
- endpoint: 'https://otel.kloudmate.com:4318'
- headers:
- Authorization: xxxxxxxx # Use the auth key
-
-service:
- pipelines:
-
- metrics:
- receivers: [postgresql]
- processors: [batch, resourcedetection, resource] # Apply the correct resourcedetection
- exporters: [otlphttp]
-
- logs:
- receivers: [pgsql_log] #use the correct filelog receiver as per your pg version
- processors: [batch, resourcedetection, resource] # Apply the correct resourcedetection
- exporters: [otlphttp]
-
- extensions: [health_check, pprof, zpages, file_storage]
-```
-
-### **Step 5: Save Configuration and Restart Agent**
-
-After updating the configuration, click **Save Configuration** in the KloudMate UI.
-
-
-
-You can also restart the agent from your server’s console.
-
-**For Linux:**
-
-1. Execute the following commands:
-
-```none
-systemctl restart kmagent
-systemctl status kmagent
-```
-
-These commands restart the KloudMate Agent and display its current status.
-
-**For Windows**
-
-1. Open the **Services** window
- - Press `Win + R`, type `services.msc`, and press `OK`
- - Or search for **Services** from the Start menu
-2. Locate the **KloudMate Agent** service.
-3. Right-click the service and select **Restart**.
-
-After restarting the agent, verify that PostgreSQL metrics and logs are visible in the KloudMate dashboard.
-
-## Post‑Integration Data Validation
-
-Verify that metrics are flowing into KloudMate using the **Explore** view.
-
-After the agent restarts:
-
-1. Log in to KloudMate
-2. Navigate to **Explore**
-3. Select **OpenTelemetry → Metrics**
-4. Choose a PostgreSQL metric and run the query
-
-Seeing time-series data confirms that PostgreSQL telemetry is flowing successfully.
-
-
-
-## **Standard PostgreSQL Dashboards**
-
-KloudMate provides prebuilt PostgreSQL dashboards through [dashboard templates](https://templates.kloudmate.com/postgress/index.html). These dashboards visualize PostgreSQL database performance, query operations, connections, and resource usage.
-
-To import and start using these templates, follow the steps described in [Importing a Dashboard](../../../visualize-data/dashboards/#importing-a-dashboard). Alternatively, use [From Template](../../../visualize-data/dashboards/create-a-dashboard/#from-template) when creating a new dashboard to pick from KloudMate's built-in templates.
-
-## Default Metrics for PostgreSQL
-
-| Name | Description | Type | Unit |
-| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------- |
-| postgresql\_backends | The number of backends | SUM | NONE |
-| postgresql\_connection\_max | Configured the maximum number of client connections allowed | GAUGE | NONE |
-| postgresql\_db\_size | The database disk usage | SUM | Bytes |
-| postgresql\_commits | The number of commits | SUM | NONE |
-| postgresql\_deadlocks | The number of deadlocks. | SUM | NONE |
-| postgresql\_database\_locks | The number of database locks. | GAUGE | COUNT |
-| postgresql\_wal\_lag | The time between flushing the recent WAL locally and receiving notification that the standby server has completed an operation on it. | GAUGE | seconds |
-| postgresql\_replication\_data\_delay | The amount of data delayed in replication | GAUGE | Bytes |
-
-For the complete metrics list, refer to the [metrics reference](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/postgresqlreceiver/documentation.md).
-
-For complete observability, ensure **PostgreSQL logging** is enabled first. See: [PostgreSQL Logging: Enable Slow Query Logs & JSON Output (v15+)](https://blog.kloudmate.com/postgresql-logging-enable-slow-query-logs-json-output-v15-6ad493f28241)
diff --git a/src/content/docs/docs/database-monitoring/database-integrations/redis-monitoring.mdx b/src/content/docs/docs/database-monitoring/database-integrations/redis-monitoring.mdx
deleted file mode 100644
index f544845b..00000000
--- a/src/content/docs/docs/database-monitoring/database-integrations/redis-monitoring.mdx
+++ /dev/null
@@ -1,104 +0,0 @@
----
-title: "Redis"
-description: "Monitor Redis health and performance with the KloudMate Agent and OpenTelemetry."
----
-This guide sets up Redis monitoring with the **KloudMate Agent** and OpenTelemetry. It collects metrics so you can watch the health and performance of Redis instances running on cloud virtual machines or on-premise servers.
-
-### What This Integration Provides
-
-With Redis Monitoring enabled, KloudMate collects telemetry that provides visibility into:
-
-- Command execution and throughput
-- Memory usage and fragmentation
-- Client connections and blocked clients
-- Network input and output
-- Replication and persistence behavior
-- Redis server uptime and CPU time
-
-This visibility helps detect performance degradation, memory pressure, command latency issues, connection limits, and replication problems.
-
-### **Configuring Redis Metrics Collection**
-
-You can achieve Redis performance monitoring by using the KloudMate Agent, which automatically collects Redis metrics and sends them to KloudMate for analysis and visualization.
-
-If you are already running the KloudMate Agent and want to configure it to send Redis telemetry data to KloudMate, update your YAML configuration as shown below.
-
-### **Agent Configuration**
-
-```yaml
-extensions:
- health_check:
- pprof:
- endpoint: 0.0.0.0:1777
- zpages:
- endpoint: 0.0.0.0:55679
-
-receivers:
- otlp:
- protocols:
- grpc:
- http:
-
- redis:
- endpoint: localhost:6379
- collection_interval: 10s
-
-processors:
- resourcedetection:
- detectors: [env, system]
- cumulativetodelta:
- batch:
- send_batch_size: 5000
- timeout: 10s
-
-exporters:
- otlphttp:
- endpoint: 'https://otel.kloudmate.com:4318'
- headers:
- Authorization:
-
-service:
-
- pipelines:
-
- metrics:
- receivers: [otlp, opencensus, prometheus, redis]
- processors: [cumulativetodelta, batch, resourcedetection]
- exporters: [otlphttp]
-
- extensions: [health_check, pprof, zpages]
-```
-
-## **Post-Integration Data Validation**
-
-Verify that Redis metrics are flowing into KloudMate using the Explore view.
-
-After the agent restarts:
-
-1. Log in to your KloudMate account
-2. Navigate to **Explore**
-3. Select **OpenTelemetry → Metrics**
-4. Choose a Redis metric exposed by the integration
-5. Click **Run Query** to view time-series data
-
-Seeing data confirms that Redis telemetry is being collected successfully.
-
-
-
-## Redis metrics
-
-| **Name** | **Description** |
-| ----------------------------------- | ------------------------------------------------------------------ |
-| redis\_commands | Number of commands processed per second |
-| redis\_cmd\_latency | Command execution latency |
-| redis\_memory\_used | Total number of bytes allocated by Redis using its allocator |
-| redis\_memory\_fragmentation\_ratio | Ratio between used\_memory\_rss and used\_memory |
-| redis\_clients\_connected | Number of client connections (excluding connections from replicas) |
-| redis\_keys\_evicted | Number of evicted keys due to maxmemory limit |
-| redis\_net\_input | The total number of bytes read from the network |
-| redis\_net\_output | The total number of bytes written to the network |
-| redis\_replication\_offset | The server's current replication offset |
-| redis\_uptime | Number of seconds since the Redis server started |
-
-For the complete Redis metrics list, refer to the [metrics reference](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/redisreceiver/documentation.md).
-
diff --git a/src/content/docs/docs/database-monitoring/databases-list.mdx b/src/content/docs/docs/database-monitoring/databases-list.mdx
new file mode 100644
index 00000000..01553c41
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/databases-list.mdx
@@ -0,0 +1,61 @@
+---
+title: "The Databases list"
+description: "Find the databases that need attention by status and health, filter by engine, and add a new database to monitor."
+sidebar:
+ order: 2
+---
+
+The **Databases** page lists every monitored database instance with its status, health, query load, and connections. Use it to spot the instances that have stopped reporting or are running hot, then click an instance to open its [details](../instance-overview/).
+
+
+
+## Status and health
+
+**Status** tells you whether the instance is reporting. An instance is **Available** while the agent keeps sending its heartbeat, and **Not reporting** when no heartbeat has arrived in the last five minutes. On wide time ranges the window stretches to three data points.
+
+**Health** summarizes how the instance is doing, as the worst level across these categories:
+
+| Category | Measures | Warning | Critical |
+|---|---|---|---|
+| CPU | Host CPU utilization | 80% | 90% |
+| Memory | Host memory utilization | 85% | 95% |
+| Disk | Host filesystem utilization | 80% | 90% |
+| Connections | Current connections as a share of the connection limit | 80% | 90% |
+| Cache hit | MySQL and MariaDB buffer pool hit ratio | 90% or lower | 80% or lower |
+| Replication lag | Seconds behind the primary, on MySQL and MariaDB replicas | 30 s | 120 s |
+
+Hover the health icon to see the level of each category. A few rules affect how you read it:
+
+- **CPU**, **Memory**, and **Disk** only count when the database runs on the same host as the agent. For a managed database, such as Amazon RDS, the agent's host tells you nothing about the database, so these categories are left out.
+- A category with no data doesn't count as healthy. If no category has data, the icon shows that there's no health data yet.
+- An instance that isn't reporting shows **Not reporting** for health too.
+
+Health comes from thresholds on the collected metrics. It isn't tied to your alert rules. When an alert rule is firing for an instance, the instance's header shows it, as described in [Instance details and Overview](../instance-overview/#the-instance-header).
+
+## Filter and search
+
+Use the filters above the table to show one status or one engine at a time. Selecting a filter replaces the previous one, and clicking the active filter again clears it. **All** clears every filter.
+
+The search box matches an instance's name, endpoint, host, and engine.
+
+## Replicas and clusters
+
+When two or more members of the same cluster are monitored, they're grouped under the primary, with the cluster type and each member's role in the **Cluster** column. Collapse a group to hide its replicas. Paging counts a group as one row, so a group never splits across pages.
+
+## Read the columns
+
+- **Queries per second** and **Queries total** come from the instance's query rate. **Queries total** is that rate over the selected time range.
+- **Queries total time** is the time the database spent running queries in the range.
+- A dash in a query column means the instance hasn't reported query metrics yet.
+- **Connections current** shows the latest connection count next to its trend over the range.
+
+You can sort by instance name, the query columns, connections, and uptime. Instances with no value for the sorted column always sort last.
+
+## Add a database
+
+1. Click **Monitor a database**.
+2. Choose the agent that will monitor the database. The agent must be able to reach the database: install it on the database's own host for a self-hosted database, or on a host with network access to a managed one. Agents in manual configuration mode can't be used here; switch the agent to managed in **Settings** first.
+3. Choose the engine. The list shows the engines that agent supports.
+4. Configure the database in the agent's database monitoring settings, which open with the agent and engine already selected. See [Database monitoring overview](../../kloudmate-agent/database-monitoring/overview/) for the coverages and credentials.
+
+Data starts arriving within a minute of saving.
diff --git a/src/content/docs/docs/database-monitoring/direct-database-monitoring.mdx b/src/content/docs/docs/database-monitoring/direct-database-monitoring.mdx
deleted file mode 100644
index 9d2c5a5b..00000000
--- a/src/content/docs/docs/database-monitoring/direct-database-monitoring.mdx
+++ /dev/null
@@ -1,491 +0,0 @@
----
-title: "Direct Database Monitoring"
-description: "Connect the KloudMate Agent directly to a database with a read-only user for query metrics, execution plans, and schema metadata."
-sidebar:
- order: 4
----
-
-The KloudMate Agent can connect directly to your database using a dedicated monitoring user to collect deep query metrics, execution plans, and schema metadata. It collects the most detailed query-level data, including per-query latency breakdowns, explain plans, and wait event analysis, across self-hosted and managed database services.
-
-:::note
- When to Use
-
- Use Direct Database Monitoring when you need deep query-level analytics, including execution plans, query digest metrics, wait event analysis, and schema discovery. This is the most complete monitoring approach and works on any environment: self-hosted, RDS, Aurora, Cloud SQL, Azure, or AlloyDB.
-:::
-
-## How It Works
-
-1. You create a dedicated `kloudmate` database user with minimal read-only permissions
-2. Helper procedures/functions (using `SECURITY DEFINER`) allow the agent to access query stats and run `EXPLAIN` without needing superuser privileges
-3. The KloudMate Agent connects using this user and periodically collects query metrics, live queries, execution plans, and schema metadata
-
-## MySQL Setup
-
-### 1. Create Monitoring User
-
-Connect as a user with admin privileges (for RDS/Aurora, use the master user):
-
-```sql
-CREATE USER IF NOT EXISTS 'kloudmate'@'%'
- IDENTIFIED BY '';
-
--- Limit connections to prevent exhausting the pool
-ALTER USER 'kloudmate'@'%' WITH MAX_USER_CONNECTIONS 5;
-```
-
-### **2. Grant Base Permissions**
-
-```sql
--- PROCESS: See all running threads and queries
--- REPLICATION CLIENT: Check replication lag and binary log position
--- SELECT on perf_schema: Read query digest stats, wait events, etc.
-
-GRANT REPLICATION CLIENT ON *.* TO 'kloudmate'@'%';
-GRANT PROCESS ON *.* TO 'kloudmate'@'%';
-GRANT SELECT ON performance_schema.* TO 'kloudmate'@'%';
-```
-
-### 3. Create Helper Procedures
-
-Create a dedicated schema and the helper procedures the agent uses:
-
-```sql
-CREATE SCHEMA IF NOT EXISTS kloudmate;
-GRANT EXECUTE ON kloudmate.* TO 'kloudmate'@'%';
-```
-
-**Enable Performance Schema, Consumers**
-
-On RDS/Aurora/Cloud SQL, you cannot permanently enable `performance_schema` consumers via config. This procedure lets the agent enable them at runtime:
-
-```sql
-DELIMITER $$
-CREATE PROCEDURE IF NOT EXISTS kloudmate.enable_events_statements_consumers()
-SQL SECURITY DEFINER
-BEGIN
- UPDATE performance_schema.setup_consumers
- SET enabled = 'YES'
- WHERE name LIKE 'events_statements_%';
-
- UPDATE performance_schema.setup_consumers
- SET enabled = 'YES'
- WHERE name = 'events_waits_current';
-END $$
-DELIMITER ;
-
-GRANT EXECUTE ON PROCEDURE kloudmate.enable_events_statements_consumers TO 'kloudmate'@'%';
-```
-
-**Explain Statement**
-
-Collects JSON execution plans for sampled queries:
-
-```sql
-DELIMITER $$
-CREATE PROCEDURE IF NOT EXISTS kloudmate.explain_statement(IN query TEXT)
-SQL SECURITY DEFINER
-BEGIN
- SET @explain := CONCAT('EXPLAIN FORMAT=json ', query);
- PREPARE stmt FROM @explain;
- EXECUTE stmt;
- DEALLOCATE PREPARE stmt;
-END $$
-DELIMITER ;
-
-GRANT EXECUTE ON PROCEDURE kloudmate.explain_statement TO 'kloudmate'@'%';
-```
-
-### **4. Enable Performance Schema**
-
-:::caution
- RDS / Aurora
-
- Performance Schema must be enabled via the **Parameter Group** in the AWS Console or CLI — it cannot be set via SQL.
-
- Set the following parameters and **reboot** the instance:
-
- - `performance_schema = 1`
- - `max_digest_length = 4096`
- - `performance_schema_max_digest_length = 4096`
- - `performance_schema_max_sql_text_length = 4096`
-:::
-
-**Self-Hosted** — add to your `my.cnf` / `mysqld.cnf`:
-
-```ini
-[mysqld]
-performance_schema = ON
-performance-schema-consumer-events-statements-current = ON
-performance-schema-consumer-events-statements-history = ON
-performance-schema-consumer-events-statements-history-long = ON
-performance-schema-consumer-events-waits-current = ON
-max_digest_length = 4096
-performance_schema_max_digest_length = 4096
-performance_schema_max_sql_text_length = 4096
-```
-
-### 5. Optional: Schema Collection
-
-To enable the KloudMate Schema Explorer, grant `SELECT` on the databases the agent should discover:
-
-```sql
--- Option A: All databases
-GRANT SELECT ON *.* TO 'kloudmate'@'%';
-
--- Option B: Specific databases only
-GRANT SELECT ON your_app_db.* TO 'kloudmate'@'%';
-```
-
-:::note
- The agent queries `information_schema` for metadata — it does **not** read your actual row data.
-:::
-
-### **6. Finalize**
-
-```sql
-FLUSH PRIVILEGES;
-```
-
-### 7. Verify Setup
-
-Run these as the `kloudmate` user:
-
-```sql
--- Check performance_schema is enabled
-SELECT @@performance_schema;
-
--- Check consumers are active
-SELECT name, enabled FROM performance_schema.setup_consumers
- WHERE name LIKE 'events_statements_%' OR name = 'events_waits_current';
-
--- Check query digests are being collected
-SELECT COUNT(*) FROM performance_schema.events_statements_summary_by_digest;
-
--- Verify procedures exist
-SHOW PROCEDURE STATUS WHERE db = 'kloudmate';
-
--- Test explain
-CALL kloudmate.explain_statement('SELECT 1');
-```
-
-### **MySQL Agent Configuration**
-
-```yaml
-receivers:
- kloudmate_mysql:
- endpoint: ":3306"
- username: "kloudmate"
- password: "${KLOUDMATE_DB_PASSWORD}"
- database: "" # empty = monitor all databases
- collect_query_metrics: true
- collect_query_samples: true
- collect_explain_plans: true
- collect_schema: true # requires SELECT grants (step 5)
- collection_interval: 10s
- query_samples_rate: 1 # sample every Nth query
- tls:
- enabled: true # recommended for RDS
- skip_verify: false
-```
-
-For RDS with IAM authentication:
-
-```yaml
-receivers:
- kloudmate_mysql:
- endpoint: ":3306"
- auth_type: "iam"
- username: "kloudmate"
- aws:
- region: "ap-south-1"
- instance_endpoint: ""
-```
-
-***
-
-## PostgreSQL Setup
-
-### 1. Create Monitoring User
-
-Connect as a superuser (or rds\_superuser on RDS/Aurora) to the postgres database:
-
-```sql
-CREATE USER kloudmate WITH PASSWORD '';
-```
-
-### **2. Grant Base Permissions**
-
-```sql
--- pg_monitor: Built-in role (PG 10+) granting read access to
--- pg_stat_activity, pg_stat_replication, pg_stat_database, and more.
-GRANT pg_monitor TO kloudmate;
-```
-
-### 3. Enable pg\_stat\_statements
-
-`pg_stat_statements` is the primary data source for query metrics (latency, calls, rows):
-
-```sql
-CREATE EXTENSION IF NOT EXISTS pg_stat_statements SCHEMA public;
-```
-
-:::caution
- RDS / Aurora
-
- Add `pg_stat_statements` to s`hared_preload_libraries` in your RDS parameter group and **reboot** the instance.
-:::
-
-**Self-Hosted** — add to `postgresql.conf`:
-
-```sql
-shared_preload_libraries = 'pg_stat_statements'
-pg_stat_statements.track = all
-pg_stat_statements.max = 10000
-track_activity_query_size = 4096
-```
-
-### 4. Create Helper Functions
-
-Create a dedicated schema and the SECURITY DEFINER wrapper functions:
-
-```sql
-CREATE SCHEMA IF NOT EXISTS kloudmate;
-GRANT USAGE ON SCHEMA kloudmate TO kloudmate;
-GRANT USAGE ON SCHEMA public TO kloudmate;
-```
-
-:::note
- The `SECURITY DEFINER` pattern means these functions execute with the privileges of the admin who created them, not the `kloudmate` user calling them. This is how the agent gains full visibility into query stats and activity on managed databases without requiring superuser access.
-:::
-
-**pg\_stat\_activity Wrapper**
-
-Ensures full visibility into all sessions' query text (required on RDS where non-superusers have restricted access):
-
-```sql
-CREATE OR REPLACE FUNCTION kloudmate.pg_stat_activity()
-RETURNS SETOF pg_stat_activity
-AS $$
- SELECT * FROM pg_catalog.pg_stat_activity;
-$$
-LANGUAGE sql
-SECURITY DEFINER;
-```
-
-**pg\_stat\_statements Wrapper**
-
-Ensures full visibility into all query statistics:
-
-```sql
-CREATE OR REPLACE FUNCTION kloudmate.pg_stat_statements()
-RETURNS SETOF pg_stat_statements
-AS $$
- SELECT * FROM pg_stat_statements;
-$$
-LANGUAGE sql
-SECURITY DEFINER;
-```
-
-**Explain Statement**
-
-Collects JSON execution plans for sampled queries:
-
-```sql
-CREATE OR REPLACE FUNCTION kloudmate.explain_statement(
- l_query TEXT,
- OUT explain JSON
-)
-RETURNS SETOF JSON
-AS $$
-DECLARE
- curs REFCURSOR;
- plan JSON;
-BEGIN
- OPEN curs FOR EXECUTE pg_catalog.concat('EXPLAIN (FORMAT JSON) ', l_query);
- LOOP
- FETCH curs INTO plan;
- EXIT WHEN NOT FOUND;
- RETURN QUERY SELECT plan;
- END LOOP;
- CLOSE curs;
- RETURN;
-END;
-$$
-LANGUAGE plpgsql
-SECURITY DEFINER;
-```
-
-### 5. Optional Functions
-
-**Stats Reset**
-
-Safe way to reset `pg_stat_statements` counters without direct superuser access:
-
-```sql
-CREATE OR REPLACE FUNCTION kloudmate.pg_stat_statements_reset()
-RETURNS VOID
-AS $$
- SELECT pg_stat_statements_reset();
-$$
-LANGUAGE sql
-SECURITY DEFINER;
-```
-
-**Replication Monitoring** Only needed on the **primary / writer** instance for monitoring replication lag:
-
-```sql
-CREATE OR REPLACE FUNCTION kloudmate.pg_stat_replication()
-RETURNS SETOF pg_stat_replication
-AS $$
- SELECT * FROM pg_catalog.pg_stat_replication;
-$$
-LANGUAGE sql
-SECURITY DEFINER;
-```
-
-### 6. Configure Parameters
-
-:::caution
- RDS / Aurora Parameter Group
-
- Set the following in the AWS Console or CLI and **reboot**:
-
- - `shared_preload_libraries = 'pg_stat_statements'`
- - `track_activity_query_size = 4096`
- - `pg_stat_statements.track = all`
- - `pg_stat_statements.max = 10000` (recommended)
-:::
-
-**Self-Hosted** — add to `postgresql.conf`:
-
-```sql
-shared_preload_libraries = 'pg_stat_statements'
-pg_stat_statements.track = all
-pg_stat_statements.max = 10000
-track_activity_query_size = 4096
-track_io_timing = on # required for I/O metrics
-log_min_duration_statement = 1000 # log slow queries (ms)
-```
-
-### 7. Optional: auto\_explain
-
-For full `EXPLAIN ANALYZE` plans (actual execution times, not just estimates):
-
-```sql
-shared_preload_libraries = 'pg_stat_statements,auto_explain'
-auto_explain.log_min_duration = 1000
-auto_explain.log_analyze = true
-auto_explain.log_buffers = true
-auto_explain.log_timing = true
-auto_explain.log_triggers = true
-auto_explain.log_verbose = true
-auto_explain.log_format = json
-auto_explain.log_nested_statements = true
-auto_explain.sample_rate = 1 # 1 = all, 0.1 = 10% sample
-```
-
-:::caution
- `auto_explain` with `log_analyze=true` adds overhead because it executes the query plan. Use `sample_rate < 1` in production.
-:::
-
-### **8. Repeat for Each Database**
-
-:::note
- `pg_stat_statements` and the kloudmate functions must exist in **every** database the agent monitors. The agent connects to one database at a time.
-:::
-
-For each additional database (e.g. `app_db`, `analytics_db`), connect and re-run the extension + function creation:
-
-```sql
-for db in $(psql -t -c "SELECT datname FROM pg_database WHERE datistemplate = false"); do
- psql -d "$db" -f kloudmate_functions.sql
-done
-```
-
-### 9. Verify Setup
-
-Run as the `kloudmate` user:
-
-```sql
--- Check pg_stat_statements is loaded
-SELECT count(*) FROM pg_stat_statements;
-
--- Check functions exist
-SELECT routine_name FROM information_schema.routines
- WHERE routine_schema = 'kloudmate';
-
--- Test pg_stat_activity wrapper
-SELECT count(*) FROM kloudmate.pg_stat_activity();
-
--- Test explain
-SELECT * FROM kloudmate.explain_statement('SELECT 1');
-
--- Check track_activity_query_size
-SHOW track_activity_query_size;
-
--- Check track_io_timing
-SHOW track_io_timing;
-```
-
-**PostgreSQL Agent Configuration**
-
-```yaml
-receivers:
- kloudmate_postgresql:
- endpoint: ":5432"
- username: "kloudmate"
- password: "${KLOUDMATE_DB_PASSWORD}"
- database: "postgres" # primary connection database
- databases_to_monitor: # additional databases
- - "app_db"
- - "analytics_db"
- collect_query_metrics: true
- collect_query_samples: true
- collect_explain_plans: true
- collect_schema: true
- collect_replication_stats: true
- collection_interval: 10s
- tls:
- mode: "require" # require | verify-ca | verify-full
- ca_cert: "/path/to/rds-ca-bundle.pem"
-```
-
-For RDS with IAM authentication:
-
-```yaml
-receivers:
- kloudmate_postgresql:
- endpoint: ":5432"
- auth_type: "iam"
- username: "kloudmate"
- database: "postgres"
- aws:
- region: "ap-south-1"
- instance_endpoint: ""
-```
-
-***
-
-## Cleanup / Uninstall
-
-### MySQL
-
-```sql
-DROP PROCEDURE IF EXISTS kloudmate.enable_events_statements_consumers;
-DROP PROCEDURE IF EXISTS kloudmate.explain_statement;
-DROP SCHEMA IF EXISTS kloudmate;
-DROP USER IF EXISTS 'kloudmate'@'%';
-```
-
-### **PostgreSQL**
-
-```sql
-DROP FUNCTION IF EXISTS kloudmate.pg_stat_activity();
-DROP FUNCTION IF EXISTS kloudmate.pg_stat_statements();
-DROP FUNCTION IF EXISTS kloudmate.explain_statement(TEXT);
-DROP FUNCTION IF EXISTS kloudmate.pg_stat_statements_reset();
-DROP FUNCTION IF EXISTS kloudmate.pg_stat_replication();
-DROP SCHEMA IF EXISTS kloudmate;
-DROP USER IF EXISTS kloudmate;
-```
-
diff --git a/src/content/docs/docs/database-monitoring/images/agent-jobs.png b/src/content/docs/docs/database-monitoring/images/agent-jobs.png
new file mode 100644
index 00000000..ce04d333
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/agent-jobs.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/calling-services.png b/src/content/docs/docs/database-monitoring/images/calling-services.png
new file mode 100644
index 00000000..1b55fd49
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/calling-services.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/databases-list.png b/src/content/docs/docs/database-monitoring/images/databases-list.png
new file mode 100644
index 00000000..eb81946c
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/databases-list.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/databases-overview-1.jpeg b/src/content/docs/docs/database-monitoring/images/databases-overview-1.jpeg
deleted file mode 100644
index dd12b7a7..00000000
Binary files a/src/content/docs/docs/database-monitoring/images/databases-overview-1.jpeg and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/images/databases-overview-2.jpeg b/src/content/docs/docs/database-monitoring/images/databases-overview-2.jpeg
deleted file mode 100644
index ceea4930..00000000
Binary files a/src/content/docs/docs/database-monitoring/images/databases-overview-2.jpeg and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/images/databases-overview-3.jpeg b/src/content/docs/docs/database-monitoring/images/databases-overview-3.jpeg
deleted file mode 100644
index d9fed1d1..00000000
Binary files a/src/content/docs/docs/database-monitoring/images/databases-overview-3.jpeg and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/images/databases-overview-4.jpeg b/src/content/docs/docs/database-monitoring/images/databases-overview-4.jpeg
deleted file mode 100644
index 25dfea61..00000000
Binary files a/src/content/docs/docs/database-monitoring/images/databases-overview-4.jpeg and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/images/databases-overview-5.jpeg b/src/content/docs/docs/database-monitoring/images/databases-overview-5.jpeg
deleted file mode 100644
index b89bb9ab..00000000
Binary files a/src/content/docs/docs/database-monitoring/images/databases-overview-5.jpeg and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/images/databases-overview-6.jpeg b/src/content/docs/docs/database-monitoring/images/databases-overview-6.jpeg
deleted file mode 100644
index 81f8eaac..00000000
Binary files a/src/content/docs/docs/database-monitoring/images/databases-overview-6.jpeg and /dev/null differ
diff --git a/src/content/docs/docs/database-monitoring/images/instance-overview.png b/src/content/docs/docs/database-monitoring/images/instance-overview.png
new file mode 100644
index 00000000..9e86dd64
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/instance-overview.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/metrics-dashboard.png b/src/content/docs/docs/database-monitoring/images/metrics-dashboard.png
new file mode 100644
index 00000000..00b327e4
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/metrics-dashboard.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/query-drawer-explain-plan.png b/src/content/docs/docs/database-monitoring/images/query-drawer-explain-plan.png
new file mode 100644
index 00000000..ae497d32
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/query-drawer-explain-plan.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/query-patterns.png b/src/content/docs/docs/database-monitoring/images/query-patterns.png
new file mode 100644
index 00000000..f79a71ce
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/query-patterns.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/running-queries-blocking-chain.png b/src/content/docs/docs/database-monitoring/images/running-queries-blocking-chain.png
new file mode 100644
index 00000000..1c5e2fb0
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/running-queries-blocking-chain.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/running-queries-sessions.png b/src/content/docs/docs/database-monitoring/images/running-queries-sessions.png
new file mode 100644
index 00000000..019730d0
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/running-queries-sessions.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/running-queries.png b/src/content/docs/docs/database-monitoring/images/running-queries.png
new file mode 100644
index 00000000..56e8a407
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/running-queries.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/schema.png b/src/content/docs/docs/database-monitoring/images/schema.png
new file mode 100644
index 00000000..b61e87a6
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/schema.png differ
diff --git a/src/content/docs/docs/database-monitoring/images/topology-redis-sentinel.png b/src/content/docs/docs/database-monitoring/images/topology-redis-sentinel.png
new file mode 100644
index 00000000..af15eb8a
Binary files /dev/null and b/src/content/docs/docs/database-monitoring/images/topology-redis-sentinel.png differ
diff --git a/src/content/docs/docs/database-monitoring/index.mdx b/src/content/docs/docs/database-monitoring/index.mdx
index c2728d8a..3f1412e8 100644
--- a/src/content/docs/docs/database-monitoring/index.mdx
+++ b/src/content/docs/docs/database-monitoring/index.mdx
@@ -1,57 +1,51 @@
---
-title: "Database Monitoring"
-description: "Compare KloudMate's database monitoring approaches and choose the right setup for your environment."
+title: "Database monitoring"
+description: "Track the health, query performance, sessions, and replication topology of your databases from the Databases page."
sidebar:
label: "Overview"
order: 1
---
-The KloudMate Agent provides three approaches to database monitoring, from zero-configuration kernel-level profiling to deep SQL-level query analytics, so you can choose the right level of visibility for your environment.
+import { LinkCard, CardGrid } from '@astrojs/starlight/components';
-:::tip
-To collect server-side database metrics and logs, the KloudMate Agent can wire up database receivers for you through a guided wizard. See [Agent database monitoring](../kloudmate-agent/database-monitoring/overview/).
-:::
+The **Databases** page lists every database the KloudMate agent monitors, with its status, health, query load, and connections. Open an instance to see its metrics, the queries that take the most time, the sessions that are blocking each other, the services that call it, and where it sits in its replication cluster.
-:::note
- Which approach should I use?
+## Get your databases onto the page
- **eBPF (DAM)** is the fastest way to get started, zero configuration, no database credentials needed. For deep query analytics with execution plans and schema discovery, use **Direct Database Monitoring** . **OpenTelemetry** is ideal when your application already uses OTel SDKs, and you want database spans as part of your distributed traces.
-:::
+A database appears on the **Databases** page after you set up monitoring for it in the KloudMate agent. You do this through a wizard: pick the engine, choose what to collect, and give the agent a monitoring user's credentials. See [Database monitoring overview](../kloudmate-agent/database-monitoring/overview/) for the setup, and [Engines and coverages](../kloudmate-agent/database-monitoring/engines-and-coverages/) for what each engine can collect.
-## Monitoring Approaches
+What you see on an instance depends on the **coverages** you turned on for it in the wizard. For example, **Top queries** and **Query patterns** need the **Query performance (samples + top queries)** coverage, and **Topology** needs **Replication topology & lag**. When a tab has no data because a coverage is off, the tab tells you which coverage to turn on.
-### [Database Activity Monitoring (DAM)](database-activity-monitoring-dam/)
+Some panels also need **Collect all optional metrics**, a setting on the database in the same wizard. Panels that depend on it are marked on the page.
-eBPF-powered, zero-configuration database profiling at the kernel level. Includes DAM-specific workflows such as the ML classifier deployment guide.
+The **Calling services** tab works from traces rather than from the database. It lists the services whose database calls appear as spans, whether those spans come from [eBPF monitoring](../kloudmate-agent/ebpf-observability/) or from OpenTelemetry instrumentation in your application.
-### [OpenTelemetry Database Monitoring](opentelemetry-database-monitoring/)
+## Supported engines
-Use OpenTelemetry SDK instrumentation for rich database query tracing with custom attributes.
+Every instance has an **Overview**, a **Metrics** dashboard, and a **Calling services** tab. The other tabs depend on the engine:
-### [Direct Database Monitoring](direct-database-monitoring/)
+| Engine | Additional tabs |
+|---|---|
+| PostgreSQL, MySQL, MariaDB | Top queries, Query patterns, Running queries, Schema, Sensitive data, Topology |
+| SQL Server | Top queries, Query patterns, Running queries, Schema, Sensitive data, Agent jobs, Topology |
+| Oracle | Top queries, Query patterns, Running queries, Schema, Sensitive data, AWR, Topology |
+| MongoDB | Top queries, Query patterns, Running queries, Collections, Sensitive data |
+| Redis, ClickHouse | Topology |
+| Elasticsearch, SAP HANA, MongoDB Atlas, Snowflake | None |
-SQL-based setup with a dedicated monitoring user for deep query metrics, execution plans, and schema discovery.
+MariaDB instances are shown and handled the same way as MySQL.
-## Supported Databases
+## Explore the Databases page
-| Database | eBPF (DAM) | OpenTelemetry | Direct (SQL Setup) |
-| -------------- | ---------- | ------------- | ------------------ |
-| **MySQL** | ✅ | ✅ | ✅ |
-| **PostgreSQL** | ✅ | ✅ | ✅ |
-| **Redis** | ✅ | ✅ | — |
-| **MongoDB** | — | ✅ | — |
-
-## Comparison
-
-| **Feature** | **eBPF (DAM)** | **OpenTelemetry** | **Direct (SQL Setup)** |
-| ------------------------------------ | ------------------ | --------------------- | -------------------------- |
-| **Setup complexity** | None | SDK integration | SQL scripts + agent config |
-| **Database credentials** | Not required | Not required | Required |
-| **Query text** | ✅ | ✅ (sanitized) | ✅ |
-| **Execution plans (EXPLAIN)** | — | — | ✅ |
-| **Wait event analysis** | — | — | ✅ |
-| **Schema discovery** | — | — | ✅ |
-| **Distributed trace context** | — | ✅ | — |
-| **Custom span attributes** | — | ✅ | — |
-| **Works on managed DBs (RDS, etc.)** | ✅ | ✅ | ✅ |
-| **Linux kernel requirement** | ≥ 5.8 | None | None |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/content/docs/docs/database-monitoring/instance-overview.mdx b/src/content/docs/docs/database-monitoring/instance-overview.mdx
new file mode 100644
index 00000000..4f9b1489
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/instance-overview.mdx
@@ -0,0 +1,74 @@
+---
+title: "Instance details and Overview"
+description: "Read an instance's header, move between its tabs, and use the Overview tab to check connections, key ratios, and which services load the database."
+sidebar:
+ order: 3
+---
+
+Open an instance from the [Databases list](../databases-list/) to see everything KloudMate collects for it. The page opens on the tab you viewed last, and every tab uses the time range in the top-right corner.
+
+
+
+## The instance header
+
+The header shows the engine, the instance's role, and its cluster, for example **Primary · Streaming replication, 2 nodes**. Hover the cluster to see the other members. An instance that belongs to more than one hierarchy, such as an Oracle database that's both in a container and in Data Guard, shows each one. Oracle instances also show whether they're a container database (CDB) or a pluggable database (PDB).
+
+When alert rules are firing for the instance, the header shows the number of active alerts. Open the list to go to an alert. Alerts match the agent's host name when the database runs on that host, and the instance itself otherwise.
+
+The health icon is the same one shown on the [Databases list](../databases-list/#status-and-health). When the worst category is **CPU**, **Memory**, or **Connections**, click the icon to open the Overview tab with a triage banner for that category. See [Triage from the health icon](#triage-from-the-health-icon).
+
+## Tabs and page controls
+
+The tabs available depend on the engine. See [Supported engines](../#supported-engines) for the full list.
+
+- **Database** narrows the query, session, schema, and sensitive data tabs to one database on the instance. It appears for PostgreSQL, where databases are discovered from their size metric, and for Oracle container databases, where you pick a container. An Oracle CDB has no **All databases** option; its own container is selected by default.
+- **Ask Assistant** opens the KloudMate Assistant with a prompt that already includes the instance's status, health, connections, and query rate.
+- **Refresh** reloads the current tab.
+
+## The Overview tab
+
+The Overview tab brings together the numbers you usually check first.
+
+### Instance info and gauges
+
+The info card shows the instance's status, connections, query rate, database size, uptime, and host. When the database doesn't run on the agent's host, the host row is labelled **Monitored from** and shows the agent's host. The card also lists engine settings when the engine reports them:
+
+| Engine | Settings |
+|---|---|
+| PostgreSQL | Version, role, WAL level, archive mode, autovacuum, shared buffers |
+| MySQL, MariaDB | Version, flavor, read-only state, binary log, buffer pool, time zone |
+| SQL Server | Version, edition, product level, Always On, CPUs, max server memory |
+| Oracle | Version, open mode, Data Guard role, log mode, archiver, SGA target |
+
+Cloud details such as provider, account, region, and availability zone appear when the agent reports them. When it doesn't report a cloud provider, KloudMate infers one from the endpoint, for example Amazon RDS or Azure SQL Database, and marks it as inferred.
+
+Next to the card, gauges show the ratios that matter most for the engine:
+
+| Engine | Gauges |
+|---|---|
+| PostgreSQL | Connection utilization, buffer cache hit ratio |
+| MySQL, MariaDB | Buffer pool utilization, buffer pool hit ratio |
+| SQL Server | Oldest backup age, memory usage |
+| Oracle | Session utilization, database CPU utilization, oldest backup age |
+| MongoDB | Connection utilization, cache hit ratio |
+| Redis | Memory utilization, network input, network output |
+
+A gauge only appears when it has data. Cache hit ratio gauges turn red below 60% and amber below 85%. A Redis instance without a `maxmemory` limit has no memory utilization, because there's no ceiling to measure against.
+
+ClickHouse, Elasticsearch, SAP HANA, MongoDB Atlas, and Snowflake show status, connections, query rate, and database size as separate tiles instead of the card and gauges.
+
+### Trend charts
+
+Below the card, charts show how the instance changed over the time range. Every engine gets a **Connections** chart. The rest depend on the engine, for example **Database Sizes** on PostgreSQL, **Keyspace Hit Ratio** on Redis, **Backup Status** on SQL Server and Oracle, and the query rate, failed queries, and parts charts on ClickHouse.
+
+When query statistics are collected, the tab also shows **Query Volume**, **Total Duration by Query**, and **Average Query Duration** for the busiest queries, with a link to [Query patterns](../query-performance/#query-patterns).
+
+A note above the charts names any chart that needs **Collect all optional metrics**.
+
+### Load by calling service
+
+The **Load by calling service** chart shows how the database's call time splits across the services that call it, for the six busiest services plus the rest. Hover a segment to see that service's share. It's built from traces, so it only has data when your services' database calls are traced. See [Calling services](../calling-services/). It isn't shown for Oracle.
+
+## Triage from the health icon
+
+Clicking the health icon opens the Overview tab with a banner that names the category and its current level. For **Connections**, the banner links to [Running queries](../running-queries/) so you can see what's holding connections. On PostgreSQL it also offers to create an SLO on connection utilization, so you're alerted before the limit is reached next time.
diff --git a/src/content/docs/docs/database-monitoring/metrics.mdx b/src/content/docs/docs/database-monitoring/metrics.mdx
new file mode 100644
index 00000000..802119c9
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/metrics.mdx
@@ -0,0 +1,43 @@
+---
+title: "Metrics dashboards"
+description: "The prebuilt Metrics dashboard for each database engine, and the settings some of its panels need."
+sidebar:
+ order: 4
+---
+
+The **Metrics** tab is a prebuilt dashboard for the instance's engine. Stat tiles across the top show the headline numbers, and the panels below are grouped into collapsible rows such as **Throughput**, **Locks**, and **Replication**.
+
+
+
+Collapse the rows you don't need. A collapsed row doesn't load its panels, and it stays collapsed the next time you open an instance of the same engine.
+
+## Panels that need more collection
+
+Some panels only have data after you turn on more collection for the instance in the agent's database monitoring settings. The row says so above its panels:
+
+- **Collect all optional metrics** adds the engine's optional metrics. On PostgreSQL, for example, it fills in **Temporary File Usage** and **Buffer Cache Hit Ratio**.
+- A named coverage adds a specific data set. For example, **Blocked Sessions** on PostgreSQL needs the **Locks & blocking** coverage, and the **TempDB** row on SQL Server needs **TempDB usage**.
+
+See [Engines and coverages](../../kloudmate-agent/database-monitoring/engines-and-coverages/) for what each coverage collects.
+
+## What each engine's dashboard covers
+
+| Engine | Stat tiles | Rows |
+|---|---|---|
+| PostgreSQL | Backends, max connections, database count, database size, table count | Resource utilization, throughput, locks, replication, checkpoints |
+| MySQL, MariaDB | Connected threads, running threads, uptime, replication lag | Throughput, buffer pool and table locks, handlers and InnoDB, connections and errors, queries and network, table cache, tables and indexes |
+| SQL Server | User connections, batch requests, buffer cache hit ratio, page life expectancy | Compilations and locks, transactions, pages and checkpoints, waits and file I/O, sessions, index health, Agent jobs, memory, schedulers, TempDB, plan cache |
+| Oracle | Active sessions, active processes, active transactions, PGA allocation rate | Locks, throughput, I/O and CPU, capacity, time model, memory and cursors, workload detail, efficiency |
+| MongoDB | Current connections, active sessions, database count, open cursors | Operations, cache and memory, network and cursors, storage, collections and indexes, server health, latency and replication, locks |
+| MongoDB Atlas | Connections, open cursors, page faults, restarts | CPU and memory, operations, storage and disk, network, locks and replication, tickets and cache |
+| Redis | Connected clients, blocked clients, connected replicas, uptime | Memory, throughput, connections and keys, replication, persistence and CPU |
+| ClickHouse | Running queries, rows inserted, rows read, average query duration, merges running, max parts per partition | Queries, inserts, reads, merges and parts, caches, resources, background pools and compression, replication and distributed |
+| Elasticsearch | Cluster status, cluster nodes, pending tasks, in-flight fetches | Shards and documents, JVM, disk and OS, caches, circuit breakers and thread pools |
+| SAP HANA | Uptime, connections, blocked transactions, active alerts | Memory, CPU and transactions, replication, disk, volumes, and services |
+| Snowflake | Warehouse credits used, credits used by warehouse | Storage |
+
+The ClickHouse dashboard also has cards for server version and inventory, CPU, memory, and disk utilization, the query mix by kind, and data parts by state, with the busiest partition marked against the limits where inserts are delayed (1,000 parts) and rejected (3,000 parts).
+
+## Engines without a dashboard
+
+An instance of an engine KloudMate doesn't have a dashboard for gets a dashboard built from traces instead. It shows operations per second, average and P99 latency, and error rate, broken down by service and operation, for the database calls your traced services make.
diff --git a/src/content/docs/docs/database-monitoring/opentelemetry-database-monitoring.mdx b/src/content/docs/docs/database-monitoring/opentelemetry-database-monitoring.mdx
deleted file mode 100644
index 7f9c20ed..00000000
--- a/src/content/docs/docs/database-monitoring/opentelemetry-database-monitoring.mdx
+++ /dev/null
@@ -1,51 +0,0 @@
----
-title: "OpenTelemetry Database Monitoring"
-description: "Capture database query spans with OpenTelemetry SDK auto-instrumentation and send them to KloudMate."
-sidebar:
- order: 3
----
-
-In addition to the eBPF-based [Database Access Monitoring (DAM)](../database-activity-monitoring-dam/), you can use OpenTelemetry SDK instrumentation to capture detailed database query traces with custom attributes.
-
-## How It Works
-
-OpenTelemetry auto-instrumentation agents and SDKs provide library-specific hooks that capture database operations as trace spans. These spans include:
-
-- SQL query text (sanitized)
-- Database system and name
-- Operation type (SELECT, INSERT, UPDATE, DELETE)
-- Duration and status
-- Connection details
-
-## When to Use
-
-- When you need rich, library-specific database query details
-- When you want custom attributes on database spans
-- When your application already uses OpenTelemetry SDKs
-- When combined with [Manual Instrumentation](../../apm-and-tracing/manual-instrumentation/) for the business context
-
-## Supported Database Libraries
-
-| **Language** | **Libraries Auto-Instrumented** |
-| ------------ | ----------------------------------------- |
-| **Java** | JDBC, Hibernate, Spring Data |
-| **Python** | SQLAlchemy, psycopg2, PyMySQL, Django ORM |
-| **Node.js** | pg, mysql2, mongoose, Prisma |
-| **.NET** | Entity Framework, Npgsql, MySqlConnector |
-| **Go** | database/sql, GORM, pgx |
-
-## Configuration
-
-Configure your OpenTelemetry SDK to export to the KloudMate Agent OTLP endpoint:
-
-```bash
-OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
-OTEL_SERVICE_NAME="your-service-name"
-```
-
-Database spans are captured automatically when you use supported auto-instrumentation libraries.
-
-:::note
- For zero-configuration kernel-level profiling, see [Database Access Monitoring (DAM)](../database-activity-monitoring-dam/). For deep query analytics with execution plans and schema discovery, see [Direct Database Monitoring](../direct-database-monitoring/).
-:::
-
diff --git a/src/content/docs/docs/database-monitoring/query-performance.mdx b/src/content/docs/docs/database-monitoring/query-performance.mdx
new file mode 100644
index 00000000..4339ddc3
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/query-performance.mdx
@@ -0,0 +1,81 @@
+---
+title: "Query performance"
+description: "Find the queries that cost your database the most time, see how they trend, and read their execution plans."
+sidebar:
+ order: 5
+---
+
+The **Top queries** and **Query patterns** tabs rank the statements your database runs by the time they cost. The queries at the top of the list are usually the ones worth tuning first. Open a query from **Query patterns** to read its execution plan and per-query metrics.
+
+These tabs are available for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, and MongoDB. They need the **Query performance (samples + top queries)** coverage turned on for the instance in the agent's database monitoring settings. See [Engines and coverages](../../kloudmate-agent/database-monitoring/engines-and-coverages/).
+
+:::note
+MongoDB query statistics need a KloudMate agent built on OpenTelemetry Collector 0.155.0 or later.
+:::
+
+Queries are grouped by their normalized form, with literal values replaced, so every execution of `SELECT * FROM orders WHERE id = ?` counts as one query no matter which ID it used.
+
+## Top queries
+
+**Top queries** lists the queries that ran in the selected time range, sorted by total duration. Expand a row to read the full query text.
+
+The columns depend on what the engine reports:
+
+- **Database** shows the database the query ran in. MySQL doesn't report it.
+- **Avg rows** shows the rows returned or affected per execution, on PostgreSQL, SQL Server, and Oracle.
+- **Shared blocks hit ratio** shows how many of the blocks a query read came from shared memory, on PostgreSQL.
+
+## Query patterns
+
+**Query patterns** adds a trend and a plan status to each query, and lets you filter for the queries that most likely need attention.
+
+
+
+The **Trend** column compares the query's total duration with the previous period of the same length. A change under 5% shows as **Flat**. An increase is marked in red and a decrease in green. Queries that didn't run in the previous period have no trend.
+
+The **Plan** column shows whether an execution plan was captured for the query in the time range. Plans are only kept when **Collect execution plans** is on for the instance, and they're only checked for the 20 queries that took the most time.
+
+Use the filters above the table to narrow the list:
+
+- **Index recommendations** shows queries whose plan has a sequential scan over an estimated 10,000 rows or more, a common sign of a missing index. Only the plans of the 20 most time-consuming queries are checked.
+- **Slow queries** shows queries that averaged one second or more per execution.
+- **High row count** shows queries that returned or affected 10,000 rows or more per execution.
+
+Below the table, **Query Volume** and **Total Duration by Query** chart the ten busiest queries over the range, and **Load by calling service** shows which services generated the load.
+
+## Query details
+
+Click a query in **Query patterns** to open its details in a side panel over the list. The panel shows the full query text and tabs for **Explain plan**, **Metrics**, and **Calling services**. To share a query with a teammate, copy the page URL while the panel is open.
+
+
+
+### Explain plan
+
+Execution plans are available for PostgreSQL, MySQL, MariaDB, SQL Server, and Oracle, when **Collect execution plans** is on for the instance. A new query can take a few collection intervals to get its first plan.
+
+The tab shows the plan's total cost, number of nodes, and depth, with three views of the plan:
+
+- **Map view** draws the plan as a tree of operations. Click a node to see its costs, rows, row width, relation, filter, and join details.
+- **List view** lists each operation with its share of the total cost.
+- **Raw JSON** shows the plan exactly as the database returned it.
+
+When the plan has a sequential scan over an estimated 10,000 rows or more, a warning suggests indexing the filtered or joined column.
+
+If the query ran under more than one plan in the time range, **Plan history** lists each plan with the time it was first seen and how often it was captured. Select an older plan to compare it with the current one. A plan that changed is a common cause of a query that suddenly got slower.
+
+Click **Ask AI** to ask the KloudMate Assistant about the plan.
+
+### Metrics
+
+The **Metrics** tab charts the query on its own: executions, average and total duration, and rows per execution, plus the engine-specific panels the database reports. For example:
+
+- PostgreSQL shows blocks accessed, buffer cache hit ratio, planning time, and temporary blocks written per execution.
+- SQL Server shows logical reads, CPU versus wait time, and memory grants per execution.
+- Oracle shows logical reads, CPU versus wait time, and wait time by class.
+- MongoDB shows documents examined versus returned.
+
+When session samples are collected, the tab also shows **Active Sessions by Client** and **Sessions by Wait Type** for sessions running the query.
+
+### Calling services
+
+The **Calling services** tab lists the services that ran this query, from your traces. See [Match a query to its traces](../calling-services/#match-a-query-to-its-traces).
diff --git a/src/content/docs/docs/database-monitoring/running-queries.mdx b/src/content/docs/docs/database-monitoring/running-queries.mdx
new file mode 100644
index 00000000..2aae9300
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/running-queries.mdx
@@ -0,0 +1,54 @@
+---
+title: "Running queries and blocking"
+description: "See which sessions were active, what they were waiting on, and which session was blocking the others."
+sidebar:
+ order: 6
+---
+
+The **Running queries** tab shows the sessions that were active on the database, captured as snapshots over the time range. Use it when connections pile up or queries stall, to find what the sessions were waiting on and which session was holding the others up.
+
+The tab is available for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, and MongoDB. Session snapshots need the **Query performance (samples + top queries)** coverage turned on for the instance. See [Engines and coverages](../../kloudmate-agent/database-monitoring/engines-and-coverages/).
+
+
+
+## What sessions were waiting on
+
+**Sessions by Wait Type** counts the active sessions in each snapshot by what they were waiting on, such as a lock, I/O, or the client. Sessions that weren't waiting on anything are counted as **CPU**. A band that grows over time, for example **Lock** during a batch job, points you at the kind of contention to look for.
+
+## Step through snapshots
+
+The table shows one snapshot at a time, starting with the newest. Use the arrows next to the snapshot time to move to older and newer snapshots. The toolbar also shows how many sessions were active and how long the longest one had been running.
+
+Each row shows the session, its state, the query it was running, its wait type, the database user, the client application, and how long it had been running. Click a query to read its full text.
+
+The tab loads the newest 500 session rows in the time range. If a busy database produces more than that, older snapshots are left out, and narrowing the time range brings them back.
+
+## Find the session that's blocking others
+
+When a snapshot contains blocked sessions, a banner counts the blocked sessions and the root blockers, and the **Blocking chain** graph draws who's waiting on whom:
+
+
+
+
+
+- A **root blocker** is holding a lock and isn't waiting on anyone else. It's usually the session to investigate first. Root blockers are often sessions that are idle in a transaction: they still hold the locks they took, but aren't running anything, so their wait shows as **Client**.
+- Each blocking session shows how many sessions it blocks, what it's waiting on, and the relation it's waiting for.
+- A blocker that wasn't captured in the snapshot is drawn with a dashed outline.
+
+Click a session in the graph to read its full query text.
+
+On SQL Server, some blockers aren't user sessions. These are shown by name, such as an orphaned distributed transaction or a deferred recovery transaction.
+
+Turn on **Blocking only** to hide the sessions that aren't part of a blocking chain.
+
+## Add blocking columns to the table
+
+Use the **Columns** button to add the blocking columns to the session table. They're hidden by default, and your choice is saved in your browser.
+
+| Column | Shows |
+|---|---|
+| **Blocked by** | The session directly blocking this one. |
+| **Blocking hierarchy** | The whole chain of sessions above this one, starting from the root blocker. |
+| **Root blocker** | Marks the sessions at the top of a chain. |
+| **Blocked count** | How many sessions this one blocks directly. |
+| **Blocked (all levels)** | How many sessions this one blocks directly or through other sessions. |
diff --git a/src/content/docs/docs/database-monitoring/schema.mdx b/src/content/docs/docs/database-monitoring/schema.mdx
new file mode 100644
index 00000000..518d2494
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/schema.mdx
@@ -0,0 +1,30 @@
+---
+title: "Schema and indexes"
+description: "Browse a database's tables, columns, indexes, and constraints, MongoDB collections, and SQL Server's missing index suggestions."
+sidebar:
+ order: 8
+---
+
+The **Schema** tab lists the tables in the database with their size and structure. Check a table's indexes and constraints here while you investigate a slow query. On MongoDB, the same tab is called **Collections**.
+
+## Tables
+
+The **Schema** tab is available for PostgreSQL, MySQL, MariaDB, SQL Server, and Oracle. It shows the newest schema scan from the last 24 hours, and needs the **Schema catalog** coverage turned on for the instance in the agent's database monitoring settings.
+
+
+
+Each table shows its type, an estimated row count, its total size including indexes, and its table size. The row estimate comes from the database's optimizer statistics, so it can lag behind the real count until the statistics are refreshed. On PostgreSQL, an estimate of `-1` means the table hasn't been analyzed yet. Run `ANALYZE` on the table to fill it in.
+
+Expand a table to see its columns, indexes, and constraints, including primary keys, foreign keys, unique, and check constraints. A scan lists up to 5,000 tables.
+
+On an Oracle container database, tables live in the pluggable databases. Select a PDB in the **Database** menu to see its tables.
+
+## MongoDB collections
+
+MongoDB has no fixed schema, so the **Collections** tab lists collections instead of tables, with the number of index accesses for each. A collection appears once a query uses one of its indexes. It doesn't need a separate coverage.
+
+## Missing index suggestions
+
+On SQL Server, the **Missing index suggestions** card below the tables lists the indexes SQL Server's own optimizer recommends, with the estimated improvement and how often the index would have been used for seeks and scans. The card only appears when there are suggestions.
+
+For other engines, use the **Index recommendations** filter in [Query patterns](../query-performance/#query-patterns) to find queries that scan large tables without an index.
diff --git a/src/content/docs/docs/database-monitoring/sensitive-data.mdx b/src/content/docs/docs/database-monitoring/sensitive-data.mdx
new file mode 100644
index 00000000..896d5291
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/sensitive-data.mdx
@@ -0,0 +1,32 @@
+---
+title: "Sensitive data"
+description: "Find the tables and columns that hold personal or health data, how sensitive they are, and how each column was classified."
+sidebar:
+ order: 9
+---
+
+The **Sensitive data** tab shows which tables and columns hold personally identifiable information (PII) or protected health information (PHI), such as email addresses, phone numbers, or national IDs. Use it to see where sensitive data lives in each database.
+
+The tab is available for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, and MongoDB. To start scanning, turn on PII classification for the instance in the agent's database monitoring settings. The tab shows results from the last seven days.
+
+## Read the results
+
+The tiles across the top count the tables scanned, the tables with sensitive data, and the sensitive columns out of all columns scanned. They also name the classifier version that produced the results.
+
+Each table in the list shows its highest sensitivity level, how many of its columns are sensitive, the categories found in it, and when it was last classified. Sensitivity levels run from **Critical** and **High** through **Medium** and **Low** to **No PII**. A column that couldn't be classified is marked **Unclassified**.
+
+Expand a table to see every column, with the sensitive columns listed first. Hover a column's sensitivity to see its categories and how it was detected:
+
+| Detected by | Meaning |
+|---|---|
+| Dictionary | The column name matched a built-in list of sensitive column names. |
+| Pattern | The column matched a pattern rule. |
+| Model | The GLiNER-PII model detected sensitive data in the column, with a confidence score. |
+| Cached | An earlier result for the same column was reused. |
+| Override | Someone set the classification by hand. It takes precedence over automatic classification. |
+
+:::caution
+Only scanned tables appear in the list. A table that isn't listed hasn't been scanned yet, so it isn't known to be free of sensitive data.
+:::
+
+On an Oracle container database, tables live in the pluggable databases. Select a PDB in the **Database** menu to see its results.
diff --git a/src/content/docs/docs/database-monitoring/topology.mdx b/src/content/docs/docs/database-monitoring/topology.mdx
new file mode 100644
index 00000000..77ed1ca1
--- /dev/null
+++ b/src/content/docs/docs/database-monitoring/topology.mdx
@@ -0,0 +1,48 @@
+---
+title: "Topology"
+description: "See where an instance sits in its replication setup or cluster, the role and state of each member, and how far replicas lag."
+sidebar:
+ order: 10
+---
+
+The **Topology** tab shows the replication setup or cluster the instance belongs to: its primary, its replicas, the role and state of each member, and how far each replica lags behind. Use it to confirm which node is the primary after a failover, or to find the replica that's falling behind.
+
+The tab is available for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, Redis, and ClickHouse. It needs a topology coverage turned on for the instance in the agent's database monitoring settings:
+
+| Engine | Coverage |
+|---|---|
+| PostgreSQL, MySQL, MariaDB, SQL Server, Redis, ClickHouse | **Replication topology & lag** |
+| Oracle | **Container topology (CDB / PDB)**, **RAC topology**, or **Data Guard topology & lag** |
+
+
+
+## Read the tree
+
+The tree starts at the primary and branches to its members. The instance you opened has a thicker border. Each member shows its role, such as primary, replica, or standby, and its current state.
+
+A member that reports into the cluster but isn't monitored by KloudMate is drawn in grey and marked **Not monitored**, and a banner above the tree lists its address. Set up monitoring for it to see its own metrics. See [Database monitoring overview](../../kloudmate-agent/database-monitoring/overview/).
+
+The heading counts the members and names this instance's role. When an instance belongs to more than one hierarchy, each gets its own section. For example, an Oracle database can show its container, its RAC cluster, and its Data Guard configuration separately.
+
+## Check replication lag
+
+The table lists every member with its role and state. When the members report them, it adds:
+
+- **Lag**, how far the member is behind the primary.
+- **Send backlog** and **Apply backlog**, how much data is waiting to be sent to the member and waiting to be applied on it.
+
+Not every engine reports every column. For example, MySQL reports lag but no backlog.
+
+Hover a member's name to see how KloudMate matched it to a monitored instance: by an exact match on its identity, by address, or by host name. When a member could match more than one instance, it's marked as ambiguous.
+
+For clusters that replicate, **Replication Lag vs Primary Write Rate** charts each replica's lag against how fast the primary is writing, so you can tell whether a replica fell behind because of a write burst or on its own.
+
+## Redis Sentinel and Redis Cluster
+
+For a Redis group managed by Sentinel, the heading also shows how many Sentinels watch the group and the quorum they need to agree on a failover. While a failover is in progress, a banner says so, and roles and lag can change until the Sentinels finish promoting the new primary. After the failover, the tree shows the new primary at the root.
+
+For Redis Cluster, each primary is shown with its replicas beneath it, and every node shows the range of hash slots it serves.
+
+## If the tab is empty
+
+An instance that reports itself as standalone isn't part of a cluster, so there's no tree to draw. If the instance reported no cluster membership at all in the time range, check that the topology coverage is turned on for it.
diff --git a/src/content/docs/docs/incident-management/escalation-policy/adding-escalation-policy.mdx b/src/content/docs/docs/incident-management/escalation-policy/adding-escalation-policy.mdx
index 18b8a4f2..8ebd43fc 100644
--- a/src/content/docs/docs/incident-management/escalation-policy/adding-escalation-policy.mdx
+++ b/src/content/docs/docs/incident-management/escalation-policy/adding-escalation-policy.mdx
@@ -13,7 +13,7 @@ This page walks through building a policy from scratch — the policy-level sett
## Create the policy
-1. Open **Incident Management → Escalation Policies** and click **Add Escalation Policy**.
+1. Open **Incidents → Escalation policies** and click **Add Escalation Policy**.
2. Enter a **name** and **description**.
@@ -30,9 +30,9 @@ Each step names its recipients and how long to wait before escalating.
1. In a step, add one or more **recipients**:
- - **Users** — pick a user, then choose how to notify them: **email**, **SMS**, or **voice**. Select at least one method. SMS and voice are disabled (with a tooltip) unless your plan includes them and the user has a verified phone number.
+ - **Users** — pick a user, then choose how to notify them. Select at least one method. **Email** is always available. **SMS** and **voice** appear only when your plan includes them, and they need a verified phone number for the user.
- **On-call schedule** — pick a schedule; the step notifies whoever is on call when it fires — possibly several people. Requires the Pro plan.
- - **Slack channel** — pick a connected channel and set its fallback order (`0` is primary). Connect Slack first under [Slack Integration](../../slack-integration/).
+ - **Slack channel** — click **Add Slack Channel** and pick a connected channel. A step can post to one shared channel, and it posts there every time the step runs. Per-user Slack DMs aren't supported. Connect Slack first under [Slack Integration](../../slack-integration/).
2. Set **Escalate after** — minutes to wait for an acknowledgement before the next step fires.
diff --git a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-1.jpeg b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-1.jpeg
index cda7d54d..7928658f 100644
Binary files a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-1.jpeg and b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-1.jpeg differ
diff --git a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-form.png b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-form.png
index ef9db0ec..51b83352 100644
Binary files a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-form.png and b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-policy-form.png differ
diff --git a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-recipients.png b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-recipients.png
index 053565a7..41a39564 100644
Binary files a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-recipients.png and b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-recipients.png differ
diff --git a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-retry.png b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-retry.png
index 4ef91f87..44bf2488 100644
Binary files a/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-retry.png and b/src/content/docs/docs/incident-management/escalation-policy/images/escalation-step-retry.png differ
diff --git a/src/content/docs/docs/incident-management/escalation-policy/index.mdx b/src/content/docs/docs/incident-management/escalation-policy/index.mdx
index 63d1bb7e..150fa525 100644
--- a/src/content/docs/docs/incident-management/escalation-policy/index.mdx
+++ b/src/content/docs/docs/incident-management/escalation-policy/index.mdx
@@ -8,7 +8,7 @@ sidebar:
An escalation policy decides who gets paged when an incident opens, and what happens if no one acknowledges. A service points at one policy as its default; when an incident opens, the policy runs until someone acknowledges, the steps run out, or the policy finishes repeating.
-
+
The policies list shows each policy's name, description, initial wait time, and number of steps.
diff --git a/src/content/docs/docs/incident-management/images/slack-integration-1.png b/src/content/docs/docs/incident-management/images/slack-integration-1.png
index b8f622d6..8ea5e436 100644
Binary files a/src/content/docs/docs/incident-management/images/slack-integration-1.png and b/src/content/docs/docs/incident-management/images/slack-integration-1.png differ
diff --git a/src/content/docs/docs/incident-management/images/what-is-incident-management_-1.jpeg b/src/content/docs/docs/incident-management/images/what-is-incident-management_-1.jpeg
index 299f36ff..3e318803 100644
Binary files a/src/content/docs/docs/incident-management/images/what-is-incident-management_-1.jpeg and b/src/content/docs/docs/incident-management/images/what-is-incident-management_-1.jpeg differ
diff --git a/src/content/docs/docs/incident-management/incidents/creating-an-incident.mdx b/src/content/docs/docs/incident-management/incidents/creating-an-incident.mdx
index 3879cc68..7498a629 100644
--- a/src/content/docs/docs/incident-management/incidents/creating-an-incident.mdx
+++ b/src/content/docs/docs/incident-management/incidents/creating-an-incident.mdx
@@ -1,6 +1,6 @@
---
title: "Create an Incident"
-description: "Create a manual incident and assign its integration, severity, and escalation policy."
+description: "Create a manual incident and assign its service, severity, and escalation policy."
sidebar:
label: "Create an Incident"
order: 2
@@ -8,23 +8,23 @@ sidebar:
Use the steps below to create an incident manually.
-1. Click **Create Incident** in the top-right corner of the Incidents screen.
+1. Open **Incidents → Incidents** and click **Create Incident**.
-
+
-2. Enter the incident **Title** and **Incident Details**.
-3. Select a severity level: **Critical** , **High** , **Medium** , or **Low**.
+2. Select the **Service** the incident belongs to.
+3. Enter a **Title** and the **Incident details**.
+4. Select a **Severity**: **Critical**, **High**, **Medium**, or **Low**.
-
+
-4. Select the integration the incident belongs to.
-5. Optionally choose an escalation policy to override the integration default.
+5. To override the service's default escalation policy, select a policy in **Escalation policy (optional)**.
6. Click **Create**.
-
+
:::note
- Selecting an integration is required. If you do not have an integration yet, create one first in [Adding Integrations](../../integrations/adding-integrations/).
+ **Service**, **Title**, **Incident details**, and **Severity** are required. If you don't have a service yet, [create a service](../../services/creating-services/) first.
- If no escalation policy is selected, the policy assigned to the selected integration is applied automatically.
+ If you don't select an escalation policy, the incident uses the service's default escalation policy.
:::
diff --git a/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-1.png b/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-1.png
index b8220ee7..af6eacfa 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-1.png and b/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-1.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-2.png b/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-2.png
index aa5e1cb6..3ae3988b 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-2.png and b/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-2.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-3.png b/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-3.png
index ec22431f..c6caaa30 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-3.png and b/src/content/docs/docs/incident-management/incidents/images/creating-an-incident-3.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/incident-activity-timeline.png b/src/content/docs/docs/incident-management/incidents/images/incident-activity-timeline.png
index 194c4fb7..2d9cb2b5 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/incident-activity-timeline.png and b/src/content/docs/docs/incident-management/incidents/images/incident-activity-timeline.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/incident-details-1.png b/src/content/docs/docs/incident-management/incidents/images/incident-details-1.png
index 6bf0da41..662c18ea 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/incident-details-1.png and b/src/content/docs/docs/incident-management/incidents/images/incident-details-1.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/incident-linked-slos.png b/src/content/docs/docs/incident-management/incidents/images/incident-linked-slos.png
new file mode 100644
index 00000000..d33b24e0
Binary files /dev/null and b/src/content/docs/docs/incident-management/incidents/images/incident-linked-slos.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/incident-publish-status-page.png b/src/content/docs/docs/incident-management/incidents/images/incident-publish-status-page.png
index 60ab17cd..2af6e072 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/incident-publish-status-page.png and b/src/content/docs/docs/incident-management/incidents/images/incident-publish-status-page.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/incident-responders.png b/src/content/docs/docs/incident-management/incidents/images/incident-responders.png
index d0052663..20bd9ac9 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/incident-responders.png and b/src/content/docs/docs/incident-management/incidents/images/incident-responders.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/images/incidents-list-filters.png b/src/content/docs/docs/incident-management/incidents/images/incidents-list-filters.png
index 0302597b..9ae24224 100644
Binary files a/src/content/docs/docs/incident-management/incidents/images/incidents-list-filters.png and b/src/content/docs/docs/incident-management/incidents/images/incidents-list-filters.png differ
diff --git a/src/content/docs/docs/incident-management/incidents/incident-details.mdx b/src/content/docs/docs/incident-management/incidents/incident-details.mdx
index 10f3213d..e6d0be45 100644
--- a/src/content/docs/docs/incident-management/incidents/incident-details.mdx
+++ b/src/content/docs/docs/incident-management/incidents/incident-details.mdx
@@ -7,7 +7,7 @@ sidebar:
---
Open any incident from the list to view its detail page. It's the single place to see what happened, who was paged, and how the response unfolded.
-
+
## The header
@@ -16,7 +16,8 @@ The header identifies the incident and its routing:
- **Responders** — who's assigned. Add or remove them from here.
- **Service**, **alert source**, and **escalation policy** — each links to its own page.
- **Status** — Triggered, Acknowledged, or Resolved, derived from the incident's state.
-- **Severity** — critical, high, medium, or low. Edit it inline.
+- **Severity** — edit it inline.
+- **Recurrence of** — a link to the earlier incident this one repeats, when there is one.
Use **Acknowledge** or **Resolve** in the top-right to update the status; you can also unacknowledge or unresolve.
@@ -24,9 +25,10 @@ Use **Acknowledge** or **Resolve** in the top-right to update the status; you ca
Alongside the header, the page shows the numbers that matter during response:
-- **Occurrence count** and **suppressed count**
+- **Occurrence count** and **Suppressed**
- **First seen**, **Triggered at**, **Acknowledged at**, **Resolved at**
-- **Time to resolve**
+- **Elapsed** — how long the incident has been open, or how long it took to resolve
+- **Escalation** — how far the escalation policy got, and the step it reached
## Manage the incident
@@ -34,11 +36,17 @@ Alongside the header, the page shows the numbers that matter during response:
- **Severity** — change the severity; the change is recorded on the timeline.
- **Summary** — edit the incident description with the context of what happened and its impact.
-
+
+
+## Incident summary
+
+The summary sits between the metrics and the tabs. Write it yourself to record what happened and its impact, or let KloudMate keep it current: when an incident is fed by an alert group, each new signal is appended to the summary.
+
+An appended summary opens with how many signals have arrived since the incident was created, followed by the totals across the group — newly firing, still firing, and newly resolved. Below that, each alert rule gets its own breakdown with a few of its most recent signals, their labels, and when they fired. **Drilldown** opens the rest of a rule's signals, and **View alert group** opens the group itself.
## Publish to a status page
-When the affected service is linked to a status page, admins see a **Publish to status page** action on an open incident. It opens a dialog that pre-fills the page and affected components from the incident's service, so you can post a customer-facing incident in a couple of clicks. See [Post incidents and updates](../../status-pages/posting-incidents/).
+When the affected service is linked to a status page, users with the **Developer** role see a **Publish to Status Page** action on an open incident. It opens a dialog that pre-fills the page and affected components from the incident's service, so you can post a customer-facing incident in a couple of clicks. See [Post incidents and updates](../../status-pages/posting-incidents/).

@@ -46,11 +54,13 @@ When the affected service is linked to a status page, admins see a **Publish to
When the affected service has at least one incident-based [SLO](../../../reliability/), a **Linked SLOs** panel appears below the standard service / alert source / escalation-policy labels. Each linked SLO shows as a card with its name, kind + target + window, and current compliance %. Clicking a card opens the [SLO detail](../../../reliability/slo-detail/) page.
+
+
The panel only renders when at least one incident-based SLO exists on the affected service — incidents on services without reliability tracking show no panel.
## Activity timeline
-The activity timeline is the incident's audit trail. It merges two kinds of entry into one time-ordered stream:
+The **Logs** tab holds the incident's audit trail. It merges two kinds of entry into one time-ordered stream:
- **Lifecycle events** — Created, Acknowledged, Unacknowledged, Resolved, Unresolved, Assigned, Unassigned, Severity changed, and Summary updated.
- **Notification deliveries** — one entry per attempt to reach a recipient, tagged with its **channel** (email, SMS, voice, Slack, MS Teams, webhook, Jira, or SNS) and a **status**: queued, sent, delivered, failed, skipped — unverified (the recipient had no verified phone), or skipped — no credits.
@@ -59,9 +69,9 @@ Delivery entries also note which **step** paged the recipient, the **retry attem

-## Alarms
+## Alerts
-The **Alarms** tab lists the raw alerts that fed this incident, each with its received time and whether it was suppressed. Expand a row to see the full payload that arrived from the alert source — useful for confirming what the source actually sent.
+The **Alerts** tab lists the raw alerts that fed this incident, each with its received time and whether it was suppressed. Expand a row to see the full payload that arrived from the alert source — useful for confirming what the source actually sent.
## Notes
diff --git a/src/content/docs/docs/incident-management/incidents/index.mdx b/src/content/docs/docs/incident-management/incidents/index.mdx
index 47adb765..26b91f65 100644
--- a/src/content/docs/docs/incident-management/incidents/index.mdx
+++ b/src/content/docs/docs/incident-management/incidents/index.mdx
@@ -37,13 +37,13 @@ The full list is where you work through incidents at scale. Each row shows the i
Filter the list by:
- **Status** — triggered, acknowledged, or resolved.
-- **Service**, **alert source**, and **escalation policy**.
+- **Services**, **Source**, and **Escalation policies**.
- **Assigned to me** — only incidents you're a responder on.
- **Show Resolved** — include resolved incidents, which are hidden by default.
You can also act in bulk: select several incidents and acknowledge, resolve, or change the severity of all of them at once — handy when one root cause spawned a wave of incidents.
-
+
## Related
diff --git a/src/content/docs/docs/incident-management/index.mdx b/src/content/docs/docs/incident-management/index.mdx
index 2f3076c8..55cea03f 100644
--- a/src/content/docs/docs/incident-management/index.mdx
+++ b/src/content/docs/docs/incident-management/index.mdx
@@ -21,7 +21,7 @@ Use Incident Management to organize incident response workflows, route alerts to
- [Slack Integration](./slack-integration/)
:::note
-**Two features share the name "Routing Rules."** The [Routing Rules](./routing-rules/) listed here decide how an incoming alert becomes an incident — which service, escalation policy, and severity it gets, and whether to suppress it. To route alert *notifications* to channels like Slack, see [Alerts → Routing Rules](../alerts/routing-rules/) instead.
+**Two features share the name "Routing rules."** The [Routing Rules](./routing-rules/) listed here decide how an incoming alert becomes an incident: which service, escalation policy, and severity it gets, and whether to suppress it. To route alert *notifications* to channels like Slack, see [Alerts → Routing rules](../alerts/routing-rules/) instead.
:::
## Related Resources
diff --git a/src/content/docs/docs/incident-management/integrations/adding-integrations.mdx b/src/content/docs/docs/incident-management/integrations/adding-integrations.mdx
index ba006a28..cd8dae0a 100644
--- a/src/content/docs/docs/incident-management/integrations/adding-integrations.mdx
+++ b/src/content/docs/docs/incident-management/integrations/adding-integrations.mdx
@@ -13,7 +13,7 @@ An alert source receives alerts and turns them into incidents on a service. This
## Create an alert source
-1. Click **Add** from the Alert Sources page.
+1. Open **Incidents → Alert sources** and click **Add Source**.
2. Choose a **type**: KloudMate Alert group, CloudWatch Alarm, or Webhook.
diff --git a/src/content/docs/docs/incident-management/integrations/images/alert-sources-list.png b/src/content/docs/docs/incident-management/integrations/images/alert-sources-list.png
index cb01ddd4..afffa519 100644
Binary files a/src/content/docs/docs/incident-management/integrations/images/alert-sources-list.png and b/src/content/docs/docs/incident-management/integrations/images/alert-sources-list.png differ
diff --git a/src/content/docs/docs/incident-management/integrations/images/kloudmate-incidents-channel-form.png b/src/content/docs/docs/incident-management/integrations/images/kloudmate-incidents-channel-form.png
index 83fa91b5..ddfaf538 100644
Binary files a/src/content/docs/docs/incident-management/integrations/images/kloudmate-incidents-channel-form.png and b/src/content/docs/docs/incident-management/integrations/images/kloudmate-incidents-channel-form.png differ
diff --git a/src/content/docs/docs/incident-management/integrations/images/routing-rule-km-incidents.png b/src/content/docs/docs/incident-management/integrations/images/routing-rule-km-incidents.png
index 4e0268d7..6f4d150e 100644
Binary files a/src/content/docs/docs/incident-management/integrations/images/routing-rule-km-incidents.png and b/src/content/docs/docs/incident-management/integrations/images/routing-rule-km-incidents.png differ
diff --git a/src/content/docs/docs/incident-management/integrations/index.mdx b/src/content/docs/docs/incident-management/integrations/index.mdx
index c29c10fa..de8b8df9 100644
--- a/src/content/docs/docs/incident-management/integrations/index.mdx
+++ b/src/content/docs/docs/incident-management/integrations/index.mdx
@@ -26,7 +26,7 @@ KloudMate Alert group and CloudWatch Alarm sources use built-in templates. To in
## The alert sources list
-Open **Incident Management → Alert Sources** to see every source with its name, description, type, service, and escalation policy.
+Open **Incidents → Alert sources** to see every source with its name, description, type, service, and escalation policy.

diff --git a/src/content/docs/docs/incident-management/integrations/integrating-with-aws-cloudwatch.mdx b/src/content/docs/docs/incident-management/integrations/integrating-with-aws-cloudwatch.mdx
index 62ba3af3..c1f5bfca 100644
--- a/src/content/docs/docs/incident-management/integrations/integrating-with-aws-cloudwatch.mdx
+++ b/src/content/docs/docs/incident-management/integrations/integrating-with-aws-cloudwatch.mdx
@@ -23,9 +23,9 @@ Before you start, make sure you have:
Create a unique ingestion endpoint in KloudMate to receive your CloudWatch alerts.
-1. Log into the KloudMate Console and open **Incident Management → Alert Sources**.
+1. Log into the KloudMate Console and open **Incidents → Alert sources**.
-2. Click **Add** (or navigate to a specific [Service](../../services/) and click **Create Alert Source** under details).
+2. Click **Add Source** (or open a specific [service](../../services/) and click **Add Source** in its **Alert Sources** section).
3. Set the **Type** to `CloudWatch Alarm`.
diff --git a/src/content/docs/docs/incident-management/integrations/integrating-with-kloudmate-alerts.mdx b/src/content/docs/docs/incident-management/integrations/integrating-with-kloudmate-alerts.mdx
index a2017d70..fae1c0b5 100644
--- a/src/content/docs/docs/incident-management/integrations/integrating-with-kloudmate-alerts.mdx
+++ b/src/content/docs/docs/incident-management/integrations/integrating-with-kloudmate-alerts.mdx
@@ -13,13 +13,13 @@ To open an Incident Management incident whenever an alert fires, you wire two th
This replaces the older "webhook channel + notification policy with Alarm condition" flow. The KloudMate Incidents channel handles the bridge plumbing for you; routing rules give you label-level control over which alerts trigger which IM services.
:::note
-The **Routing Rule** on this page is an [Alerts routing rule](../../../alerts/routing-rules/) — it routes alert *notifications* to channels. Don't confuse it with [Incident Management → Routing Rules](../../routing-rules/), which decides how an alert becomes an incident once it has reached IM.
+The **Routing Rule** on this page is an [Alerts routing rule](../../../alerts/routing-rules/). It routes alert *notifications* to channels. Don't confuse it with [Incidents → Routing rules](../../routing-rules/), which decides how an alert becomes an incident once it has reached IM.
:::
## Prerequisites
- An [IM service](../../services/) you want incidents to open against.
-- A workspace admin role (you need it to create channels and routing rules).
+- The **Developer** role (you need it to create channels and routing rules).
## Step 1 — Create a KloudMate Incidents channel
@@ -28,9 +28,11 @@ The **Routing Rule** on this page is an [Alerts routing rule](../../../alerts/ro
2. Pick **KloudMate Incidents** as the channel type.
-3. Pick the IM service you want to link from the **Linked IM service** dropdown.
+3. Give the channel a **Channel name**.
-4. Click **Submit**.
+4. Pick the IM service you want to link from the **Linked IM service** dropdown.
+
+5. Click **Create channel**.
KloudMate creates the IM integration row under your service automatically and points the new channel at the integration's signed loop URL. You don't need to copy URLs or paste them anywhere. See [Notification Channels](../../../platform/settings/notification-channels/#kloudmate-incidents) for the channel-side details.
@@ -40,11 +42,11 @@ KloudMate creates the IM integration row under your service automatically and po
## Step 2 — Route alerts to the channel
-1. Open **Alerts → Routing Rules → Create**.
+1. Open **Alerts → Routing rules** and click **Create rule**.
2. Name the rule and set a priority (lower wins).
-3. Add **matchers** that identify which alerts should feed this IM service, e.g. `service equals payments` to route every payments alert to the payments IM service.
+3. Add **matchers** that identify which alerts should feed this IM service, e.g. `service equals checkout` to route every checkout alert to the Checkout API IM service.
4. Select **group-by** keys (e.g. `service`, `env`) so related alerts fold into a single notifiable group.
@@ -52,7 +54,7 @@ KloudMate creates the IM integration row under your service automatically and po
6. Optionally enable **Auto-RCA** if you want KloudMate's AI investigator to attach a summary to the incident automatically.
-7. Click **Save**.
+7. Click **Create rule**.
See [Routing Rules](../../../alerts/routing-rules/) for the full rule editor walk-through.
@@ -66,7 +68,7 @@ Trigger a matching alert. Within the routing rule's `group_wait` (default 30 sec
Things to check:
- The incident shows up on the IM service's incidents tab.
-- The originating [Alert Group](../../../alerts/alert-groups/) on the **Alerts → Groups** page lists the KloudMate Incidents channel under its Notifications tab with the new incident's ID.
+- The originating [Alert Group](../../../alerts/alert-groups/) in **Alerts → Groups** lists the KloudMate Incidents channel under its Notifications tab with the new incident's ID.
- If Auto-RCA was enabled, the investigation summary attaches to both the group and the incident.
## Migrating from the older flow
diff --git a/src/content/docs/docs/incident-management/oncall/creating-a-schedule.mdx b/src/content/docs/docs/incident-management/oncall/creating-a-schedule.mdx
index 70f42f40..2fe0b205 100644
--- a/src/content/docs/docs/incident-management/oncall/creating-a-schedule.mdx
+++ b/src/content/docs/docs/incident-management/oncall/creating-a-schedule.mdx
@@ -11,13 +11,13 @@ import { Steps } from '@astrojs/starlight/components';
The fastest way to understand on-call schedules is to build the common one: a single rotation that hands the on-call duty between a few people every week. This page walks that end to end through the template picker and the calendar editor, then covers each rotation field so you can adjust it.
:::note[Availability]
-On-Call Schedules are a paid Incident Management feature. If you don't see **On-Call Schedules** in the Incident Management navigation, your plan doesn't include it yet — contact your account team to turn it on.
+On-call schedules are a paid Incident Management feature. If you don't see an **On-call** tab in **Incidents**, your plan doesn't include it yet. Contact your account team to turn it on.
:::
## Create a schedule
-1. Open **Incident Management → On-Call Schedules** and click **New schedule**.
+1. Open **Incidents → On-call** and click **New schedule**.
2. Pick a starting point from the **Quick start** template picker (see below). For a single weekly rotation, choose **Weekly handoff**.
@@ -115,7 +115,7 @@ Every rotation has the same fields. The defaults below are what a fresh rotation
The calendar is the quickest check that a schedule does what you meant. It lays out who's on call across the coming weeks — every rotation's shifts, color-coded per person, with the exact handoff moments. Where two rotations overlap, you'll see both responders on the calendar at once — the additive default. If you gave one rotation a higher **priority**, the lower one drops out of the overlap instead, so a shift can disappear from the calendar on purpose.
-If a handoff lands on the wrong day or the wrong person leads, open the rotation, fix the handoff weekday or the member order, and watch the calendar update before you save. The **Schedules** tab's coverage timeline is also where you confirm there are no gaps with nobody on call.
+If a handoff lands on the wrong day or the wrong person leads, open the rotation, fix the handoff weekday or the member order, and watch the calendar update before you save. The coverage timeline in the **Schedules** view is also where you confirm there are no gaps with nobody on call.

diff --git a/src/content/docs/docs/incident-management/oncall/images/oncall-schedules-tab.png b/src/content/docs/docs/incident-management/oncall/images/oncall-schedules-tab.png
index 1bdcf43d..1fc0ed33 100644
Binary files a/src/content/docs/docs/incident-management/oncall/images/oncall-schedules-tab.png and b/src/content/docs/docs/incident-management/oncall/images/oncall-schedules-tab.png differ
diff --git a/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-editor.png b/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-editor.png
index b2dc88c3..b6910ca1 100644
Binary files a/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-editor.png and b/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-editor.png differ
diff --git a/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-preview.png b/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-preview.png
index 0d623472..15cf61a9 100644
Binary files a/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-preview.png and b/src/content/docs/docs/incident-management/oncall/images/schedule-calendar-preview.png differ
diff --git a/src/content/docs/docs/incident-management/oncall/images/schedule-quick-start.png b/src/content/docs/docs/incident-management/oncall/images/schedule-quick-start.png
index 8c80d0c5..98a9d1ed 100644
Binary files a/src/content/docs/docs/incident-management/oncall/images/schedule-quick-start.png and b/src/content/docs/docs/incident-management/oncall/images/schedule-quick-start.png differ
diff --git a/src/content/docs/docs/incident-management/oncall/images/schedule-rotation-drawer.png b/src/content/docs/docs/incident-management/oncall/images/schedule-rotation-drawer.png
index 0445ed6f..7a6b30d7 100644
Binary files a/src/content/docs/docs/incident-management/oncall/images/schedule-rotation-drawer.png and b/src/content/docs/docs/incident-management/oncall/images/schedule-rotation-drawer.png differ
diff --git a/src/content/docs/docs/incident-management/oncall/index.mdx b/src/content/docs/docs/incident-management/oncall/index.mdx
index 336afb93..1626c442 100644
--- a/src/content/docs/docs/incident-management/oncall/index.mdx
+++ b/src/content/docs/docs/incident-management/oncall/index.mdx
@@ -9,7 +9,7 @@ sidebar:
On-call schedules decide who's on call when an incident needs a person. Rather than wiring a specific name into an escalation policy, you point the policy — or a [routing rule](../routing-rules/) — at a schedule, and KloudMate resolves it to whoever is on call at that exact moment.
:::note[Availability]
-On-Call Schedules are a paid Incident Management feature. If **On-Call Schedules** isn't in your Incident Management navigation, your plan doesn't include it yet — contact your account team to turn it on.
+On-call schedules are a paid Incident Management feature. If you don't see an **On-call** tab in **Incidents**, your plan doesn't include it yet. Contact your account team to turn it on.
:::
## How a schedule decides who's on call
@@ -42,18 +42,18 @@ When two rotations' coverage windows overlap, both current responders are on cal
## See who's on call
-The **On-call** section has two tabs:
+Open **Incidents → On-call** and switch between these views:
- **My on-call** — your personal, cross-schedule view: whether you're on call right now and your upcoming shifts across *every* schedule you belong to. This is the page to check before you log off for the day.
- **Schedules** — a compact list of every schedule, each showing who's on call now, a roughly two-week coverage timeline, and the timezone the times are shown in. Use it to scan coverage at a glance and spot windows where no one is rostered.
-
+
## Mind the gaps
Because rotations are additive rather than a fallback stack, **custom coverage windows can leave moments with no rotation active — and so nobody on call**. If an incident fires during such a gap, an escalation step that targets the schedule has no one to reach.
-For any schedule an escalation policy depends on, keep at least one **24/7 rotation** (or make sure your rotations' windows tile the clock with no gap). The **Schedules** tab's coverage timeline is the quickest way to spot an uncovered window before you rely on the schedule. See [Rotations and coverage](./rotations-and-coverage/) for worked examples.
+For any schedule an escalation policy depends on, keep at least one **24/7 rotation** (or make sure your rotations' windows tile the clock with no gap). The coverage timeline in the **Schedules** view is the quickest way to spot an uncovered window before you rely on the schedule. See [Rotations and coverage](./rotations-and-coverage/) for worked examples.
## How schedules connect to incidents
diff --git a/src/content/docs/docs/incident-management/oncall/overrides.mdx b/src/content/docs/docs/incident-management/oncall/overrides.mdx
index 4887bb86..9a1d6888 100644
--- a/src/content/docs/docs/incident-management/oncall/overrides.mdx
+++ b/src/content/docs/docs/incident-management/oncall/overrides.mdx
@@ -11,7 +11,7 @@ import { Steps } from '@astrojs/starlight/components';
An override puts a specific person on call for a fixed window, then restores the normal rotations. Reach for one when the rotations are right in general but wrong for a few days — someone's on vacation, swapping a shift, or covering a deploy. The rotations underneath are untouched; the override sits on top of them for its window only.
:::note[Availability]
-On-Call Schedules are a paid Incident Management feature. If you don't see **On-Call Schedules** in the Incident Management navigation, your plan doesn't include it yet — contact your account team to turn it on.
+On-call schedules are a paid Incident Management feature. If you don't see an **On-call** tab in **Incidents**, your plan doesn't include it yet. Contact your account team to turn it on.
:::
## Add an override
diff --git a/src/content/docs/docs/incident-management/oncall/rotations-and-coverage.mdx b/src/content/docs/docs/incident-management/oncall/rotations-and-coverage.mdx
index 0091fd7c..44e36eb5 100644
--- a/src/content/docs/docs/incident-management/oncall/rotations-and-coverage.mdx
+++ b/src/content/docs/docs/incident-management/oncall/rotations-and-coverage.mdx
@@ -9,7 +9,7 @@ sidebar:
A single rotation covers the simple case. Real coverage often needs more: a daytime team alongside an after-hours rotation, a roster that follows working hours around the globe, or an overnight shift that runs past midnight. You build all of these by adding **more rotations** and giving each one the **coverage window** and **timezone** it should run in.
:::note[Availability]
-On-Call Schedules are a paid Incident Management feature. If you don't see **On-Call Schedules** in the Incident Management navigation, your plan doesn't include it yet — contact your account team to turn it on.
+On-call schedules are a paid Incident Management feature. If you don't see an **On-call** tab in **Incidents**, your plan doesn't include it yet. Contact your account team to turn it on.
:::
## How overlapping rotations resolve
@@ -130,7 +130,7 @@ This covers each weeknight from 22:00 until 05:00 the next morning, including Fr
Custom coverage windows create uncovered time on purpose, but a schedule made entirely of windowed rotations can leave moments with **nobody on call**. If an incident fires during such a gap, an escalation step that targets the schedule has no one to reach.
-Make it a habit: for any schedule an escalation policy depends on, keep at least one **24/7 rotation**, or make sure your rotations' coverage windows tile the clock with no gap. The **Schedules** tab's coverage timeline (and the schedule's own calendar) is the quickest way to scan for an uncovered window before you rely on the schedule.
+Make it a habit: for any schedule an escalation policy depends on, keep at least one **24/7 rotation**, or make sure your rotations' coverage windows tile the clock with no gap. The coverage timeline in the **Schedules** view (and the schedule's own calendar) is the quickest way to scan for an uncovered window before you rely on the schedule.
## Related
diff --git a/src/content/docs/docs/incident-management/routing-rules/images/rule-action-picker.png b/src/content/docs/docs/incident-management/routing-rules/images/rule-action-picker.png
index 1342dec2..3707f128 100644
Binary files a/src/content/docs/docs/incident-management/routing-rules/images/rule-action-picker.png and b/src/content/docs/docs/incident-management/routing-rules/images/rule-action-picker.png differ
diff --git a/src/content/docs/docs/incident-management/routing-rules/images/rule-editor-conditions.png b/src/content/docs/docs/incident-management/routing-rules/images/rule-editor-conditions.png
index 3679c3bf..78a17f7f 100644
Binary files a/src/content/docs/docs/incident-management/routing-rules/images/rule-editor-conditions.png and b/src/content/docs/docs/incident-management/routing-rules/images/rule-editor-conditions.png differ
diff --git a/src/content/docs/docs/incident-management/routing-rules/index.mdx b/src/content/docs/docs/incident-management/routing-rules/index.mdx
index 9c0a9af2..16459d42 100644
--- a/src/content/docs/docs/incident-management/routing-rules/index.mdx
+++ b/src/content/docs/docs/incident-management/routing-rules/index.mdx
@@ -11,11 +11,11 @@ import { Steps } from '@astrojs/starlight/components';
Routing rules decide what happens to an alert the moment it reaches Incident Management: which **service** the incident opens against, which **escalation policy** pages, what **severity** it gets, and whether to suppress it, add responders, or notify an on-call schedule.
:::caution[Two features share this name]
-This page is about **incident routing inside IM** — turning an incoming alert into the right incident. It is *not* the same as [**Alerts → Routing Rules**](../../alerts/routing-rules/), which routes alert *notifications* to channels like Slack. If you're trying to control which Slack channel gets pinged, you want that page instead.
+This page is about **incident routing inside IM**: turning an incoming alert into the right incident. It is *not* the same as [**Alerts → Routing rules**](../../alerts/routing-rules/), which routes alert *notifications* to channels like Slack. If you're trying to control which Slack channel gets pinged, you want that page instead.
:::
:::note[Availability]
-Routing Rules are a paid Incident Management feature. If you don't see **Routing Rules** in the Incident Management navigation, your plan doesn't include it yet — contact your account team to turn it on.
+Routing rules are a paid Incident Management feature. If you don't see a **Routing rules** tab in **Incidents**, your plan doesn't include it yet. Contact your account team to turn it on.
:::
## What a routing rule decides
@@ -106,7 +106,7 @@ Use it to confirm a new rule does what you expect — and that it doesn't accide
## Create a routing rule
-1. Open **Incident Management → Routing Rules** and click **Create rule**.
+1. Open **Incidents → Routing rules** and click **Add Rule**.
2. Name the rule, set its **scope** (workspace-wide or a single service), and set a **priority** (lower wins).
@@ -121,8 +121,8 @@ Use it to confirm a new rule does what you expect — and that it doesn't accide
## Related
-- [Alert Sources](../integrations/) — where alerts enter IM.
-- [Services](../services/) — the routing target and its default escalation policy.
-- [Escalation Policies](../escalation-policy/) — what a rule assigns.
-- [On-Call Schedules](../oncall/) — what "Notify on-call schedules" pages.
-- [Alerts → Routing Rules](../../alerts/routing-rules/) — the different feature that routes alert notifications to channels.
+- [Alert Sources](../integrations/): where alerts enter IM.
+- [Services](../services/): the routing target and its default escalation policy.
+- [Escalation Policies](../escalation-policy/): what a rule assigns.
+- [On-Call Schedules](../oncall/): what "Notify on-call schedules" pages.
+- [Alerts → Routing rules](../../alerts/routing-rules/): the different feature that routes alert notifications to channels.
diff --git a/src/content/docs/docs/incident-management/services/creating-services.mdx b/src/content/docs/docs/incident-management/services/creating-services.mdx
index 5ba074b5..54316f4e 100644
--- a/src/content/docs/docs/incident-management/services/creating-services.mdx
+++ b/src/content/docs/docs/incident-management/services/creating-services.mdx
@@ -11,9 +11,9 @@ import { Steps } from '@astrojs/starlight/components';
Use the steps below to create a new service.
-1. Click **Create Service** from the Services page.
+1. Open **Incidents → Services** and click **Create Service**.
-2. Enter the **service name** and a **description**. Both are required.
+2. Enter the **service name**. A **description** is optional.
3. Select the **service owner** — a user in your workspace.
diff --git a/src/content/docs/docs/incident-management/services/images/creating-services-1.png b/src/content/docs/docs/incident-management/services/images/creating-services-1.png
index e0c06d45..70120588 100644
Binary files a/src/content/docs/docs/incident-management/services/images/creating-services-1.png and b/src/content/docs/docs/incident-management/services/images/creating-services-1.png differ
diff --git a/src/content/docs/docs/incident-management/services/images/services-1.jpeg b/src/content/docs/docs/incident-management/services/images/services-1.jpeg
index ca153349..612841bc 100644
Binary files a/src/content/docs/docs/incident-management/services/images/services-1.jpeg and b/src/content/docs/docs/incident-management/services/images/services-1.jpeg differ
diff --git a/src/content/docs/docs/incident-management/services/index.mdx b/src/content/docs/docs/incident-management/services/index.mdx
index 17f80aae..42501125 100644
--- a/src/content/docs/docs/incident-management/services/index.mdx
+++ b/src/content/docs/docs/incident-management/services/index.mdx
@@ -12,7 +12,7 @@ To track a service against a reliability target, define a [Service Level Objecti

-The Services page lists each service with its name, description, owner, default escalation policy, and current health.
+**Incidents → Services** lists each service with its name, description, owner, default escalation policy, and current health.
:::note
A service can own several alert sources. The escalation policy on the service is the default — an alert source uses it unless it sets its own override, and a [routing rule](../routing-rules/) can override it per incident.
@@ -27,7 +27,7 @@ Open a service to review:
- The default escalation policy (linked)
- The service's **alert sources**, listed with their name, description, type, and escalation policy
-You can add an alert source directly from the service page, and use the settings menu to edit or archive the service.
+You can add an alert source directly from the service page with **Add Source**, and click **Edit** or **Archive** to update or archive the service.

diff --git a/src/content/docs/docs/incident-management/slack-integration.mdx b/src/content/docs/docs/incident-management/slack-integration.mdx
index b6cf81e4..6ab793d5 100644
--- a/src/content/docs/docs/incident-management/slack-integration.mdx
+++ b/src/content/docs/docs/incident-management/slack-integration.mdx
@@ -9,25 +9,29 @@ sidebar:
KloudMate uses a single unified Slack app for both notification delivery and IM ChatOps. Installing it once gives you:
- Threaded notifications on alert groups and incidents (replies stay attached to the parent message).
-- Per-user ChatOps actions — acknowledge, resolve, comment on incidents without leaving Slack.
-- Cross-surface visibility — the same install shows up under both **Settings → Notification Channels** and **Incidents → Settings → ChatOps**.
+- Per-user ChatOps actions: acknowledge, resolve, or comment on incidents without leaving Slack.
+- Cross-surface visibility: the same install shows up under both **Settings → Notification Channels** and **Incidents → Chat Ops**.
-You can start the install from either entry point — the OAuth flow lands the same row in both surfaces.
+You can start the install from either place, and it shows up in both.
## Connect Slack from Incident Management
-1. Open **Incidents → Settings** from the left navigation.
+1. Open **Incidents → Chat Ops** and click **Connect** on the Slack card. You need the **Developer** role for this step.
-
+
-2. In the **Chat Ops** tab, click **Connect** under the Slack card. Only workspace admins can run this step.
-3. Complete the Slack OAuth flow — pick the Slack workspace and channel KloudMate should post into.
-4. KloudMate returns you to the Chat Ops tab and shows the linked workspace + channel.
+2. Complete the Slack OAuth flow: pick the Slack workspace and channel KloudMate should post into.
+3. KloudMate returns you to the Chat Ops tab and shows the linked workspace and channel.
-The new Slack row appears under both **Settings → Notification Channels** and **Incidents → Settings → Chat Ops**. You can use it as a notification destination on a [Routing Rule](../../alerts/routing-rules/) immediately.
+The new Slack row appears under both **Settings → Notification Channels** and **Incidents → Chat Ops**. You can use it as a notification destination on a [Routing Rule](../../alerts/routing-rules/) immediately.
## Per-user account linking
-After the org-level install is complete, each responder needs to link their own Slack account before they can acknowledge or resolve incidents from Slack. From the Chat Ops tab, scroll to **User Settings** and click **Connect account** — a one-time OAuth flow links your KloudMate identity to your Slack account.
+After the org-level install is complete, each responder needs to link their own Slack account before they can acknowledge or resolve incidents from Slack. From the Chat Ops tab, scroll to **User Settings** and click **Connect account**. A one-time OAuth flow links your KloudMate account to your Slack account.
Once linked, the same tab shows your Slack username with a **Disconnect** action.
+
+## Related
+
+- [Connections](/platform/settings/connections/): the Slack install is a connection, so one credential serves notifications, ChatOps, and workflows. Manage or rotate it there. Before a workflow step or trigger can use it, add the **Workflows** capability to it.
+- [Workflows](../../workflows/): post to Slack from a [workflow step](../../workflows/actions/#slack), or start a workflow from a [Slack event](../../workflows/triggers/#app-event), using the same install.
diff --git a/src/content/docs/docs/incident-management/status-pages/branding-and-custom-domain.mdx b/src/content/docs/docs/incident-management/status-pages/branding-and-custom-domain.mdx
index e3f0f3d3..2ea57a45 100644
--- a/src/content/docs/docs/incident-management/status-pages/branding-and-custom-domain.mdx
+++ b/src/content/docs/docs/incident-management/status-pages/branding-and-custom-domain.mdx
@@ -1,6 +1,6 @@
---
title: "Branding and Custom Domain"
-description: "Add your logo, colors, and favicon, and serve the status page from your own domain with automatic TLS."
+description: "Add your logo, favicon, and primary color, and serve the status page from your own domain with automatic TLS."
sidebar:
label: "Branding and Custom Domain"
order: 3
@@ -16,13 +16,16 @@ Set these in **Settings → Branding**:
- **Logo** — PNG, JPEG, or WebP, up to **2 MB**. KloudMate downscales it to a 512px longest edge.
- **Favicon** — PNG, JPEG, or WebP, up to **1 MB**, square (1:1). It's downscaled to 256px.
-- **Primary color** — the page's accent color.
- **Hide "Powered by KloudMate"** — removes the KloudMate footer badge. This is a paid add-on; you'll be prompted to upgrade if your plan doesn't include it.
When you upload a logo or favicon, a crop dialog lets you frame it before it's saved.

+The page's **primary color**, its accent, is set separately in **Settings → General**, alongside the name, slug, and description.
+
+
+
## The default URL
Every status page is reachable for free at a KloudMate-hosted address built from its slug:
diff --git a/src/content/docs/docs/incident-management/status-pages/images/status-page-branding.png b/src/content/docs/docs/incident-management/status-pages/images/status-page-branding.png
index 74e962b5..e6c8fde0 100644
Binary files a/src/content/docs/docs/incident-management/status-pages/images/status-page-branding.png and b/src/content/docs/docs/incident-management/status-pages/images/status-page-branding.png differ
diff --git a/src/content/docs/docs/incident-management/status-pages/images/status-page-components-tab.png b/src/content/docs/docs/incident-management/status-pages/images/status-page-components-tab.png
index 916ea0d6..150225da 100644
Binary files a/src/content/docs/docs/incident-management/status-pages/images/status-page-components-tab.png and b/src/content/docs/docs/incident-management/status-pages/images/status-page-components-tab.png differ
diff --git a/src/content/docs/docs/incident-management/status-pages/images/status-page-custom-domain.png b/src/content/docs/docs/incident-management/status-pages/images/status-page-custom-domain.png
index 0c9cea68..be0b2f08 100644
Binary files a/src/content/docs/docs/incident-management/status-pages/images/status-page-custom-domain.png and b/src/content/docs/docs/incident-management/status-pages/images/status-page-custom-domain.png differ
diff --git a/src/content/docs/docs/incident-management/status-pages/images/status-page-general-settings.png b/src/content/docs/docs/incident-management/status-pages/images/status-page-general-settings.png
new file mode 100644
index 00000000..ac3b061e
Binary files /dev/null and b/src/content/docs/docs/incident-management/status-pages/images/status-page-general-settings.png differ
diff --git a/src/content/docs/docs/incident-management/status-pages/images/status-page-incident-composer.png b/src/content/docs/docs/incident-management/status-pages/images/status-page-incident-composer.png
index dcc9b158..0b6099b5 100644
Binary files a/src/content/docs/docs/incident-management/status-pages/images/status-page-incident-composer.png and b/src/content/docs/docs/incident-management/status-pages/images/status-page-incident-composer.png differ
diff --git a/src/content/docs/docs/incident-management/status-pages/images/status-page-public.png b/src/content/docs/docs/incident-management/status-pages/images/status-page-public.png
index 8d72f526..46d27fbd 100644
Binary files a/src/content/docs/docs/incident-management/status-pages/images/status-page-public.png and b/src/content/docs/docs/incident-management/status-pages/images/status-page-public.png differ
diff --git a/src/content/docs/docs/incident-management/status-pages/images/status-pages-list.png b/src/content/docs/docs/incident-management/status-pages/images/status-pages-list.png
index 99cbbe68..6c468780 100644
Binary files a/src/content/docs/docs/incident-management/status-pages/images/status-pages-list.png and b/src/content/docs/docs/incident-management/status-pages/images/status-pages-list.png differ
diff --git a/src/content/docs/docs/incident-management/status-pages/index.mdx b/src/content/docs/docs/incident-management/status-pages/index.mdx
index 3b40c2bf..dd871ce8 100644
--- a/src/content/docs/docs/incident-management/status-pages/index.mdx
+++ b/src/content/docs/docs/incident-management/status-pages/index.mdx
@@ -11,7 +11,7 @@ import { Steps } from '@astrojs/starlight/components';
A status page tells your customers what's working and what isn't, without them opening a ticket. You publish component health, active incidents, and uptime history at a public URL; when something breaks, you post updates there instead of answering the same question a hundred times.
:::note[Availability]
-Status Pages are a paid Incident Management feature. If you don't see **Status Pages** in the Incident Management navigation, your plan doesn't include it yet — contact your account team to turn it on.
+Status pages are a paid Incident Management feature. If you don't see a **Status pages** tab in **Incidents**, your plan doesn't include it yet. Contact your account team to turn it on.
:::
## What visitors see
@@ -37,7 +37,7 @@ You manage everything from one editor with three tabs:
## Create a status page
-1. Open **Incident Management → Status Pages** and click **Create**.
+1. Open **Incidents → Status pages** and click **Create Status Page**.
2. Enter a **name** — the title visitors see.
diff --git a/src/content/docs/docs/infrastructure/hosts.mdx b/src/content/docs/docs/infrastructure/hosts.mdx
index 34484fb5..257a25ab 100644
--- a/src/content/docs/docs/infrastructure/hosts.mdx
+++ b/src/content/docs/docs/infrastructure/hosts.mdx
@@ -12,7 +12,7 @@ From this view, you can navigate from a fleet-wide overview down to the metrics
## Overview
-To access the Hosts module, navigate to **Infrastructure > Hosts** in the KloudMate sidebar.
+To access the Hosts module, open **Hosts**.
The view is divided into two primary resource types:
- **Hosts:** Physical or virtual machines running the KloudMate Agent.
diff --git a/src/content/docs/docs/infrastructure/integrations/kafka-integration.mdx b/src/content/docs/docs/infrastructure/integrations/kafka-integration.mdx
index 2f915a6d..1a80b243 100644
--- a/src/content/docs/docs/infrastructure/integrations/kafka-integration.mdx
+++ b/src/content/docs/docs/infrastructure/integrations/kafka-integration.mdx
@@ -4,25 +4,31 @@ description: "Collect Kafka broker, topic, and consumer metrics in KloudMate thr
sidebar:
order: 3
---
-Use this integration when you want to monitor Kafka brokers, topics, and consumers in KloudMate through the OpenTelemetry `kafkametrics` receiver running in the KloudMate Agent.
+Use this integration when you want to monitor Kafka brokers, topics, and consumers in KloudMate through the OpenTelemetry `kafka_metrics` receiver running in the KloudMate Agent.
## Prerequisites
1. Install and run the [KloudMate Agent](../../../kloudmate-agent/).
-2. Ensure your Kafka brokers are reachable from the node where the Agent is running.
+2. Ensure your Kafka brokers are reachable from the host or cluster where the Agent is running.
3. Have your Kafka broker addresses, protocol version, and authentication credentials ready.
-## Step 1: Define the Kafka Metrics Receiver
+## Step 1: Open the Collector Configuration
-Add the following `kafkametrics` receiver to your Agent configuration file (typically `/etc/km-agent/config.yaml`). Replace the broker addresses, credentials, and protocol version to match your environment.
+1. Go to **Settings → Agents**.
+2. Find the agent you want to configure and select the three dots.
+3. Choose **Configure**.
+4. Go to **Collector config**.
-```yaml
-extensions:
- health_check:
- endpoint: 0.0.0.0:13133
+## Step 2: Add the Kafka Metrics Configuration
+
+Add the following configuration. Replace the broker addresses, credentials, and protocol version to match your environment.
+- If the agent runs in managed mode, add it to the **Custom config override** section. See [Custom config override](../../../kloudmate-agent/custom-config-override/).
+- If the agent runs in manual mode, add it directly to the collector configuration.
+
+```yaml
receivers:
- kafkametrics:
+ kafka_metrics:
brokers:
- kafka.example.com:9095
- kafka.example.com:9096
@@ -47,37 +53,23 @@ receivers:
enabled: true
kafka.topic.min_insync_replicas:
enabled: true
-```
-
-The `scrapers` block controls which data is collected. `brokers`, `topics`, and `consumers` are the three available options. The metrics listed under the `metrics` block are not enabled by default and must be explicitly set to `enabled: true` to be collected.
-## Step 2: Add to Pipeline
-
-Add the `kafkametrics` receiver to the metrics pipeline in your Agent configuration.
-
-```yaml
service:
- extensions:
- - health_check
- telemetry:
- metrics:
- level: none
pipelines:
- metrics/kafkametrics:
+ metrics/kafka:
receivers:
- - kafkametrics
+ - kafka_metrics
processors:
- batch
exporters:
- otlphttp
```
-## Step 3: Restart the Agent
+The `scrapers` block controls which data is collected. `brokers`, `topics`, and `consumers` are the three available options. The metrics listed under the `metrics` block are not enabled by default and must be explicitly set to `enabled: true` to be collected.
-```bash
-systemctl restart kmagent
-systemctl status kmagent
-```
+## Step 3: Save the Configuration
+
+Click **Save**. The Agent restarts and applies the new configuration.
## Step 4: Validate in KloudMate
@@ -85,7 +77,7 @@ After the Agent restarts:
- Open [Explore](../../../visualize-data/explore/) in KloudMate.
- Query one of the scraped Kafka metrics, such as `kafka.brokers` or `kafka.topic.partitions`.
-- Build dashboards or alarms once the data is visible.
+- Build dashboards or alerts once the data is visible.
## Standard Kafka Dashboards
diff --git a/src/content/docs/docs/infrastructure/integrations/kafka-jmx-exporter-integration.mdx b/src/content/docs/docs/infrastructure/integrations/kafka-jmx-exporter-integration.mdx
index 8c9ff050..db6ccf50 100644
--- a/src/content/docs/docs/infrastructure/integrations/kafka-jmx-exporter-integration.mdx
+++ b/src/content/docs/docs/infrastructure/integrations/kafka-jmx-exporter-integration.mdx
@@ -1,42 +1,46 @@
---
title: "Kafka JMX Exporter Integration"
-description: "Collect Kafka broker, topic, and consumer group metrics from a Strimzi Kafka cluster on Kubernetes through the JMX Prometheus Exporter and the OpenTelemetry Collector's Prometheus receiver."
+description: "Collect Kafka broker, topic, and consumer group metrics from a Strimzi Kafka cluster on Kubernetes through the JMX Prometheus Exporter and the KloudMate Agent's Prometheus receiver."
sidebar:
order: 3.5
---
-Use this integration when your Kafka cluster runs on Kubernetes through Strimzi and already exposes JMX metrics in Prometheus format. It's a Kubernetes-native alternative to [Kafka Integration](../kafka-integration/)'s `kafkametrics` receiver: instead of connecting to the Kafka protocol directly, the OpenTelemetry Collector scrapes the JMX Prometheus Exporter endpoint that Strimzi already exposes on each broker and exporter pod.
+Use this integration when your Kafka cluster runs on Kubernetes through Strimzi and already exposes JMX metrics in Prometheus format. It's a Kubernetes-native alternative to [Kafka Integration](../kafka-integration/)'s `kafka_metrics` receiver: instead of connecting to the Kafka protocol directly, the KloudMate Agent scrapes the JMX Prometheus Exporter endpoint that Strimzi already exposes on each broker and exporter pod.
## Prerequisites
1. A Strimzi Kafka cluster.
2. The JMX Prometheus Exporter enabled on the cluster.
-3. Metrics exposed on port `9404`.
+3. Metrics are exposed on port `9404`.
## Step 1: Install the Agent
Install and run the [KloudMate Agent](../../../kloudmate-agent/) in your Kubernetes cluster. See [Kubernetes](../../../kloudmate-agent/installation/kubernetes-agent/) for the Helm install steps.
-## Step 2: Define the Prometheus Receiver for Kafka JMX Metrics
+## Step 2: Open the Collector Configuration
-Add the following configuration to the Agent's collector configuration.
+1. Go to **Settings → Agents**.
+2. Find the agent you want to configure and select the three dots.
+3. Choose **Configure**.
+4. Go to **Collector config**.
-```yaml
-extensions:
- health_check:
- endpoint: 0.0.0.0:13133
+## Step 3: Add the Kafka JMX Configuration
+
+Add the following configuration. Replace the `kafka-prod` regex with your own Kafka namespace.
+
+- If the agent runs in managed mode, add it to the **Custom config override** section. See [Custom config override](../../../kloudmate-agent/custom-config-override/).
+- If the agent runs in manual mode, add it directly to the collector configuration.
+```yaml
receivers:
prometheus:
config:
global:
scrape_interval: 30s
-
scrape_configs:
- job_name: kafka-resources
kubernetes_sd_configs:
- role: pod
-
relabel_configs:
- source_labels:
- __meta_kubernetes_namespace
@@ -52,21 +56,8 @@ receivers:
- __meta_kubernetes_pod_container_port_name
action: keep
regex: tcp-prometheus
-```
-
-This uses Kubernetes service discovery to find every pod in the cluster, then narrows the scrape targets down through three `relabel_configs` stages: the first keeps only pods in the `kafka-prod` namespace, the second keeps only pods whose Strimzi component-type label is `kafka` or `kafka-exporter`, and the third keeps only the `tcp-prometheus` container port those pods expose. Replace the `kafka-prod` regex with your own Kafka namespace before applying the configuration.
-
-## Step 3: Add to Pipeline
-
-Add the `prometheus` receiver to the metrics pipeline in your Agent configuration.
-```yaml
service:
- extensions:
- - health_check
- telemetry:
- metrics:
- level: none
pipelines:
metrics/kafka-jmx:
receivers:
@@ -77,25 +68,13 @@ service:
- otlphttp
```
-## Step 4: Apply the Configuration
-
-If you run the OpenTelemetry Collector through the KloudMate Agent in managed mode, add this configuration through [Custom config override](../../../kloudmate-agent/custom-config-override/) instead of editing the deployment's YAML directly — the override merges it into the running configuration without switching the agent to manual mode. If you operate your own OpenTelemetry Collector, merge this block into its existing configuration instead.
+This uses Kubernetes service discovery to find every pod in the cluster, then narrows the scrape targets down through three `relabel_configs` stages: the first keeps only pods in the `kafka-prod` namespace, the second keeps only pods whose Strimzi component-type label is `kafka` or `kafka-exporter`, and the third keeps only the `tcp-prometheus` container port those pods expose.
-## Step 5: Restart the Collector
+## Step 4: Save the Configuration
-If the Agent runs as a Kubernetes workload, restart whichever one runs your Collector:
+Click **Save**. The Agent restarts and applies the new configuration.
-```bash
-kubectl rollout restart deployment -n
-```
-
-or
-
-```bash
-kubectl rollout restart daemonset -n
-```
-
-## Step 6: Validate in KloudMate
+## Step 5: Validate in KloudMate
After the Agent restarts:
@@ -112,6 +91,10 @@ kafka_consumergroup_lag
- Build dashboards or alerts once the data is visible.
+## Kafka Exporter Dashboard
+
+Once the metrics are flowing, you can visualize broker availability, partition and replica health, message rates, and consumer group lag.
+
## Metrics Collected
This integration collects:
@@ -129,7 +112,7 @@ This integration collects:
## Related
-- [Kafka Integration](../kafka-integration/) — the `kafkametrics` receiver alternative, for brokers reachable directly over the Kafka protocol instead of through JMX.
+- [Kafka Integration](../kafka-integration/) — the `kafka_metrics` receiver alternative, for brokers reachable directly over the Kafka protocol instead of through JMX.
- [Prometheus Integration](../prometheus-integration/) — the general-purpose pattern this integration builds on.
- [Custom config override](../../../kloudmate-agent/custom-config-override/) — how to add this receiver to a managed KloudMate Agent.
- [Explore](../../../visualize-data/explore/) — where to query the collected metrics.
diff --git a/src/content/docs/docs/infrastructure/integrations/kafka-prometheus-servicemonitor-integration.mdx b/src/content/docs/docs/infrastructure/integrations/kafka-prometheus-servicemonitor-integration.mdx
index 062e2190..92162d27 100644
--- a/src/content/docs/docs/infrastructure/integrations/kafka-prometheus-servicemonitor-integration.mdx
+++ b/src/content/docs/docs/infrastructure/integrations/kafka-prometheus-servicemonitor-integration.mdx
@@ -9,7 +9,7 @@ Use this integration when a Kafka cluster (for example, a Bitnami Helm release)
## Prerequisites
-1. The [KloudMate Agent](../../../kloudmate-agent/) deployed and running in the cluster.
+1. The [KloudMate Agent](../../../kloudmate-agent/) is deployed and running in the cluster.
2. The scrape details from the `ServiceMonitor`(s) already collecting Kafka metrics.
## Step 1: Gather Details from the Existing ServiceMonitor
@@ -26,12 +26,19 @@ From the output, note:
4. `spec.endpoints[].interval` → match this in your scrape config to stay consistent with the existing setup.
5. `spec.namespaceSelector` → confirms the namespace to target.
-## Step 2: Define the Prometheus Receiver
+## Step 2: Open the Collector Configuration
-Go to **Settings → Agents**, select the Kubernetes agent, and click **Configure**.
+1. Go to **Settings → Agents**.
+2. Find the agent you want to configure and select the three dots.
+3. Choose **Configure**.
+4. Go to **Collector config**.
-- If the agent runs in managed mode, add the configuration below through the [custom config override](../../../kloudmate-agent/custom-config-override/) section for the deployment collector.
-- If the agent runs in manual mode, add the configuration as-is.
+## Step 3: Add the Prometheus Receiver
+
+Add the following configuration. Replace ``, ``, and `` with your own values, and adjust the ports and interval if your `ServiceMonitor` differs from the Bitnami defaults.
+
+- If the agent runs in managed mode, add it to the **Custom config override** section. See [Custom config override](../../../kloudmate-agent/custom-config-override/).
+- If the agent runs in manual mode, add it directly to the collector configuration.
```yaml
receivers:
@@ -53,33 +60,25 @@ receivers:
- targets: ['-kafka-exporter..svc.cluster.local:9308']
labels:
cluster: ''
+
service:
pipelines:
metrics/kafka:
- receivers: [prometheus]
- processors: [batch]
- exporters: [otlphttp]
+ receivers:
+ - prometheus
+ processors:
+ - batch
+ exporters:
+ - otlphttp
```
-Replace ``, ``, and `` with your own values, and adjust the ports and interval if your `ServiceMonitor` differs from the Bitnami defaults.
-
-## Step 3: Restart the Agent
-
-If you edited the configuration directly instead of using a custom config override, restart whichever workload runs the agent:
+## Step 4: Save the Configuration
-```bash
-kubectl rollout restart deployment -n
-```
-
-or
-
-```bash
-kubectl rollout restart daemonset -n
-```
+Click **Save**. The Agent restarts and applies the new configuration.
-## Step 4: Validate in KloudMate
+## Step 5: Validate in KloudMate
-After the agent restarts:
+After the Agent restarts:
- Open [Explore](../../../visualize-data/explore/) in KloudMate.
- Query one of the scraped metrics, for example:
diff --git a/src/content/docs/docs/infrastructure/integrations/keycloak-integration.mdx b/src/content/docs/docs/infrastructure/integrations/keycloak-integration.mdx
index 8ca2ec51..64878d91 100644
--- a/src/content/docs/docs/infrastructure/integrations/keycloak-integration.mdx
+++ b/src/content/docs/docs/infrastructure/integrations/keycloak-integration.mdx
@@ -10,17 +10,23 @@ Use this integration when you want to scrape Keycloak metrics from its Prometheu
1. Install and run the [KloudMate Agent](../../../kloudmate-agent/).
2. Ensure your Keycloak service exposes the `/auth/metrics` endpoint. If this endpoint is not available, no metrics will be scraped.
-3. Keycloak must be reachable from the node where the KloudMate Agent is running.
+3. Keycloak must be reachable from the host or cluster where the KloudMate Agent is running.
-## Step 1: Define the Prometheus Receiver
+## Step 1: Open the Collector Configuration
-Add the following Prometheus receiver to your Agent configuration file (typically `/etc/km-agent/config.yaml`). Replace `` and `` with your actual Kubernetes service name and namespace.
+1. Go to **Settings → Agents**.
+2. Find the agent you want to configure and select the three dots.
+3. Choose **Configure**.
+4. Go to **Collector config**.
-```yaml
-extensions:
- health_check:
- endpoint: 0.0.0.0:13133
+## Step 2: Add the Keycloak Configuration
+
+Add the following configuration. Replace `` and `` with your actual Kubernetes service name and namespace.
+
+- If the agent runs in managed mode, add it to the **Custom config override** section. See [Custom config override](../../../kloudmate-agent/custom-config-override/).
+- If the agent runs in manual mode, add it directly to the collector configuration.
+```yaml
receivers:
prometheus:
config:
@@ -31,23 +37,8 @@ receivers:
static_configs:
- targets:
- ..svc.cluster.local:8080
-```
-
-:::info
-Validate indentation carefully when copying `scrape_configs` into the Agent configuration.
-:::
-## Step 2: Add to Pipeline
-
-Add the Prometheus receiver to the metrics pipeline in your Agent configuration.
-
-```yaml
service:
- extensions:
- - health_check
- telemetry:
- metrics:
- level: none
pipelines:
metrics/keycloak:
receivers:
@@ -58,12 +49,13 @@ service:
- otlphttp
```
-## Step 3: Restart the Agent
+:::info
+Validate indentation carefully when copying `scrape_configs` into the Agent configuration.
+:::
+
+## Step 3: Save the Configuration
-```bash
-systemctl restart kmagent
-systemctl status kmagent
-```
+Click **Save**. The Agent restarts and applies the new configuration.
## Step 4: Validate in KloudMate
@@ -71,4 +63,4 @@ After the Agent restarts:
- Open [Explore](../../../visualize-data/explore/) in KloudMate.
- Query one of the scraped Keycloak metrics.
-- Build dashboards or alarms once data is visible.
+- Build dashboards or alerts once data is visible.
diff --git a/src/content/docs/docs/infrastructure/integrations/prometheus-integration.mdx b/src/content/docs/docs/infrastructure/integrations/prometheus-integration.mdx
index e94c6fc1..6c9fae7e 100644
--- a/src/content/docs/docs/infrastructure/integrations/prometheus-integration.mdx
+++ b/src/content/docs/docs/infrastructure/integrations/prometheus-integration.mdx
@@ -4,7 +4,7 @@ description: "Ingest Prometheus metrics into KloudMate through the KloudMate Age
sidebar:
order: 1
---
-Use this integration when your workloads already expose Prometheus metrics and you want KloudMate to ingest them without changing application code.
+Use this integration when your workloads already expose Prometheus metrics, and you want KloudMate to ingest them without changing application code.
KloudMate does not scrape Prometheus directly. Instead, you configure the KloudMate Agent with the Prometheus receiver and export the collected metrics to KloudMate.
@@ -13,19 +13,25 @@ KloudMate does not scrape Prometheus directly. Instead, you configure the KloudM
1. Install and run the [KloudMate Agent](../../../kloudmate-agent/).
2. Ensure you can access the Prometheus configuration or know the scrape targets you want the Agent to use.
-## Step 1: Define the Prometheus Receiver
+## Step 1: Open the Collector Configuration
-Add a Prometheus receiver to the Agent configuration file (typically `/etc/km-agent/config.yaml`). You can copy scrape jobs from your existing Prometheus configuration or define a smaller metrics-only subset for KloudMate.
+1. Go to **Settings → Agents**.
+2. Find the agent you want to configure and select the three dots.
+3. Choose **Configure**.
+4. Go to **Collector config**.
+
+## Step 2: Add the Prometheus Configuration
+
+Add the following configuration. You can copy scrape jobs from your existing Prometheus configuration or define a smaller metrics-only subset for KloudMate.
+
+- If the agent runs in managed mode, add it to the **Custom config override** section. See [Custom config override](../../../kloudmate-agent/custom-config-override/).
+- If the agent runs in manual mode, add it directly to the collector configuration.
```yaml
receivers:
prometheus:
config:
scrape_configs:
- - job_name: km-agent
- scrape_interval: 5s
- static_configs:
- - targets: ["0.0.0.0:8888"]
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
@@ -37,31 +43,27 @@ receivers:
- source_labels: [__name__]
regex: "(request_duration_seconds.*|response_duration_seconds.*)"
action: keep
+
+service:
+ pipelines:
+ metrics/prometheus:
+ receivers:
+ - prometheus
+ processors:
+ - batch
+ exporters:
+ - otlphttp
```
+The `otlphttp` exporter is already configured by default when you install the KloudMate Agent.
+
:::info
Validate indentation carefully when copying `scrape_configs` into the Agent configuration.
:::
-## Step 2: Add to Pipeline
-
-Configure the Agent's pipeline to include the Prometheus receiver. The `otlphttp` exporter is already configured by default when you install the KloudMate Agent.
+## Step 3: Save the Configuration
-```yaml
-service:
- pipelines:
- metrics:
- receivers: [prometheus, hostmetrics] # Add prometheus to your existing receivers
- processors: [resource, batch]
- exporters: [debug, otlphttp]
-```
-
-## Step 3: Restart the Agent
-
-```bash
-systemctl restart kmagent
-systemctl status kmagent
-```
+Click **Save**. The Agent restarts and applies the new configuration.
## Step 4: Validate in KloudMate
diff --git a/src/content/docs/docs/infrastructure/integrations/vcenter-integration.mdx b/src/content/docs/docs/infrastructure/integrations/vcenter-integration.mdx
index 132c563d..a8532beb 100644
--- a/src/content/docs/docs/infrastructure/integrations/vcenter-integration.mdx
+++ b/src/content/docs/docs/infrastructure/integrations/vcenter-integration.mdx
@@ -4,7 +4,7 @@ description: "Collect VMware vCenter metrics in KloudMate through the OpenTeleme
sidebar:
order: 2
---
-Use this guide to collect VMware vCenter metrics in KloudMate with the OpenTelemetry `vcenter` receiver running via the KloudMate Agent.
+Use this guide to collect VMware vCenter metrics in KloudMate with OpenTelemetry `vcenter` receiver running via the KloudMate Agent.
## Prerequisites
@@ -13,9 +13,19 @@ Use this guide to collect VMware vCenter metrics in KloudMate with the OpenTelem
3. Identify the vCenter or ESXi endpoint with SDK access enabled.
4. Install the [KloudMate Agent](../../../kloudmate-agent/) on a host that can reach vCenter.
-## Step 1: Configure the Receiver
+## Step 1: Open the Collector Configuration
-Add the `vcenter` receiver to your Agent configuration file (typically `/etc/km-agent/config.yaml`):
+1. Go to **Settings → Agents**.
+2. Find the agent you want to configure and select the three dots.
+3. Choose **Configure**.
+4. Go to **Collector config**.
+
+## Step 2: Add the vCenter Configuration
+
+Add the following configuration. Replace the endpoint and credentials to match your environment.
+
+- If the agent runs in managed mode, add it to the **Custom config override** section. See [Custom config override](../../../kloudmate-agent/custom-config-override/).
+- If the agent runs in manual mode, add it directly to the collector configuration.
```yaml
receivers:
@@ -27,33 +37,23 @@ receivers:
initial_delay: 1s
tls:
insecure: true
-```
-
-If you need to disable specific metrics, add them under the `metrics` section of the receiver configuration.
-## Step 2: Add to Pipeline
-
-Update the `metrics` pipeline in the `service` section to include the `vcenter` receiver.
-
-```yaml
service:
pipelines:
- metrics:
- receivers: [vcenter, hostmetrics] # Add vcenter here
- processors: [resource, batch]
- exporters: [debug, otlphttp]
+ metrics/vcenter:
+ receivers:
+ - vcenter
+ processors:
+ - batch
+ exporters:
+ - otlphttp
```
-## Step 3: Restart the Agent
-
-For Linux:
+If you need to disable specific metrics, add them under the `metrics` section of the receiver configuration.
-```bash
-systemctl restart kmagent
-systemctl status kmagent
-```
+## Step 3: Save the Configuration
-For Windows, restart the `KloudMate Agent` service from `services.msc`.
+Click **Save**. The Agent restarts and applies the new configuration.
## Step 4: Validate in KloudMate
diff --git a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-jobs-tab.png b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-jobs-tab.png
new file mode 100644
index 00000000..d0e95e47
Binary files /dev/null and b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-jobs-tab.png differ
diff --git a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-nodes-tab.png b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-nodes-tab.png
index d2520732..c2cbbb0d 100644
Binary files a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-nodes-tab.png and b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-nodes-tab.png differ
diff --git a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-reporting-filter.png b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-reporting-filter.png
new file mode 100644
index 00000000..8e1ed7a2
Binary files /dev/null and b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-reporting-filter.png differ
diff --git a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-storage-tab.png b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-storage-tab.png
index c835f340..41641971 100644
Binary files a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-storage-tab.png and b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-storage-tab.png differ
diff --git a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-workloads-tab.png b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-workloads-tab.png
index 9cc6e1ed..7150e07e 100644
Binary files a/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-workloads-tab.png and b/src/content/docs/docs/infrastructure/kubernetes/images/kubernetes-workloads-tab.png differ
diff --git a/src/content/docs/docs/infrastructure/kubernetes/index.mdx b/src/content/docs/docs/infrastructure/kubernetes/index.mdx
index 67a70040..d0327e69 100644
--- a/src/content/docs/docs/infrastructure/kubernetes/index.mdx
+++ b/src/content/docs/docs/infrastructure/kubernetes/index.mdx
@@ -7,7 +7,7 @@ sidebar:
---
Kubernetes Monitoring in KloudMate gives you visibility into cluster health, workload availability, resource usage, pod behavior, and related telemetry collected by the KloudMate Agent.
-KloudMate organizes Kubernetes data into views for clusters, nodes, namespaces, pods, and workloads so you can move from fleet-level health to pod-level troubleshooting quickly.
+KloudMate organizes Kubernetes data into tabs for clusters, nodes, namespaces, pods, storage, workloads, and jobs, so you can move from fleet-level health to pod-level troubleshooting quickly.
## What This Covers
@@ -61,7 +61,7 @@ The **Clusters** tab lists every connected cluster, with overall health, capacit
The **Nodes** tab lists every node across your clusters, showing node health and infrastructure-level resource usage: allocatable CPU and memory alongside current usage for each. Use it to spot nodes running hot before they affect scheduling.
-
+
### Namespaces
@@ -73,15 +73,29 @@ The **Pods** tab lists every pod across your clusters, with its performance, res
### Storage
-The **Storage** tab lists Persistent Volume Claims (PVCs) across your clusters, with their status, claim, storage class, and capacity. Use it to see what's provisioned and bound without checking each cluster individually.
+The **Storage** tab lists Persistent Volumes across your clusters, with their status, claim, storage class, and capacity. Use it to see what's provisioned and bound without checking each cluster individually. To list Persistent Volume Claims instead, select **Persistent Volume Claim** as the storage type.
-
+
### Workloads
The **Workloads** tab lists deployments, daemonsets, statefulsets, and other workload types across your clusters, showing their health and availability alongside pod counts and CPU and memory requests and limits. Filter by workload type, namespace, or cluster to narrow the list.
-
+
+
+### Jobs
+
+The **Jobs** tab lists Jobs across your clusters, with their status, completions (succeeded out of desired, the same count `kubectl` shows), and active, failed, and parallelism counts. Filter by cluster, namespace, or status to find Jobs that failed or haven't completed. To list CronJobs instead, select **CronJob** as the job type. Each CronJob shows its cluster, namespace, and number of active jobs.
+
+
+
+## Find resources that stopped reporting
+
+Every tab includes a **Last seen** column. A resource stays in the list for seven days after it last reported, and its **Last seen** dot turns from green to gray once it has gone 15 minutes without reporting. If a resource reported nothing in the selected time range, its metric cells show a dash.
+
+Set the **Reporting** filter to **Reporting now** to list only live resources, or to **Stopped reporting** to find resources that stopped sending data. When the filter is empty, the pod, ReplicaSet, and Job lists show only resources that are reporting now, and every other list shows everything seen in the last seven days.
+
+
## Validation Checklist
diff --git a/src/content/docs/docs/issues/images/issues-notification-settings.png b/src/content/docs/docs/issues/images/issues-notification-settings.png
index 68a5d7ca..8235cf00 100644
Binary files a/src/content/docs/docs/issues/images/issues-notification-settings.png and b/src/content/docs/docs/issues/images/issues-notification-settings.png differ
diff --git a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/dotnet.mdx b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/dotnet.mdx
index fe0b903e..89a41c30 100644
--- a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/dotnet.mdx
+++ b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/dotnet.mdx
@@ -73,7 +73,7 @@ On Amazon ECS, the agent injects the .NET instrumentation into the services you
## On Docker
-When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your .NET containers per service through the **Off / eBPF** toggle in Discovered Services, with no code change and no restart.
+When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your .NET containers per service through the **Off / eBPF** toggle in the **Application instrumentation** card, with no code change and no restart.
The agent can't inject the in-process SDK into a running container, so SDK mode isn't offered for containers. For full SDK depth, add OpenTelemetry to your image yourself and point it at the KloudMate agent's OTLP endpoint. See [Docker platform notes](../../platform-notes/docker/) for the endpoint addresses. On Windows, systemd, Kubernetes, or Amazon ECS, the agent injects the profiler for you instead.
diff --git a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/go.mdx b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/go.mdx
index 20cd30f8..6707c272 100644
--- a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/go.mdx
+++ b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/go.mdx
@@ -27,7 +27,7 @@ The result is close to what a language SDK would produce, without changing your
eBPF monitoring and the language agents both propagate W3C trace context (`traceparent`). So when an instrumented service calls a Go service, the Go service's eBPF span joins the caller's trace. You see one connected trace with the correct parent-child links, not two separate views.
:::note
-In the Discovered Services list, a Go service offers **Off** and **eBPF** but not **SDK**. eBPF is already tracing it at the kernel level, so there's no in-process agent to add.
+In the **Application instrumentation** card, a Go service offers **Off** and **eBPF** but not **SDK**. eBPF is already tracing it at the kernel level, so there's no in-process agent to add.
:::
## Next steps
diff --git a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/java.mdx b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/java.mdx
index b570e612..ea54d647 100644
--- a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/java.mdx
+++ b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/java.mdx
@@ -15,7 +15,7 @@ The Java agent ships inside the KloudMate agent package and upgrades with it, so
### systemd services
-This is the main method on Linux. When you turn on SDK tracing for a systemd service, the agent writes a small startup config that loads the Java agent through `JAVA_TOOL_OPTIONS`, then restarts the service after you confirm. Because the service restarts, the agent sets everything up front: a fixed `service.name` (the name shown in Discovered Services), the sampling rate, and where to send traces.
+This is the main method on Linux. When you turn on SDK tracing for a systemd service, the agent writes a small startup config that loads the Java agent through `JAVA_TOOL_OPTIONS`, then restarts the service after you confirm. Because the service restarts, the agent sets everything up front: a fixed `service.name` (the name shown in the **Application instrumentation** card), the sampling rate, and where to send traces.
Turning tracing off removes the config and restarts the service, leaving it as it was. Running the same action twice changes nothing, so it's safe to repeat.
@@ -52,7 +52,7 @@ On Amazon ECS, the agent injects the OpenTelemetry Java agent into the services
## On Docker
-When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your Java containers per service through the **Off / eBPF** toggle in Discovered Services, with no code change and no restart.
+When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your Java containers per service through the **Off / eBPF** toggle in the **Application instrumentation** card, with no code change and no restart.
The agent can't inject the in-process SDK into a running container, so SDK mode isn't offered for containers. For full SDK depth, add the OpenTelemetry Java agent to your image yourself and point it at the KloudMate agent's OTLP endpoint. See [Docker platform notes](../../platform-notes/docker/) for the endpoint addresses. On systemd, Kubernetes, or Amazon ECS, the agent injects the Java agent for you instead.
diff --git a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/nodejs.mdx b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/nodejs.mdx
index 5ff74bc3..e539adbc 100644
--- a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/nodejs.mdx
+++ b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/nodejs.mdx
@@ -23,7 +23,7 @@ For a Node.js app under PM2, the agent restarts it with tracing switched on (`pm
### Standalone processes
-The agent can instrument a Node.js process only when it runs under systemd or PM2. A process started on its own can't be instrumented automatically. Run it under systemd or PM2 first; the Discovered Services list flags these and tells you so.
+The agent can instrument a Node.js process only when it runs under systemd or PM2. A process started on its own can't be instrumented automatically. Run it under systemd or PM2 first; the **Application instrumentation** card flags these and tells you so.
## On Kubernetes
@@ -43,7 +43,7 @@ On Amazon ECS, the agent injects the Node.js instrumentation into the services y
## On Docker
-When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your Node.js containers per service through the **Off / eBPF** toggle in Discovered Services, with no code change and no restart.
+When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your Node.js containers per service through the **Off / eBPF** toggle in the **Application instrumentation** card, with no code change and no restart.
The agent can't inject the in-process SDK into a running container, so SDK mode isn't offered for containers. For full SDK depth, add OpenTelemetry to your image yourself and point it at the KloudMate agent's OTLP endpoint. See [Docker platform notes](../../platform-notes/docker/) for the endpoint addresses. On systemd, Kubernetes, or Amazon ECS, the agent injects the instrumentation for you instead.
diff --git a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/overview.mdx b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/overview.mdx
index 5e132862..97a30c76 100644
--- a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/overview.mdx
+++ b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/overview.mdx
@@ -17,8 +17,8 @@ Application performance monitoring (APM) is opt-in and per service. For each ser
The flow is the same across every deployment mode:
-1. **Discover.** The agent finds your services and lists them under **Discovered Services**. See [Discovery](../../concepts/discovery/).
-2. **Choose a mode.** For each service, pick Off, eBPF, or SDK, then apply. eBPF takes effect on the next agent check-in with no restart.
+1. **Discover.** The agent finds your services and lists them in the **Application instrumentation** card on the agent's **APM** tab. To open it, go to **Settings → Agents**, open the agent's actions menu (⋮), and select **Configure**. See [Discovery](../../concepts/discovery/).
+2. **Choose a mode.** For each service, pick Off, eBPF, or SDK, then click **Apply**. eBPF takes effect on the next agent check-in with no restart.
3. **Confirm the restart (SDK only).** SDK instrumentation takes effect when the process restarts, so the agent asks before applying. After you confirm, the service shows **Pending** while the agent applies the change and restarts the app.
4. **Traces flow.** The service's chip flips to **Instrumented** and it sends application traces to the agent's local collector, correlated with its eBPF spans.
@@ -33,7 +33,7 @@ Each service selected for SDK instrumentation shows a status chip so you always
Services covered by eBPF (Go, Ruby, or anything without an SDK injector) don't restart and carry no chip. eBPF traces them as soon as you turn them on. A separate **Restart pending** marker on the *agents list* is unrelated: it means the agent's own collector configuration is queued for a restart, not a per-service instrumentation change.
-
+
## Restart consent and health gating
diff --git a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/php.mdx b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/php.mdx
index e3bbb090..5073b17d 100644
--- a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/php.mdx
+++ b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/php.mdx
@@ -19,7 +19,7 @@ When you instrument a PHP service, the agent adds a config file for the target's
## On Docker
-A PHP container offers an **SDK** option in Discovered Services. Turn it on and the agent injects the tracer, matches it to the container's PHP build, and reloads the web server (Apache graceful or php-fpm) with no redeploy and no container restart. The instrumented application sends traces to the agent through the container's host gateway.
+A PHP container offers an **SDK** option in the **Application instrumentation** card. Turn it on and the agent injects the tracer, matches it to the container's PHP build, and reloads the web server (Apache graceful or php-fpm) with no redeploy and no container restart. The instrumented application sends traces to the agent through the container's host gateway.
This SDK option is on by default in Docker mode. On a Linux host agent it's opt-in: set `KM_CONTAINER_INSTRUMENT_ENABLED=true`. To instrument PHP containers automatically without picking each one, use `KM_CONTAINER_INSTRUMENT_ALL`, `KM_CONTAINER_INSTRUMENT_NAMES`, or `KM_CONTAINER_INSTRUMENT_IMAGES`.
diff --git a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/python.mdx b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/python.mdx
index 1f7c928f..a2b5a1b1 100644
--- a/src/content/docs/docs/kloudmate-agent/auto-instrumentation/python.mdx
+++ b/src/content/docs/docs/kloudmate-agent/auto-instrumentation/python.mdx
@@ -23,7 +23,7 @@ For a Python app under PM2, the agent restarts it with tracing switched on (`pm2
### Standalone processes
-The agent can instrument a Python process only when it runs under systemd or PM2. A process started on its own can't be instrumented automatically. Run it under systemd or PM2 first; the Discovered Services list flags these and tells you so.
+The agent can instrument a Python process only when it runs under systemd or PM2. A process started on its own can't be instrumented automatically. Run it under systemd or PM2 first; the **Application instrumentation** card flags these and tells you so.
## On Kubernetes
@@ -43,7 +43,7 @@ On Amazon ECS, the agent injects the Python instrumentation into the services yo
## On Docker
-When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your Python containers per service through the **Off / eBPF** toggle in Discovered Services, with no code change and no restart.
+When the agent monitors Docker containers, whether in Docker mode or as a Linux service on a Docker host, eBPF traces your Python containers per service through the **Off / eBPF** toggle in the **Application instrumentation** card, with no code change and no restart.
The agent can't inject the in-process SDK into a running container, so SDK mode isn't offered for containers. For full SDK depth, add OpenTelemetry to your image yourself and point it at the KloudMate agent's OTLP endpoint. See [Docker platform notes](../../platform-notes/docker/) for the endpoint addresses. On systemd, Kubernetes, or Amazon ECS, the agent injects the instrumentation for you instead.
diff --git a/src/content/docs/docs/kloudmate-agent/concepts/discovery.mdx b/src/content/docs/docs/kloudmate-agent/concepts/discovery.mdx
index 55118120..f05ac51d 100644
--- a/src/content/docs/docs/kloudmate-agent/concepts/discovery.mdx
+++ b/src/content/docs/docs/kloudmate-agent/concepts/discovery.mdx
@@ -18,9 +18,9 @@ The agent finds two kinds of things:
For each service, discovery records how it is managed and which ports it listens on.
-## The Discovered Services list
+## Where discovered services appear
-Discovery shows its inventory as a **Discovered Services** list on the host detail page in the web interface. Each entry shows:
+To see what discovery found, go to **Settings → Agents**, open the agent's actions menu (⋮), and select **Configure**. Application services are listed in the **Application instrumentation** card on the **APM** tab, and databases on the **Databases** tab. Discovery reports these details for each service:
- **Name** and **runtime** (for example, `checkout` running Java).
- **Runtime version**, where it affects a decision (for example, the PHP major and minor version, or whether a .NET app runs on .NET Framework or .NET Core).
@@ -30,14 +30,14 @@ Discovery shows its inventory as a **Discovered Services** list on the host deta
- **Instrumentable** flag: whether the agent has a way to attach application tracing to that runtime.
:::note
-The `instrumentable` flag shows whether the agent can inject tracing into that runtime. Java, .NET, Node.js, and Python are instrumentable. PHP is instrumentable when a tracer is available for the detected version. Go is not injected: it is covered by eBPF monitoring instead, so it appears as covered rather than instrumentable.
+The `instrumentable` flag shows whether the agent can inject tracing into that runtime. Java, .NET, Node.js, and Python are instrumentable. PHP is instrumentable when a tracer is available for the detected version. Go isn't injected. eBPF monitoring covers it instead, so a Go service offers **Off** and **eBPF** but no **SDK** option.
:::

## Stable service identity
-Each discovered service gets a **stable identity** that survives restarts and process-ID changes. This is what lets an **Instrument** choice keep applying after a service restarts: you instrument `checkout`, not a process that will be gone after the next deploy.
+Each discovered service gets a **stable identity** that survives restarts and process-ID changes. This is what lets a tracing choice keep applying after a service restarts: you instrument `checkout`, not a process that will be gone after the next deploy.
## Privacy and safety
@@ -48,10 +48,10 @@ Discovery keeps sensitive data on the host:
## How discovery powers the rest of the agent
-The Discovered Services list is the entry point for the opt-in features:
+Discovered services are the entry point for the opt-in features:
-- **Application APM:** each instrumentable service has an **Instrument** toggle that starts tracing for that service. See [Application APM](../../auto-instrumentation/overview/).
-- **Database monitoring:** a discovered database starts the monitoring wizard, which prefills the engine and endpoint. See [Database monitoring](../../database-monitoring/overview/).
+- **Application APM:** on the **APM** tab, choose **Off**, **eBPF**, or **SDK** for each service, then click **Apply**. **SDK** is offered only for instrumentable services. See [Application APM](../../auto-instrumentation/overview/).
+- **Database monitoring:** on the **Databases** tab, click **Monitor** next to a discovered database to start the monitoring wizard, which prefills the engine and endpoint. See [Database monitoring](../../database-monitoring/overview/).
## Next steps
diff --git a/src/content/docs/docs/kloudmate-agent/ebpf-observability.mdx b/src/content/docs/docs/kloudmate-agent/ebpf-observability.mdx
index 9683d05f..56a295e1 100644
--- a/src/content/docs/docs/kloudmate-agent/ebpf-observability.mdx
+++ b/src/content/docs/docs/kloudmate-agent/ebpf-observability.mdx
@@ -24,7 +24,7 @@ When eBPF monitoring is active, the agent collects:
Because eBPF observes traffic in the kernel, it can trace a service with no code change and no restart — including Go and other services no language agent can inject. eBPF RED metrics and trace spans are a per-service choice you turn on (the **eBPF** APM mode); the network topology map and database activity monitoring run automatically. When a service also runs application APM, its eBPF spans and the application spans share the same trace, so hops you haven't put on the SDK still appear in end-to-end traces. See the [Application APM overview](../auto-instrumentation/overview/).
-On a host running Docker, the agent reads each container's name and listen ports from the Docker socket, so a container can be traced with eBPF the same way a host process can, whether the agent runs as a Linux service or in Docker mode. In **Discovered Services** a container offers **Off** or **eBPF**, because the agent can't attach an in-process SDK to a running container. PHP containers can also offer an **SDK** option that injects a tracer with no redeploy. See [Docker platform notes](../platform-notes/docker/).
+On a host running Docker, the agent reads each container's name and listen ports from the Docker socket, so a container can be traced with eBPF the same way a host process can, whether the agent runs as a Linux service or in Docker mode. In the **Application instrumentation** card on the agent's **APM** tab, a container offers **Off** or **eBPF**, because the agent can't attach an in-process SDK to a running container. PHP containers can also offer an **SDK** option that injects a tracer with no redeploy. See [Docker platform notes](../platform-notes/docker/).
## Deployment modes and requirements
@@ -46,7 +46,7 @@ Whether eBPF turns on, and how much of it, depends on the host's kernel version,
- **Supported host:** eBPF turns on automatically.
- **Kernel 4.14 to 5.8, or no BTF:** eBPF runs with reduced features.
-- **Unsupported host** (kernel older than 4.14, `debugfs` not mounted, or missing privileges): eBPF is skipped, and KloudMate shows the reason, for example "eBPF unavailable: kernel 4.9". The host still gets host metrics and logs.
+- **Unsupported host** (kernel older than 4.14, `debugfs` not mounted, or missing privileges): eBPF is skipped, and the agent writes the reason to its log on the host, for example `kernel 4.9 < 4.14`. The host still gets host metrics and logs.
If eBPF cannot load on a host that should support it, the host keeps its metrics and logs and KloudMate reports why.
@@ -120,11 +120,11 @@ Once the agent is collecting eBPF telemetry, confirm it in these views:
- **[APM](../../apm-and-tracing/apm-views/):** look for the service map, latency, and throughput.
- **[Traces](../../apm-and-tracing/):** search for the spans generated from your traffic.
-- **[Database Activity Monitoring](../../database-monitoring/database-activity-monitoring-dam/):** look for captured database queries.
+- **[Databases](../../database-monitoring/calling-services/):** open a database and check its **Calling services** tab for the services and queries captured from your traffic.
## Troubleshooting
-- **No eBPF metrics or service map:** check the host detail page for an "eBPF unavailable" reason. The most common causes are a kernel older than 4.14, `debugfs` not mounted, or missing privileges.
+- **No eBPF metrics or service map:** check the agent's log for `eBPF baseline not enabled`, which gives the reason. On a Linux host, run `journalctl -u kmagent | grep "eBPF baseline"`. The most common causes are a kernel older than 4.14, `debugfs` not mounted, or missing privileges.
- **Metrics are present but sparse:** the host may be on a kernel between 4.14 and 5.8, or without BTF, so eBPF is running with reduced features. Host metrics and logs are unaffected.
- **Confirm the agent's own view:** the agent reports whether eBPF loaded through its self-health metrics, and surfaces load errors through its self-logs. See [Agent health metrics](../self-observability/health-metrics/) and [Troubleshooting](../troubleshooting/).
diff --git a/src/content/docs/docs/kloudmate-agent/images/agent-runbook-scripts-tab.png b/src/content/docs/docs/kloudmate-agent/images/agent-runbook-scripts-tab.png
new file mode 100644
index 00000000..36db3007
Binary files /dev/null and b/src/content/docs/docs/kloudmate-agent/images/agent-runbook-scripts-tab.png differ
diff --git a/src/content/docs/docs/kloudmate-agent/installation/docker-agent.mdx b/src/content/docs/docs/kloudmate-agent/installation/docker-agent.mdx
index 0d0cde13..be7b7c21 100644
--- a/src/content/docs/docs/kloudmate-agent/installation/docker-agent.mdx
+++ b/src/content/docs/docs/kloudmate-agent/installation/docker-agent.mdx
@@ -63,7 +63,7 @@ View all collected data in these KloudMate sections:
**Step 1:** Navigate to the **Settings → Agents** section in your KloudMate account.
-**Step 2:** Select **Docker** from the list of available integrations.
+**Step 2:** Click **Add Agent**, select **Docker**, and click **Continue**. On the **API key** step, click **Create backend key**, or select **Use existing key** and paste your backend ingest key. Then click **Continue**.
**Step 3:** Copy and run the Docker agent install command in your terminal to download and install the agent.
@@ -89,9 +89,9 @@ New agents start in managed mode, so KloudMate generates the collector configura
## **Next Steps**
-- Open **Infrastructure Monitoring** to see all running containers and host metrics
+- Open **Hosts** to see all running containers and host metrics
- Go to **Dashboards** to visualize container and host performance
-- Use **Log Explorer** to investigate container logs and host logs
+- Use **Logs** to investigate container logs and host logs
- Configure **alerts** for container or host thresholds
## Uninstall Agent
diff --git a/src/content/docs/docs/kloudmate-agent/installation/ecs-agent.mdx b/src/content/docs/docs/kloudmate-agent/installation/ecs-agent.mdx
index 46f497b6..b19fdda0 100644
--- a/src/content/docs/docs/kloudmate-agent/installation/ecs-agent.mdx
+++ b/src/content/docs/docs/kloudmate-agent/installation/ecs-agent.mdx
@@ -97,7 +97,7 @@ Instrumenting a service runs a task-definition injection step. This happens on e
**Step 1:** Go to **Settings → Agents** in your KloudMate account.
-**Step 2:** Select **Amazon ECS** from the list of available integrations.
+**Step 2:** Click **Add Agent**, select **Amazon ECS**, and click **Continue**. On the **API key** step, click **Create backend key**, or select **Use existing key** and paste your backend ingest key. Then click **Continue**.
**Step 3:** Copy the command for your launch type and run it from a machine with AWS credentials.
@@ -230,7 +230,7 @@ kmagent ecs instrument \
The two launch types appear differently in KloudMate:
- **EC2 daemon:** each container instance appears in **Settings → Agents** as **Amazon ECS**, one row per instance. Its **APM** tab has no per-service toggle. Instead, it points you to the `kmagent ecs discover` and `instrument` commands and explains the language-aware split (SDK for Java/Node/Python/.NET, eBPF for the rest).
-- **Fargate tasks are autonomous:** each collector sidecar ships telemetry on its own and **does not appear as an agent row**. Find that data in **APM**, **Log Explorer**, and **Infrastructure** keyed by service name. The install flow's verify step accounts for this, so it won't wait for a Fargate check-in that never comes.
+- **Fargate tasks are autonomous:** each collector sidecar ships telemetry on its own and **does not appear as an agent row**. Find that data in **APM & Services** and **Logs**, keyed by service name. Fargate tasks don't appear in **Hosts**. The install flow's verify step accounts for this, so it won't wait for a Fargate check-in that never comes.
## Log Integration
@@ -244,9 +244,9 @@ To collect additional application log files with the agent, see [Collect File Lo
## Next Steps
-- Open **Infrastructure Monitoring** to see your container instances, tasks, and their metrics
-- Use **APM & Tracing** to follow requests across your instrumented services
-- Investigate container and application logs in **Log Explorer**
+- Open **Hosts** to see your EC2 container instances and their metrics
+- Use **APM & Services** to check service health, and **Traces** to follow requests across your instrumented services
+- Investigate container and application logs in **Logs**
- Configure **alerts** for cluster, task, or service thresholds
- If an agent looks unhealthy, check its own error logs: see [See the agent's own errors](../../management/#see-the-agents-own-errors)
diff --git a/src/content/docs/docs/kloudmate-agent/installation/images/agent-install-api-key.png b/src/content/docs/docs/kloudmate-agent/installation/images/agent-install-api-key.png
new file mode 100644
index 00000000..01de7708
Binary files /dev/null and b/src/content/docs/docs/kloudmate-agent/installation/images/agent-install-api-key.png differ
diff --git a/src/content/docs/docs/kloudmate-agent/installation/images/kubernetes-agent-1.png b/src/content/docs/docs/kloudmate-agent/installation/images/kubernetes-agent-1.png
index 1f5f622c..ec533b08 100644
Binary files a/src/content/docs/docs/kloudmate-agent/installation/images/kubernetes-agent-1.png and b/src/content/docs/docs/kloudmate-agent/installation/images/kubernetes-agent-1.png differ
diff --git a/src/content/docs/docs/kloudmate-agent/installation/kubernetes-agent.mdx b/src/content/docs/docs/kloudmate-agent/installation/kubernetes-agent.mdx
index 63c51356..73b713b9 100644
--- a/src/content/docs/docs/kloudmate-agent/installation/kubernetes-agent.mdx
+++ b/src/content/docs/docs/kloudmate-agent/installation/kubernetes-agent.mdx
@@ -148,11 +148,11 @@ There are two ways to enable **Application Performance Monitoring (APM)** in you
### 1. Enable APM via Dashboard
-- After installing the agent, open the APM configuration tab in the dashboard.
+- After installing the agent, go to **Settings → Agents**, open the cluster's actions menu (⋮), and select **Configure** to open its **APM** tab.
- The agent lists the workloads it discovered, grouped by namespace, with each workload's detected language. Use the namespace filter or the search box to find one in a large cluster.
- For each workload, choose **Off**, **eBPF**, or **SDK**, then apply. eBPF starts tracing on the next check-in with no restart; SDK rolls that workload's pods. Workloads with no SDK injector — Go, Ruby, or an unrecognized runtime — offer **Off** or **eBPF** only.
-
+
- After you turn a workload on, wait a few minutes for its traces to start, then verify them on the dashboard.
@@ -234,12 +234,12 @@ On Kubernetes, one agent represents one cluster. A fleet-manager pod applies the
- Node, pod, and cluster telemetry starts flowing automatically. See [Host metrics and logs](../../baseline/host-metrics-and-logs/).
- Where nodes run a supported kernel, [eBPF monitoring](../../ebpf-observability/) turns on per node.
-- Use the **Discovered Services** view to instrument workloads for APM, or enable auto-instrumentation as shown above. See [Application APM on Kubernetes](../../auto-instrumentation/kubernetes/).
+- Use the cluster's **APM** tab to instrument workloads for APM, or enable auto-instrumentation as shown above. See [Application APM on Kubernetes](../../auto-instrumentation/kubernetes/).
- To monitor in-cluster databases, provide credentials through a Kubernetes Secret referenced by the `dbMonitoring.secretName` Helm value. See [Database monitoring on Kubernetes](../../database-monitoring/on-kubernetes/).
## **Next Steps**
-- Open **Kubernetes Monitoring** to view metrics and events for your *cluster*, **nodes**, **namespaces**, **pods**, and **workloads**
+- Open **Kubernetes** to view metrics and events for your *cluster*, **nodes**, **namespaces**, **pods**, and **workloads**
- Use **Dashboards** to create visualizations or view **pre-built dashboards**
-- Use **Log Explorer** to investigate logs and correlate them with metrics
+- Use **Logs** to investigate logs and correlate them with metrics
- Configure **alerts** for any of these components based on thresholds
diff --git a/src/content/docs/docs/kloudmate-agent/installation/linux-agent.mdx b/src/content/docs/docs/kloudmate-agent/installation/linux-agent.mdx
index 305e08b7..12f8f122 100644
--- a/src/content/docs/docs/kloudmate-agent/installation/linux-agent.mdx
+++ b/src/content/docs/docs/kloudmate-agent/installation/linux-agent.mdx
@@ -60,7 +60,9 @@ View all data in these KloudMate sections:
**Step 1:** Navigate to the **Settings → Agents** section in your KloudMate account.
-**Step 2:** Select **Linux** from the list of available integrations.
+**Step 2:** Click **Add Agent**, select **Linux**, and click **Continue**. On the **API key** step, click **Create backend key**, or select **Use existing key** and paste your backend ingest key. Then click **Continue**.
+
+
**Step 3:** Copy and run the Linux agent install command in your terminal to download and install the agent.
@@ -86,9 +88,9 @@ For a hand-written `filelog` receiver in manual mode, see [Collect File Logs wit
If the host runs Docker, the agent finds the Docker socket and starts collecting **container metrics and container logs** on its own, the same telemetry a Docker-mode agent collects. This happens as soon as the agent starts, before and independent of application tracing. To turn it off, set `KM_DOCKER_STATS=false` for metrics or `KM_DOCKER_LOGS=false` for logs.
-Each container also shows up in **Discovered Services**, where you can turn on per-container tracing. Most containers offer **Off** or **eBPF**: eBPF captures RED metrics and trace spans in the kernel, with no code change and no restart. There's no in-process **SDK** option for them, because the agent can't change a running container's start-time environment. Host processes that aren't containerized still offer **Off**, **eBPF**, or **SDK**.
+Each container also shows up in the **Application instrumentation** card on the agent's **APM** tab, where you can turn on per-container tracing. Most containers offer **Off** or **eBPF**: eBPF captures RED metrics and trace spans in the kernel, with no code change and no restart. There's no in-process **SDK** option for them, because the agent can't change a running container's start-time environment. Host processes that aren't containerized still offer **Off**, **eBPF**, or **SDK**.
-PHP containers are the exception: they can take an **SDK** option that injects the tracer and reloads the web server (Apache or php-fpm), with no redeploy and no container restart. On a Linux host this is opt-in. Set `KM_CONTAINER_INSTRUMENT_ENABLED=true`, and PHP containers start offering **SDK** in Discovered Services. In Docker mode that option is on by default.
+PHP containers are the exception: they can take an **SDK** option that injects the tracer and reloads the web server (Apache or php-fpm), with no redeploy and no container restart. On a Linux host this is opt-in. Set `KM_CONTAINER_INSTRUMENT_ENABLED=true`, and PHP containers start offering **SDK** on the agent's **APM** tab. In Docker mode that option is on by default.
For monitoring containers, a Linux service and a Docker-mode agent are equivalent. Choose between them by how you want to run the agent, not by what they can see. See [Docker mode vs the Linux agent](../../platform-notes/docker/#docker-mode-vs-the-linux-agent).
@@ -102,13 +104,13 @@ After the agent is running, confirm what it collects and explore what it found:
- Host metrics and logs start flowing automatically. See [Host metrics and logs](../../baseline/host-metrics-and-logs/).
- On a supported Linux kernel (4.14 or newer), [eBPF monitoring](../../ebpf-observability/) turns on automatically and adds RED metrics and a service map.
-- Open the **Discovered Services** list for the host to see what the agent found and instrument a service. See [Discovery](../../concepts/discovery/).
+- To see what the agent found and instrument a service, go to **Settings → Agents**, open the agent's actions menu (⋮), and select **Configure**. See [Discovery](../../concepts/discovery/).
## Next Steps
-- Open **Infrastructure Monitoring** to confirm your Linux hosts are visible
+- Open **Hosts** to confirm your Linux hosts are visible
- Go to **Dashboards** to review system performance
-- Use **Log Explorer** to start investigating logs
+- Use **Logs** to start investigating logs
- Set up **alerts and notifications** for critical events
## Uninstall Agent
diff --git a/src/content/docs/docs/kloudmate-agent/installation/windows-agent.mdx b/src/content/docs/docs/kloudmate-agent/installation/windows-agent.mdx
index ed2c12b6..c6119a29 100644
--- a/src/content/docs/docs/kloudmate-agent/installation/windows-agent.mdx
+++ b/src/content/docs/docs/kloudmate-agent/installation/windows-agent.mdx
@@ -56,7 +56,7 @@ View all collected data in these KloudMate sections:
**Step 1:** Navigate to the **Settings → Agents** section in your KloudMate account.
-**Step 2:** Select **Windows** from the list of available integrations.
+**Step 2:** Click **Add Agent**, select **Windows**, and click **Continue**. On the **API key** step, click **Create backend key**, or select **Use existing key** and paste your backend ingest key. Then click **Continue**.
**Step 3:** Download the Windows executable using the provided link.
@@ -104,9 +104,9 @@ The agent installs as a Windows service running as Local System. It needs that p
## **Next Steps**
-- Open **Infrastructure Monitoring** to see host metrics
+- Open **Hosts** to see host metrics
- Go to **Dashboards** to visualize host performance and logs
-- Set up [log monitoring](../../log-monitoring/), then use **Log Explorer** to investigate the logs it collects
+- Set up [log monitoring](../../log-monitoring/), then use **Logs** to investigate the logs it collects
- Configure **alerts** for host metric thresholds
## Uninstall Agent
diff --git a/src/content/docs/docs/kloudmate-agent/log-monitoring.mdx b/src/content/docs/docs/kloudmate-agent/log-monitoring.mdx
index 7997b818..ecccef4c 100644
--- a/src/content/docs/docs/kloudmate-agent/log-monitoring.mdx
+++ b/src/content/docs/docs/kloudmate-agent/log-monitoring.mdx
@@ -13,7 +13,7 @@ It applies to Linux and Windows virtual machine (VM) hosts and to Docker. It is
## How it fits
-Log monitoring is a managed-mode feature. You configure sources from the agent's **Configuration** page, on the **Logs** tab. See the [configuration model](../concepts/config-model/) for how managed mode works.
+Log monitoring is a managed-mode feature. To configure sources, go to **Settings → Agents**, open the agent's actions menu (⋮), select **Configure**, and open the **Logs** tab. See the [configuration model](../concepts/config-model/) for how managed mode works.
- **Managed mode only.** The Logs tab is available when the agent is in managed mode. If the agent is in manual mode, the tab prompts you to switch. You can still add a `filelog` receiver by hand in manual mode: see [Manual configuration](#manual-configuration).
- **Independent sources.** Each source is isolated, so one badly configured source cannot affect the others or your host system logs.
@@ -25,7 +25,7 @@ You add each source through the wizard on the Logs tab.
-1. **Open the Logs tab.** Go to the agent's Configuration page and select **Logs**. The agent must be in managed mode.
+1. **Open the Logs tab.** In **Settings → Agents**, open the agent's actions menu (⋮), select **Configure**, and then select **Logs**. The agent must be in managed mode.
2. **Name the source.** The name identifies the source and is stamped on every record it collects (as the `km.log.source` attribute), so give it something you will recognize in the Log Explorer.
diff --git a/src/content/docs/docs/kloudmate-agent/management.mdx b/src/content/docs/docs/kloudmate-agent/management.mdx
index 6c2ecb69..193a0b61 100644
--- a/src/content/docs/docs/kloudmate-agent/management.mdx
+++ b/src/content/docs/docs/kloudmate-agent/management.mdx
@@ -48,7 +48,7 @@ Open the actions menu (⋮) for an agent to manage it:

-- **Configure:** Opens the agent's Configuration page, with tabs for **APM**, **Databases**, **Logs**, **Processes**, and **Collector config**. Updates apply automatically wherever the agent is installed.
+- **Configure:** Opens the agent's Configuration page, with tabs for **APM**, **Databases**, **Logs**, **Processes**, **Runbook scripts**, and **Collector config**. Updates apply automatically wherever the agent is installed.
- **Restart agent:** Restart the agent when needed.
- **Delete:** Remove the agent from the KloudMate Agent landing page and clear all of its configuration.
@@ -69,6 +69,16 @@ You can switch an agent between modes. Switching from manual to managed replaces
To undo a managed change, turn the toggle back. There is no separate history or rollback button. Switching an agent from manual to managed mode saves your hand-edited YAML first, so you never lose it. See the [configuration model](../concepts/config-model/).
+### Runbook scripts
+
+The **Runbook scripts** tab decides whether a [workflow](../../workflows/) can run commands on this host with the [Run Command on Host](../../workflows/actions/#run-command-on-host) step. Scripts are off until someone with the **Developer** role turns on **Allow runbook scripts on this host**. The agent picks up the change within a minute. Only Linux host agents have this setting.
+
+
+
+Scripts run as root and can access anything the host can already access, such as a kubeconfig, a cloud instance profile, or an SSH key on disk. Allow them only on hosts where that's acceptable.
+
+To block scripts on a host regardless of this setting, set `job-types` to `none` in the host's `agent.yaml`. See [Runbook scripts](../reference/agent-config/#runbook-scripts) in the agent configuration reference.
+
### Common Configurations
:::note
diff --git a/src/content/docs/docs/kloudmate-agent/platform-notes/docker.mdx b/src/content/docs/docs/kloudmate-agent/platform-notes/docker.mdx
index 2b8c399b..ac87d4e1 100644
--- a/src/content/docs/docs/kloudmate-agent/platform-notes/docker.mdx
+++ b/src/content/docs/docs/kloudmate-agent/platform-notes/docker.mdx
@@ -27,7 +27,7 @@ For monitoring containers, the two are equivalent. A Linux (systemd) agent on a
## Application traces
-Application tracing (Rate, Errors, and Duration metrics and trace spans) is **not** automatic. The agent lists every host process and container it finds under **Discovered Services**, and for each one you choose how it's traced:
+Application tracing (Rate, Errors, and Duration metrics and trace spans) is **not** automatic. The agent lists every host process and container it finds in the **Application instrumentation** card on the agent's **APM** tab, and for each one you choose how it's traced:
- **Off:** not traced. This is the default, so nothing is traced (and nothing costs) until you turn it on, which matters on a host running many containers.
- **eBPF:** RED metrics and trace spans, captured in the host kernel with no code change and no restart.
@@ -35,7 +35,7 @@ Application tracing (Rate, Errors, and Duration metrics and trace spans) is **no
The in-process OpenTelemetry SDK can't be injected into a running container, because it reads its configuration from the process's start-time environment, and that can't change without recreating the container. So Java, Node.js, Python, .NET, and Go containers offer **Off** or **eBPF** only. For full SDK depth on those runtimes, add OpenTelemetry to the app yourself (below), or run it on a [Linux host](../../installation/linux-agent/) or [Amazon ECS](../../installation/ecs-agent/), where the agent injects it for you.
-| Runtime | In Discovered Services | Full SDK depth |
+| Runtime | On the APM tab | Full SDK depth |
|---|---|---|
| Java, Node.js, Python, .NET | **Off / eBPF**: RED and spans, opt-in, no restart | Not injectable on Docker. [Add OpenTelemetry yourself](#send-full-opentelemetry-traces-from-your-containers), or run on Linux or ECS. |
| Go | **Off / eBPF** | eBPF only. Go is a static binary with no runtime to attach. |
@@ -43,7 +43,7 @@ The in-process OpenTelemetry SDK can't be injected into a running container, bec
## Instrument PHP containers
-A PHP container offers an **SDK** option in **Discovered Services**. Turn it on for a container and the agent injects the tracer, matches it to the container's PHP build, and reloads the web server (an Apache graceful reload or a php-fpm reload). Tracing starts with no redeploy and no container restart.
+A PHP container offers an **SDK** option on the agent's **APM** tab. Turn it on for a container and the agent injects the tracer, matches it to the container's PHP build, and reloads the web server (an Apache graceful reload or a php-fpm reload). Tracing starts with no redeploy and no container restart.
The SDK option appears whenever the PHP-container instrumentation path is enabled. It's on by default in Docker mode. On a [Linux host agent](../../installation/linux-agent/) it's opt-in: set `KM_CONTAINER_INSTRUMENT_ENABLED=true`.
@@ -70,7 +70,7 @@ The agent cannot instrument a container that serves requests from the **PHP buil
### How PHP-container tracing is selected
-By default, a PHP container is traced when you set it to **SDK** in **Discovered Services**, the same per-service choice as any other service. The environment variables above override that for automated rollouts: `KM_CONTAINER_INSTRUMENT_ALL`, `KM_CONTAINER_INSTRUMENT_NAMES`, or `KM_CONTAINER_INSTRUMENT_IMAGES` instrument a fixed set with no UI step, and [autonomous mode](../../advanced-configuration/) instruments every PHP container. Setting `KM_CONTAINER_INSTRUMENT_ENABLED=false` turns the SDK path off, so PHP containers fall back to **Off** or **eBPF**.
+By default, a PHP container is traced when you set it to **SDK** on the **APM** tab, the same per-service choice as any other service. The environment variables above override that for automated rollouts: `KM_CONTAINER_INSTRUMENT_ALL`, `KM_CONTAINER_INSTRUMENT_NAMES`, or `KM_CONTAINER_INSTRUMENT_IMAGES` instrument a fixed set with no UI step, and [autonomous mode](../../advanced-configuration/) instruments every PHP container. Setting `KM_CONTAINER_INSTRUMENT_ENABLED=false` turns the SDK path off, so PHP containers fall back to **Off** or **eBPF**.
## Send full OpenTelemetry traces from your containers
diff --git a/src/content/docs/docs/kloudmate-agent/platform-notes/windows.mdx b/src/content/docs/docs/kloudmate-agent/platform-notes/windows.mdx
index 245b37e1..d8619a97 100644
--- a/src/content/docs/docs/kloudmate-agent/platform-notes/windows.mdx
+++ b/src/content/docs/docs/kloudmate-agent/platform-notes/windows.mdx
@@ -15,7 +15,7 @@ Windows has no `LD_PRELOAD` and no production eBPF, so the agent uses Windows-na
## Discovery
-To build the Discovered Services list, the agent looks at:
+To find the services it lists in the **Application instrumentation** card, the agent looks at:
- **IIS** sites and application pools, including each pool's managed runtime version and state. These appear with a kind of `iis`.
- **Windows services**, through the Service Control Manager. These appear with a kind of `windows_service`.
diff --git a/src/content/docs/docs/kloudmate-agent/reference/agent-config.mdx b/src/content/docs/docs/kloudmate-agent/reference/agent-config.mdx
index 7c9b10d0..a020299e 100644
--- a/src/content/docs/docs/kloudmate-agent/reference/agent-config.mdx
+++ b/src/content/docs/docs/kloudmate-agent/reference/agent-config.mdx
@@ -122,6 +122,16 @@ In managed mode you pick log files from the web interface. In autonomous mode th
For the file format and worked examples, see [Auto Scaling & Elastic Beanstalk](../../installation/aws-asg-deployment/#configure-log-collection) and [Log monitoring](../../log-monitoring/).
+## Runbook scripts
+
+Leave these unset on an ordinary install. The host's **Runbook scripts** setting under **Settings → Agents** decides whether it runs [workflow scripts](../../../workflows/actions/#run-command-on-host), and the agent checks every job against signing keys that KloudMate publishes to it. On a host that must never run scripts, set `job-types` to `none`, regardless of that setting.
+
+| Setting | Environment variable | Default | Description |
+|---|---|---|---|
+| `job-types` | `KM_JOB_TYPES` | Empty (the host's **Runbook scripts** setting decides) | Job types this host accepts, as a comma-separated list, or `none` to refuse every job. `run_script` runs a workflow's shell script as the agent's user, which is root on a standard install. Name each type exactly; there's no wildcard. |
+| `job-signing-keys` | `KM_JOB_SIGNING_KEYS` | Empty (the keys KloudMate publishes) | Ed25519 public keys a job must be signed with, as a comma-separated list, each in base64 or `:` form. When set, the agent accepts only these keys and ignores the published ones. |
+| `job-agent-id` | `KM_JOB_AGENT_ID` | Empty | This host's agent ID. When set, the agent refuses jobs addressed to a different agent. |
+
## Related
diff --git a/src/content/docs/docs/kloudmate-agent/troubleshooting.mdx b/src/content/docs/docs/kloudmate-agent/troubleshooting.mdx
index c3f9bebc..1aca457e 100644
--- a/src/content/docs/docs/kloudmate-agent/troubleshooting.mdx
+++ b/src/content/docs/docs/kloudmate-agent/troubleshooting.mdx
@@ -17,13 +17,13 @@ Start with two agent features. The [health metrics](../self-observability/health
## Metrics are not flowing
-- **eBPF is unavailable.** If RED metrics and the service map are missing, the host may not support eBPF. Check the host detail for an "eBPF unavailable" reason (kernel older than 4.14, `debugfs` not mounted, or missing privileges). Host metrics and logs are unaffected. See [eBPF observability](../ebpf-observability/).
+- **eBPF is unavailable.** If RED metrics and the service map are missing, the host may not support eBPF. Check the agent's log for `eBPF baseline not enabled`, which gives the reason (kernel older than 4.14, `debugfs` not mounted, or missing privileges). On a Linux host, run `journalctl -u kmagent | grep "eBPF baseline"`. Host metrics and logs are unaffected. See [eBPF observability](../ebpf-observability/).
- **The integration is not enabled.** In managed mode, confirm the feature or integration is toggled on. In manual mode, confirm the receiver is in the YAML.
- **A collection error.** A receiver that cannot reach its target or authenticate logs a scrape error rather than crashing the collector. Check the self-logs for the specific receiver.
## Traces are not flowing
-- **The service was not discovered.** Only discovered services can be instrumented. Confirm the service appears in the Discovered Services list. See [Discovery](../concepts/discovery/).
+- **The service was not discovered.** Only discovered services can be instrumented. Confirm the service appears in the **Application instrumentation** card on the agent's **APM** tab. See [Discovery](../concepts/discovery/).
- **The toggle has not applied.** Instrumentation takes effect on restart. Check the service's status chip: `pending restart` means the change is written but the service has not restarted. See [Application APM overview](../auto-instrumentation/overview/).
- **Injection failed and rolled back.** If the status shows failed and rolled back, the service crash-looped after instrumentation and the agent reverted the change. Read the reason in the self-logs, then retry. eBPF monitoring continues to cover the service throughout.
diff --git a/src/content/docs/docs/kloudmate-assistant/chat-modes.mdx b/src/content/docs/docs/kloudmate-assistant/chat-modes.mdx
index bcbd102f..ed531aa4 100644
--- a/src/content/docs/docs/kloudmate-assistant/chat-modes.mdx
+++ b/src/content/docs/docs/kloudmate-assistant/chat-modes.mdx
@@ -43,7 +43,7 @@ This helps users move from a natural-language request to a structured dashboard
KloudMate Docs is optimized for documentation search and platform reference questions. Use this mode when you want answers grounded in the KloudMate documentation rather than a general operational conversation.
:::note[Free usage]
-The Docs agent is **free** and does not consume credits. KloudMate Assistant runs on credits. A chat message costs 1 credit and a full investigation costs 2 credits. Every plan includes a monthly allowance: 5 credits on Free and 20 on Pro, and prepaid credits can be purchased anytime. Enterprise teams can bring their own AI provider key instead of using credits.
+The Docs agent is **free** and does not consume credits. KloudMate Assistant runs on credits. A chat message costs 1 credit and a full investigation costs 5 credits. Every plan includes a monthly allowance: 5 credits on Free and 20 on Pro, and prepaid credits can be purchased anytime. Enterprise teams can bring their own AI provider key instead of using credits.
:::
This mode is useful for:
diff --git a/src/content/docs/docs/kloudmate-assistant/images/assistant-module-tabs.png b/src/content/docs/docs/kloudmate-assistant/images/assistant-module-tabs.png
new file mode 100644
index 00000000..0e60a7d9
Binary files /dev/null and b/src/content/docs/docs/kloudmate-assistant/images/assistant-module-tabs.png differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/investigations-1.jpeg b/src/content/docs/docs/kloudmate-assistant/images/investigations-1.jpeg
index d7649cc8..9d8f367e 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/investigations-1.jpeg and b/src/content/docs/docs/kloudmate-assistant/images/investigations-1.jpeg differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/investigations-1.png b/src/content/docs/docs/kloudmate-assistant/images/investigations-1.png
index 44c849b2..b4d88d2a 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/investigations-1.png and b/src/content/docs/docs/kloudmate-assistant/images/investigations-1.png differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/investigations-2.jpeg b/src/content/docs/docs/kloudmate-assistant/images/investigations-2.jpeg
index 8db855d2..5838ad02 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/investigations-2.jpeg and b/src/content/docs/docs/kloudmate-assistant/images/investigations-2.jpeg differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.jpeg b/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.jpeg
index 7f79907a..70334a10 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.jpeg and b/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.jpeg differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.png b/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.png
index 6fd768ea..308c7143 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.png and b/src/content/docs/docs/kloudmate-assistant/images/mcp-integrations-1.png differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/prompts-1.jpeg b/src/content/docs/docs/kloudmate-assistant/images/prompts-1.jpeg
index 3c8b918e..3c31b88f 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/prompts-1.jpeg and b/src/content/docs/docs/kloudmate-assistant/images/prompts-1.jpeg differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/prompts-1.png b/src/content/docs/docs/kloudmate-assistant/images/prompts-1.png
index 7f16f0e0..6ab514c9 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/prompts-1.png and b/src/content/docs/docs/kloudmate-assistant/images/prompts-1.png differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/skills-1.jpeg b/src/content/docs/docs/kloudmate-assistant/images/skills-1.jpeg
index 6fb5c4f9..7d9afca4 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/skills-1.jpeg and b/src/content/docs/docs/kloudmate-assistant/images/skills-1.jpeg differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/skills-1.png b/src/content/docs/docs/kloudmate-assistant/images/skills-1.png
index 56c2919c..0617da02 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/skills-1.png and b/src/content/docs/docs/kloudmate-assistant/images/skills-1.png differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/skills-2.jpeg b/src/content/docs/docs/kloudmate-assistant/images/skills-2.jpeg
index 65882ae7..4b7b12a9 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/skills-2.jpeg and b/src/content/docs/docs/kloudmate-assistant/images/skills-2.jpeg differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/skills-2.png b/src/content/docs/docs/kloudmate-assistant/images/skills-2.png
index 449cde9d..59f7f7f0 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/skills-2.png and b/src/content/docs/docs/kloudmate-assistant/images/skills-2.png differ
diff --git a/src/content/docs/docs/kloudmate-assistant/images/usage-1.png b/src/content/docs/docs/kloudmate-assistant/images/usage-1.png
index 7bc8662c..d72d8bc7 100644
Binary files a/src/content/docs/docs/kloudmate-assistant/images/usage-1.png and b/src/content/docs/docs/kloudmate-assistant/images/usage-1.png differ
diff --git a/src/content/docs/docs/kloudmate-assistant/index.mdx b/src/content/docs/docs/kloudmate-assistant/index.mdx
index b43b0786..5b317a90 100644
--- a/src/content/docs/docs/kloudmate-assistant/index.mdx
+++ b/src/content/docs/docs/kloudmate-assistant/index.mdx
@@ -19,9 +19,13 @@ KloudMate Assistant supports three core modes of operation:
### Accessing KloudMate Assistant
-To access KloudMate Assistant, click the **KloudMate Assistant** icon in the top navigation bar. This opens the assistant panel where you can start a chat, run investigations, and manage settings. The assistant is also accessible from the pages listed below:
+To start a chat or run an investigation, click the Assistant icon in the top navigation bar to open the assistant panel.
+
+Open **Assistant** to review investigations, track AI credit usage, and manage the Assistant's prompts, skills, and MCP integrations. It has tabs for **Investigations**, **Prompts**, **Skills**, **MCP Integrations**, and **Usage**. For details, see:
- [Investigations](./investigations/)
- [Chat Modes](./chat-modes/)
-- [Settings](./settings/)
+- [Prompts, Skills, and MCP Integrations](./settings/)
- [Usage](./usage/)
+
+
diff --git a/src/content/docs/docs/kloudmate-assistant/investigations.mdx b/src/content/docs/docs/kloudmate-assistant/investigations.mdx
index 6bf05b1c..444211d3 100644
--- a/src/content/docs/docs/kloudmate-assistant/investigations.mdx
+++ b/src/content/docs/docs/kloudmate-assistant/investigations.mdx
@@ -6,11 +6,13 @@ sidebar:
---
Investigations is your primary workspace for diagnosing issues, exploring anomalies, and uncovering insights. You can ask questions across metrics, logs, and traces, and iteratively troubleshoot problems in a conversational flow.
-When you land on the Investigations page, the main area presents three quick-access cards to help you get started:
+Opening **Assistant** lands you on the **Investigations** tab, which lists past and active investigations. Click **New investigation** to start one. The new investigation opens with quick-access cards to help you get started:
-- **Previous Investigations:** Opens the full list of past investigations so you can find and revisit completed or in-progress RCA reports.
-- **Integration Hub:** Takes you to the SDK and integration guide for connecting external systems to the Assistant.
-- **Skills:** Opens the Skills configuration page where you can create and edit agent skills.
+- **Previous investigations:** Returns you to the **Investigations** tab, where you can find and revisit completed or in-progress RCA reports.
+- **Integration hub:** Opens the **MCP Integrations** tab, where you connect external systems to the Assistant.
+- **Skills:** Opens the **Skills** tab, where you can create and edit agent skills.
+
+
## Adding Context to a Message
@@ -51,13 +53,11 @@ The Assistant analyzes the full context, including stack traces, attributes, and
## Investigations List
-
-
-Clicking **Previous Investigations** from the main Investigations page opens the full investigations list.
+Open **Assistant → Investigations** to see the full investigations list.
-
+
-This shows all past and active investigations in your workspace. Each row in the list displays the following information:
+The list shows all past and active investigations in your workspace. Each row displays the following information:
- **Title:** A brief summary or auto-generated description of the investigation.
- **Status:** The current state of the investigation, typically shown as Completed or In Progress.
@@ -67,7 +67,7 @@ This shows all past and active investigations in your workspace. Each row in the
## Starting a New Investigation
-To start a new investigation, click the **New Investigation** button in the top-right corner of the Investigations page. This brings you to the Investigations chat interface where you can describe the issue or symptom you want analyzed in the input bar at the bottom of the screen. The KloudMate Investigator collects your description and begins a background RCA workflow.
+To start a new investigation, open **Assistant → Investigations** and click **New investigation**. In the chat that opens, describe the issue or symptom you want analyzed. The KloudMate Investigator collects your description and begins a background RCA workflow.
## Viewing an Investigation
diff --git a/src/content/docs/docs/kloudmate-assistant/mcp-server.mdx b/src/content/docs/docs/kloudmate-assistant/mcp-server.mdx
index 77399d35..dd9d983a 100644
--- a/src/content/docs/docs/kloudmate-assistant/mcp-server.mdx
+++ b/src/content/docs/docs/kloudmate-assistant/mcp-server.mdx
@@ -8,11 +8,11 @@ sidebar:
import { Tabs, TabItem, Steps, LinkCard, CardGrid } from '@astrojs/starlight/components';
-The KloudMate MCP server lets your AI coding tools and agents pull observability data straight from your workspace. Point a client like Claude Code, Cursor, or VS Code at it, and you can ask about errors, latency, traces, and firing alerts in plain language — without leaving your editor or opening the KloudMate UI.
+The KloudMate MCP server lets your AI coding tools and agents pull observability data straight from your workspace. Point a client like Claude Code, Cursor, or VS Code at it, and you can ask about errors, latency, traces, and firing alerts in plain language, without leaving your editor or opening the KloudMate UI.
This is the mirror image of the [MCP integrations](../settings/#mcp-integrations) you connect *into* the Assistant. There, KloudMate is the client calling out to other tools. Here, KloudMate is the server, and your agent is the client calling in. The connection is built on the [Model Context Protocol](https://modelcontextprotocol.io/), an open standard for giving language models access to external tools and data.
-Once you're connected, your agent can chain KloudMate's query tools to investigate an incident, answer a question about a service, or run a repeatable workflow you've defined — all grounded in your real telemetry instead of guesswork.
+After you connect, your agent can chain KloudMate's query tools to investigate an incident, answer a question about a service, or run a repeatable workflow you've defined.
## Connection details
@@ -31,7 +31,7 @@ A personal key carries your access to every workspace you belong to, so the conn
You'll need:
-- **A KloudMate Personal API Key.** From the account menu (top-right), open **API keys** and click **Create Key**. It's tied to your account and reads whatever you can read. Copy it when it's shown (it's shown only once) and keep it handy for your client's config. Any signed-in user can create one; no admin role needed. See [API Keys](../../platform/settings/api-keys/#personal-api-keys) for the full walkthrough.
+- **A KloudMate Personal API Key.** From the account menu (top-right), open **API keys** and click **Create Key**. It's tied to your account and reads whatever you can read. Copy it when it's shown (it's shown only once) and keep it handy for your client's config. Any signed-in user can create one; you don't need the **Developer** role. See [API Keys](../../platform/settings/api-keys/#personal-api-keys) for the full walkthrough.
- **The workspace ID** you want to query. Find it in the workspace's URL in the KloudMate app; it goes in the `x-workspace-id` header.
- **An MCP-capable client.** Any tool that supports remote MCP servers over Streamable HTTP with custom headers works. The steps below cover Claude Code, Cursor, VS Code, and Claude Desktop.
@@ -144,7 +144,7 @@ Open `claude_desktop_config.json` (**Settings → Developer → Edit Config**) a
}
```
-The `Bearer ` token goes in the `AUTH_HEADER` environment variable, and the `--header` argument has no spaces around the colon. That split is deliberate: it sidesteps a known bug where Claude Desktop mangles spaces inside `args` when it launches `npx`. Quit and reopen Claude Desktop fully — it reads the config only at startup — then check that `kloudmate` appears in the tools menu.
+The `Bearer ` token goes in the `AUTH_HEADER` environment variable, and the `--header` argument has no spaces around the colon. That split is deliberate: it sidesteps a known bug where Claude Desktop mangles spaces inside `args` when it launches `npx`. Quit Claude Desktop fully and reopen it, because it reads the config only at startup. Then check that `kloudmate` appears in the tools menu.
The config file lives at:
@@ -170,40 +170,58 @@ Every tool is read-only. The server queries your telemetry; it never creates, ed
**Issues and alerts**
-- `search_issues` — errors and warnings aggregated from logs and traces.
-- `search_alarm_rules`, `search_alarm_states`, `search_alarm_states_history` — list alert rules, see what's firing now, and review how states changed over time.
+- `search_issues`: errors and warnings aggregated from logs and traces.
+- `search_alert_rules`, `search_alert_states`, `search_alert_states_history`: list alert rules, see what's firing now, and review how states changed over time.
+- `search_alert_groups`, `get_alert_group_details`: find alert groups and see a single group's alert instances, routing rule, and any root-cause investigation attached to it.
+
+**Incidents**
+
+- `search_incidents`, `get_incident_details`: find open incidents, then look up an incident by its key, such as `INC-42`, to read its responders, notes, alerts, and activity timeline.
+- `get_incident_metrics`: incident counts by status, plus MTTA and MTTR.
**Service health (APM)**
-- `get_service_error_count`, `get_service_requests_count_and_avg_latency` — error counts, throughput, and average latency for a service.
-- `get_service_to_service_requests_count`, `get_service_to_service_failed_requests_count`, `get_service_to_service_request_duration` — request volume, failures, and latency between services, to scope a blast radius.
+- `get_service_error_count`, `get_service_requests_count_and_avg_latency`: error counts, throughput, and average latency for a service.
+- `get_service_to_service_requests_count`, `get_service_to_service_failed_requests_count`, `get_service_to_service_request_duration`: request volume, failures, and latency between services, to scope a blast radius.
+
+**SLOs**
+
+- `search_slos`, `get_slo_details`: list SLOs with their latest compliance and remaining error budget, and read a single SLO's SLI definition, compliance history, and burn-rate alerts.
+- `search_slo_burn_rate_alerts`: burn-rate alerts across every SLO, with firing alerts listed first.
**Logs, traces, and spans**
-- `search_logs`, `search_traces`, `search_spans` — query telemetry with filters.
-- `get_log_details`, `get_span_details`, `get_trace_spans`, `get_trace_logs` — pull the full record for a single log line, span, or trace.
+- `search_logs`, `search_traces`, `search_spans`: query telemetry with filters.
+- `search_trace_groups`: trace count, p95 duration, and error count for each service and operation, to find the busiest and slowest endpoints before you open individual traces.
+- `get_log_details`, `get_span_details`, `get_trace_spans`, `get_trace_logs`: pull the full record for a single log line, span, or trace.
+- `find_cloudwatch_log_groups`, `search_cloudwatch_logs`: find the CloudWatch log groups for an AWS resource and read their events directly from your connected AWS accounts, even for logs that aren't sent to KloudMate.
**Schema discovery**
-- `discover_dataset_fields`, `discover_field_values` — find which fields and values exist before building a filter.
-- `discover_metrics`, `discover_metric_label_values` — find metric names and the values their labels take.
+- `discover_dataset_fields`, `discover_field_values`: find which fields and values exist before building a filter.
+- `discover_metrics`, `discover_metric_label_values`: find metric names and the values their labels take.
**Aggregations and queries**
-- `run_aggregation_query`, `run_metric_aggregation_query` — grouped counts, percentiles, and timeseries over logs and traces, or over metrics.
-- `run_multiple_log_trace_aggregations`, `run_multiple_metric_aggregations` — run several aggregations in one call.
+- `run_aggregation_query`, `run_metric_aggregation_query`: grouped counts, percentiles, and timeseries over logs and traces, or over metrics.
+- `run_multiple_log_trace_aggregations`, `run_multiple_metric_aggregations`: run several aggregations in one call.
+
+**Profiling**
+
+- `discover_profiles`: list the services that send profiles and the profile types each service reports, such as CPU, allocated memory, or goroutines.
+- `analyze_profile_hotspots`, `analyze_profile_trend`, `compare_profile_windows`: find the functions using the most CPU or memory, track a profile type over time to spot a memory leak, and compare two time windows, such as before and after a deploy.
**Dashboards**
-- `search_dashboards`, `get_dashboard_panels`, `run_dashboard_panel_query` — find a dashboard, inspect its panels, and run a panel's query.
+- `search_dashboards`, `get_dashboard_panels`, `run_dashboard_panel_query`: find a dashboard, inspect its panels, and run a panel's query.
**Kubernetes**
-- `list_kubernetes_resources`, `search_kubernetes_events` — inspect clusters, namespaces, deployments, and pods, and read recent cluster events.
+- `list_kubernetes_resources`, `search_kubernetes_events`: inspect clusters, namespaces, deployments, and pods, and read recent cluster events.
**Synthetics**
-- `search_synthetic_monitors`, `search_synthetic_incidents` — check uptime and browser monitors and the incidents they've raised.
+- `search_synthetic_monitors`, `search_synthetic_incidents`: check uptime and browser monitors and the incidents they've raised.
## Example prompts
@@ -217,7 +235,7 @@ Once connected, ask in plain language. The agent picks the right tools and fills
## Build custom workflows
-Because your agent can chain these tools and combine them with whatever else it can reach — your codebase, a runbook, your deployment history — you can codify investigations that used to mean clicking through several screens.
+Your agent can chain these tools and combine them with whatever else it can reach, such as your codebase, a runbook, or your deployment history. You can turn an investigation that takes several screens in the KloudMate UI into a repeatable workflow.
A triage workflow, for example, might pull the currently firing alerts, scope the affected services with the service-to-service tools, gather the relevant traces and logs, and hand you a single summary with a probable cause. Save it as a reusable command or prompt in your tool of choice, and the next on-call run is one sentence instead of twenty clicks.
@@ -234,7 +252,7 @@ A few things worth keeping in mind:
-
+
diff --git a/src/content/docs/docs/kloudmate-assistant/settings.mdx b/src/content/docs/docs/kloudmate-assistant/settings.mdx
index f716721c..6bf83d8a 100644
--- a/src/content/docs/docs/kloudmate-assistant/settings.mdx
+++ b/src/content/docs/docs/kloudmate-assistant/settings.mdx
@@ -1,39 +1,37 @@
---
-title: "Settings"
-description: "Configure what appears in the Assistant experience and manage its capabilities and integrations."
+title: "Prompts, Skills, and MCP Integrations"
+description: "Create quickstart prompts and skills for the Assistant, and connect external systems with MCP integrations."
sidebar:
order: 3
---
-Assistant Settings lets you configure what appears in the Assistant experience and what the Assistant is permitted to do.
-
-Settings are organized into three sections: Prompts, Skills, and MCP Integrations.
+Prompts, skills, and MCP integrations control what appears in the Assistant experience and what the Assistant is allowed to do. Each one has its own tab in **Assistant**.
## Prompts
-Quickstart prompts are suggested questions displayed in the chat interface to help users get immediate value. Clicking a prompt inserts its text directly into the chat input, saving users from having to type common queries from scratch.
+Quickstart prompts are suggested questions displayed in the chat interface to help users get immediate value. Clicking a prompt inserts its text directly into the chat input, saving users from having to type common queries from scratch. Open **Assistant → Prompts** to create and manage them.
-
+
The prompts list displays each prompt’s name, a preview of the prompt text, its visibility (Private or Shared), and whether it is currently enabled. The list can be filtered by All, Private, or Shared to quickly locate what you are looking for.
-To create a new prompt, click the **Create Prompt** button in the top-right corner.
+To create a new prompt, click **Create Prompt**.
-
+
A dialog will appear with the following fields:
-- **Title:** A short, scannable label for the prompt
+- **Title:** A short, scannable label for the prompt
- **Prompt:** The actual text that gets inserted into the chat input when the prompt is selected. This field is required.
- **Private:** When enabled, only you can see and use the prompt. Disable it to share the prompt with everyone in the workspace.
- **Enabled:** Controls whether the prompt is active and visible in the chat interface. You can disable a prompt when it is outdated or seasonal without deleting it.
-Once you have filled in the required fields, click **Create** to save the prompt.
+After you fill in the required fields, click **Create** to save the prompt.
## Skills
-Skills are reusable instructions the Assistant can follow. They are useful for standardizing investigations, codifying runbooks, and enabling repeatable workflows across your team.
+Skills are reusable instructions the Assistant can follow. They are useful for standardizing investigations, codifying runbooks, and enabling repeatable workflows across your team. Open **Assistant → Skills** to create, import, and manage skills.
-
+
Each skill is defined by the following properties:
@@ -46,38 +44,38 @@ The skills list can be filtered by All, Private, or Shared to help you quickly l
### Creating a New Skill
-To create a skill from scratch, click the **New Skill** button in the top-right corner.
+To create a skill from scratch, click **New skill**.
-
+
-This opens the skill editor screen, which shows a **Title** field at the top and a **Content** editor below it. The content editor comes pre-populated with a structured template, it includes a metadata block at the top where you fill in the skill name and description, followed by a content section where you describe the steps the Assistant should follow in detail.
+This opens the skill editor screen, which shows a **Title** field at the top and a **Content** editor below it. The content editor comes pre-populated with a structured template, it includes a metadata block at the top where you fill in the skill name and description, followed by a content section where you describe the steps the Assistant should follow in detail.
-
+
Fill in the title, replace the template placeholders with your actual skill instructions, configure the **Private** toggle as needed, then click **Save** to create the skill.
### Importing a Skill from a Template
-To get started quickly from a pre-built starting point, click **Browse Templates** in the top-right corner.
+To start from a pre-built template, click **Browse templates**.
-
+
This opens the Import Skill Template dialog, which lists a set of curated templates ready to import. Click **Import** next to any template to add it to your skills list. Once imported, you can open and customize the skill to suit your specific environment and workflows.
## MCP Integrations
-**MCP (Model Context Protocol) integrations** connect the Assistant to external systems such as OAuth providers or your own custom MCP servers. After connecting an integration, you configure which of its tools are enabled and whether the Assistant must request approval before running any of them.
+**MCP (Model Context Protocol) integrations** connect the Assistant to external systems such as OAuth providers or your own custom MCP servers. Open **Assistant → MCP Integrations** to connect and manage them. After connecting an integration, you configure which of its tools are enabled and whether the Assistant must request approval before running any of them.
-
+
### Connected Integrations
-The Connected Integrations section lists all integrations you have already configured. For each integration, you can see its display name, whether it is scoped privately to your account or shared across the workspace, and its current connection status.
+The Connected Integrations section lists all integrations you have already configured. For each integration, you can see its display name, whether it is scoped privately to your account or shared across the workspace, and its current connection status.
:::info
A status of **Error** indicates the integration requires attention. Use the **Validate** button to re-test the connection, or the menu to edit or remove the integration.
- If you see a **Reconnect Required status**, the integration’s authorization has expired and needs to be refreshed. Click the validate or reconnect option to re-authenticate.
+ If you see a **Reconnect required** status, the integration’s authorization has expired and needs to be refreshed. Click **Reconnect** to re-authenticate.
:::
### Available Integrations
@@ -88,33 +86,32 @@ Click **Connect** next to any provider to begin the connection flow.
### Connecting a Custom MCP Server
-To connect an MCP server that is not listed among the available providers, click **Connect Custom Server** in the top-right corner.
+To connect an MCP server that is not listed among the available providers, click **Connect custom server**.
-
+
This opens the **Connect Custom MCP Server** dialog where you configure the following:
-- **Integration Name:** A friendly label for the integration.
-- **MCP Server URL:** The publicly accessible endpoint for your MCP server. This field is only required when connecting a custom server; for managed OAuth providers, it is pre-filled.
-- **Headers (optional):** Additional HTTP headers to be sent with every MCP request. Use this for API key or token-based authentication, tenant or workspace routing, or any other custom metadata the server requires.
-- **Private:** When enabled, only you can use the integration. When disabled, it is shared across the workspace. Keep integrations private for personal credentials, testing environments, or dev and staging connections, and share them when they are organization-managed and intended for the whole team.
-- **Enable Integration:** The master on/off switch for the integration. Disable it during incidents, provider outages, security reviews, or when you want to deprecate the integration without permanently deleting it.
-- **Agent Access:** Select which agents are permitted to use this integration. You must choose at least one. The available options are **KloudMate Assistant** , **KloudMate Builder** , and **KloudMate Investigator**. Narrow the scope for sensitive or high-impact integrations where only specific agents should have access.
+- **Integration name:** A friendly label for the integration.
+- **MCP server URL:** The publicly accessible endpoint for your MCP server. You enter it only for a custom server. Managed providers don't ask for a URL.
+- **Headers:** Optional HTTP headers to be sent with every MCP request. Use this for API key or token-based authentication, tenant or workspace routing, or any other custom metadata the server requires.
+- **Visibility:** Choose **Private** so only you can use the integration, or **Shared** to share it across the workspace. Keep integrations private for personal credentials, testing environments, or dev and staging connections, and share them when they are organization-managed and intended for the whole team.
+- **Status:** The master on/off switch for the integration. Set it to **Disabled** during incidents, provider outages, security reviews, or when you want to deprecate the integration without permanently deleting it.
-After completing the form, click **Connect** to save the integration.
+After completing the form, click **Save** to connect the integration.
### Tool-Level Settings
-After connecting an integration, you can configure each of its individual tools. For every tool exposed by the integration, you can control two things:
+After connecting an integration, you can configure each of its individual tools. For every tool exposed by the integration, you can control:
- **Enabled:** Determines whether the Assistant is allowed to call the tool at all. Disable individual tools you do not need to reduce the Assistant’s scope of action.
-- **Approval Mode:** Determines whether the Assistant must ask for confirmation before running the tool. Setting it to **Default** uses the platform’s built-in behavior. Setting it to **Ask Before Run** requires the Assistant to confirm with you every time it wants to invoke the tool. Setting it to **Auto Approve** allows the tool to run without any confirmation prompt.
+- **Approval mode:** Determines whether the Assistant must ask for confirmation before running the tool. Setting it to **Default** uses the platform’s built-in behavior. Setting it to **Ask before run** requires the Assistant to confirm with you every time it wants to invoke the tool. Setting it to **Auto approve** allows the tool to run without any confirmation prompt.
-Tools may also carry labels that hint at their behavior.
+Tools may also carry labels that hint at their behavior.
-- A **Read** label indicates the tool is intended to be read-only and carries lower risk.
-- A **Write** label indicates the tool may perform destructive or state-changing actions and should be used with care.
+- A **Read** label indicates the tool is intended to be read-only and carries lower risk.
+- A **Write** label indicates the tool may perform destructive or state-changing actions and should be used with care.
:::info
- When deciding on approval mode, it is advisable to apply stricter settings such as Ask Before Run for Write tools and for any tool with access to sensitive or production systems.
+ When deciding on approval mode, it is advisable to apply stricter settings such as **Ask before run** for Write tools and for any tool with access to sensitive or production systems.
:::
diff --git a/src/content/docs/docs/kloudmate-assistant/usage.mdx b/src/content/docs/docs/kloudmate-assistant/usage.mdx
index 8e9ec134..885af1f4 100644
--- a/src/content/docs/docs/kloudmate-assistant/usage.mdx
+++ b/src/content/docs/docs/kloudmate-assistant/usage.mdx
@@ -23,7 +23,7 @@ A bar chart displays daily AI credit consumption over the selected period. This
### Usage by User
-A stacked bar chart breaks down daily credit consumption by individual user, identified by their email address. This view helps team leads or administrators understand which users are driving the most AI activity across the workspace.
+A stacked bar chart breaks down daily credit consumption by individual user, identified by their email address. Use it to see which users are driving the most AI activity across the workspace.
### Usage by Agent
diff --git a/src/content/docs/docs/network-monitoring/discover-devices.mdx b/src/content/docs/docs/network-monitoring/discover-devices.mdx
index 549751d7..27febb1c 100644
--- a/src/content/docs/docs/network-monitoring/discover-devices.mdx
+++ b/src/content/docs/docs/network-monitoring/discover-devices.mdx
@@ -19,7 +19,7 @@ Discovery credentials are v1/v2c only. To monitor a device that requires SNMPv3,
## Add a scan
-On the **Network** page, open the **Add** menu and choose **Discover devices**. Fill in the scan:
+In **Network Monitoring**, open the **Add** menu and choose **Discover devices**. Fill in the scan:
- **Ranges to scan**: one CIDR or range per line (or comma-separated), for example `10.0.0.0/24` or `10.0.1.1-50`.
- **Candidate communities**: the v1/v2c community strings to try, such as `public, private`. These are sent as plain text and used only for discovery.
diff --git a/src/content/docs/docs/network-monitoring/flow-monitoring.mdx b/src/content/docs/docs/network-monitoring/flow-monitoring.mdx
index bdeb9b38..49045cfa 100644
--- a/src/content/docs/docs/network-monitoring/flow-monitoring.mdx
+++ b/src/content/docs/docs/network-monitoring/flow-monitoring.mdx
@@ -12,7 +12,7 @@ The agent listens for NetFlow, IPFIX, and sFlow. It classifies each flow by its
## Add a listener
-On the **Network** page, open the **Add** menu and choose **Flow monitoring**:
+In **Network Monitoring**, open the **Add** menu and choose **Flow monitoring**:
- **Flow protocol**: **NetFlow / IPFIX** or **sFlow**.
- **Listener port**: the UDP port the agent listens on. It defaults to `2055` for NetFlow and `6343` for sFlow.
diff --git a/src/content/docs/docs/network-monitoring/images/all-devices.png b/src/content/docs/docs/network-monitoring/images/all-devices.png
index eb85bcfe..562e96c9 100644
Binary files a/src/content/docs/docs/network-monitoring/images/all-devices.png and b/src/content/docs/docs/network-monitoring/images/all-devices.png differ
diff --git a/src/content/docs/docs/network-monitoring/images/network-overview.png b/src/content/docs/docs/network-monitoring/images/network-overview.png
index 552be99c..52d1628d 100644
Binary files a/src/content/docs/docs/network-monitoring/images/network-overview.png and b/src/content/docs/docs/network-monitoring/images/network-overview.png differ
diff --git a/src/content/docs/docs/network-monitoring/images/reachability-tab.png b/src/content/docs/docs/network-monitoring/images/reachability-tab.png
index c889088e..78d57674 100644
Binary files a/src/content/docs/docs/network-monitoring/images/reachability-tab.png and b/src/content/docs/docs/network-monitoring/images/reachability-tab.png differ
diff --git a/src/content/docs/docs/network-monitoring/images/topology.png b/src/content/docs/docs/network-monitoring/images/topology.png
index 50d7098e..6fd2eb2d 100644
Binary files a/src/content/docs/docs/network-monitoring/images/topology.png and b/src/content/docs/docs/network-monitoring/images/topology.png differ
diff --git a/src/content/docs/docs/network-monitoring/images/traffic-tab.png b/src/content/docs/docs/network-monitoring/images/traffic-tab.png
index b6d1f504..9523bdfc 100644
Binary files a/src/content/docs/docs/network-monitoring/images/traffic-tab.png and b/src/content/docs/docs/network-monitoring/images/traffic-tab.png differ
diff --git a/src/content/docs/docs/network-monitoring/index.mdx b/src/content/docs/docs/network-monitoring/index.mdx
index afc8f210..31476595 100644
--- a/src/content/docs/docs/network-monitoring/index.mdx
+++ b/src/content/docs/docs/network-monitoring/index.mdx
@@ -6,7 +6,7 @@ sidebar:
order: 0
---
-Network monitoring watches the routers, switches, firewalls, access points, and PDUs that carry your traffic, all from one **Network** page. You poll them over SNMP for health and interface metrics, check reachability for the sites that can't speak SNMP, and read the application traffic mix from NetFlow or sFlow.
+Use **Network Monitoring** to watch the routers, switches, firewalls, access points, and PDUs that carry your traffic. You poll them over SNMP for health and interface metrics, check reachability for the sites that can't speak SNMP, and read the application traffic mix from NetFlow or sFlow.
A Linux or Docker [KloudMate Agent](/kloudmate-agent/) does the collecting. It polls devices over the network, so one agent can watch an entire site, and nothing gets installed on the devices themselves.
@@ -21,9 +21,9 @@ Network monitoring runs on Linux and Docker agents only. Kubernetes agents can't
- **Traffic flow**: the per-application byte and packet mix on a link, from NetFlow, IPFIX, or sFlow exporters.
- **Routing and links**: BGP peers, MPLS L3VPN VRFs, IP SLA probes, and LLDP/CDP topology, once you turn those capabilities on.
-## The Network page
+## The Network Monitoring tabs
-Open **Network** from the sidebar. It has five tabs:
+Open **Network Monitoring**. You land on the **Overview** tab.
| Tab | What it shows |
|---|---|
@@ -33,13 +33,13 @@ Open **Network** from the sidebar. It has five tabs:
| **Traffic** | The application traffic mix from your flow listeners. |
| **Topology** | A link map drawn from LLDP and CDP neighbour data. |
-You land on the Overview tab. For how to read each surface, see [Using the Network page](/network-monitoring/network-page/).
+For how to read each tab, see [Using Network Monitoring](/network-monitoring/network-page/).
-
+
## Set it up
-Everything is configured from the Network page. There are no collector files to hand-edit. Use the **Add** menu in the top-right:
+Everything is configured in **Network Monitoring**. There are no collector files to hand-edit. Use the **Add** menu:
- [Add SNMP devices](/network-monitoring/monitoring-snmp/): poll known devices with the guided wizard.
- [Discover devices](/network-monitoring/discover-devices/): scan IP ranges and monitor what answers.
diff --git a/src/content/docs/docs/network-monitoring/manage-troubleshoot.mdx b/src/content/docs/docs/network-monitoring/manage-troubleshoot.mdx
index 5d3f098d..7330eb32 100644
--- a/src/content/docs/docs/network-monitoring/manage-troubleshoot.mdx
+++ b/src/content/docs/docs/network-monitoring/manage-troubleshoot.mdx
@@ -10,7 +10,7 @@ SNMP devices, ping probes, flow listeners, and discovery scans are all managed f
## Manage what's configured
-On the **Network** page, select **Configure**. Pick a collector agent to see everything it's set to monitor, grouped by kind: **SNMP device**, **Ping probe**, **Flow monitoring**, and **Discovery**. For any entry you can:
+In **Network Monitoring**, select **Configure**. Pick a collector agent to see everything it's set to monitor, grouped by kind: **SNMP device**, **Ping probe**, **Flow monitoring**, and **Discovery**. For any entry you can:
- **Enable or disable** it. Disabling stops the polling without deleting the configuration, so you can turn it back on later.
- **Edit** it to change credentials, targets, capabilities, ports, or scan ranges.
diff --git a/src/content/docs/docs/network-monitoring/monitoring-snmp.mdx b/src/content/docs/docs/network-monitoring/monitoring-snmp.mdx
index 4cb3f77f..02343704 100644
--- a/src/content/docs/docs/network-monitoring/monitoring-snmp.mdx
+++ b/src/content/docs/docs/network-monitoring/monitoring-snmp.mdx
@@ -19,7 +19,7 @@ To sweep a subnet and monitor whatever answers instead, use [Discover devices](/
## Open the wizard
-On the **Network** page, open the **Add** menu and choose **SNMP device**. The wizard has six steps.
+In **Network Monitoring**, open the **Add** menu and choose **SNMP device**. The wizard has six steps.

@@ -74,7 +74,7 @@ Under **Extra capabilities (optional)**, turn on tables that only matter for rou
### 5. Tags
-Add key-value tags such as `site`, `group`, or `role`, or anything you use to organize devices. They apply to every device in this batch and drive the filtering and grouping on the Network page. Grouping the Overview status board by `site` or `role`, for example, only works if your devices carry that tag.
+Add key-value tags such as `site`, `group`, or `role`, or anything you use to organize devices. They apply to every device in this batch and drive the filtering and grouping in Network Monitoring. Grouping the Overview status board by `site` or `role`, for example, only works if your devices carry that tag.
### 6. Review & apply
diff --git a/src/content/docs/docs/network-monitoring/network-page.mdx b/src/content/docs/docs/network-monitoring/network-page.mdx
index 57ce62c5..aded6146 100644
--- a/src/content/docs/docs/network-monitoring/network-page.mdx
+++ b/src/content/docs/docs/network-monitoring/network-page.mdx
@@ -1,12 +1,12 @@
---
-title: "Using the Network Page"
-description: "Read the Overview, drill into devices, and understand device statuses on the KloudMate Network page."
+title: "Using Network Monitoring"
+description: "Read the Overview, drill into devices, and understand device statuses in KloudMate Network Monitoring."
sidebar:
- label: "Using the Network page"
+ label: "Using Network Monitoring"
order: 5
---
-Once devices are reporting, the **Network** page is where you watch them. It opens on an Overview built to answer one question fast: is anything wrong, and where? From there you can drill into any device. This page explains how to read each surface.
+Once devices are reporting, you watch them in **Network Monitoring**. It opens on the **Overview** tab, built to answer one question fast: is anything wrong, and where? From there you can drill into any device. This page explains how to read each surface.
## Overview tab
diff --git a/src/content/docs/docs/network-monitoring/reachability-checks.mdx b/src/content/docs/docs/network-monitoring/reachability-checks.mdx
index 7434a24a..193d12b4 100644
--- a/src/content/docs/docs/network-monitoring/reachability-checks.mdx
+++ b/src/content/docs/docs/network-monitoring/reachability-checks.mdx
@@ -28,7 +28,7 @@ If the site has a single internet connection, the agent uses that same link to s
## Add a reachability check
-On the **Network** page, open the **Add** menu and choose **Reachability check**:
+In **Network Monitoring**, open the **Add** menu and choose **Reachability check**:
- **Agent**: pick the agent running **at this site**. The gateway probe uses this agent's own gateway.
- **Site name**: a label such as `hotel-blr-01`. It applies to both probes so they group under one site.
@@ -40,7 +40,7 @@ The agent discovers and pings its own default gateway. Saving adds both probes a
## Where results show up
-Open the **Reachability** tab on the Network page. Each probe is a row with its endpoint, site, role (gateway or internet), protocol, round-trip time, packet loss, status, and when it was last seen. Summary tiles at the top count how many checks are up, down, and not reporting.
+Open **Network Monitoring → Reachability**. Each probe is a row with its endpoint, site, role (gateway or internet), protocol, round-trip time, packet loss, status, and when it was last seen. Summary tiles at the top count how many checks are up, down, and not reporting.

diff --git a/src/content/docs/docs/platform/settings/api-keys.mdx b/src/content/docs/docs/platform/settings/api-keys.mdx
index 14dff3a2..000f4bd5 100644
--- a/src/content/docs/docs/platform/settings/api-keys.mdx
+++ b/src/content/docs/docs/platform/settings/api-keys.mdx
@@ -17,7 +17,7 @@ KloudMate has two kinds of API key, each for a different job:
A personal key acts on your behalf. It's tied to **your** account and pinned to your current organization, so it can reach every workspace you belong to in that org, with exactly the permissions you already have, and nothing more.
-Find your personal keys in the account menu (top-right) under **API keys**, on the **Personal API Keys** screen in your profile. Any signed-in user can create one; you don't need an admin role, because the key can only do what you can already do.
+Find your personal keys in the account menu (top-right) under **API keys**, on the **Personal API Keys** screen in your profile. Any signed-in user can create one; you don't need the **Developer** role, because the key can only do what you can already do.
Pass it as a bearer token:
@@ -39,7 +39,7 @@ A personal key spans every workspace you can reach, so a tool that reads from **
## Ingest Keys
-Ingest keys send telemetry into a single workspace. A key belongs to the workspace it's created in, so create it in the workspace whose data you want to fill. Find them in that workspace under **Settings → Ingest Keys**; adding one requires the **Admin** role.
+Ingest keys send telemetry into a single workspace. A key belongs to the workspace it's created in, so create it in the workspace whose data you want to fill. Find them in that workspace under **Settings → Ingest Keys**; adding one requires the **Developer** role.
There are two kinds.
@@ -81,7 +81,7 @@ A Frontend ingest key is for telemetry sent straight from a browser or mobile ap
-1. In the workspace you want to send data to, go to **Settings → Ingest Keys** and click **Add New**. (The button appears only if you have the Admin role.)
+1. In the workspace you want to send data to, go to **Settings → Ingest Keys** and click **Add New**. (The button appears only if you have the **Developer** role.)
2. Choose **Ingest Key – Backend** or **Ingest Key – Frontend**. You can't change the type after the key is created, so pick the one that fits.
@@ -101,7 +101,7 @@ Copy it then and store it somewhere safe — a secret manager, your client's con
## Edit or delete a key
-Manage a personal key from the **Personal API Keys** screen, and an ingest key from **Settings → Ingest Keys**. Open the actions menu on a key's row to find both actions. You can edit or delete the personal keys you own. With the Admin role, you can do the same to the ingest keys in your workspace.
+Manage a personal key from the **Personal API Keys** screen, and an ingest key from **Settings → Ingest Keys**. Open the actions menu on a key's row to find both actions. You can edit or delete the personal keys you own. With the **Developer** role, you can do the same to the ingest keys in your workspace.
- **Edit** lets you change the note, and for a Frontend ingest key its allowed hosts. The key type is fixed once created.
- **Delete** is permanent: *this action cannot be undone*. Any agent, collector, app, script, or MCP client using that key stops working the moment you delete it, so swap in a replacement first if the key is in active use.
diff --git a/src/content/docs/docs/platform/settings/connections/_meta.json b/src/content/docs/docs/platform/settings/connections/_meta.json
new file mode 100644
index 00000000..4b55d589
--- /dev/null
+++ b/src/content/docs/docs/platform/settings/connections/_meta.json
@@ -0,0 +1,4 @@
+{
+ "label": "Connections",
+ "order": 9.5
+}
diff --git a/src/content/docs/docs/platform/settings/connections/connect-an-account.mdx b/src/content/docs/docs/platform/settings/connections/connect-an-account.mdx
new file mode 100644
index 00000000..d497e323
--- /dev/null
+++ b/src/content/docs/docs/platform/settings/connections/connect-an-account.mdx
@@ -0,0 +1,59 @@
+---
+title: "Connect an account"
+description: "Add a credential through an OAuth install or a credential form, then set what it can be used for and who in the workspace can use it."
+sidebar:
+ label: "Connect an account"
+ order: 2
+---
+
+You need the **Developer** role to connect an account. Open **Settings → Connections** and click **Connect**.
+
+The picker lists only the providers your organization can connect. If a provider you expect is missing, see [Providers](../providers/#providers-in-your-plan).
+
+
+
+Pick a provider and fill in the **Connection name**. How you finish depends on the provider.
+
+A connection made here starts with the provider's usual capability: **Notifications** for Slack and Jira Cloud; **Workflows** for AWS, Azure, a custom MCP server, and OAuth 2.0; and **AI** for the assistant tools. Change what it's used for, and who can use it, afterward under [Edit](../manage-connections/#edit).
+
+## Connect through OAuth
+
+Slack, Jira Cloud, Sentry, and the Google Cloud providers send you to the provider's own site. Click **Connect** and approve the access there. KloudMate adds the connection when you're done.
+
+The connection records the account you authorized, so you can tell two Slack installs or two Google accounts apart.
+
+## Connect with a credential form
+
+AWS, Azure, GitHub, Atlassian, a custom MCP server, and a generic OAuth 2.0 API take their credentials in a form. Fill in the provider's fields and click **Connect**.
+
+Where a provider supports more than one method, choose one under **Authentication**. AWS offers **Assume Role** (recommended) or **Access keys**; a custom MCP server offers no auth, a bearer token, or an API key in a named header.
+
+KloudMate checks the credential as soon as you save. If the check fails, it still saves the connection, so you don't lose what you typed, and marks it **Degraded**. Click **Edit** beside it and fix the values.
+
+:::note
+The generic **OAuth 2.0** provider works with any API that uses OAuth 2.0 and has no provider of its own in KloudMate: you supply the authorize and token URLs, the scopes, and your own client id and secret. With the client-credentials grant it works the moment you save. With the authorization-code grant, KloudMate saves it as **Pending** first, then sends you to approve it. If the approval doesn't finish, the row stays **Pending** with an **Authorize** button beside it. For a worked example with Google, see [Trigger a workflow when a new email is received in Gmail](/guides/workflows/trigger-on-new-gmail/).
+:::
+
+## Connect from a workflow step or trigger
+
+You don't have to start in Settings. A workflow step's connection picker and an app-event trigger's **Account** picker both offer **New connection**, or **Connect** followed by the app's name when the app has no connection yet. They open the same form, and the new connection can be used for **Workflows** straight away.
+
+That matters most for app-event triggers. A Slack account connected from Settings for notifications still appears in a trigger's **Account** picker, but it's flagged **Not enabled for workflows** until you add **Workflows** to it under [Edit](../manage-connections/#edit).
+
+## Visibility
+
+Every connection is **Workspace** or **Private**.
+
+**Workspace** is how every connection starts, and it's almost always the right choice. The connection belongs to the workspace, so anyone with the **Developer** role can use and manage it, and it keeps working when whoever connected it leaves.
+
+**Private** restricts a connection to its creator. Only a connection whose one capability is **AI** can be private, so use it for a personal GitHub or Google account that you want the assistant to use on your behalf and nobody else's.
+
+A private connection is invisible to everyone else: it's absent from their list, and they can't use it even if they know its id.
+
+To make a connection private, set **Visibility** under **Edit**. To share a private connection with the workspace, use **Share with workspace** on its row menu.
+
+## Related
+
+- [Providers](../providers/) for what each one covers and what a plan includes.
+- [Manage a connection](../manage-connections/) for editing, reconnecting, and deleting.
+- [Workflows](/workflows/) for binding a connection to a step or a trigger.
diff --git a/src/content/docs/docs/platform/settings/connections/images/connect-provider-picker.png b/src/content/docs/docs/platform/settings/connections/images/connect-provider-picker.png
new file mode 100644
index 00000000..328bf4d4
Binary files /dev/null and b/src/content/docs/docs/platform/settings/connections/images/connect-provider-picker.png differ
diff --git a/src/content/docs/docs/platform/settings/connections/images/connections-list.png b/src/content/docs/docs/platform/settings/connections/images/connections-list.png
new file mode 100644
index 00000000..f6588355
Binary files /dev/null and b/src/content/docs/docs/platform/settings/connections/images/connections-list.png differ
diff --git a/src/content/docs/docs/platform/settings/connections/images/delete-blocked.png b/src/content/docs/docs/platform/settings/connections/images/delete-blocked.png
new file mode 100644
index 00000000..3bd76b5d
Binary files /dev/null and b/src/content/docs/docs/platform/settings/connections/images/delete-blocked.png differ
diff --git a/src/content/docs/docs/platform/settings/connections/index.mdx b/src/content/docs/docs/platform/settings/connections/index.mdx
new file mode 100644
index 00000000..203a9e46
--- /dev/null
+++ b/src/content/docs/docs/platform/settings/connections/index.mdx
@@ -0,0 +1,68 @@
+---
+title: "Connections"
+description: "One stored credential per account, reusable across workflows, the AI assistant, notifications, and dashboard queries."
+sidebar:
+ label: "Overview"
+ order: 1
+---
+
+A connection is one credential for one account: an AWS role, a Slack bot install, an Atlassian grant, an MCP server token. Connect the account once, and every workflow step, notification channel, and dashboard query that needs it uses the same connection.
+
+When a credential changes, you rotate it once. Before you delete a connection, KloudMate lists everything that still uses it, and one switch takes it out of service everywhere.
+
+Open **Settings → Connections** for the full list. The **Connections** tab inside [Workflows](/workflows/) shows only the connections that can be used for **Workflows**.
+
+:::note
+AWS accounts connected for inventory and metrics live under [Settings → Data Sources](../data-sources/), and don't appear in this list. Notification channels live under [Settings → Notification Channels](../notification-channels/). A channel isn't listed here, but the Slack or Jira account it sends through is, as a connection used for **Notifications**.
+:::
+
+## Capabilities
+
+Each connection is granted for particular uses, and KloudMate checks the grant every time something uses the connection. The **Can be used for** column lists those uses:
+
+| Capability | What it grants |
+|---|---|
+| **Workflows** | Workflow steps can use this connection, and events from its account can start a workflow. |
+| **Notifications** | Notification channels can send through it. |
+| **Data source** | Dashboards and alert rules can query through it. |
+| **AI** | AI assistants can call it as a tool. |
+
+Capabilities decide:
+
+- **Whether a workflow can use it.** A workflow step's connection picker and an app-event trigger's **Account** picker list every connection for the app, and flag one without **Workflows** as **Not enabled for workflows**. Publishing refuses a workflow that uses one.
+- **Whether it needs reconnecting.** Adding a capability that needs permissions the connection wasn't granted marks it **Needs reconnect** until you approve them.
+- **Whether it can be private.** Only a connection whose one capability is **AI** can be. See [Connect an account](./connect-an-account/#visibility).
+- **Whether a run can use it.** KloudMate checks the grant at the moment of use, not only when you set it up. If you withdraw **Workflows** from a connection, every step that uses it starts failing and its app-event triggers stop.
+
+A capability grants permission. It doesn't mean anything has used the connection that way yet.
+
+## Read the connections list
+
+Filter the list by provider or by capability, or search it by name, provider, or account.
+
+| Status | Meaning |
+|---|---|
+| **Verified** | The last check reached the provider, or the last approval succeeded. |
+| **Pending** | Its approval never finished, for example an OAuth 2.0 connection that's still waiting for someone to approve it. |
+| **Degraded** | The last check failed. Workflow steps and dashboard queries still run through it, but its app-event triggers pause until it verifies again. |
+| **Needs reconnect** | The grant is dead, or too narrow for what the connection is now used for. Reconnect it. |
+| **Revoked** | The provider refused the credential when a workflow step or trigger used it. Workflows and dashboards can't use it until you reconnect or edit it. |
+| **Disabled** | Someone switched it off. Nothing can use it to reach the provider. |
+
+When a connection isn't working, a banner at the top of the page says how many, and **Show these** narrows the list to them. The banner counts connections that are **Degraded**, **Needs reconnect**, or **Revoked**, and an OAuth 2.0 connection still waiting for approval.
+
+A row that needs attention has its fix beside its status: **Authorize** for an approval that never finished, **Reconnect** for a provider you approve at its own site, or **Edit** for one whose credential you type in.
+
+
+
+## In this section
+
+- [Connect an account](./connect-an-account/)
+- [Providers](./providers/)
+- [Manage a connection](./manage-connections/)
+
+## Related
+
+- [Workflows](/workflows/) for the steps and triggers that bind a connection.
+- [Notification channels](../notification-channels/) for the delivery destinations.
+- [KloudMate Assistant](/kloudmate-assistant/) for the AI tools a connection can serve.
diff --git a/src/content/docs/docs/platform/settings/connections/manage-connections.mdx b/src/content/docs/docs/platform/settings/connections/manage-connections.mdx
new file mode 100644
index 00000000..305426f6
--- /dev/null
+++ b/src/content/docs/docs/platform/settings/connections/manage-connections.mdx
@@ -0,0 +1,84 @@
+---
+title: "Manage a connection"
+description: "Rename, rotate, re-check, reconnect, switch off, replace, and delete a connection, and see what uses it before you delete it."
+sidebar:
+ label: "Manage a connection"
+ order: 4
+---
+
+Every action here is on the row menu in **Settings → Connections**, and all of them need the **Developer** role. A row that needs attention also has its fix beside its status: **Authorize**, **Reconnect**, or **Edit**.
+
+## Edit
+
+**Edit** changes the connection's name, capabilities, visibility, enabled state, and the provider's own settings.
+
+Rotate a credential the same way: type the new value and save. To keep a stored secret, leave its field blank. When you save a changed credential or setting, KloudMate checks the connection against the provider again.
+
+You can't change the provider or the authentication method after you create a connection, so an AWS connection made with access keys can't switch to a role. Connect a new one instead.
+
+:::caution
+Changing **Can be used for** can break things that are already working. Removing a capability stops every use that depends on it at the next call. Adding one can mark the connection **Needs reconnect** until you approve the extra permissions, and you can't add a capability the provider is no longer offered for.
+:::
+
+## Re-check
+
+**Re-check** asks the provider whether the credential still works and updates the status. Use it after you've fixed something at the provider's end. It can clear **Revoked** when the credential works again, but not **Needs reconnect**, which only a reconnect clears.
+
+A connection that fails the check becomes **Degraded**. That's a warning, not an outage: workflow steps and dashboard queries still run through it. App-event triggers are the exception, and pause until it verifies again.
+
+## Reconnect
+
+**Reconnect** re-runs the OAuth approval and rotates the credential in place. The connection keeps its id, so every workflow, channel, and dashboard that uses it keeps working. Slack, Jira Cloud, Sentry, the Google Cloud providers, and an OAuth 2.0 connection that uses the authorization-code grant have it. For the other providers, use **Edit** instead. While a connection's first approval is unfinished, the action is called **Authorize**.
+
+**Needs reconnect** shows one of these reasons when you hover over it:
+
+| Reason | What happened |
+|---|---|
+| The provider now needs permissions this connection does not have. | You added a capability, or the permissions a capability requires changed. Re-consenting grants them. |
+| The provider revoked this access. | Someone removed the app at the provider's end. |
+| The access expired and cannot renew itself. | The grant expired and has no way to refresh itself. |
+| The provider rejected the last call made with this connection. | The provider refused a call made through the connection. |
+
+Approving again can add permissions but never removes any, so reconnecting to add permissions doesn't cost you the ones you had.
+
+A reconnect must use the same account. KloudMate refuses an approval that resolves to a different one.
+
+## Switch a connection off
+
+The **Enabled** switch in the edit dialog stops anything from using a connection, without deleting it or touching the credential. Everything that uses it fails until you switch it back on, and publishing refuses a workflow that uses it.
+
+Use it to take a compromised credential out of service immediately while you work out what was using it.
+
+## Replace
+
+**Replace** moves every workflow that uses one connection to another connection of the same provider. It's the way to retire a credential that's still in use.
+
+The target must be shared with the workspace and be usable for **Workflows**. The picker offers only valid targets.
+
+**It rewrites drafts, not live versions.** A published version keeps the old connection until you republish it, so the result lists the workflows to republish. To retire a connection, replace it, republish each workflow on that list, then delete the connection.
+
+Replace doesn't touch dashboards or alert rules. Change their connection in their own editors.
+
+## Delete
+
+KloudMate refuses to **Delete** a connection while anything still uses it. The **Still in use** dialog lists every workflow, dashboard, and alert rule that uses it, each linked to its own editor.
+
+
+
+When workflows are among them, the dialog also offers **Replace connection**.
+
+When nothing uses the connection, deleting asks for confirmation, and you can't undo it.
+
+## Who can manage a connection
+
+A **workspace** connection belongs to the workspace, so anyone with the **Developer** role can edit, rotate, or delete it, and it keeps working after the person who connected it leaves.
+
+Only the creator can edit or rotate a **private** connection. An organization **Owner** can still delete one, so a workspace isn't stuck when someone leaves.
+
+When someone leaves the organization or the workspace, their workspace connections keep working and lose their owner. KloudMate deletes their private connections.
+
+## Related
+
+- [Connect an account](../connect-an-account/)
+- [Providers](../providers/)
+- [Workflows](/workflows/) for what binds a connection.
diff --git a/src/content/docs/docs/platform/settings/connections/providers.mdx b/src/content/docs/docs/platform/settings/connections/providers.mdx
new file mode 100644
index 00000000..38955da5
--- /dev/null
+++ b/src/content/docs/docs/platform/settings/connections/providers.mdx
@@ -0,0 +1,85 @@
+---
+title: "Providers"
+description: "What each provider can be used for, how you supply its credential, and which providers your plan includes."
+sidebar:
+ label: "Providers"
+ order: 3
+---
+
+Each provider decides how you supply its credential and what a connection to it can be used for. The **Can be used for** column shows what each provider supports. A new connection starts with the provider's usual capability, and you change which ones it holds under [Edit](../manage-connections/#edit).
+
+## Cloud and infrastructure
+
+| Provider | Credential | Can be used for |
+|---|---|---|
+| **AWS** | Assume Role with a role ARN and external ID (recommended), or an access key pair. A default region comes with it, and a workflow step can override it. | Workflows, AI |
+| **Azure** | A service principal: directory (tenant) id, application (client) id, client secret, and a default subscription id. | Workflows |
+
+Both allow more than one connection, so a production account and a staging account are two rows.
+
+The IAM policy or Azure role assignment on the credential decides what a step can do with it. There's no second permission list inside KloudMate, so narrow the role itself. Gate risky steps with an [approval](/workflows/approvals-and-input/).
+
+:::note
+This isn't the same as the AWS account you connect for inventory and metrics. That one lives under [Settings → Data Sources](/platform/settings/data-sources/) and doesn't appear in the Connections list. Connect AWS here when a workflow step needs to call an AWS API, or when the AI assistant needs AWS as a tool.
+:::
+
+## Chat and issue tracking
+
+| Provider | Credential | Can be used for |
+|---|---|---|
+| **Slack** | OAuth install. | Notifications, Workflows |
+| **Jira Cloud** | OAuth through Atlassian. | Notifications, Workflows |
+
+One Slack install serves alert notifications, incident ChatOps, the [Slack workflow actions](/workflows/actions/#slack), and Slack event triggers. A Slack or Jira Cloud connection made in **Settings → Connections** starts out used for **Notifications**. Add **Workflows** under [Edit](../manage-connections/#edit) before a workflow step or trigger can use it, and reconnect it if it's then marked **Needs reconnect**.
+
+One Jira credential can access several Atlassian sites, which is why every Jira action and trigger starts with a site picker.
+
+## Tools and custom endpoints
+
+| Provider | Credential | Can be used for |
+|---|---|---|
+| **Custom MCP Server** | A server URL, a transport of Streamable HTTP or SSE, and either no auth, a bearer token, or an API key in a header you name. Extra headers can be sent alongside, for a server that wants a routing header as well as a credential. | Workflows |
+| **OAuth 2.0** | Your own authorize and token URLs, scopes, client id, and client secret, with either the client-credentials or the authorization-code grant. | Workflows |
+
+Use **OAuth 2.0** to give an [HTTP Request](/workflows/actions/#http-request) step a credential that's refreshed for you and never stored in the workflow definition.
+
+## AI assistant tools
+
+These connect an MCP server that the [KloudMate Assistant](/kloudmate-assistant/) can call as a tool. They can be used for **AI** only, which also makes them the only connections that can be [private](../connect-an-account/#visibility).
+
+| Provider | Credential |
+|---|---|
+| **GitHub** | A GitHub token. |
+| **Atlassian** | An Atlassian API token. |
+| **Sentry** | OAuth. |
+| **Google Cloud BigQuery** | Google OAuth. |
+| **Google Cloud Compute Engine** | Google OAuth. |
+| **Google Cloud GKE** | Google OAuth. |
+| **Google Cloud Logging** | Google OAuth. |
+| **Google Cloud Monitoring** | Google OAuth. |
+| **Google Cloud Resource Manager** | Google OAuth. |
+
+AWS can also be used for **AI**, with an access key pair. See the cloud table above.
+
+## Providers in your plan
+
+Your plan decides which providers you can connect. The **Connect** picker shows only the providers your plan includes and the platform currently offers. If you expect a provider but can't see it, either your plan doesn't include it or the platform doesn't offer it right now. Upgrading fixes only the first.
+
+The platform can also offer a provider for some uses and not others. If a provider appears in the picker but you can't grant it the capability you wanted, the platform doesn't offer that capability for it right now.
+
+If the platform stops offering a provider, or one of its uses, only new connections are affected. Existing workflows, dashboards, and notification channels keep working.
+
+## Link your Slack account for ChatOps
+
+One Slack step is done per person, not per workspace. After someone with the **Developer** role connects Slack and adds a Chat Ops channel, each responder links their own Slack account before they can acknowledge or resolve incidents from Slack.
+
+Open **Incidents → Chat Ops**, scroll to **User Settings**, and click **Connect account**. It's a one-time step that links your KloudMate account to your Slack user.
+
+Everyone links their own account, because KloudMate has to match the Slack user who pressed a button to a workspace member before it acts.
+
+See [Slack Integration](/incident-management/slack-integration/) for the full setup.
+
+## Related
+
+- [Connect an account](../connect-an-account/)
+- [Manage a connection](../manage-connections/)
diff --git a/src/content/docs/docs/platform/settings/data-sources.mdx b/src/content/docs/docs/platform/settings/data-sources.mdx
index 72865ac6..cdbc4aaa 100644
--- a/src/content/docs/docs/platform/settings/data-sources.mdx
+++ b/src/content/docs/docs/platform/settings/data-sources.mdx
@@ -40,7 +40,7 @@ Enter the AWS account name and follow the on-screen setup flow when adding a new
Sync Settings control what KloudMate inventories for a connected account. Open an account's menu and choose **Sync settings**.
-Two account-level controls decide what gets synced: **Primary regions** and an optional **tag allowlist**. The set of resource types KloudMate inventories is platform-managed and the same for every account. See [Covered services](#covered-services). The **Region sync status** section on the same page surfaces any regions that have stopped syncing and lets an admin turn them back on.
+Two account-level controls decide what gets synced: **Primary regions** and an optional **tag allowlist**. The set of resource types KloudMate inventories is platform-managed and the same for every account. See [Covered services](#covered-services). The **Region sync status** section on the same page surfaces any regions that have stopped syncing, and with the **Developer** role you can turn them back on.

@@ -59,7 +59,7 @@ For each region, the panel shows why it stopped:
- If a permission error disabled it, the recorded reason appears, for example an `AccessDenied` message naming the missing permission.
- If it was turned off some other way, it shows **Disabled. No recent error recorded.**
-A workspace admin can select **Re-enable** on a region. That turns the region back on, clears its stored error, and starts a sync right away, so its resources return without waiting for the next cycle. Non-admins see the status but can't re-enable.
+With the **Developer** role, you can select **Re-enable** on a region. That turns the region back on, clears its stored error, and starts a sync right away, so its resources return without waiting for the next cycle. With the **Viewer** role, you can see the status but can't re-enable a region.
Most permission problems recover on their own once you fix the underlying access. To find the cause and confirm the fix, see [Troubleshoot AWS sync](../troubleshoot-aws-sync/).
diff --git a/src/content/docs/docs/platform/settings/images/channel-km-incidents-form.png b/src/content/docs/docs/platform/settings/images/channel-km-incidents-form.png
index c4a5c203..b35af536 100644
Binary files a/src/content/docs/docs/platform/settings/images/channel-km-incidents-form.png and b/src/content/docs/docs/platform/settings/images/channel-km-incidents-form.png differ
diff --git a/src/content/docs/docs/platform/settings/images/data-sources-4.png b/src/content/docs/docs/platform/settings/images/data-sources-4.png
index 5c81a336..d8bd71a6 100644
Binary files a/src/content/docs/docs/platform/settings/images/data-sources-4.png and b/src/content/docs/docs/platform/settings/images/data-sources-4.png differ
diff --git a/src/content/docs/docs/platform/settings/images/jira-1.png b/src/content/docs/docs/platform/settings/images/jira-1.png
deleted file mode 100644
index 50bf0df7..00000000
Binary files a/src/content/docs/docs/platform/settings/images/jira-1.png and /dev/null differ
diff --git a/src/content/docs/docs/platform/settings/images/jira-3.png b/src/content/docs/docs/platform/settings/images/jira-3.png
index df63a2c4..e46b7ea1 100644
Binary files a/src/content/docs/docs/platform/settings/images/jira-3.png and b/src/content/docs/docs/platform/settings/images/jira-3.png differ
diff --git a/src/content/docs/docs/platform/settings/images/notification-channels-list.png b/src/content/docs/docs/platform/settings/images/notification-channels-list.png
index 70c81c22..89a936c5 100644
Binary files a/src/content/docs/docs/platform/settings/images/notification-channels-list.png and b/src/content/docs/docs/platform/settings/images/notification-channels-list.png differ
diff --git a/src/content/docs/docs/platform/settings/images/slack-1.png b/src/content/docs/docs/platform/settings/images/slack-1.png
index 7f067a64..77550bfd 100644
Binary files a/src/content/docs/docs/platform/settings/images/slack-1.png and b/src/content/docs/docs/platform/settings/images/slack-1.png differ
diff --git a/src/content/docs/docs/platform/settings/images/webhook-customize-panel.png b/src/content/docs/docs/platform/settings/images/webhook-customize-panel.png
new file mode 100644
index 00000000..f162fb18
Binary files /dev/null and b/src/content/docs/docs/platform/settings/images/webhook-customize-panel.png differ
diff --git a/src/content/docs/docs/platform/settings/images/webhooks-1.png b/src/content/docs/docs/platform/settings/images/webhooks-1.png
index 3fc2913b..62e383fc 100644
Binary files a/src/content/docs/docs/platform/settings/images/webhooks-1.png and b/src/content/docs/docs/platform/settings/images/webhooks-1.png differ
diff --git a/src/content/docs/docs/platform/settings/index.mdx b/src/content/docs/docs/platform/settings/index.mdx
index 1ca18d62..5ea45c79 100644
--- a/src/content/docs/docs/platform/settings/index.mdx
+++ b/src/content/docs/docs/platform/settings/index.mdx
@@ -19,5 +19,6 @@ The Settings area is where you manage workspaces, users, security, billing, and
- [Troubleshoot AWS sync](./troubleshoot-aws-sync/)
- [Remove an AWS Account](./deleting-your-aws-account-from-kloudmate/)
- [Notification Channels](./notification-channels/)
+- [Connections](./connections/)
- [Subscription & Billing](./subscription-billing/)
- [Usage Report](./usage-report/)
diff --git a/src/content/docs/docs/platform/settings/notification-channels.mdx b/src/content/docs/docs/platform/settings/notification-channels.mdx
index 80c0deb4..94789776 100644
--- a/src/content/docs/docs/platform/settings/notification-channels.mdx
+++ b/src/content/docs/docs/platform/settings/notification-channels.mdx
@@ -10,19 +10,21 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
To receive notifications from KloudMate, you need to set up notification channels — the communication platforms where alerts and incident updates are sent.
KloudMate supports the following notification channels:
-- Email
-- Slack
-- SNS
-- MS Teams
-- Webhook
-- Jira
-- KloudMate Incidents — bridges alerts and synthetic checks into [Incident Management](../../../incident-management/)
+- [Email](#email)
+- [Slack](#slack)
+- [SNS](#sns)
+- [MS Teams](#ms-teams)
+- [Webhook](#webhooks)
+- [Jira](#jira)
+- [KloudMate Incidents](#kloudmate-incidents) — bridges alerts and synthetic checks into [Incident Management](../../../incident-management/)
+
+A Slack or Jira channel can reuse an account you've already connected instead of asking for its own credential. The channel form lists the [connections](../connections/) that can be used for **Notifications**, and lets you connect a new one without leaving the form. When the credential changes, you then rotate it in one place.
:::info
In the case of multiple workspaces and AWS accounts, you must set up notifications for each account in their respective **Settings**.
:::
-Navigate to **Settings** > **Notification Channels**. The screen displays all existing channels with their name, type, and address.
+Navigate to **Settings** > **Notification Channels**. The screen displays all existing channels with their name, type, and address. A channel whose last delivery failed is marked **Error**, with the reason and the time it happened shown under its address.

@@ -49,11 +51,14 @@ Below are the configuration steps for each specific channel type:
KloudMate uses a single unified Slack app for both notifications and IM ChatOps. Installing it once gives you threaded notifications, message attribution, and the ability to acknowledge or resolve incidents directly from Slack.
-1. Select **Slack** from the dropdown menu, then click the **Add to Slack** button.
-2. You will be redirected to Slack's OAuth flow. Pick the channel KloudMate should post into and approve the bot scopes.
-3. On approval, KloudMate creates the channel row and the Slack integration is also visible under **Incidents → Settings → ChatOps**.
+1. Select **Slack** from the dropdown menu.
+2. Pick a connected **Slack account**. If none is connected yet, click **Connect Slack** and approve the install in the popup that opens.
+3. Enter a **Channel name** for the KloudMate side, then pick the **Slack channel** to post into. The list searches your Slack workspace's public and private channels through the connected bot, so type to narrow it.
+4. Click **Create channel**.
+
+
-
+The same Slack install appears under [Settings → Connections](../connections/) and under **Incidents → Chat Ops**. Connect it once and reuse it for every Slack channel you create here.
### MS Teams
@@ -80,27 +85,34 @@ Optionally, you can download the KloudMate logo from the link provided on the sc
### Webhooks
1. Select **Webhook** from the dropdown menu.
-2. Enter a **Webhook Name** and the **Webhook URL** in their respective fields.
-3. Under **Secret**, provide a secure key. This secret is used to generate an HMAC-SHA256 signature for each webhook payload, allowing you to verify that the request came from KloudMate.
-4. Click **Submit**.
+2. Enter a **Webhook Name** and the **Webhook Url** in their respective fields.
+3. Click **Create channel**.

+KloudMate generates a secret for the channel and uses it to sign every request with an HMAC-SHA256 signature, so you can verify that a request came from KloudMate. With the **Developer** role, you can copy the secret from the channel list with **Copy Secret**.
+
+A webhook channel also sends alerts to a [workflow](../../../workflows/). Set the channel's URL to the workflow's webhook URL, then add the channel to a routing rule. Each notification then starts a run, with the alert group payload as the request body. See [Triggers](../../../workflows/triggers/#start-a-workflow-from-an-alert).
+
#### Customize the Request Body and Headers
By default, a webhook sends KloudMate's standard JSON payload, which some receivers reject. Google Chat, for example, accepts only a message object like `{"text": "..."}`, with its auth token in the webhook URL, so no header or secret is needed. The same `{"text": ...}` body also works for Slack incoming webhooks and Microsoft Teams. A webhook channel can rewrite its outgoing body and add headers to match.
In the webhook channel editor, click **Customize request body and headers** to expand the options.
+
+
**Request body** takes JSON with Liquid `{{ }}` placeholders inside string values, filled from the event. Leave it empty to send the default payload. The **Examples** menu fills it with a starter template, and [Notification Payloads](#notification-payloads) lists the fields each event type exposes.
**Custom headers** are key/value pairs sent with every request. `Content-Type` and `X-KM-Signature` are reserved and set automatically.
**Test** previews the rendered body against a sample event. The **Event type** list picks which event to sample.
+Saving runs the same check as **Test**, so a channel with invalid JSON, invalid Liquid, or a reserved header won't save. If a custom body still fails to render when a notification goes out, the channel is marked **Error** and that delivery isn't retried.
+
#### Webhook Signature Validation
-When KloudMate sends a webhook to your configured URL, it includes an `X-KM-Signature` HTTP header. This signature is an HMAC-SHA256 hash generated using the payload body and the **Secret** you provided during configuration.
+When KloudMate sends a webhook to your configured URL, it includes an `X-KM-Signature` HTTP header. This signature is an HMAC-SHA256 hash generated using the payload body and the channel's secret.
To ensure the webhook is genuinely from KloudMate, you should compute the hash on your server and compare it with the `X-KM-Signature` header.
@@ -356,8 +368,9 @@ KloudMate Incidents is the channel type that bridges into [Incident Management](

1. Select **KloudMate Incidents** from the dropdown menu.
-2. Pick the IM service you want to bridge into from the **Linked IM service** dropdown. If you don't have any services yet, KloudMate prompts you to create one first.
-3. Click **Submit**.
+2. Enter a **Channel name**, such as `KloudMate Incidents – Payments`.
+3. Pick the IM service you want to bridge into from the **Linked IM service** dropdown. If you don't have any services yet, KloudMate prompts you to create one first.
+4. Click **Create channel**.
KloudMate automatically creates two rows:
@@ -370,26 +383,27 @@ To wire it into the alert flow, add the channel as a destination on a [Routing R
### Jira
-1. Select **Jira** from the dropdown menu and click the **Connect Jira** button.
-
-
+1. Select **Jira** from the dropdown menu, then pick a connected **Jira account**. If none is connected yet, click **Connect Jira**.
-2. You will be redirected to the Jira authorization page. Click **Accept** to authorize the integration between KloudMate and your Atlassian account.
+2. Approve access in the popup that opens.
-
+
-3. You will be redirected back to the Jira Notification Channel configuration page in KloudMate.
+3. Back on the channel form, enter a **Channel name** and fill in the rest:
-
+
-Enter a **Channel Name** and select the following:
- **Site**: your Jira site
- **Project**: the Jira project to link
-- **Issue Type:** the type of issue to create (e.g., Task)
-- **Status for Open state:** the Jira status that corresponds to an open ticket (e.g., To Do, In Progress)
-- **Status for Closed state:** the Jira status that corresponds to a closed ticket (e.g., Done)
+- **Issue type**: the type of issue to create, for example Task
+- **Status that maps to Open**: the Jira status for an open ticket, for example To Do
+- **Status that maps to Closed**: the Jira status for a closed ticket, for example Done
+
+Each list loads from the one above it, so changing the site clears the project, and changing the project clears the three below it.
+
+4. Click **Create channel**.
-4. Click **Submit** to complete the integration.
+If the Jira authorization has expired, the form says so and offers **Reconnect Jira**.
:::info
The Jira integration is one-way. Status changes in KloudMate are reflected in Jira, but changes made in Jira are not reflected in KloudMate.
diff --git a/src/content/docs/docs/platform/settings/sso/images/sso-settings-page.png b/src/content/docs/docs/platform/settings/sso/images/sso-settings-page.png
index e7d78471..c2568d3c 100644
Binary files a/src/content/docs/docs/platform/settings/sso/images/sso-settings-page.png and b/src/content/docs/docs/platform/settings/sso/images/sso-settings-page.png differ
diff --git a/src/content/docs/docs/platform/settings/sso/provisioning-and-enforcement.mdx b/src/content/docs/docs/platform/settings/sso/provisioning-and-enforcement.mdx
index 6af90906..76e60fc3 100644
--- a/src/content/docs/docs/platform/settings/sso/provisioning-and-enforcement.mdx
+++ b/src/content/docs/docs/platform/settings/sso/provisioning-and-enforcement.mdx
@@ -19,7 +19,7 @@ In the **User provisioning** card on **Settings → Single Sign-On**, pick one o
### Which one should you pick?
-- Use **Invite only** when you want to control the member list yourself and treat the IdP purely as the login method. A user who authenticates but was never invited is turned away with **Your account isn't set up for this org — ask an admin to invite you.**
+- Use **Invite only** when you want to control the member list yourself and treat the IdP purely as the login method. A user who authenticates but was never invited is turned away with **Your account isn't set up for this org — ask an organization Owner to invite you.**
- Use **JIT** when your IdP app is already scoped to the right group of people, and you'd rather not invite each person by hand. Everyone in that IdP group can sign in and is added on first login.
You can switch modes at any time. Switching to JIT doesn't retroactively add anyone — accounts are created as people sign in.
diff --git a/src/content/docs/docs/platform/settings/sso/troubleshooting.mdx b/src/content/docs/docs/platform/settings/sso/troubleshooting.mdx
index a5a213f4..53f04b2d 100644
--- a/src/content/docs/docs/platform/settings/sso/troubleshooting.mdx
+++ b/src/content/docs/docs/platform/settings/sso/troubleshooting.mdx
@@ -20,7 +20,7 @@ These appear on the callback screen after the user comes back from the IdP.
| **Couldn't verify your identity provider's response** (`OidcCallbackFailed`) | The OIDC token or ID token failed validation | Check the **Client secret** (a wrong one shows `invalid_client` in the details), confirm the redirect URI `https://api.kloudmate.com/sso/oidc` is registered in the IdP, and for Entra ID use the **tenant-specific** discovery URL, not `common`. Large clock skew on either side can also cause this |
| **Couldn't verify your identity provider's response** (`SamlValidationFailed`) | The SAML assertion's signature, audience, or destination didn't check out | Re-import the IdP signing certificate (or fresh metadata), make sure the IdP **Audience** equals your **SP Entity ID**, and that the **Recipient/Destination** equals your **ACS URL** (`https://api.kloudmate.com/sso/saml`) |
| **Your identity provider didn't return an email address** (`OidcMissingEmail` / `SamlMissingEmail`) | The IdP didn't send an email claim | Map the email claim (OIDC) or send email in the **NameID** or an `email` attribute (SAML). For Entra ID, ensure the user has a mailbox or map the optional `email` claim — see [the Entra guide](../providers/entra-id/) |
-| **Your account isn't set up for this org — ask an admin to invite you** (`UserNotProvisioned` / `UserNotInOrg`) | Provisioning is **invite-only** and this person wasn't invited | [Invite them](../../users-permissions/), or switch provisioning to **JIT** — see [Provisioning & enforcement](../provisioning-and-enforcement/) |
+| **Your account isn't set up for this org — ask an organization Owner to invite you** (`UserNotProvisioned` / `UserNotInOrg`) | Provisioning is **invite-only** and this person wasn't invited | [Invite them](../../users-permissions/), or switch provisioning to **JIT** — see [Provisioning & enforcement](../provisioning-and-enforcement/) |
| **Login session expired — please try again** (`SsoStateInvalid`) | The login attempt's state/session timed out or was reused | Start the sign-in again from the login page |
| **Something went wrong signing you in — please try again** (`SsoLoginFailed`) | A generic failure, or the callback was reached with no token | Retry; if it persists, open **Technical details** and check your connection settings |
diff --git a/src/content/docs/docs/reliability/burn-rate-alerts.mdx b/src/content/docs/docs/reliability/burn-rate-alerts.mdx
index f2c1fdbf..a97b9d4d 100644
--- a/src/content/docs/docs/reliability/burn-rate-alerts.mdx
+++ b/src/content/docs/docs/reliability/burn-rate-alerts.mdx
@@ -10,7 +10,7 @@ A **burn-rate alert** fires when an SLO's burn rate is too high over **both** a
Burn-rate alerts live in two places:
- **Per SLO**, under the SLO detail page's Burn-rate alerts section — for creating, editing, and deleting alerts.
-- **Workspace-wide**, on the [Burn-rate alerts page](#workspace-wide-burn-rate-alerts) (`//slos/alerts`) — the on-call surface that lists every burn-rate alert in the workspace, firing-first.
+- **Workspace-wide**, on [**Reliability → Burn-rate alerts**](#workspace-wide-burn-rate-alerts), which lists every burn-rate alert in the workspace with firing alerts first.
## Anatomy of a burn-rate alert
@@ -50,7 +50,7 @@ Clicking a preset fills the form fields and seeds a default name (`Critical burn
## Workspace-wide burn-rate alerts
-The workspace-wide page at `//slos/alerts` lists every burn-rate alert across every SLO.
+**Reliability → Burn-rate alerts** lists every burn-rate alert across every SLO.
Columns: **State** / **Alert** / **SLO** / **Channels**. Each row shows its parent SLO's name and SLI kind — there's no Service column, since most SLOs aren't tied to a service.
@@ -64,7 +64,7 @@ Click any row to open the parent SLO's detail page — alerts are created and ed
## Permissions
-Workspace **admin** is required to create, edit, or delete burn-rate alerts. Everyone else can read the list.
+You need the **Developer** role to create, edit, or delete burn-rate alerts. With the **Viewer** role, you can read the list.
## Related
diff --git a/src/content/docs/docs/reliability/create-an-slo.mdx b/src/content/docs/docs/reliability/create-an-slo.mdx
index 09d02209..f1cf8a17 100644
--- a/src/content/docs/docs/reliability/create-an-slo.mdx
+++ b/src/content/docs/docs/reliability/create-an-slo.mdx
@@ -9,14 +9,14 @@ import { Steps } from '@astrojs/starlight/components';
Creating an SLO is a **two-step wizard**: pick the signal you want to measure (the SLI) and configure it, then set the target and window. A live preview on the right shows the projected compliance and error budget before you save.
-Workspace **admin** is required. Non-admins don't see the Create SLO button.
+You need the **Developer** role to create an SLO. With the **Viewer** role, **Create SLO** isn't available.
## Where to start the wizard
Four entry points lead to the same wizard at `//slos/create`:
-- **Reliability Overview → Create SLO** (top-right).
-- **Reliability Overview → Quick start → Create an SLO** card.
+- **Create SLO** on any **Reliability** tab.
+- The **Create an SLO** card in **Quick start** on **Reliability → Overview**.
- **Service detail → Reliability tab → Create SLO** — deep-links with `?service_id=…`, which pre-selects the **Incident availability** kind and prefills that service in the SLI (since the service lives inside the SLI, not on the SLO).
- **Alert detail → Define an SLO** action — opens the wizard with no prefill.
@@ -76,7 +76,7 @@ The Edit form uses the same layout as Create, with both steps open at once. Diff
- The **preview still runs**, showing the effect of your *unsaved* edits before you save.
- **Save only writes the fields you changed.**
-You can reach Edit from the SLO detail page header or via the row menu on the [SLOs list](../slo-detail/).
+You can reach Edit from the SLO detail page header or from the row menu in **Reliability → SLOs**.
## Deleting an SLO
diff --git a/src/content/docs/docs/reliability/images/burn-rate-alert-form.png b/src/content/docs/docs/reliability/images/burn-rate-alert-form.png
index 15d4dee2..0f35d115 100644
Binary files a/src/content/docs/docs/reliability/images/burn-rate-alert-form.png and b/src/content/docs/docs/reliability/images/burn-rate-alert-form.png differ
diff --git a/src/content/docs/docs/reliability/images/incident-linked-slos.png b/src/content/docs/docs/reliability/images/incident-linked-slos.png
index a738c29c..38eff5e5 100644
Binary files a/src/content/docs/docs/reliability/images/incident-linked-slos.png and b/src/content/docs/docs/reliability/images/incident-linked-slos.png differ
diff --git a/src/content/docs/docs/reliability/images/reliability-overview-hub.png b/src/content/docs/docs/reliability/images/reliability-overview-hub.png
index c2270d51..1f51df3f 100644
Binary files a/src/content/docs/docs/reliability/images/reliability-overview-hub.png and b/src/content/docs/docs/reliability/images/reliability-overview-hub.png differ
diff --git a/src/content/docs/docs/reliability/images/service-reliability-tab.png b/src/content/docs/docs/reliability/images/service-reliability-tab.png
index 40bd5e3a..24a0597a 100644
Binary files a/src/content/docs/docs/reliability/images/service-reliability-tab.png and b/src/content/docs/docs/reliability/images/service-reliability-tab.png differ
diff --git a/src/content/docs/docs/reliability/images/slo-compliance-report.png b/src/content/docs/docs/reliability/images/slo-compliance-report.png
index 8b2f2fc3..f0940498 100644
Binary files a/src/content/docs/docs/reliability/images/slo-compliance-report.png and b/src/content/docs/docs/reliability/images/slo-compliance-report.png differ
diff --git a/src/content/docs/docs/reliability/images/slo-create-wizard.png b/src/content/docs/docs/reliability/images/slo-create-wizard.png
index 0eaa51ff..098773d3 100644
Binary files a/src/content/docs/docs/reliability/images/slo-create-wizard.png and b/src/content/docs/docs/reliability/images/slo-create-wizard.png differ
diff --git a/src/content/docs/docs/reliability/images/slo-detail-page.png b/src/content/docs/docs/reliability/images/slo-detail-page.png
index f1c78ca1..b21bf892 100644
Binary files a/src/content/docs/docs/reliability/images/slo-detail-page.png and b/src/content/docs/docs/reliability/images/slo-detail-page.png differ
diff --git a/src/content/docs/docs/reliability/index.mdx b/src/content/docs/docs/reliability/index.mdx
index d6dd2d78..3d283af4 100644
--- a/src/content/docs/docs/reliability/index.mdx
+++ b/src/content/docs/docs/reliability/index.mdx
@@ -10,7 +10,7 @@ import { LinkCard, CardGrid } from '@astrojs/starlight/components';
The **Reliability** area is where you set, monitor, and respond to reliability promises about your services. Define a service-level objective (SLO), track compliance over time, and get paged before the error budget runs out.
-This page is the workspace's reliability home — the hub you land on when you click **Reliability** in the left nav.
+The **Overview** tab is the workspace's reliability home, and it's where you land when you open **Reliability**.
## What's on this page
diff --git a/src/content/docs/docs/reliability/service-reliability-tab.mdx b/src/content/docs/docs/reliability/service-reliability-tab.mdx
index 57f7fda5..733f51eb 100644
--- a/src/content/docs/docs/reliability/service-reliability-tab.mdx
+++ b/src/content/docs/docs/reliability/service-reliability-tab.mdx
@@ -11,8 +11,8 @@ Reliability isn't only a top-level area. KloudMate surfaces SLO context where yo
Every IM service detail page (`//im/services/`) now has tabs:
-- **Overview** (default) — the existing stat cards and integrations table.
-- **Reliability** (new) — a service-scoped Create SLO button and a table of the service's **Incident availability** SLOs. Those are the only SLOs tied to a service; metric- and APM-based SLOs are workspace-level and don't appear here. Same row shape as the workspace [SLO list](../slo-detail/). The Create SLO button deep-links into the wizard with the service prefilled (it pre-selects the Incident availability kind). Empty state with explanatory copy when the service has no incident-based SLOs yet.
+- **Overview** (default): the stat cards and the **Alert Sources** table.
+- **Reliability** (new): a service-scoped Create SLO button and a table of the service's **Incident availability** SLOs. Those are the only SLOs tied to a service; metric- and APM-based SLOs are workspace-level and don't appear here. Same row shape as **Reliability → SLOs**. The Create SLO button deep-links into the wizard with the service prefilled (it pre-selects the Incident availability kind). Empty state with explanatory copy when the service has no incident-based SLOs yet.
Tab is URL-synced — `?tab=overview` or `?tab=reliability`. Refreshing or sharing the link preserves the active tab.
@@ -29,7 +29,7 @@ Clicking the chip jumps to the Reliability tab. The chip is **only rendered when
## Incident detail — Linked SLOs panel
-On incident detail (`//im/incidents/`), a **Linked SLOs** panel appears below the standard service / integration / escalation-policy labels — but only when the affected service has at least one **incident-based-availability** SLO.
+On incident detail (`//im/incidents/`), a **Linked SLOs** panel appears below the **Service**, **Alert source**, and **Escalation policy** labels, but only when the affected service has at least one **Incident availability** SLO.

diff --git a/src/content/docs/docs/reliability/slo-compliance-report.mdx b/src/content/docs/docs/reliability/slo-compliance-report.mdx
index c8ff1b13..70e9657b 100644
--- a/src/content/docs/docs/reliability/slo-compliance-report.mdx
+++ b/src/content/docs/docs/reliability/slo-compliance-report.mdx
@@ -1,44 +1,43 @@
---
title: "SLO Compliance report"
-description: "Schedule a workspace-wide rollup of every enabled SLO's compliance over a chosen time range."
+description: "Email a scheduled summary of every enabled SLO's latest compliance, error budget, and burn-rate alert state."
sidebar:
order: 8
---
-The **SLO Compliance** report is a source on the Reports module. It produces one row per enabled SLO with its latest compliance snapshot, error budget, and burn-rate-alert state.
+The SLO Compliance report lists every enabled SLO in the workspace with its latest compliance, remaining error budget, and burn-rate alert state. Schedule it to send your team a regular reliability summary as a PDF or XLSX file.
-The source has **no additional configuration** — it's a workspace-wide rollup of every enabled SLO. No filters, no per-SLO toggles. For a narrower view, filter the export afterward.
+The report always includes every enabled SLO, with no filters or per-SLO settings. For a narrower view, send it as XLSX and filter the spreadsheet.
## Create an SLO Compliance report
-1. Open **Settings → Reports → Create Report**.
-2. In the **Report Type** picker, select **SLO Compliance**.
-3. An info banner explains: e.g. *"Workspace-wide rollup: includes every enabled SLO over the selected time range. No additional filters are required."*
-4. Pick a **Time Range**, **Report Format** (PDF or XLSX), **Schedule** (Fixed or Recurring), and **Recipients** — same as other report sources. See [Create Report](../../reports/create-report/) for the full walk-through.
-5. Click **Save**.
+1. Open **Reports** and click **Create Report**.
+2. Select **SLO Compliance** as the source.
+3. Choose **Previous period** or **Fixed range**, a format, a schedule, and recipients. [Create Report](../../reports/create-report/) explains each setting.
+4. Click **Save** or **Save & Close**.
-
+
-## Discovery entry point
-
-You can also reach this flow from the [Reliability Overview hub](../) — the **Quick-start → Build a compliance report** card deep-links into the report builder with SLO Compliance pre-selected. This means users don't need to navigate to Reports separately to set this up.
+You can also start from **Reliability → Overview**. The **Build a compliance report** card opens the report builder with **Incidents** selected, so change the source to **SLO Compliance** before you continue.
## What the report contains
-For every enabled SLO in the workspace, the export has one row with:
+Compliance and error budget come from each SLO's most recent evaluation for its default window, not from the report period. For example, reports set to 1 **Week** and 1 **Month** that run at the same time show the same compliance for each SLO.
+
+Each enabled SLO gets one row with:
-- **Service** — only for **Incident availability** SLOs (resolved from the SLI); blank for the workspace-level kinds.
-- **SLO** name.
-- **SLI kind**.
-- **Window** — the SLO's default window.
-- **Target %**.
-- **Compliance %** — from the SLO's latest cached snapshot for its default window.
-- **Error budget** — remaining at that snapshot, in the SLI's unit (a duration or an event count). A negative value means breached.
-- **Burn-rate alert** — the worst current burn-rate-alert state on the SLO (e.g. *Firing for 2.0h*, *configured*, or *—*).
-- **Status** — `OK`, `BREACHED`, or `no snapshot yet` for a newly-created SLO before its first evaluation.
+- **Service:** set only for **Incident availability** SLOs. It's blank for the other SLI kinds.
+- **SLO:** the SLO's name.
+- **SLI kind:** the kind of indicator the SLO measures.
+- **Window:** the SLO's default window.
+- **Target:** the SLO's target percentage.
+- **Compliance:** the compliance percentage from the latest evaluation.
+- **Error budget:** the budget remaining at that evaluation, in the SLI's unit (a duration or an event count). A negative value means the SLO is breached.
+- **Burn-rate alert:** how long the SLO's longest-firing burn-rate alert has been firing, such as `Firing for 2.0h`. It shows `configured` when the SLO has burn-rate alerts but none is firing, and a dash when it has none.
+- **Status:** `OK`, `BREACHED`, or `no snapshot yet` for a new SLO that hasn't been evaluated.
## Related
-- [Reports — Create Report](../../reports/create-report/) — the surrounding report flow.
-- [SLO detail](../slo-detail/) — for ad-hoc, single-SLO views.
-- [Reliability Overview](../) — the deep-link entry point.
+- [Create Report](../../reports/create-report/): every report setting, including schedules and recipients.
+- [SLO detail](../slo-detail/): view a single SLO in detail.
+- [Reliability Overview](../): the Overview tab with the **Build a compliance report** card.
diff --git a/src/content/docs/docs/reliability/slo-detail.mdx b/src/content/docs/docs/reliability/slo-detail.mdx
index 9b887107..c4055798 100644
--- a/src/content/docs/docs/reliability/slo-detail.mdx
+++ b/src/content/docs/docs/reliability/slo-detail.mdx
@@ -5,7 +5,7 @@ sidebar:
order: 5
---
-The SLO detail page is where you read compliance and manage burn-rate alerts for a single SLO. It lives at `//slos/` and is reached from the [SLOs list](../), the breached cards on the [Overview hub](../), the service detail [Reliability tab](../service-reliability-tab/), or the **Linked SLOs** panel on incident detail.
+The SLO detail page is where you read compliance and manage burn-rate alerts for a single SLO. It lives at `//slos/` and is reached from **Reliability → SLOs**, the breached cards on the [Overview hub](../), the service detail [Reliability tab](../service-reliability-tab/), or the **Linked SLOs** panel on incident detail.

@@ -15,7 +15,7 @@ The page is composed of, top to bottom:
### Header
-Name, SLI kind chip, target / window summary, and a `Disabled` chip if applicable. For an **Incident availability** SLO the header also shows a **Service** link (a soft-deleted service still renders, suffixed *"(deleted)"*); every other kind is workspace-level and shows no service. Admins see **Edit** and **Delete** buttons.
+Name, SLI kind chip, target / window summary, and a `Disabled` chip if applicable. For an **Incident availability** SLO the header also shows a **Service** link (a soft-deleted service still renders, suffixed *"(deleted)"*); every other kind is workspace-level and shows no service. With the **Developer** role, you also see **Edit** and **Delete** buttons.
### Window picker
@@ -46,7 +46,7 @@ A line chart over the last 30 days for the currently-selected window.
### Burn-rate alerts section
-A list of every burn-rate alert configured for this SLO with its firing state. Admins see an **Add burn-rate alert** button and a per-alert Edit / Delete menu.
+A list of every burn-rate alert configured for this SLO with its firing state. With the **Developer** role, you also see an **Add burn-rate alert** button and a per-alert Edit / Delete menu.
For the full preset set and form fields, see [Burn-rate alerts](../burn-rate-alerts/).
diff --git a/src/content/docs/docs/reliability/what-is-an-slo.mdx b/src/content/docs/docs/reliability/what-is-an-slo.mdx
index 2d4da1e6..44076177 100644
--- a/src/content/docs/docs/reliability/what-is-an-slo.mdx
+++ b/src/content/docs/docs/reliability/what-is-an-slo.mdx
@@ -28,7 +28,7 @@ An SLO is a target plus a window. Each evaluation produces a compliance % (the s
## Permissions
-Workspace **admin** is required to create, edit, or delete SLOs and burn-rate alerts. Everyone else can view all reliability surfaces; the create, edit, and delete actions are hidden for non-admins.
+You need the **Developer** role to create, edit, or delete SLOs and burn-rate alerts. With the **Viewer** role, you can view every reliability page, but the create, edit, and delete actions are hidden.
## Related
diff --git a/src/content/docs/docs/reports/availability-report.mdx b/src/content/docs/docs/reports/availability-report.mdx
index 7c592270..ce75cb89 100644
--- a/src/content/docs/docs/reports/availability-report.mdx
+++ b/src/content/docs/docs/reports/availability-report.mdx
@@ -1,40 +1,75 @@
---
title: "Availability Report"
-description: "Generate detailed availability statistics for a group of alert rules, including triggers, down duration, and per-alert breakdowns, over a selected time range."
+description: "Track uptime, outages, and downtime for synthetic monitors or alert rules over each report period."
sidebar:
order: 3
---
-An Availability Report provides detailed availability statistics for a group of alert rules, helping you track outages and downtime over a selected time range.
+An Availability report shows how much of the report period your synthetic monitors or alert rules were up, and where the downtime came from. The **Monitor type** you choose when you [create the report](../create-report/#choose-a-source) sets which view you get: [Synthetic monitors](#synthetic-monitors) or [Alerts](#alerts).
-
+## Synthetic monitors
-## Report header
+With **Monitor type** set to **Synthetic monitors**, the report has one row per monitor. A monitor's **Availability** is the share of its checks that passed. Checks that run during a maintenance window don't count toward availability, and that time appears under **Maintenance** instead.
-Each report shows its **Source**, **Schedule**, **Format**, and whether it is **Active**, plus buttons to **Send Report now**, **View Generated Reports**, or **Edit** the report. Use the time range control (for example, **Last 1 Day**) to change the window the statistics below cover.
+
-## Summary
+The summary combines every monitor in the report.
-Above the alert table, the report shows overall figures for the selected time range:
+| Figure | What it measures |
+| --- | --- |
+| **Overall availability** | Up time across all monitors, divided by up time plus down time. It's weighted by time, so it isn't an average of the rows. |
+| **Monitors** | The number of monitors in the report. |
+| **Outages** | The number of times a monitor went from up to down. |
+| **Downtime** | The down time of every monitor, added together. |
+| **MTTR** | The average length of an outage: total down time divided by the number of outages. |
+| **MTBF** | The average up time between outages: total up time divided by the number of outages. |
-- **Overall availability** — the aggregate availability percentage across all included alert rules.
-- **Alert rules** — the number of alert rules included in the report.
-- **Triggers** — the total number of times those alert rules fired.
-- **Down duration** — the cumulative time spent in an alerting state, across all rules.
-- **Instances** — the total number of affected instances.
+Each row shows the monitor's **Availability**, **Outages**, **Down duration**, **Maintenance** time, and **MTTR**.
-## Alert rules and Instances tabs
+## Alerts
-The report has two tabs. **Alert rules** lists one row per alert rule, with:
+With **Monitor type** set to **Alerts**, the report measures availability for alert rules and for the instances they evaluate, such as each host or pod.
-- **Alert** — the name of the alert rule.
-- **Availability** — the percentage of the time range the rule was not in an alerting state.
-- **Triggered** — the number of times the rule fired.
-- **Down duration** — the cumulative time the rule was in an alerting state.
-- **Instances** — the number of instances the rule affected.
-- **Labels** — the labels attached to the rule, if any.
+
-**Instances** shows the same data broken down per instance instead of per alert rule.
+### How the numbers are calculated
+
+- **Availability** is up time divided by up time, down time, and no data time. Time inside a maintenance window is left out entirely.
+- **No data** is time an instance stopped reporting or its evaluation failed. It isn't counted as downtime, but it still lowers availability.
+- Downtime starts when the alert condition began. If an alert waits for its condition to persist before notifying, downtime starts before the notification you received.
+- An alert rule is down whenever any of its instances is down. A rule's **Down duration** is the clock time during which at least one instance was down, so two instances that are down during the same hour add one hour.
+
+The summary shows **Overall availability**, **Alert rules**, **Triggers**, **Cumulative downtime**, and **Instances**. **Cumulative downtime** adds up the down duration of every rule, so if two rules are down during the same hour, that hour counts twice. **Instances** counts every instance the rules evaluated in the period, including healthy ones.
+
+### Alert rules tab
+
+The **Alert rules** tab has one row per rule:
+
+- **Alert:** the name of the alert rule.
+- **Availability:** how much of its monitored time the rule was up.
+- **Triggered:** the number of times the rule went from up to down.
+- **Down duration:** the clock time during which at least one of the rule's instances was down.
+- **No data:** the time none of the rule's instances had data.
+- **Instances:** the number of instances the rule evaluated.
+- **Labels:** the labels on the rule, if any.
+
+### Instances tab
+
+The **Instances** tab has one row per instance, so you can see which host or pod caused a rule's downtime. **Availability**, **Triggered**, **Down duration**, and **No data** are measured for that instance alone.
+
+
+
+- **Instance:** the labels that identify the instance, such as `host=vm1, pod=api`.
+- **Monitored:** how much of the period the instance had data. If an instance was added or removed partway through the period, its availability covers only the time it was monitored. An instance at 100% with a short **Monitored** time was healthy while it ran, not for the whole period.
+- **First seen** and **Last seen:** the first and last time the instance reported during the period.
+
+When a report matches more instances than it can list, it keeps the instances with the most downtime.
+
+### PDF and XLSX files
+
+The PDF lists alert rules only. The XLSX adds an **Alert Instances** sheet with a row for each instance.
:::tip
-Reliability tracking has moved to SLOs. The SLA Target, SLA Achieved, and SLA Status columns are no longer part of the Availability Report — see [Reliability](../../reliability/) to set up service-level objectives, or the [SLO Compliance report](../../reliability/slo-compliance-report/) for a scheduled rollup.
+The Availability report doesn't include SLA Target, SLA Achieved, or SLA Status columns. To track reliability targets, [set up SLOs](../../reliability/), or schedule an [SLO Compliance report](../../reliability/slo-compliance-report/).
:::
+
+To send the report outside its schedule or download earlier files, see [Send a report now](../#send-a-report-now) and [Download generated reports](../#download-generated-reports).
diff --git a/src/content/docs/docs/reports/create-report.mdx b/src/content/docs/docs/reports/create-report.mdx
index 7d01638d..fcd08288 100644
--- a/src/content/docs/docs/reports/create-report.mdx
+++ b/src/content/docs/docs/reports/create-report.mdx
@@ -1,61 +1,92 @@
---
title: "Create Report"
-description: "Learn how to configure report sources, schedules, formats, and recipients to create custom reports."
+description: "Choose a report source and period, schedule delivery, and pick the email channels that receive the report."
sidebar:
order: 2
---
-Reports are created from the Reports Overview page. The Create Report page is divided into four sections.
+Create a report to email your team the same summary every day, week, or month, or to send it once on a date you choose. Open **Reports** and click **Create Report**. You need the **Developer** role in the workspace.
-
+
-### 1. Configure Report Source, Time Range and Format
+## Choose a source
-
+Select the source the report pulls from, and fill in the settings for that source.
-- **Source:** Select what the report pulls data from — [Availability](../availability-report/), [Incident Report](../incident-report/), [Dashboard Report](../dashboard-report/), or [SLO Compliance](../../reliability/slo-compliance-report/). When generating an Availability report, you can also filter alerts by labels or select specific alerts using the dropdown.
-- **Time Range:** Select the duration for which the report should be generated.
-- **Report Format:** Choose the output format — PDF or XLSX.
-- **Report type:** For an Incidents report, choose **Services**, **Responders**, or **Services & Responders** to set what the report covers.
-- **Services (Optional):** When **Report type** includes Services, optionally filter to specific services using the dropdown. Leave it empty to include all services.
+
-### 2. Configure Schedule
+- **Availability:** uptime for synthetic monitors or alert rules. Choose a **Monitor type**:
+ - **Synthetic monitors** is the default. To report on specific monitors, pick them in **Synthetic monitors (Optional)**. Leave it empty to include every synthetic monitor.
+ - **Alerts** reports on alert rules. Use **Match alerts by tags** to match the labels on your alert rules, or pick rules in **Select Alerts**. A rule must carry every tag you list. If you select alerts, the tags are ignored. Leave both empty to include every alert rule.
+- **Incidents:** incident metrics from Incident Management. Choose a **Report type** of **Services**, **Responders**, or **Services & Responders**. **Services (Optional)** limits the report to the services you pick, and it applies to both tabs. **Responders (Optional)** appears when the report type includes responders, and it limits the Responders tab to the people you pick.
+- **Dashboards:** PDF snapshots of dashboards. Pick one or more in **Select dashboards**, and set variable values for each dashboard that has variables. Select **Attach table panels as XLSX** to also get table, logs, and spans panels as a spreadsheet.
+- **SLO Compliance:** every enabled SLO in the workspace. This source has no other settings.
-Scheduling is optional. Disable the **Active** toggle to skip it.
-Users can choose between two scheduling options:
+For what each report contains, see [Availability Report](../availability-report/), [Incident Report](../incident-report/), [Dashboard Report](../dashboard-report/), and [SLO Compliance report](../../reliability/slo-compliance-report/).
-**A. Fixed**
-Send the report once at a specific date and time.
+## Set the period the report covers
-- **Select Date:** Choose the exact date for the report.
-- **Select Time:** Specify the time the report should be sent.
-- **Select Timezone:** Set the timezone for the selected time.
+Choose **Previous period** or **Fixed range**.
-
+**Previous period** covers the most recent complete days, weeks, or months in the **Report timezone**. Set the number of periods and choose **Day**, **Week**, or **Month**. New reports default to 1 **Day**. Weeks run Monday through Sunday.
-**B. Recurring**
-Automatically generate and send reports at a defined interval.
+A scheduled run covers the periods that ended before its scheduled time, and a manual send covers the periods that ended before you send it. For example, a report set to 1 **Week** that runs on a Wednesday covers Monday through Sunday of the previous week. A report set to 1 **Month** that runs on March 1 covers February.
-- **Frequency:** Choose how often the report should be sent — Daily, Weekly, or Monthly.
-- **Daily:** Select the time of day.
-- **Weekly:** Select the day of the week and time.
-- **Monthly:** Select the date of the month and time.
-- **Select Timezone:** Set the timezone for the selected time.
+**Fixed range** covers a specific start and end date and time, and every run covers those same dates. Use it for a one-time report on a known window, such as the week of a migration.
-
+:::tip
+For a recurring schedule, use **Previous period**. A **Fixed range** repeats the same dates on every run.
+:::
+
+The report timezone also affects which period a scheduled run covers. See [Delivery time and report timezone](#delivery-time-and-report-timezone).
+
+## Choose a format
+
+Choose **PDF** or **XLSX**. Dashboard reports are always PDF. To get a dashboard's tables as a spreadsheet, select **Attach table panels as XLSX**.
+
+## Schedule delivery
+
+Turn on **Active** to send the report on a schedule. Leave it off if you only want to send the report manually.
+
+In **Fixed/Recurring**, choose **Fixed** or **Recurring**.
+
+- **Fixed** sends the report one time, on the **Select Date** and **Select Time** you set.
+- **Recurring** sends the report at a regular interval. Set **Frequency** to **Daily**, **Weekly**, or **Monthly**, and set **Select Time**. A weekly report also needs a **Day of week**, and a monthly report needs a **Date of Month**.
+
+
+
+
-:::info
-Check **Send report immediately** after saving to dispatch the report instantly upon saving.
+:::caution
+If you set **Date of Month** to 29, 30, or 31, the report isn't sent in months that don't have that day. For a month-end report, choose **Last day**.
:::
-### 3. Add Recipients
+Select **Send report immediately after saving** to also send the report as soon as you save it.
+
+### Delivery time and report timezone
+
+- **Select Timezone** sets when the report is delivered. Delivery stays at the same local time when daylight saving time starts or ends.
+- **Report timezone** sets where each day, week, or month starts and ends, and the timezone of the timestamps inside the report. With **Fixed range**, set the timezone in the date range picker instead.
+
+Both start out as your browser's timezone.
+
+Each run covers the most recent periods that have fully ended in the report timezone at the time of delivery. When the two timezones match, a daily report covers the previous day, whatever time it's delivered. When they differ, a run can cover an earlier period than you expect.
+
+For example, suppose you deliver a daily report at 8:00 AM Asia/Tokyo with **Report timezone** set to UTC. At 8:00 AM in Tokyo, it's still 11:00 PM the previous day in UTC. That UTC day hasn't ended yet, so the run covers the day before it. By 9:00 AM in Tokyo, the UTC day has ended, so a delivery at 9:00 AM or later covers it.
+
+With **Previous period** and an active schedule, the schedule step shows which dates the next run will cover. It also suggests a later delivery time when delivering later the same day would cover a more recent period.
+
+## Add recipients
+
+Select at least one email channel to receive the report. The list includes the email channels set up in [Notification Channels](../../platform/settings/notification-channels/).
+
+
-Select one or more recipients from the dropdown. The recipient list includes all email channels configured in KloudMate’s notification channels.
+## Name and save the report
-### 4. Add Report Details
+Enter a **Name** and, optionally, a **Description**. Both appear in the Reports list.
-- **Name:** The title of the report, displayed on the Reports Overview page.
-- **Description:** A brief summary describing the report’s purpose.
+
-
+Click **Save** to save and keep editing, or **Save & Close** to save and return to the Reports list.
-Once all sections are filled in, click **Save** to schedule the report or **Save & Close** to save and exit.
+After you save, you can [send the report now](../#send-a-report-now) with a different period, format, or recipients, without changing the saved report.
diff --git a/src/content/docs/docs/reports/dashboard-report.mdx b/src/content/docs/docs/reports/dashboard-report.mdx
index b161600d..ce1313f2 100644
--- a/src/content/docs/docs/reports/dashboard-report.mdx
+++ b/src/content/docs/docs/reports/dashboard-report.mdx
@@ -1,18 +1,27 @@
---
title: "Dashboard Report"
-description: "Generate scheduled or ad-hoc PDF reports of your KloudMate dashboards."
+description: "Email PDF snapshots of one or more dashboards on a schedule, with optional XLSX files for table panels."
sidebar:
order: 4
---
-The **Dashboard Report** generates a PDF version of a selected dashboard.
+A Dashboards report emails PDF snapshots of one or more dashboards on a schedule. Each PDF shows the dashboard over the report's period, with the variable values you set when you [created the report](../create-report/#choose-a-source).
-
+
-The report details page displays the **source**, **schedule**, **format**, and **active** status of the report, plus the specific dashboard (or dashboards) the report includes.
+## What each run produces
-
+Each run produces one PDF per dashboard. If you selected **Attach table panels as XLSX**, each dashboard also gets an XLSX file with a sheet for each table, logs, or spans panel.
-You can click **Send Report now** to trigger the report immediately, or click **View Generated Reports** to access and download previously generated report files.
+## The report page
-
+The report page lists the dashboards in the report. Select a dashboard's name to open it with the report's variable values applied.
+
+
+Click **Send Report now** to send the report right away, with a different period or different recipients if you need them. See [Send a report now](../#send-a-report-now).
+
+## Download past files
+
+Click **View Generated Reports** to download files from earlier runs. Each PDF and XLSX file from a run has its own row, and **Period Covered** shows the dates the file covers. See [Download generated reports](../#download-generated-reports).
+
+
diff --git a/src/content/docs/docs/reports/images/alarm-report-1.png b/src/content/docs/docs/reports/images/alarm-report-1.png
index 52cfcc27..88fb0d7e 100644
Binary files a/src/content/docs/docs/reports/images/alarm-report-1.png and b/src/content/docs/docs/reports/images/alarm-report-1.png differ
diff --git a/src/content/docs/docs/reports/images/alarm-report-instances.png b/src/content/docs/docs/reports/images/alarm-report-instances.png
new file mode 100644
index 00000000..d33b681b
Binary files /dev/null and b/src/content/docs/docs/reports/images/alarm-report-instances.png differ
diff --git a/src/content/docs/docs/reports/images/availability-report-synthetic.png b/src/content/docs/docs/reports/images/availability-report-synthetic.png
new file mode 100644
index 00000000..41e5b1ee
Binary files /dev/null and b/src/content/docs/docs/reports/images/availability-report-synthetic.png differ
diff --git a/src/content/docs/docs/reports/images/create-report-1.png b/src/content/docs/docs/reports/images/create-report-1.png
index 5f276d27..3f682ea9 100644
Binary files a/src/content/docs/docs/reports/images/create-report-1.png and b/src/content/docs/docs/reports/images/create-report-1.png differ
diff --git a/src/content/docs/docs/reports/images/create-report-2.png b/src/content/docs/docs/reports/images/create-report-2.png
index 03669783..75f49f14 100644
Binary files a/src/content/docs/docs/reports/images/create-report-2.png and b/src/content/docs/docs/reports/images/create-report-2.png differ
diff --git a/src/content/docs/docs/reports/images/create-report-3.png b/src/content/docs/docs/reports/images/create-report-3.png
index fe8dc2f0..b67ced1c 100644
Binary files a/src/content/docs/docs/reports/images/create-report-3.png and b/src/content/docs/docs/reports/images/create-report-3.png differ
diff --git a/src/content/docs/docs/reports/images/create-report-4.png b/src/content/docs/docs/reports/images/create-report-4.png
index 3391223e..c28106fc 100644
Binary files a/src/content/docs/docs/reports/images/create-report-4.png and b/src/content/docs/docs/reports/images/create-report-4.png differ
diff --git a/src/content/docs/docs/reports/images/create-report-5.png b/src/content/docs/docs/reports/images/create-report-5.png
index c5e2e885..606d988b 100644
Binary files a/src/content/docs/docs/reports/images/create-report-5.png and b/src/content/docs/docs/reports/images/create-report-5.png differ
diff --git a/src/content/docs/docs/reports/images/create-report-6.png b/src/content/docs/docs/reports/images/create-report-6.png
new file mode 100644
index 00000000..198df1b6
Binary files /dev/null and b/src/content/docs/docs/reports/images/create-report-6.png differ
diff --git a/src/content/docs/docs/reports/images/dashboard-report-1.png b/src/content/docs/docs/reports/images/dashboard-report-1.png
index eac93c63..d1d56e5c 100644
Binary files a/src/content/docs/docs/reports/images/dashboard-report-1.png and b/src/content/docs/docs/reports/images/dashboard-report-1.png differ
diff --git a/src/content/docs/docs/reports/images/dashboard-report-2.png b/src/content/docs/docs/reports/images/dashboard-report-2.png
index 84b2fd96..407698a2 100644
Binary files a/src/content/docs/docs/reports/images/dashboard-report-2.png and b/src/content/docs/docs/reports/images/dashboard-report-2.png differ
diff --git a/src/content/docs/docs/reports/images/dashboard-report-3.png b/src/content/docs/docs/reports/images/dashboard-report-3.png
index 1aca54e3..704e432a 100644
Binary files a/src/content/docs/docs/reports/images/dashboard-report-3.png and b/src/content/docs/docs/reports/images/dashboard-report-3.png differ
diff --git a/src/content/docs/docs/reports/images/incident-report-1.png b/src/content/docs/docs/reports/images/incident-report-1.png
index 4bb42f27..196df58f 100644
Binary files a/src/content/docs/docs/reports/images/incident-report-1.png and b/src/content/docs/docs/reports/images/incident-report-1.png differ
diff --git a/src/content/docs/docs/reports/images/incident-report-responders.png b/src/content/docs/docs/reports/images/incident-report-responders.png
new file mode 100644
index 00000000..b4ba5846
Binary files /dev/null and b/src/content/docs/docs/reports/images/incident-report-responders.png differ
diff --git a/src/content/docs/docs/reports/images/reports-overview-1.png b/src/content/docs/docs/reports/images/reports-overview-1.png
index d60ec1b9..8c63d81c 100644
Binary files a/src/content/docs/docs/reports/images/reports-overview-1.png and b/src/content/docs/docs/reports/images/reports-overview-1.png differ
diff --git a/src/content/docs/docs/reports/images/send-report-now-dialog.png b/src/content/docs/docs/reports/images/send-report-now-dialog.png
new file mode 100644
index 00000000..6ee0d536
Binary files /dev/null and b/src/content/docs/docs/reports/images/send-report-now-dialog.png differ
diff --git a/src/content/docs/docs/reports/incident-report.mdx b/src/content/docs/docs/reports/incident-report.mdx
index d95beafb..6f04c629 100644
--- a/src/content/docs/docs/reports/incident-report.mdx
+++ b/src/content/docs/docs/reports/incident-report.mdx
@@ -1,44 +1,43 @@
---
title: "Incident Report"
-description: "Analyze incident statistics for services and responders to track resolution efficiency and performance."
+description: "Measure how quickly incidents are acknowledged and resolved, by service and by responder."
sidebar:
order: 5
---
-An Incident Report provides detailed statistics about incidents associated with services and responders in the Incident Management module, offering insights into performance and resolution efficiency over a selected time range.
+An Incident report shows how many incidents were triggered in the report period and how quickly your team acknowledged and resolved them. The tabs follow the **Report type** you chose when you [created the report](../create-report/#choose-a-source): **Services**, **Responders**, or both for **Services & Responders**.
-Each report shows its **Source**, **Schedule**, **Format**, and whether it is **Active**, plus buttons to **Send Report now**, **View Generated Reports**, or **Edit** the report. Use the time range control (for example, **Last 1 Day**) to change the window the statistics below cover.
+Every count covers incidents **triggered** in the report period, whenever they were acknowledged or resolved. For example, an incident triggered at 11:00 PM on Monday and resolved at 1:00 AM on Tuesday counts in a report for Monday. If that report runs before the incident is resolved, the incident counts toward **Total Incidents** but not **Resolved**.
-The report is split into two tabs; Services and Responders.
+## Services
-### Services
+The **Services** tab has one row per service. If you picked services in **Services (Optional)**, the report includes only those services, and the Responders tab counts only their incidents.
-The **Services** tab focuses on incidents related to specific services.
+
-
-
-It displays the following details:
-
-- **Service:** The name of the service.
-- **Owner:** The owner responsible for the service.
-- **Total Incidents:** The total number of incidents triggered during the selected time range.
-- **Acknowledged:** The number of incidents acknowledged during the selected time range.
-- **Resolved:** The number of incidents resolved during the selected time range.
-- **Repeated Incidents:** The number of recurring incidents for the service within the time range.
-- **MTTA (Mean Time to Acknowledge):** The average time taken to acknowledge incidents after they are triggered.
-- **MTTR (Mean Time to Resolve):** The average time taken to resolve incidents after they are triggered.
+- **Service:** the name of the service.
+- **Owner:** the owner responsible for the service.
+- **Total Incidents:** the number of incidents triggered in the period.
+- **Acknowledged:** how many of those incidents have been acknowledged.
+- **Resolved:** how many of those incidents have been resolved.
+- **Repeated Incidents:** how many of those incidents recurred.
+- **MTTA (Mean Time to Acknowledge):** the average time from trigger to acknowledgment.
+- **MTTR (Mean Time to Resolve):** the average time from trigger to resolution.
:::tip
-Reliability tracking has moved to SLOs. The SLA Target, SLA Achieved, and SLA Status columns are no longer part of the Incident Report — see [Reliability](../../reliability/) to set up service-level objectives, or the [SLO Compliance report](../../reliability/slo-compliance-report/) for a scheduled rollup.
+The Incident report doesn't include SLA Target, SLA Achieved, or SLA Status columns. To track reliability targets, [set up SLOs](../../reliability/), or schedule an [SLO Compliance report](../../reliability/slo-compliance-report/).
:::
-### Responders
+## Responders
+
+The **Responders** tab lists the people on the escalation policies of the services in the report. Someone who isn't on one of those escalation policies doesn't appear, even if they acknowledged an incident. If you picked people in **Responders (Optional)**, only those people appear.
-The **Responders** tab evaluates the performance of responders assigned to incidents. It displays the following details:
+
-- **Name:** The name of the responder.
-- **Assigned Incidents:** The number of incidents assigned to the responder during the selected time range.
-- **Acknowledged:** The number of incidents acknowledged by the responder.
-- **Resolved:** The number of incidents resolved by the responder.
-- **MTTA (Mean Time to Acknowledge):** The average time taken by the responder to acknowledge assigned incidents.
-- **MTTR (Mean Time to Resolve):** The average time taken by the responder to resolve assigned incidents.
+- **Name:** the name of the responder.
+- **Assigned Incidents:** the number of incidents the responder was assigned.
+- **Acknowledged:** the number of incidents the responder acknowledged.
+- **Resolved:** the number of incidents the responder resolved.
+- **MTTA (Mean Time to Acknowledge):** the average time from trigger to this responder's acknowledgment.
+- **MTTR (Mean Time to Resolve):** the average time from trigger to this responder's resolution.
+To send the report outside its schedule or download earlier files, see [Send a report now](../#send-a-report-now) and [Download generated reports](../#download-generated-reports).
diff --git a/src/content/docs/docs/reports/index.mdx b/src/content/docs/docs/reports/index.mdx
index be155967..1625b91c 100644
--- a/src/content/docs/docs/reports/index.mdx
+++ b/src/content/docs/docs/reports/index.mdx
@@ -1,26 +1,59 @@
---
title: "Reports"
-description: "Centralised view of all generated and scheduled reports in KloudMate."
+description: "Email your team scheduled PDF or XLSX reports on availability, incidents, dashboards, and SLO compliance."
sidebar:
label: "Introduction"
order: 1
---
-The **Overview** page provides a centralised view of all your generated and scheduled reports. From here, you can view report details, edit or delete existing reports, and create new ones.
+Use reports to email your team a regular summary as a PDF or XLSX file. Each report pulls from one source:
-Each report displays the following details:
+- [Availability](./availability-report/): uptime, outages, and downtime for synthetic monitors or alert rules.
+- [Incidents](./incident-report/): incident counts, MTTA, and MTTR by service and by responder.
+- [Dashboards](./dashboard-report/): PDF snapshots of one or more dashboards.
+- [SLO Compliance](../reliability/slo-compliance-report/): the latest compliance and error budget for every enabled SLO.
-- **Name:** The name assigned to the report.
-- **Description:** A brief summary of the report’s purpose or contents.
-- **Source:** The data source used to generate the report — Availability, Incidents, Dashboards, or SLO Compliance.
-- **Schedule:** The configured schedule for sending the report. This field will be empty if no schedule has been set.
-- **Recipients:** The email address(es) the report is sent to when scheduled.
-- **Active:** Indicates whether the report is currently active (green) or inactive (red).
+## The Reports list
-
+Open **Reports** to see the reports saved in the workspace. To find the files a report has already generated, open the report and click **View Generated Reports**.
-***
+- **Name:** the report's name. Select it to open the report.
+- **Description:** a short summary of the report's purpose.
+- **Source:** the data the report pulls from.
+- **Schedule:** when the report is sent, if its schedule is active.
+- **Recipients:** the email channels the report is sent to.
+- **Active:** whether the report's schedule is turned on.
+
+Use the row menu to **Edit** or **Delete** a report.
+
+
+
+## Who can manage reports
+
+You need the **Developer** role in the workspace to create, edit, or delete reports. With the **Viewer** role, you can still open any report, send it now, and download its generated files.
+
+## Open a report
+
+On the report page, **Covers** shows the period each run reports on, such as the previous day in the report timezone.
+
+For every source except Dashboards, the page also previews the report's data, starting at the report's own period. Changing the time range updates only the preview. The saved report and the files it sends keep their own period.
-## Learn how to: [Create a Report](./create-report/)
+## Send a report now
+
+Click **Send Report now** to generate the report and email it right away, without waiting for the schedule. Before it's sent, you can change the period, format, and recipients for this one send. The saved report doesn't change.
+
+
+
+A report set to **Previous period** covers the periods that ended before you send it. For example, a report set to 1 **Day** that you send at 3:00 PM covers yesterday in the report timezone. The file is also added to the report's generated reports.
+
+## Download generated reports
+
+Click **View Generated Reports** on a report to see every file it has produced, from both scheduled runs and manual sends.
+
+**Period Covered** is shown in the report timezone. A **Previous period** report shows dates only, and a **Fixed range** report shows dates and times. Select several files to download them together as a ZIP file.
+
+***
-
+## Related Resources
+- [Create a report](./create-report/)
+- [Notification Channels](../platform/settings/notification-channels/)
diff --git a/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-1.png b/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-1.png
index 229fa5cb..837aaecd 100644
Binary files a/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-1.png and b/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-1.png differ
diff --git a/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-2.png b/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-2.png
index c6ae8dce..3460f74d 100644
Binary files a/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-2.png and b/src/content/docs/docs/rum/images/correlate-rum-traces-with-opentelemetry-backends-2.png differ
diff --git a/src/content/docs/docs/rum/instrumentation-guide/index.mdx b/src/content/docs/docs/rum/instrumentation-guide/index.mdx
index 795ba101..97ed5579 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/index.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/index.mdx
@@ -21,7 +21,7 @@ The browser SDK works on current versions of Chrome, Edge, Firefox, Safari, and
## Add an application
-Open **RUM** and click **Add application**.
+Open **Real User Monitoring** and click **Add application**.

diff --git a/src/content/docs/docs/rum/rum-interface.mdx b/src/content/docs/docs/rum/rum-interface.mdx
index be5b38ea..1120dc19 100644
--- a/src/content/docs/docs/rum/rum-interface.mdx
+++ b/src/content/docs/docs/rum/rum-interface.mdx
@@ -12,7 +12,7 @@ To send data in the first place, see the [Instrumentation Guide](../instrumentat
## Pick an application first
-Open **RUM** to see every web, Android, and iOS application reporting to your workspace, then select one to open its tabs. An application is identified by its `serviceName`, which you set as `applicationName` when the SDK initializes.
+Open **Real User Monitoring** to see every web, Android, and iOS application reporting to your workspace, then select one to open its tabs. An application is identified by its `serviceName`, which you set as `applicationName` when the SDK initializes.

diff --git a/src/content/docs/docs/synthetic/images/maintenance-windows-2.png b/src/content/docs/docs/synthetic/images/maintenance-windows-2.png
index db58f262..4d70bd7f 100644
Binary files a/src/content/docs/docs/synthetic/images/maintenance-windows-2.png and b/src/content/docs/docs/synthetic/images/maintenance-windows-2.png differ
diff --git a/src/content/docs/docs/synthetic/images/maintenance-windows-3.png b/src/content/docs/docs/synthetic/images/maintenance-windows-3.png
index 10811102..9583564b 100644
Binary files a/src/content/docs/docs/synthetic/images/maintenance-windows-3.png and b/src/content/docs/docs/synthetic/images/maintenance-windows-3.png differ
diff --git a/src/content/docs/docs/synthetic/images/synthetic-incidents-1.png b/src/content/docs/docs/synthetic/images/synthetic-incidents-1.png
index 9b298436..9846ca81 100644
Binary files a/src/content/docs/docs/synthetic/images/synthetic-incidents-1.png and b/src/content/docs/docs/synthetic/images/synthetic-incidents-1.png differ
diff --git a/src/content/docs/docs/synthetic/maintenance-windows.mdx b/src/content/docs/docs/synthetic/maintenance-windows.mdx
index efa76a62..758fa8f4 100644
--- a/src/content/docs/docs/synthetic/maintenance-windows.mdx
+++ b/src/content/docs/docs/synthetic/maintenance-windows.mdx
@@ -12,8 +12,9 @@ During a maintenance window, the monitor:

-**Maintenance Windows Overview List**
-The overview page displays all created maintenance windows with key details for quick management:
+## The Maintenance tab
+
+Open **Synthetic Monitoring → Maintenance** to see all of your maintenance windows. Each row shows:
- **Name**
The user-defined name of the maintenance window.
@@ -32,9 +33,9 @@ The overview page displays all created maintenance windows with key details for
## Creating a Maintenance Window
-1\. Navigate to the **Maintenance Window Overview** page in the Synthetic monitoring module.
+1\. Open **Synthetic Monitoring → Maintenance**.
-2\. Click on **Create Maintenance Window**.
+2\. Click **Create window**.
3\. Configure the following settings:
@@ -44,7 +45,7 @@ The overview page displays all created maintenance windows with key details for
Provide a short description of the maintenance purpose.
- **Timezone**
Select the timezone for the scheduled maintenance period.
-- **One-time**
Schedule a single maintenance period for a specific date and time.
+- **One Time**
Schedule a single maintenance period for a specific date and time.
- **Recurring**
Set up automatically repeating maintenance on a regular cycle.
- **Start Time**
Define the date and time when the maintenance window begins.
@@ -53,7 +54,7 @@ The overview page displays all created maintenance windows with key details for
- **Frequency (Recurring only)**
Choose daily, weekly, or monthly recurrence schedule.
- **Select Monitors**
Assign one or more monitors to apply the maintenance window to.
-4\. Click Save to create the new maintenance window.
+4\. Click **Save** to create the new maintenance window.

diff --git a/src/content/docs/docs/synthetic/monitors/dns-monitor.mdx b/src/content/docs/docs/synthetic/monitors/dns-monitor.mdx
index a024ceca..5f66293b 100644
--- a/src/content/docs/docs/synthetic/monitors/dns-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/dns-monitor.mdx
@@ -9,8 +9,8 @@ The DNS Monitor helps ensure your domain’s DNS resolution is functioning corre
## Creating a New DNS Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click **Add new**.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
3. From the monitor type dropdown, select **DNS - Verify DNS resolution and records**.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/monitors/grpc-monitor.mdx b/src/content/docs/docs/synthetic/monitors/grpc-monitor.mdx
index f57cf15b..07fc0b4b 100644
--- a/src/content/docs/docs/synthetic/monitors/grpc-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/grpc-monitor.mdx
@@ -9,8 +9,8 @@ The gRPC Monitor enables continuous health checks for gRPC services by invoking
## Creating a New gRPC Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click **Add new**.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
3. Select **gRPC - Check gRPC service health** from the monitor type dropdown.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/monitors/http-monitor.mdx b/src/content/docs/docs/synthetic/monitors/http-monitor.mdx
index 35a861b6..3ed2637c 100644
--- a/src/content/docs/docs/synthetic/monitors/http-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/http-monitor.mdx
@@ -9,8 +9,8 @@ The HTTP Monitor allows you to keep track of the availability and performance of
## Creating a New HTTP Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click on **Add new**.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
3. From the monitor type dropdown, select **HTTP - Monitor web endpoints and APIs**.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/monitors/icmp-monitor.mdx b/src/content/docs/docs/synthetic/monitors/icmp-monitor.mdx
index 1f968be9..05cffe6b 100644
--- a/src/content/docs/docs/synthetic/monitors/icmp-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/icmp-monitor.mdx
@@ -9,8 +9,8 @@ The ICMP Monitor performs ping tests to verify basic network connectivity to hos
## Creating a New ICMP Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click **Add new**.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
3. Select **ICMP - Ping test for basic connectivity** from the monitor type dropdown.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/monitors/images/monitors-1.png b/src/content/docs/docs/synthetic/monitors/images/monitors-1.png
index 30644692..ce65236a 100644
Binary files a/src/content/docs/docs/synthetic/monitors/images/monitors-1.png and b/src/content/docs/docs/synthetic/monitors/images/monitors-1.png differ
diff --git a/src/content/docs/docs/synthetic/monitors/images/monitors-2.png b/src/content/docs/docs/synthetic/monitors/images/monitors-2.png
index 4440d571..8a9a2add 100644
Binary files a/src/content/docs/docs/synthetic/monitors/images/monitors-2.png and b/src/content/docs/docs/synthetic/monitors/images/monitors-2.png differ
diff --git a/src/content/docs/docs/synthetic/monitors/index.mdx b/src/content/docs/docs/synthetic/monitors/index.mdx
index c8b43f77..e87ebf29 100644
--- a/src/content/docs/docs/synthetic/monitors/index.mdx
+++ b/src/content/docs/docs/synthetic/monitors/index.mdx
@@ -4,7 +4,7 @@ description: "The synthetic monitor types KloudMate supports and how to create t
sidebar:
label: "Introduction"
---
-The Monitors page displays all monitors created within the current workspace, providing key status information at a glance for quick assessment of monitoring health.
+The **Monitors** tab lists all monitors in the current workspace, with key status information for each one.
When creating or editing any monitor (except Push), click **Test** next to **Save Monitor** to run the check once from your selected locations and preview the result before you save.
@@ -14,7 +14,7 @@ When creating or editing any monitor (except Push), click **Test** next to **Sav
- **Type**: Indicates the monitor type (e.g., HTTP, SSL, DNS, TCP, UDP, gRPC, WebSocket, ICMP, Push).
- **Target**: Shows the endpoint or host the monitor is targeting (e.g., URL, host\:port, domain).
- **Frequency**: Displays the check interval and number of monitoring locations used for each check.
-- **Success (7D)**: Shows the success percentage of the monitor over the past 7 days.
+- **Success (7d)**: Shows the success percentage of the monitor over the past 7 days.
- **Status**: Indicates whether the monitor is **Enabled** or **Paused**. The colored dot next to each row shows its current health (up or down), separate from this enabled/paused state.

diff --git a/src/content/docs/docs/synthetic/monitors/ssl-monitor.mdx b/src/content/docs/docs/synthetic/monitors/ssl-monitor.mdx
index 0ec336dd..67fc05ed 100644
--- a/src/content/docs/docs/synthetic/monitors/ssl-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/ssl-monitor.mdx
@@ -9,8 +9,8 @@ The SSL Certificate Validity Monitor enables you to track the status of SSL/TLS
## Creating a New SSL Certificate Validity Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click **Add new**.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
3. Select **SSL - Check SSL certificate validity and expiration** from the monitor type dropdown.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/monitors/tcp-monitor.mdx b/src/content/docs/docs/synthetic/monitors/tcp-monitor.mdx
index 16e80d64..4d6852f6 100644
--- a/src/content/docs/docs/synthetic/monitors/tcp-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/tcp-monitor.mdx
@@ -9,8 +9,8 @@ The TCP Monitor tests the availability and responsiveness of a specific TCP host
## Creating a New TCP Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click on **Add new**.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
3. From the monitor type dropdown, select **TCP - Test TCP port connectivity**.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/monitors/udp-monitor.mdx b/src/content/docs/docs/synthetic/monitors/udp-monitor.mdx
index 84e59b58..4d2d4736 100644
--- a/src/content/docs/docs/synthetic/monitors/udp-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/udp-monitor.mdx
@@ -9,9 +9,9 @@ The UDP Monitor checks the availability of a specific UDP host and port by sendi
## Creating a New UDP Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click **Add new**.
-3. Select **UDP port availability** from the monitor type dropdown.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
+3. Select **UDP - Monitor UDP port availability** from the monitor type dropdown.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/monitors/websocket-monitor.mdx b/src/content/docs/docs/synthetic/monitors/websocket-monitor.mdx
index 9fcb4324..b9b47db5 100644
--- a/src/content/docs/docs/synthetic/monitors/websocket-monitor.mdx
+++ b/src/content/docs/docs/synthetic/monitors/websocket-monitor.mdx
@@ -9,8 +9,8 @@ The WebSocket Monitor verifies the availability and performance of WebSocket end
## Creating a New WebSocket Monitor
-1. Navigate to **Synthetic monitoring > Monitors**.
-2. Click **Add new**.
+1. Open **Synthetic Monitoring → Monitors**.
+2. Click **Add New**.
3. Select **WebSocket - Monitor WebSocket connections** from the monitor type dropdown.
## Basic Settings
diff --git a/src/content/docs/docs/synthetic/synthetic-incidents.mdx b/src/content/docs/docs/synthetic/synthetic-incidents.mdx
index c290304c..dececf9b 100644
--- a/src/content/docs/docs/synthetic/synthetic-incidents.mdx
+++ b/src/content/docs/docs/synthetic/synthetic-incidents.mdx
@@ -2,19 +2,19 @@
title: "Synthetic Incidents"
description: "Review incidents opened when synthetic monitors fail."
---
-The 'Incidents' page consolidates all incidents from monitors created within the Synthetic monitoring module, providing centralized visibility into downtime events, across all monitors.
+Open **Synthetic Monitoring → Incidents** to review downtime incidents from all of your synthetic monitors in one place.
**The Incidents Overview Table**
-The page displays a table summarizing all incidents for quick analysis:
+The tab displays a table summarizing all incidents:
- **Monitor**
Identifies the specific monitor where the incident originated.
- **Cause**
Details the root cause of the incident detected in that monitor.
-- **Started At**
+- **Started at**
Timestamp when the incident was first detected.
-- **Resolved At**
+- **Resolved at**
Timestamp when the incident was resolved, or indicates if still ongoing.
- **Duration** Total time period the incident remained active.
diff --git a/src/content/docs/docs/troubleshooting.mdx b/src/content/docs/docs/troubleshooting.mdx
index b31399b9..efbbb2e4 100644
--- a/src/content/docs/docs/troubleshooting.mdx
+++ b/src/content/docs/docs/troubleshooting.mdx
@@ -101,7 +101,7 @@ No. Every plan includes unlimited users, with no per-host or per-agent fees. You
Each plan includes a default retention window, and you can extend it. Longer retention for logs and traces raises their storage cost by the multiplier shown in the calculator, so you can trade off how long you keep data against cost. Metrics are billed by sample count, so their retention does not change the price.
## How do AI credits work?
-KloudMate Assistant runs on credits. A chat message costs 1 credit, and a full investigation costs 2. Every plan includes a monthly allowance — 5 credits on Free and 20 on Pro — and you can buy prepaid credits any time you need more. Enterprise teams can bring their own AI provider key instead of using credits.
+KloudMate Assistant runs on credits. A chat message costs 1 credit, and a full investigation costs 5. Every plan includes a monthly allowance — 5 credits on Free and 20 on Pro — and you can buy prepaid credits any time you need more. Enterprise teams can bring their own AI provider key instead of using credits.
## Do you offer annual billing?
Yes. Annual billing is available at a discount compared with paying monthly. Get in touch and we will set it up for you.
diff --git a/src/content/docs/docs/workflows/_meta.json b/src/content/docs/docs/workflows/_meta.json
new file mode 100644
index 00000000..0c89f8db
--- /dev/null
+++ b/src/content/docs/docs/workflows/_meta.json
@@ -0,0 +1,5 @@
+{
+ "label": "Workflows",
+ "order": 19.5,
+ "collapsed": true
+}
diff --git a/src/content/docs/docs/workflows/actions.mdx b/src/content/docs/docs/workflows/actions.mdx
new file mode 100644
index 00000000..1d1bac7b
--- /dev/null
+++ b/src/content/docs/docs/workflows/actions.mdx
@@ -0,0 +1,320 @@
+---
+title: "The action catalog"
+description: "Every step a workflow can run: HTTP requests, sandboxed code, AI prompts, AWS and Azure calls, Slack and Jira, MCP tools, host commands, and lookups of hosts, resources, and alert groups."
+sidebar:
+ label: "Action catalog"
+ order: 6
+---
+
+An action is one unit of work: post a message, call an endpoint, run an API operation, transform a payload. The palette groups them under **Apps**, **AI**, and **Utilities**, with [control-flow steps](../control-flow/) on their own tab. An app with several actions, such as Slack or KloudMate, is a tile you click into.
+
+Most actions return the provider's own response, unchanged. A Slack post returns Slack's response; a Jira create returns the issue Jira created. Later steps read fields by the provider's own names. To learn the shape, test the step once and read the output. See [Test a workflow](../test-a-workflow/).
+
+## Apps
+
+The **Apps** tab lists the products a step can work with. Pick the app, then its action.
+
+### Notifications
+
+These send through a [notification channel](../../platform/settings/notification-channels/) you've already set up, so you configure the destination once and reuse it.
+
+| Action | What it does |
+|---|---|
+| **Send Slack Message** | Post text or Block Kit blocks to a configured Slack channel. |
+| **Send Teams Message** | Post an adaptive card to a configured Microsoft Teams channel. |
+| **Send to Webhook** | POST a JSON body to a configured webhook channel. |
+| **Publish to SNS** | Publish a message to a configured Amazon SNS channel. |
+| **Send Email** | Email an address directly. |
+
+**Send Email** takes a typed address instead of a channel. Because it can email any address, KloudMate caps it at 20 recipients per step and 200 sends per workspace per hour.
+
+Its **Message** accepts Markdown and HTML, so [escape the values you didn't write](../templating/#escape-values-in-an-email). The step returns `delivered`, which is true when at least one address was accepted, along with `recipients`, `rejected` for the addresses the mail server refused, and `message_id`.
+
+### Slack
+
+Slack actions use a [Slack connection](/platform/settings/connections/providers/) rather than a channel, so the workflow chooses where the message goes.
+
+| Action | What it does |
+|---|---|
+| **Post Message** | Post to a channel or a person, optionally as a thread reply. |
+| **Update Message** | Edit a message the workflow posted. |
+| **Send Direct Message** | Message a person directly, as the bot. |
+| **Add Reaction** | React to a message. |
+| **Get Thread Replies** | Read a thread's replies. |
+| **Create Channel** | Create a channel, public or private. |
+| **Invite to Channel** | Invite people to a channel. |
+| **Set Channel Topic** | Set a channel's topic. |
+| **Look Up User** | Turn a Slack user id into a person. |
+| **Find User by Email** | Turn an email address into a Slack user. |
+
+When the destination is fixed, use **Send Slack Message**. When the workflow decides the channel, replies in a thread, or needs the posted message back so a later step can edit it, use **Post Message**.
+
+:::caution
+Some of these need permissions the first Slack install didn't ask for. Looking up users needs profile access, and the channel actions need channel management. If a step fails with `missing_scope`, reconnect the Slack connection. Everything else on that connection keeps working in the meantime.
+:::
+
+### Jira Cloud
+
+| Action | What it does |
+|---|---|
+| **Create Issue** | Create an issue in a project. |
+| **Update Issue** | Change the summary, description, or any other field. |
+| **Get Issue** | Read an issue with all its fields. |
+| **Search Issues** | Find issues with JQL, up to 100 at a time. |
+| **Add Comment** | Comment on an issue. |
+| **List Comments** | Read an issue's comments, newest first. |
+| **Transition Issue** | Move an issue to another status. |
+| **Assign Issue** | Set who an issue is assigned to. |
+| **Link Issues** | Link an issue to another with one of the site's link types. |
+| **Find User** | Find users by email or name, to assign or mention them. |
+
+Every Jira action opens with a site picker, because one credential can access several Atlassian sites. **Transition Issue** checks the target status against the transitions the issue offers now, so a move its Jira workflow doesn't allow fails instead of being forced.
+
+### AWS API Call
+
+Calls an AWS API action through a connected AWS account. Pick the service, then the action. **Service** offers EC2, Systems Manager (SSM), Auto Scaling, Lambda, Elastic Container Service (ECS), Relational Database Service (RDS), Elastic Load Balancing (ALB/NLB), and Step Functions. The parameter form comes from the real API model of the action you pick. **Region** overrides the connection's default region. To use the default, leave it blank.
+
+The step returns AWS's response under `result`, so a later step reads a field as `{{ steps..output.result. }}`.
+
+Changing the connection clears the region, changing the service clears the action and the parameters, and changing the action clears the parameters.
+
+The IAM policy on the credential decides what the step is allowed to do. There's no second permission list in KloudMate, so narrow the role itself. Put an [approval](../approvals-and-input/) in front of anything that changes a resource.
+
+### Azure
+
+**Azure Resource Manager** acts on an Azure resource through a connected Azure account: pick the operation, fill in the details, and optionally override the subscription. The output has `completed`, `status`, `result` when there's a resource body, and `operation_url` for a long-running operation.
+
+**Run Script on Azure VM** runs a Bash or PowerShell script on a VM through the Azure VM extension. Azure reports whether the script ran, not whether it worked, so a script that exits with an error still counts as a successful step.
+
+Both need an RBAC role assigned to the service principal.
+
+### MCP Tool Call
+
+Calls a tool on a connected MCP server. The tool list comes from the connection, and the argument form comes from that tool's own input schema. If the server publishes no schema, the form falls back to a raw JSON editor, so you can still call the tool.
+
+**The output is a single `result` field**, so a template reads it like this:
+
+```text
+{{ steps..output.result. }}
+```
+
+`result` holds the tool's structured content if it returns any. Otherwise it holds the JSON from the tool's text response, or the text itself. If the tool reports an error, the step fails with the tool's own message.
+
+A step calls the one tool you pick, with the arguments you set. The model doesn't choose tools during a run.
+
+Connect a server as a **Custom MCP Server** under [Connections](/platform/settings/connections/providers/#tools-and-custom-endpoints). That includes the [KloudMate MCP server](/kloudmate-assistant/mcp-server/), which lets a workflow query your own workspace's logs, traces, metrics, incidents, and SLOs. For a complete example, see [Post a weekly reliability report with AI](/guides/workflows/weekly-reliability-report/).
+
+### KloudMate
+
+KloudMate's own actions work on the hosts and resources in this workspace, so they need no connection. They're under the **KloudMate** tile.
+
+| Action | What it does |
+|---|---|
+| **Run Command on Host** | Run shell commands on a host through its KloudMate agent. |
+| **Look Up a Host or Resource** | Find the host or resource that a name or a set of labels points at. |
+| **Look Up an Alert Group's Resources** | Find the resource that each firing instance of an alert group belongs to. |
+| **Check a Host Agent** | Find the agent on a host and check whether it's reporting. |
+
+#### Look Up a Host or Resource
+
+Finds the resource that a name or a set of labels refers to, among the resources KloudMate already knows about. Use it to turn an alert's labels into an instance id that a cloud API can act on.
+
+Pick a name from the list in **What to look up**, or paste labels from the trigger:
+
+```liquid
+{{ trigger.body.rules.0.instances.0.labels }}
+```
+
+Labels are more reliable than a name. A name matches as a substring, so `web-01` also matches `web-011`. If a resource is named exactly `web-01`, only that exact match counts. When a name matches things of different kinds, such as a `host` and a `k8s.pod`, set **Kind** to narrow it.
+
+The step never picks between several matches. `found` is true only when exactly one thing matched. Then `resource` holds it, and `resource.resource_id` is the id to call the provider with. Otherwise, `matches` lists everything that matched, and `count` says how many.
+
+#### Look Up an Alert Group's Resources
+
+Lists the resources that an alert group's firing instances belong to, with one entry per resource. Put it first in a workflow that an alert starts, so the steps after it act on resources instead of alert labels. The [Send alerts to your workflows](../alert-router/) template uses it.
+
+Set **Alert group** to `{{ trigger.body }}`.
+
+Only firing instances count. The step combines instances that point at the same resource into one entry, and reports a pod that belongs to a deployment, statefulset, or daemonset as that workload. It looks up resources only in the workspace the run belongs to, whatever the payload says.
+
+| Output | What it holds |
+|---|---|
+| `targets` | One entry per resource. |
+| `unresolved` | The firing instances that couldn't be matched to a resource, each with a `reason`. |
+| `count`, `unresolved_count` | How many entries each list holds. |
+| `group_url` | A link to the alert group in KloudMate, for a message. Empty when the group has no page. |
+
+Each entry in `targets` holds:
+
+| Field | What it holds |
+|---|---|
+| `key` | An id that stays the same every time this resource alerts, even after a workload's pods are replaced. Use it as a storage key to skip a resource you acted on recently. |
+| `kind`, `name` | What the resource is, such as `host` or `k8s.deployment`, and its name. |
+| `resource_id` | The provider's own id for the resource, such as an EC2 instance id, when it has one. |
+| `where` | Where it runs: `host`, `namespace`, `cluster`, `account`, and `region`. For a pod, `host` is its node. |
+| `pods` | The pods that fired, each with its `name` and `node`. |
+| `firing` | Each firing instance's rule, detector, value, and the time it started firing. |
+| `detectors`, `rule_ids` | The same detectors and rule ids as flat lists, so a condition can check whether a list contains one of them. |
+
+An instance ends up in `unresolved` for one of these reasons:
+
+| `reason` | What it means |
+|---|---|
+| `no_identity` | The instance's labels don't identify a resource, as with an aggregate alert. |
+| `not_found` | The resource isn't in KloudMate's inventory right now, for example a pod that has been replaced. |
+| `ambiguous` | More than one resource matches. `candidates` lists each one with its kind, name, namespace, cluster, account, and region. |
+| `rule_not_in_workspace` | The alert rule belongs to another workspace. |
+| `over_cap` | The group has more than 100 resources or 1,000 firing instances, and this instance is past the limit. |
+
+#### Check a Host Agent
+
+Finds the KloudMate agent on a host and tells you whether it's reporting. Use it before **Run Command on Host**, both to get the agent's id and to tell a machine that's down from an agent that's down.
+
+Pick the **Host name** from the list, or template it, for example `{{ trigger.body.rules.0.instances.0.labels.host_name }}`. The name must exactly match the host name the agent registered with.
+
+| Output | What it holds |
+|---|---|
+| `status` | `reachable`, `unreachable`, `never_checked_in`, `not_found`, or `ambiguous` when more than one agent has that name. |
+| `agent_id` | The agent to pass to **Run Command on Host**. Empty unless exactly one agent matched. |
+| `reachable` | True when the agent checked in within the last 5 minutes. |
+| `collector_status` | What the agent last reported about its collector. A reachable agent with a stopped collector means the machine is fine and only its telemetry stopped. |
+| `scripts_enabled` | Whether runbook scripts are allowed on the host. When it's false, **Run Command on Host** fails on this host. |
+
+It also returns `found`, `count`, `last_checkin_at`, `seconds_since_checkin`, `platform`, and `agent_version`. A name that matches no agent, or more than one, doesn't fail the step, so branch on `status`.
+
+#### Run Command on Host
+
+Runs shell commands on a Linux host through its KloudMate agent. The **Host** list shows the hosts where both of these are true:
+
+1. The [KloudMate agent](../../kloudmate-agent/installation/linux-agent/) is installed on the host.
+2. Runbook scripts are allowed on that host. See [Allow runbook scripts on a host](#allow-runbook-scripts-on-a-host).
+
+A host that hasn't checked in during the last 5 minutes stays in the list, marked **Unreachable**. To choose the host when the workflow runs, template **Host** instead, for example with the `agent_id` that **Check a Host Agent** returns.
+
+Put one command per line in **Commands**. The lines run together as one `sh` script, so a later line still runs after an earlier one fails, unless the script starts with `set -e`. The step returns `exit_code`, `stdout`, and `stderr`, and a non-zero exit code fails it. The exit code is the last command's.
+
+**Command timeout (seconds)** limits how long the commands run on the host. It's 60 seconds unless you set it, and the agent stops a command after 15 minutes at most.
+
+The agent picks up a command when it next checks in, every 60 seconds by default. A command that runs longer than a couple of seconds reports its result at the check-in after that. The step's own **Timeout**, under **Settings**, limits how long the step waits for all of this, and it's 60 seconds unless you change it. Set it to cover both check-ins as well as the command, for example `5m` for a quick command.
+
+**Commands run as root** and can access everything the host can already access, such as a kubeconfig, a cloud instance profile, or an SSH key on disk. Put an [approval](../approvals-and-input/) in front of any command that changes something.
+
+When you test this step on its own, it shows the shape of its output without running the command. See [Test a workflow](../test-a-workflow/#test-one-step).
+
+#### Allow runbook scripts on a host
+
+Runbook scripts are off on every host until someone with the **Developer** role allows them, one host at a time:
+
+1. Open **Settings → Agents**.
+2. Open the host's actions menu (⋮), select **Configure**, and open the **Runbook scripts** tab.
+3. Turn on **Allow runbook scripts on this host** and confirm.
+
+The agent picks up the change within a minute. Only Linux host agents have this setting, so Docker, Amazon ECS, Kubernetes, and Windows agents can't run runbook scripts.
+
+To keep scripts off a host regardless of its **Runbook scripts** setting, set `job-types` to `none` in the host's `agent.yaml` (or set `KM_JOB_TYPES=none`), then restart the agent. See [agent configuration](../../kloudmate-agent/reference/agent-config/#runbook-scripts).
+
+## AI
+
+### AI Prompt
+
+Sends a prompt to a model and gets text back, as `text`. For examples, see [Post a weekly reliability report with AI](/guides/workflows/weekly-reliability-report/) and [Summarize a Slack thread with AI](/guides/workflows/summarize-slack-thread/).
+
+:::caution
+If you template untrusted data into **AI Prompt**, whoever wrote that data controls part of the prompt. A Slack message body, a webhook payload, or an issue description can all steer the model. When the input isn't yours, use **AI Extract** instead.
+:::
+
+### AI Extract
+
+Pulls structured fields out of text. Use it whenever the input isn't yours. It keeps the source separate from the instruction and checks the answer against a schema, so even a steered model can't add a field or return a value the schema forbids.
+
+- **Instruction** says what to extract.
+- **Source** is the text or object to read. Pass a whole object, for example `{{ trigger.body }}`, and name the fields you want in the instruction. A labeled text string assembled by hand takes more effort, and it silently drops any field the payload gains later.
+- **Output fields** is a JSON list of the fields you want back. Each entry has a `name` and a `type`, and can add a `description`, `required`, `options` for an enum, or nested `fields` for an object. KloudMate builds the schema that the model must follow from that list, so you never write JSON Schema yourself.
+
+```json
+[
+ { "name": "severity", "type": "enum", "options": ["low", "medium", "high"] },
+ { "name": "host", "type": "string", "description": "The host the message is about" },
+ { "name": "services", "type": "string[]", "required": false }
+]
+```
+
+Publishing checks the list against these rules:
+
+- **A type is one of** `string`, `number`, `boolean`, `enum`, `object`, `string[]`, `number[]`, `boolean[]`, or `object[]`.
+- **`required` defaults to true.** Set it to `false` to make a field optional.
+- **A name uses letters, digits, and underscores, and doesn't start with a digit.** Each name can appear only once.
+- **An enum field needs at least one option.**
+- **An object field can hold nested fields.** Leave them out to accept any object.
+
+If you leave **Output fields** empty, the instruction describes the shape instead.
+
+The extracted fields are at the top level of the step output, so a later step reads `{{ steps.extract.output.severity }}`. Their shape comes from your own list, so the variable picker shows them only after you test the step once.
+
+## Utilities
+
+These actions need no connection.
+
+| Action | What it does |
+|---|---|
+| **Transform** | Extract a value from an earlier step's output with a JSONPath expression. |
+| **CSV** | Parse CSV text into rows, or format rows back into CSV. |
+| **XML** | Parse XML text into a JSON object. |
+| **Parse URL** | Split a URL into protocol, host, path, query object, and hash. |
+| **Storage** | Save values that later runs can read. See [Storage](../storage/). |
+
+**Transform** returns its result as `value`. A path that can match several things, such as one with `[*]`, `..`, a filter, or a slice, always returns a list, even when it matches one thing or nothing.
+
+### HTTP Request
+
+Calls any URL. Set the **Method**, **Headers**, and **Body**. **Body type** decides how the body is sent: **JSON**, **Form** (URL-encoded), **Raw**, or **None**.
+
+Choose **Authentication** on the **Setup** tab:
+
+- **OAuth 2.0 connection** is the better option. KloudMate sends the access token as a bearer token and refreshes it for you, and the credential never enters the workflow definition.
+- **Bearer token** and **Basic** keep the credential in the step itself, so it's saved with the workflow and appears in an export. Template it from a [secret](../variables/), as in `{{ secrets.api_token }}`, instead of typing it.
+- **None** sends no credential.
+
+An `Authorization` header you set yourself under **Headers** replaces the credential that **Authentication** would add. KloudMate masks credential-looking values as `••••` in the recorded input and in the Test panel.
+
+Timing and error handling:
+
+- **Timeout (ms)** is the request timeout, 10 seconds by default. The step's own timeout limits each attempt on top of it.
+- **Treat as success** lists the status codes that count as success, such as `200,404` or `2xx,404`. Empty means any 2xx or 3xx. A list replaces that default rather than adding to it, so `404` on its own makes a `200` fail.
+- **Retry on** has no effect on a step built in the builder, because an HTTP Request step doesn't retry.
+
+The step returns `status`, `body`, and `headers`. `body` is parsed JSON when the response is JSON and the raw text otherwise, and header names are lower-case.
+
+For a complete example that calls Google's APIs through an OAuth 2.0 connection, see [Trigger a workflow when a new email is received in Gmail](/guides/workflows/trigger-on-new-gmail/).
+
+### Run Code
+
+Runs JavaScript in a sandbox and outputs the value it returns.
+
+```js
+function main(input) {
+ const rows = input.rows || [];
+ return { count: rows.length, names: rows.map((r) => r.name) };
+}
+```
+
+Pass data to `main` through the separate **Input** field, not in the code. KloudMate runs the code exactly as written and never templates it, so it can safely contain `{{`.
+
+Avoid these mistakes:
+
+- **Return the object, not a string.** If you return `JSON.stringify(...)`, later steps get one string instead of fields, and `{{ steps.x.output.output.id }}` resolves to nothing. The step still succeeds, which is what makes the mistake hard to see.
+- **`fetch(url, opts)` blocks, so don't use `await`.** Write `const r = fetch(url)`, and keep `main` an ordinary synchronous function. Requests run one at a time.
+
+The step's output has `output` (the return value) and `logs` (the captured `console.log` lines), so a later step reads `{{ steps..output.output.count }}`.
+
+Run Code never retries, because the code can POST. If a caller might re-run the workflow, make `main` safe to call twice.
+
+## Retries and failures
+
+Every action step has the settings covered in [Build a workflow](../build-a-workflow/#step-settings). The one difference between actions is retries: anything that changes external state never retries and doesn't offer the setting, so a failed write is never silently repeated.
+
+## Related
+
+- [Templating](../templating/) for reaching an earlier step's output.
+- [Connections](/platform/settings/connections/) for the credentials these actions bind.
+- [Storage](../storage/) for the `store.*` actions.
diff --git a/src/content/docs/docs/workflows/alert-router.mdx b/src/content/docs/docs/workflows/alert-router.mdx
new file mode 100644
index 00000000..3913654e
--- /dev/null
+++ b/src/content/docs/docs/workflows/alert-router.mdx
@@ -0,0 +1,86 @@
+---
+title: "Send alerts to your workflows"
+description: "Look up the resources in an alert group, and run a workflow you built for each kind of resource: one for hosts, one for Kubernetes workloads."
+sidebar:
+ label: "Send alerts to workflows"
+ order: 4.5
+---
+
+The **Send alerts to your workflows** template runs one of your workflows for each resource in an alert group. When a group opens or new alerts join it, the template finds the resource that each firing instance belongs to. It then calls the workflow you built for that kind of resource: your host workflow for a host, and your workload workflow for a Kubernetes deployment, statefulset, or daemonset. For any other kind of resource, it emails a person.
+
+The template doesn't change anything itself. The workflows it calls decide what happens to each resource, so the same host workflow works whether an alert starts it or a person runs it by hand.
+
+## What the template does
+
+1. The **Webhook** trigger runs on the `opened` and `appended` events, which arrive when an alert group opens and when new alerts join it. Other events, such as `repeat` and `resolved`, don't start a run.
+2. **Look up the alert group's resources** uses [Look Up an Alert Group's Resources](../actions/#look-up-an-alert-groups-resources) to list the group's resources, one entry per resource.
+3. If the lookup couldn't match some firing instances to a resource, **Email: what a person must look at** lists them with the reason for each.
+4. **For each resource** loops over the resources, three at a time.
+5. **Count alert group updates for this resource** skips a resource that already had a workflow run for it during the cooldown. See [How the cooldown works](#how-the-cooldown-works).
+6. **Which workflow?** is a [Route](../control-flow/#route). Its `host` path runs your host workflow, its `workload` path runs your workload workflow, and **Otherwise** emails a person that no workflow handles this kind of resource.
+
+## Build the host and workload workflows
+
+Build and publish these first. You can't publish a workflow that calls another until the called workflow has been published at least once.
+
+Each call passes these inputs, and the called workflow reads them as `{{ trigger.inputs. }}`:
+
+| Called workflow | Inputs |
+|---|---|
+| Host | `host`, `name`, `kind`, `resource_id`, `namespace` |
+| Kubernetes workload | `name`, `kind`, `namespace`, `cluster`, `pods` |
+
+- `resource_id` is the provider's own id for the resource, such as an EC2 instance id, when it has one.
+- `kind` for a workload is `k8s.deployment`, `k8s.statefulset`, or `k8s.daemonset`.
+- `pods` lists the pods that fired, each with its `name` and `node`. It's empty when the alert is on the workload itself.
+
+Give each called workflow a **Manual / API** trigger. Declare the text inputs it uses as input parameters, so you can run it by hand with the same values the template sends. `pods` is a list, so a loop can use `{{ trigger.inputs.pods }}` as its **Items** directly.
+
+## Create the workflow from the template
+
+You need the **Developer** role.
+
+1. Open **Workflows → Templates** and click **Use template** on **Send alerts to your workflows**. The new draft opens in the builder.
+2. On **Run your host workflow** and **Run your workload workflow**, pick the workflow each step runs. If you don't have one of them, delete that step. A path with no steps is valid, but don't delete the Route itself, because that also deletes the email under **Otherwise**.
+3. Set **To** on **Email: what a person must look at** and on **Email: no workflow for this resource**. You can also replace both steps with a Slack or Teams message.
+4. To change the cooldown, set **Expires after** on **Count alert group updates for this resource**. It's `30m` by default.
+5. Click **Publish**.
+
+Publishing refuses the workflow until every call step has a workflow and both emails have an address. It names each field that's still unset, and reports the email address as `notify_email`.
+
+The new workflow's description lists the inputs each call passes.
+
+## Connect the workflow to your alerts
+
+Creating the workflow doesn't connect it to any alerts. To connect it:
+
+1. Copy the webhook URL from the workflow's trigger.
+2. Create a webhook [notification channel](../../platform/settings/notification-channels/) with that URL.
+3. Add the channel to every [routing rule](../../alerts/routing-rules/) whose alerts you want the workflow to handle.
+
+An alert goes only to the first routing rule it matches, so a channel on one rule never gets the alerts that a higher-priority rule matched first. See [Start a workflow from an alert](../triggers/#start-a-workflow-from-an-alert).
+
+## How the cooldown works
+
+The template counts each resource under its `key` before it calls a workflow. The key stays the same every time that resource alerts, even after a workload's pods are replaced.
+
+The first time it counts a resource, the template calls the workflow. Each later update to an alert group that includes the resource adds to the count and restarts the cooldown, so the template leaves a resource alone while it keeps alerting. After the resource has been quiet for the whole cooldown, its next alert calls the workflow again.
+
+The template counts before it calls, so a resource whose workflow failed also waits out the cooldown. Each workflow keeps its own count, and each update happens in one operation, so two runs can't both act on the same resource.
+
+## Add a path for another kind of resource
+
+Add a path to **Which workflow?** with a condition on `item.kind`. Then put a **Call Workflow** step in the path. For example, a path named `lambda` could match `item.kind` equal to `aws.lambda.function` and pass `{{ item.resource_id }}` and `{{ item.where.region }}` to your Lambda workflow.
+
+To see the kinds your alerts produce, [capture a real alert](../test-a-workflow/#capture-a-webhook-sample) as the trigger sample. Then test **Look up the alert group's resources**.
+
+Each resource also has `item.detectors`, the [Smart Alerts](../../alerts/smart-alerts/) detectors whose rules fired on it. A path can check whether that list contains a detector, so you can pick a workflow by the problem instead of by the resource.
+
+The template's emails pass every value that comes from the alert through the `escape` filter. If you edit them, keep the filters in place. See [Escape values in an email](../templating/#escape-values-in-an-email).
+
+## Related
+
+- [Control flow](../control-flow/#route) for the Route step.
+- [The action catalog](../actions/#look-up-an-alert-groups-resources) for everything the lookup returns, and why an instance can go unresolved.
+- [Triggers](../triggers/#start-a-workflow-from-an-alert) for the alert payload and its events.
+- [Storage](../storage/) for the counter that the cooldown uses.
diff --git a/src/content/docs/docs/workflows/approvals-and-input.mdx b/src/content/docs/docs/workflows/approvals-and-input.mdx
new file mode 100644
index 00000000..a4587fc8
--- /dev/null
+++ b/src/content/docs/docs/workflows/approvals-and-input.mdx
@@ -0,0 +1,64 @@
+---
+title: "Approvals and collected input"
+description: "Pause a workflow until a person approves it, or until someone fills in a form, and let them respond from a link in their email."
+sidebar:
+ label: "Approvals and input"
+ order: 5
+---
+
+**Approval** and **Collect Input** pause a run until a person responds. Approval asks for a yes or no. Collect Input asks the person to fill in a form, and passes the submitted values to the steps that follow.
+
+Put an approval in front of anything that changes a resource. Nothing else gates a destructive step: a run restarts an instance or deletes a file without asking unless an approval stops it.
+
+A paused run doesn't count toward the [limit on runs at the same time](../limits/#run-limits), so waiting a day for a decision costs nothing.
+
+## Approval
+
+| Field | What it takes |
+|---|---|
+| **Approvers** | Workspace members who can approve, up to 10. The first to respond decides. |
+| **Message** | What approvers see with the request. You can use templates here, for example to name the host and the command. |
+| **Timeout** | How long to wait, `24h` by default and `7d` at most. |
+| **On timeout** | **Fail the run**, or **Continue**. |
+
+`{{ steps..output.approved }}` holds the decision, and `{{ steps..output.decidedBy }}` holds the person who decided, as an object with `id`, `name`, `email`, and `timezone`, so a later Slack message can name them.
+
+### When an approval is rejected or times out
+
+**Rejected isn't a failure.** The run ends as `rejected`, skips the steps after the approval, and retries nothing. Run history shows it as **Rejected**, not **Failed**.
+
+**Timed out** gives the step `approved: null` and then follows **On timeout**. With **Continue**, a later branch can check `{{ steps..output.approved }}` and take a fallback path.
+
+## Collect Input
+
+Takes the same fields as an approval, with **Assignees** in place of **Approvers**, plus the form.
+
+**Fields** holds between 1 and 20 entries. Each has a name and a type: **Text**, **Long text**, **Email**, **Number**, **Checkbox**, or **Choice**. A Choice field needs at least one option and can allow more than one. Mark a field required, or leave it optional and give it a default.
+
+**Assignees** are the workspace members who get the form, and only the first submission counts.
+
+Later steps read `{{ steps..output.fields. }}` for the values and `{{ steps..output.submittedBy }}` for who submitted them, with `id`, `name`, and `email`.
+
+## How people receive a request
+
+Every request goes out by email, and the link in that email is the only way to respond. Each person named on the step gets their own email and their own link:
+
+- The subject names the workflow and the step, for example `Approval needed: Disk cleanup · Approve the cleanup`.
+- Your **Message** appears in the body. KloudMate escapes it, so a value from a payload shows up as plain text.
+- When more than one person was asked, the email names the others and says that the first reply decides.
+- The email shows the deadline in the recipient's own timezone.
+
+People can respond without signing in to KloudMate, because the signed link identifies them.
+
+## Restrictions
+
+- **Name at least one person.** Publishing refuses an Approval or Collect Input step with nobody on it.
+- **Someone who has left can't decide.** KloudMate leaves them out when it sends the request, and checks membership again when someone responds, so a link stops working when its recipient loses access.
+- **The first response wins.** Every other link stops working after one person answers.
+- **Neither step can go inside a loop.** Publishing rejects it.
+
+## Related
+
+- [Control flow](../control-flow/) for the other structural steps.
+- [Test a workflow](../test-a-workflow/) for testing an approval before real approvers get it.
+- [Limits](../limits/) for the timeout ceilings and the hourly cap on request emails.
diff --git a/src/content/docs/docs/workflows/build-a-workflow.mdx b/src/content/docs/docs/workflows/build-a-workflow.mdx
new file mode 100644
index 00000000..f51ed314
--- /dev/null
+++ b/src/content/docs/docs/workflows/build-a-workflow.mdx
@@ -0,0 +1,148 @@
+---
+title: "Build a workflow"
+description: "Create a workflow from scratch, from a template, or from an exported file, add and configure steps, then publish it so triggers can start runs."
+sidebar:
+ label: "Build a workflow"
+ order: 2
+---
+
+Build a workflow by picking a trigger, then adding steps under it. Everything you edit stays in a draft, and a trigger starts runs only from the published version.
+
+## Create a workflow
+
+Open **Workflows** and click **Create workflow**. Name it, then pick a trigger; nothing else is available until you do. See [Triggers](../triggers/).
+
+You can also [start from a template](#start-from-a-template) or [import](#export-and-import) a workflow exported from another workspace.
+
+Renaming a workflow or editing its description doesn't create unpublished changes.
+
+## Start from a template
+
+The **Templates** tab holds ready-made workflows that diagnose or fix common problems. Click **Use template** on a card to create a draft and open it in the builder. You need the **Developer** role.
+
+| Template | Starts | What it does |
+|---|---|---|
+| Tell a down host from a down agent | When a host alert opens | Reports whether the machine is down, its collector stopped, or the machine is up and the alert is real. |
+| Report disk usage and the largest directories | When a disk alert opens | Lists the mounts by usage, then the largest directories on the fullest mount. |
+| Report the top processes by CPU and memory | When a host load alert opens | Lists the processes by CPU and by memory, with the owner and the PID. |
+| Free disk space, with approval | When a disk alert opens | Shows what uses the space and waits for approval, then vacuums the journal, removes rotated logs, and reports the space it freed. |
+| Report why a Step Function execution failed | When a state machine alert opens | Reports the most recent failed execution's error, its cause, and the states it ran. |
+| Redrive a failed Step Function execution, with approval | When a state machine alert opens | Shows what failed and why, waits for approval, then restarts the execution from the state that failed. |
+| Tell an EC2 instance fault from a host fault | When an EC2 status check fails | Says whether the fault is in the operating system or in the AWS host underneath. |
+| Reboot an EC2 instance, with approval | When an EC2 instance check fails | Asks for approval, then reboots, and stops instead when the fault is in the AWS host underneath. |
+| Report the state of an Azure VM | By hand | Reports what Azure holds for the machine, then what the machine reports about itself. |
+| Restart an Azure VM, with approval | By hand | Asks for approval, restarts the machine, waits for it, then confirms with the machine itself. |
+| Send alerts to your workflows | When an alert group opens or new alerts join it | Runs a workflow you choose for each resource in the group. See [Send alerts to your workflows](../alert-router/). |
+
+Each card shows **Reads only** or **Changes a host**. A card also shows **Needs a host script grant** when the template runs commands on a host (see [Allow runbook scripts on a host](../actions/#allow-runbook-scripts-on-a-host)), and a missing-connection warning when the template calls AWS or Azure and the workspace has no connection for it.
+
+The draft isn't published, and it isn't connected to any alerts. Before you publish it:
+
+- **Fill in the blank fields.** Every template sends its result by email, so set the address on each **Send Email** step. Publishing fails on any field the template left blank, and names the field, for example `notify_email`.
+- **Pick a connection** on each AWS or Azure step.
+- **Name the approvers** on each **Approval** step.
+- **Send alerts to it.** A template that starts on an alert needs its webhook URL on a notification channel that a routing rule uses. See [Start a workflow from an alert](../triggers/#start-a-workflow-from-an-alert).
+
+The draft's description lists what you need to set up. The draft also comes with a sample alert payload, so the variable picker shows real field names before any alert arrives.
+
+## Add steps
+
+Click the **+** between any two nodes to open the palette. Actions are grouped under **Apps**, **AI**, and **Utilities**, with control flow on its own tab. Search spans all of them, so typing `reaction` finds the Slack action without opening Slack first.
+
+
+
+Inside a control-flow block, the palette shows only the steps that block accepts. A **Wait until** check block takes actions, Branch, and Route steps only.
+
+## Configure a step
+
+The tabs in a step's panel depend on the step.
+
+**Setup** appears when the step uses a connection, or when one field's value decides the options in another. On an AWS step, for example, pick the connection and the service on **Setup**, and **Configure** then shows that operation's parameters. A step with neither, such as Transform or Run Code, opens straight on Configure.
+
+**Configure** holds the rest of the fields. They come from the action itself, so an MCP step shows the chosen tool's own arguments.
+
+**Test** runs the step against real data. It's hidden in view mode, which is how a saved workflow opens from the list, so click **Edit** to show it. See [Test a workflow](../test-a-workflow/).
+
+
+
+### Use a variable in a field
+
+Text, JSON, and condition fields take `{{ }}` references directly, with a picker for the trigger payload, an earlier step's output, and workspace variables. To use a variable in a dropdown field, click the braces button beside it. See [Templating](../templating/).
+
+### Rename a step
+
+Edit the name at the top of the panel. That changes the display name only. The builder generates a step's id when you add it, and the id never changes, so existing `{{ steps..output }}` references keep resolving. Take references from the variable picker instead of typing them: it lists steps under their names and inserts the right id.
+
+### Step settings
+
+Action steps have a collapsible **Settings** section under their fields. Control-flow steps don't, because their timing and failure fields are ordinary inputs: a wait has a duration, an approval has a timeout.
+
+| Setting | Default | What it does |
+|---|---|---|
+| **On failure** | Stop the workflow | **Go to the next step** records the error under `steps..error` and carries on. |
+| **Retry attempts** | 3, or 1 for the AI actions | At most 5. Not offered on an action that changes external state: those run once, so a failed write is never silently repeated. |
+| **Timeout** | 60s | How long one attempt may run, up to 15m. |
+
+**No retries** gives the same default as leaving **Retry attempts** alone, so you can't set a read-only action to zero retries from the builder.
+
+## Publish and enable a workflow
+
+Your edits change only the draft. **Discard changes** reverts the draft to the last published version.
+
+The builder checks the workflow as soon as you open it. It marks each step that has a problem, with the message under the field that caused it. If problems remain when you click **Publish** or **Test run**, the builder lists them, and **Fix** on a row takes you to that step.
+
+Publishing runs every check, including whether each connection, called workflow, and secret the workflow uses still exists. Then it writes an immutable version.
+
+The first publish also switches the workflow on, so its trigger starts firing right away. A later publish never switches a workflow back on after someone turned it off. Turn the switch off to stop a scheduled, webhook, form, or app-event workflow from starting runs without deleting anything.
+
+A run always uses the version it started on, so publishing while a run is in flight doesn't change it.
+
+:::note
+You can't enable a workflow before publishing it. You can't delete one while it's enabled, while it has runs in flight, or while another workflow calls it.
+:::
+
+## Versions
+
+Click **Versions** to see every published version, who published it, and when.
+
+- **Make live** points the workflow at an earlier version without touching your draft. If that version no longer passes the checks, for example because a connection it uses was deleted, the builder lists the reasons.
+- **Restore to draft** copies an old version into the draft so you can edit it and publish again.
+
+KloudMate deletes old versions after 30 days, but always keeps the newest 20 per workflow and the live one. See [Limits](../limits/#retention).
+
+## Run a workflow by hand
+
+A published workflow with a manual or form trigger has a **Run** button. Fill in the trigger's input parameters and it starts a real run of the published version, even while the workflow is switched off. Users with the **Developer** role can always do this; a **Viewer** can when the workflow is set to allow it.
+
+**Test run** runs the draft instead of the published version. See [Test a workflow](../test-a-workflow/).
+
+## Filter and tag workflows
+
+Tag a workflow inline on its row. Filter the list by status (**Enabled**, **Disabled**, or **Draft** for a workflow that's never been published) or by tag, or search it.
+
+
+
+## Export and import
+
+**Export…** on a published workflow's row menu gives you its published version as YAML, ready to **Copy** or **Download**. Connections, notification channels, called workflows, and workspace variables appear as named inputs rather than as ids, so the same file can be imported into another workspace. Anything the export couldn't describe stays as a raw id, and the export flags it.
+
+To import, click **Import** next to **Create workflow**, then paste the exported YAML or choose a `.yaml`, `.yml`, or `.json` file. An import always creates a draft, so nothing runs until you publish it.
+
+The import matches each reference against the destination workspace:
+
+- **A notification channel** matches by name and type.
+- **A workspace variable** matches by name and kind, constant or secret.
+- **A connection or a called workflow** matches only when it's the same one, for example when you import a file back into the workspace it came from. Connections usually don't match across workspaces, so expect to pick them again. Import a called workflow before the workflow that calls it.
+
+The import leaves anything it couldn't match empty, and lists it when it finishes. Fill in the ones under **Set these before publishing**, because publishing refuses the workflow without them.
+
+When you import the same file again, KloudMate updates the workflow the first import created instead of adding another one. The import replaces the draft, and the published version keeps running until you publish again. A file imported back into the workspace it came from works the same way, and replaces the original workflow's draft.
+
+Export and import both need the **Developer** role.
+
+## Related
+
+- [Triggers](../triggers/)
+- [Control flow](../control-flow/)
+- [Test a workflow](../test-a-workflow/)
+- [Run history](../run-history/)
diff --git a/src/content/docs/docs/workflows/control-flow.mdx b/src/content/docs/docs/workflows/control-flow.mdx
new file mode 100644
index 00000000..51fa36e6
--- /dev/null
+++ b/src/content/docs/docs/workflows/control-flow.mdx
@@ -0,0 +1,138 @@
+---
+title: "Control flow"
+description: "Branch on a condition, choose between several paths by condition, loop over a list, run branches in parallel, wait for a delay or for a condition to hold, and call another workflow."
+sidebar:
+ label: "Control flow"
+ order: 4
+---
+
+Control-flow steps decide what the rest of the workflow does: which path to take, how many times to repeat, when to wait, and when to call another workflow. They don't call any outside service.
+
+Each one records a row in the run timeline and produces an output that later steps can read.
+
+
+
+## Branch (If / Else)
+
+Runs one of two paths, depending on a condition. The condition builder is the same one you use on routing rules, and a condition can check paths such as `trigger.body.severity` or `steps.lookup.output.found`.
+
+The **Else** block is optional. `{{ steps..output.taken }}` is `then` or `else`.
+
+## Route
+
+Runs the first of several paths whose condition matches. Use it when a decision has more than two outcomes, such as calling one workflow for a host, another for a Kubernetes workload, and emailing a person about anything else. For a two-way decision, **Branch** is simpler.
+
+Click **Add path** for each outcome. A path has a **Name**, a condition under **When to take it**, and its own steps. The condition builder is the same one Branch uses.
+
+Route checks the paths from the top and runs only the first one whose condition matches, even when a later path matches too. The order works as a priority list, so move a path up or down to change which one wins.
+
+**Otherwise** is optional and runs when no path matches. If nothing matches and there's no Otherwise, the Route runs nothing and the workflow continues with the next step.
+
+`{{ steps..output.route }}` holds the name of the path that ran, or `else` when no path matched.
+
+Publishing checks the paths against these rules:
+
+- **A Route holds 1 to 10 paths.**
+- **A name starts with a lowercase letter** and uses only lowercase letters, digits, hyphens, and underscores, up to 32 characters. Names must be unique within the step, and `else` is reserved.
+- **Every path needs a condition.** For a catch-all, use **Otherwise**.
+
+In run history, a Route step shows the path it took and whether each path's condition matched. If two paths both matched, the one higher in the list ran, so a wrong result usually means the order is wrong.
+
+A Route can go anywhere a Branch can, including inside a loop, a parallel branch, and a **Wait until** check block.
+
+## Loop (For Each)
+
+Runs a block once per item in a list.
+
+| Field | What it takes |
+|---|---|
+| **Items** | The list to loop over, for example `{{ steps.query.output.rows }}`. |
+| **Max items** | The loop stops after this many items. The limit is 100, and a longer list is cut off without failing. |
+| **Run at once** | How many items run at the same time, up to 10. |
+
+If **Items** resolves to something other than a list, such as an object or a piece of text, the loop step fails. A value that's missing or empty runs the loop zero times.
+
+Inside the block, `{{ item }}` is the current element and `{{ item_index }}` is its position.
+
+Unless the items are independent of each other, leave **Run at once** at 1. With more than 1, items run in no guaranteed order, and an item can't read another item's step outputs.
+
+At 1, a failed step stops the loop at that item, unless the step is set to **Go to the next step**. With more than 1, every item runs even if one of them fails, and then the loop fails with a count of the items that failed.
+
+`{{ steps..output.iterations }}` is the number of items the loop ran.
+
+:::caution
+A loop body can't hold another loop, an approval, a collect-input step, or a Wait until, including inside a parallel branch nested in the loop. Publishing rejects them.
+:::
+
+## Parallel
+
+Runs between two and ten branches at the same time and waits for all of them.
+
+**If a branch fails** decides what happens next: fail the run, or continue with the other branches' results. `{{ steps..output }}` counts the branches that ran as `branches`, and the ones that failed as `failed`.
+
+## Wait / Delay
+
+Pauses for a fixed time, up to `24h`. A wait of 1 minute or longer doesn't count toward the [20 runs a workspace can have running at the same time](../limits/#run-limits). A shorter wait still counts.
+
+## Wait until
+
+Runs a block of steps repeatedly until a condition holds, then continues. Use it to confirm that a change took effect: restart an instance, then wait until it reports running before you post the all-clear.
+
+| Field | What it takes |
+|---|---|
+| **Check until the condition is true** | The steps to run each time. Actions, Branch, and Route steps only. |
+| **Until** | The condition, read against the check block's own step outputs, for example `steps.vm.output.result.status` equals `running`. |
+| **Timeout** | How long the step waits before it fails. Default `10m`, maximum `24h`. |
+
+The timeout is the only timing field you set. It also sets how often the step checks: every 60 seconds, or `timeout / 20`, whichever is longer, for at most 20 checks. A 10-minute timeout gives you 10 checks a minute apart, and a 24-hour timeout spreads 20 checks across the day.
+
+Before you rely on a Wait until:
+
+- **The first check runs immediately.** A condition that's already true costs one check and no delay.
+- **A failed check doesn't end the wait.** The step keeps checking, because whatever you're waiting on is usually mid-change. If the action reports a permanent failure, the step stops at that check instead of waiting for the timeout.
+- **Running out of checks fails the step.** The run stops there, unless the step was imported with `on_error: continue`, which the builder keeps but has no field for.
+- **The check block keeps one row per step** in the run timeline, showing the most recent check.
+
+`{{ steps..output }}` has `checks`, `satisfied`, `failed`, and `last_error`. To read what the last check returned, use the check step's own output.
+
+:::caution
+A Wait until can't go inside a loop. To wait on each item in a list, put the Wait until in a workflow of its own and call it from the loop with **Call Workflow**.
+:::
+
+## Call Workflow
+
+Runs another published workflow and waits for its result.
+
+The called workflow receives **Inputs** as `{{ trigger.inputs.* }}` and nothing else. It doesn't see the caller's trigger payload or step outputs. Declare the same names as input parameters on the called workflow's **Manual / API** trigger, so its variable picker offers them and a person can also run it by hand.
+
+`{{ steps..output }}` is the output of the last step the called workflow ran, so end the called workflow with the step whose output the caller needs. The caller then reads its fields directly, for example `{{ steps..output.instance_id }}`. If the called workflow fails, the Call Workflow step fails too.
+
+Calls can nest five deep.
+
+## AI Route
+
+A model picks one of the routes. Write the **Situation** for the model to read, then describe each route so the model can tell them apart. When you can write the rule as a condition, use [Route](#route) instead: it doesn't call a model, and the same data always takes the same path.
+
+Add between two and eight routes, each with its own label, description, and block of steps. The run takes the **Fallback route** when the model gives no usable answer. If you leave it empty, the run fails instead.
+
+`{{ steps..output }}` has `route`, `reason`, and `confidence` of high, medium, or low.
+
+The model reads the **Situation** as data, not as instructions, so a payload templated into it can't redirect the model's task. It can still influence which route looks right. If the situation text includes anything an outsider wrote, keep the route descriptions specific.
+
+For a complete example, see [Send requests to the right team with AI Route](/guides/workflows/route-requests-with-ai/).
+
+## When a step fails
+
+You set error handling per step. Open a step's settings and set **On failure**:
+
+- **Stop the workflow** (the default) ends the run as failed.
+- **Go to the next step** records the error under `{{ steps..error }}` and continues, so a later branch can act on it.
+
+The setting covers a bad template as well as a failed call, so a step whose inputs didn't resolve behaves the same way as one the provider rejected.
+
+## Related
+
+- [Send alerts to your workflows](../alert-router/) for a Route that calls a different workflow for each kind of resource.
+- [Approvals and collected input](../approvals-and-input/) for the steps that wait for a person.
+- [The action catalog](../actions/) for the steps that do the work.
+- [Limits](../limits/) for the caps on steps, loops, and nesting.
diff --git a/src/content/docs/docs/workflows/images/builder-canvas.png b/src/content/docs/docs/workflows/images/builder-canvas.png
new file mode 100644
index 00000000..1a1d16e8
Binary files /dev/null and b/src/content/docs/docs/workflows/images/builder-canvas.png differ
diff --git a/src/content/docs/docs/workflows/images/control-flow-palette.png b/src/content/docs/docs/workflows/images/control-flow-palette.png
new file mode 100644
index 00000000..875b517f
Binary files /dev/null and b/src/content/docs/docs/workflows/images/control-flow-palette.png differ
diff --git a/src/content/docs/docs/workflows/images/run-detail-flow.png b/src/content/docs/docs/workflows/images/run-detail-flow.png
new file mode 100644
index 00000000..5429fcef
Binary files /dev/null and b/src/content/docs/docs/workflows/images/run-detail-flow.png differ
diff --git a/src/content/docs/docs/workflows/images/run-timeline.png b/src/content/docs/docs/workflows/images/run-timeline.png
new file mode 100644
index 00000000..e9862922
Binary files /dev/null and b/src/content/docs/docs/workflows/images/run-timeline.png differ
diff --git a/src/content/docs/docs/workflows/images/runs-list.png b/src/content/docs/docs/workflows/images/runs-list.png
new file mode 100644
index 00000000..ab8fa1da
Binary files /dev/null and b/src/content/docs/docs/workflows/images/runs-list.png differ
diff --git a/src/content/docs/docs/workflows/images/step-config-tabs.png b/src/content/docs/docs/workflows/images/step-config-tabs.png
new file mode 100644
index 00000000..c13986fc
Binary files /dev/null and b/src/content/docs/docs/workflows/images/step-config-tabs.png differ
diff --git a/src/content/docs/docs/workflows/images/step-palette.png b/src/content/docs/docs/workflows/images/step-palette.png
new file mode 100644
index 00000000..51573b31
Binary files /dev/null and b/src/content/docs/docs/workflows/images/step-palette.png differ
diff --git a/src/content/docs/docs/workflows/images/step-test-panel.png b/src/content/docs/docs/workflows/images/step-test-panel.png
new file mode 100644
index 00000000..9ca81a1b
Binary files /dev/null and b/src/content/docs/docs/workflows/images/step-test-panel.png differ
diff --git a/src/content/docs/docs/workflows/images/storage-tab.png b/src/content/docs/docs/workflows/images/storage-tab.png
new file mode 100644
index 00000000..f165390d
Binary files /dev/null and b/src/content/docs/docs/workflows/images/storage-tab.png differ
diff --git a/src/content/docs/docs/workflows/images/trigger-picker.png b/src/content/docs/docs/workflows/images/trigger-picker.png
new file mode 100644
index 00000000..c0bd4e22
Binary files /dev/null and b/src/content/docs/docs/workflows/images/trigger-picker.png differ
diff --git a/src/content/docs/docs/workflows/images/trigger-test-panel.png b/src/content/docs/docs/workflows/images/trigger-test-panel.png
new file mode 100644
index 00000000..e3b1a2f5
Binary files /dev/null and b/src/content/docs/docs/workflows/images/trigger-test-panel.png differ
diff --git a/src/content/docs/docs/workflows/images/variables-tab.png b/src/content/docs/docs/workflows/images/variables-tab.png
new file mode 100644
index 00000000..4f99e0a3
Binary files /dev/null and b/src/content/docs/docs/workflows/images/variables-tab.png differ
diff --git a/src/content/docs/docs/workflows/images/workflows-list.png b/src/content/docs/docs/workflows/images/workflows-list.png
new file mode 100644
index 00000000..4b60c92b
Binary files /dev/null and b/src/content/docs/docs/workflows/images/workflows-list.png differ
diff --git a/src/content/docs/docs/workflows/index.mdx b/src/content/docs/docs/workflows/index.mdx
new file mode 100644
index 00000000..c3962c37
--- /dev/null
+++ b/src/content/docs/docs/workflows/index.mdx
@@ -0,0 +1,81 @@
+---
+title: "Workflows"
+description: "Automate a response when an alert fires, a schedule comes due, a webhook arrives, or someone clicks Run."
+sidebar:
+ label: "Overview"
+ order: 1
+---
+
+A workflow is a series of steps that runs when something happens, such as an alert, a schedule, or a webhook call. A step can query a system, branch on the result, change a resource, or wait for a person to approve.
+
+A disk-space alert is a good first workflow. It reads the host from the alert and runs `df -h` on it through the KloudMate agent. Then it posts the output to Slack, asks the on-call engineer to approve a cleanup, runs the cleanup, and waits until free space recovers before it reports back.
+
+
+
+## What a workflow can do
+
+- **Act on cloud resources**: call an AWS API action, such as stopping an EC2 instance, or an Azure Resource Manager operation through a connected account, or run a script on an Azure VM.
+- **Run a command on a host**: use the KloudMate agent to run shell commands on any Linux host where someone with the **Developer** role has [allowed runbook scripts](./actions/#allow-runbook-scripts-on-a-host).
+- **Work in your tools**: post and edit Slack messages, create and transition Jira issues, call a tool on an MCP server, or send an HTTP request to anything else.
+- **Notify**: send through an existing Slack, Teams, webhook, or SNS channel, or email an address directly.
+- **Transform and decide**: extract a value with JSONPath, parse CSV and XML, run sandboxed JavaScript, choose between several paths by condition, or have a model pull structured fields out of a payload.
+- **Wait for a person**: pause for an approval or a filled-in form. Both arrive by email, and each person gets their own link.
+- **Reuse configuration**: refer to a workspace [constant or secret](./variables/) by name instead of pasting a channel id or a token into every workflow.
+- **Store data between runs**: save a cursor, a counter, or a marker such as "this alert was already handled today" for later runs to read.
+
+[The action catalog](./actions/) lists every action.
+
+## What starts a workflow
+
+| Trigger | Fires when |
+|---|---|
+| **Manual / API** | Someone clicks **Run**, or an API client starts a run. |
+| **Schedule** | An interval or cron expression comes due, in the timezone you pick. |
+| **Webhook** | Something calls the workflow's own URL. |
+| **Form submission** | Someone submits the workflow's public form. |
+| **App event** | Something happens in a connected app, such as a Slack mention or a new Jira issue. |
+
+Alerts start workflows through the webhook trigger. Point a webhook [notification channel](../platform/settings/notification-channels/) at the workflow's URL, then attach the channel to a routing rule. Each notification then starts a run with the alert group payload. See [Triggers](./triggers/). To run a different workflow for each resource in an alert group, start from the [Send alerts to your workflows](./alert-router/) template.
+
+## When to use a workflow
+
+A [routing rule](../alerts/routing-rules/) decides who gets notified about an alert. A workflow decides what to do about it, and a routing rule can start it.
+
+Use the [KloudMate Assistant](../kloudmate-assistant/) for an open-ended investigation, where you don't know what you'll ask next. Turn a task into a workflow after you've done it the same way twice.
+
+## Before you start
+
+- **You need the Developer role to create, edit, publish, or delete a workflow.** Anyone in the workspace can read workflows and run history, and can start a run by hand when the workflow permits it.
+- **Steps and triggers that use an outside account need a [connection](/platform/settings/connections/).** That includes AWS, Azure, Slack, Jira, and MCP servers. The **Connections** tab in Workflows lists the ones a workflow can use, and a step's connection picker can connect a new account without leaving the builder.
+- **Workflow automation is part of the Pro and Business plans.** See [Limits](./limits/).
+
+## Build a workflow
+
+1. Open **Workflows** and click **Create workflow**, then pick a trigger. Manual is a good place to start, because you control when it fires. To start from a ready-made workflow instead, pick one on the **Templates** tab.
+2. Add steps from the palette and configure each one. Click **Run test** on a step to see its real output, so you can build the next step from real values.
+3. Click **Test run** to run the whole draft end to end.
+4. Click **Publish**. The first publish also switches the workflow on.
+
+A trigger starts a run only from the published version, and only while the workflow is enabled. Editing the draft afterward changes nothing until you publish again.
+
+## In this section
+
+- [Build a workflow](./build-a-workflow/)
+- [Triggers](./triggers/)
+- [Control flow](./control-flow/)
+- [Send alerts to your workflows](./alert-router/)
+- [Approvals and collected input](./approvals-and-input/)
+- [The action catalog](./actions/)
+- [Templating](./templating/)
+- [Variables](./variables/)
+- [Storage](./storage/)
+- [Test a workflow](./test-a-workflow/)
+- [Run history](./run-history/)
+- [Limits](./limits/)
+
+## Related
+
+- [Workflow guides](/guides/#workflow-guides) for complete workflows you can import, such as granting temporary SSH access with an approval, deploying from Slack, onboarding a new hire, and running an IT help desk in Slack.
+- [Connections](/platform/settings/connections/) for the credentials a workflow uses.
+- [Routing rules](../alerts/routing-rules/) for sending alerts to a workflow.
+- [Notification channels](../platform/settings/notification-channels/) for the Slack, Teams, webhook, and SNS destinations that steps send through.
diff --git a/src/content/docs/docs/workflows/limits.mdx b/src/content/docs/docs/workflows/limits.mdx
new file mode 100644
index 00000000..e127f382
--- /dev/null
+++ b/src/content/docs/docs/workflows/limits.mdx
@@ -0,0 +1,81 @@
+---
+title: "Limits"
+description: "The limits on a workflow's size, its runs, and its webhook and form URLs, and the plans that include workflow automation."
+sidebar:
+ label: "Limits"
+ order: 11
+---
+
+Most workflows stay well inside these limits. The ones people hit most are the step cap, the loop cap, and the webhook rate limit.
+
+## Workflow definition limits
+
+| Limit | Value |
+|---|---|
+| Steps in one workflow | 50, counting every nested block |
+| Trigger input parameters | 20 |
+| Collect-input fields | 20 |
+| Items in one loop | 100, with up to 10 at once |
+| Parallel branches | 2 to 10 |
+| Paths on one Route | 1 to 10 |
+| Routes on one AI Route | 2 to 8 |
+| Approvers on one step | 10 |
+| Nested workflow calls | 5 deep |
+| Length of one `{{ }}` template | 8192 characters |
+| Whole definition | 100 KB |
+| Run Code source | 64 KB |
+
+## Run limits
+
+| Limit | Value |
+|---|---|
+| One step attempt | 60 seconds by default, 15 minutes at most |
+| Retries per step | 5 at most |
+| **Wait / Delay** | 24 hours |
+| **Wait until** | 24 hours, with at most 20 checks |
+| Approval and collect-input timeout | 24 hours by default, 7 days at most |
+| HTTP request timeout | 10 seconds by default |
+| **Send Email** recipients per step | 20 |
+| **Send Email** emails per workspace per hour | 200 |
+| Approval and input request emails per workspace per hour | 200, counted separately from **Send Email** |
+| Resources from one alert group lookup | 100 resources, from up to 1,000 firing instances |
+| Runs at the same time, per workspace | 20 |
+
+A run that's waiting on a person, between the checks of a **Wait until**, or on a **Wait / Delay** of 1 minute or longer doesn't count toward the 20. A shorter delay does, and so does every run that's running a step.
+
+## Trigger limits
+
+| Limit | Value |
+|---|---|
+| Webhook and form URL | 60 requests per minute, per workflow |
+| App-event trigger | 200 runs per workflow per hour |
+| Any trigger | 500 runs per workflow per 5 minutes |
+
+The 500-run limit stops a workflow that keeps triggering itself. Child runs and test runs don't count toward it.
+
+## Retention
+
+KloudMate keeps finished runs and their step detail for 30 days. It keeps published versions for 30 days too, but always keeps the newest 20 per workflow, the live one, and any version a kept run still uses.
+
+## Variable and storage limits
+
+| Limit | Value |
+|---|---|
+| Workspace variables | 200 per workspace |
+| One variable's value | 8 KB |
+| Storage keys | 10,000 per workspace |
+| One stored value | 64 KB |
+| One stored list | 10,000 items |
+
+## Plans
+
+Workflow automation is part of the Pro and Business plans. On the Free plan, **Workflows** shows an upgrade prompt instead, with **View plans** for organization owners.
+
+Your plan also decides which integrations you can connect. See [Connections → Providers](/platform/settings/connections/providers/).
+
+If a plan lapses, existing webhook deliveries, form submissions, and approval links keep working, so a decision link already in someone's inbox doesn't break. You can no longer create or edit workflows.
+
+## Related
+
+- [Build a workflow](../build-a-workflow/)
+- [Connections](/platform/settings/connections/)
diff --git a/src/content/docs/docs/workflows/run-history.mdx b/src/content/docs/docs/workflows/run-history.mdx
new file mode 100644
index 00000000..4d16a25d
--- /dev/null
+++ b/src/content/docs/docs/workflows/run-history.mdx
@@ -0,0 +1,76 @@
+---
+title: "Run history"
+description: "Find a run, read what each step received and returned, cancel one that is still going, start it again with different data, and check usage across the workspace."
+sidebar:
+ label: "Run history"
+ order: 10
+---
+
+**Workflows → Runs** lists every run in the workspace, newest first, including the runs that **Call Workflow** steps start. Filter by time range, workflow, status, or trigger. The **Child** trigger shows the runs another workflow started. The list marks a **Test run** from the builder with **Test**, so you can tell it from real traffic.
+
+The list doesn't show parallel branches, or loop items that run at the same time, on their own. They appear inside their parent run's timeline.
+
+
+
+## Run statuses
+
+| Status | Meaning |
+|---|---|
+| **Queued** | Accepted, not yet picked up. |
+| **Running** | In progress. |
+| **Waiting approval** / **Waiting input** | Paused for a person. The run doesn't count toward the limit on runs at the same time while it waits. |
+| **Waiting** | Paused on a **Wait / Delay** of 1 minute or longer, or between the checks of a **Wait until**. It doesn't count toward the limit either. |
+| **Waiting children** | Paused while a workflow it called, or one of its parallel branches, waits for a person. |
+| **Succeeded** | Every step finished. |
+| **Failed** | A step failed and the run stopped. |
+| **Rejected** | Someone declined an [approval](../approvals-and-input/). It's a deliberate outcome, not a fault: the run skips the steps after the approval and retries nothing. |
+| **Cancelled** | Someone stopped it. |
+
+KloudMate can also skip a run before it starts: a scheduled fire while the previous run is still going, and an app-event trigger after it passes its hourly cap. The list records both, as **Skipped (concurrent)** and **Skipped (cap)**, so you can see why a run is missing.
+
+## Read a run
+
+Open a row to see the run's detail. A run that another workflow started carries a **Child run** link to its parent. In the parent's timeline, the called workflow's block has an **Open run** link to the child run.
+
+The detail shows:
+
+- **Flow** shows the workflow as it looks in the builder, with each step's result. Select one to see what it received and what it returned.
+- **Timeline** lists every step that ran, in order, as a waterfall with duration and attempt count, nested like the workflow's steps.
+
+
+
+
+
+Each loop item appears as its own entry under the loop, and a step that retried shows its attempt number.
+
+KloudMate stores an input or output larger than 256 KB as a preview of its first 1,024 characters, marked `_truncated`. Each step also lists the paths that resolved to nothing.
+
+A step paused for a person shows who can respond and when the request expires.
+
+## Cancel a run
+
+**Cancel** stops a run that's still going, including one waiting on an approval. Cancelling doesn't undo steps that already ran, and you can't cancel a run that already finished.
+
+## Start a run again
+
+**Run again** starts a new run with the original run's trigger data, pre-filled and editable, so you can change only what needs to differ.
+
+It runs the **current published version**, not the version the original ran, so it picks up any fix you've published since.
+
+You can't re-run a test run this way, because it ran a draft, not a published version.
+
+## Check workflow usage
+
+**Workflows → Usage** shows how workflows are doing across the workspace over the time range you pick, the last 7 days by default. Its tiles show the **Runs** that finished, the **Failed** runs, the **Success Rate %**, and the **Credits** used, above a chart of **Runs and Failures Over Time**. It doesn't count test runs from the builder.
+
+Each run counts 1 credit, and each **AI Prompt**, **AI Extract**, or **AI Route** step it runs adds 2. A workflow that a **Call Workflow** step starts counts as a run of its own. Loop items and parallel branches don't. Workflow credits are separate from the AI credits the [KloudMate Assistant](../../kloudmate-assistant/) uses.
+
+## Run retention
+
+KloudMate keeps finished runs and their step detail for 30 days. See [Limits](../limits/#retention) for how long published versions last.
+
+## Related
+
+- [Test a workflow](../test-a-workflow/) for checking a workflow before it reaches this list.
+- [Build a workflow](../build-a-workflow/) for versions and enabling.
+- [Limits](../limits/) for the caps that produce a skipped run.
diff --git a/src/content/docs/docs/workflows/storage.mdx b/src/content/docs/docs/workflows/storage.mdx
new file mode 100644
index 00000000..36a11c87
--- /dev/null
+++ b/src/content/docs/docs/workflows/storage.mdx
@@ -0,0 +1,121 @@
+---
+title: "Storage"
+description: "Save a cursor, a counter, or a dedupe marker in one run, and read it in the next."
+sidebar:
+ label: "Storage"
+ order: 8
+---
+
+A run keeps nothing after it ends. Storage saves values that later runs can read, so a workflow can skip duplicates, keep a count, or continue from where the last run stopped.
+
+Use it to notify about something only once a day, continue from where the last run stopped, stop repeating a fix that isn't working, or pause automation during maintenance. See [Examples](#examples).
+
+The actions are under **Utilities → Storage** and need no connection.
+
+| Action | What it does | Output |
+|---|---|---|
+| **Storage: Get** | Read a stored value, with a default when the key isn't there. | `value`, `found` |
+| **Storage: Put** | Store a value, replacing what was there. | `value` |
+| **Storage: Increment** | Add to a stored number. A negative amount counts down, and a key that doesn't exist starts at 0. | `value` |
+| **Storage: Add to List** | Add one item, or several at once. `added` is `false` when **Skip if already there** stopped it. | `value`, `length`, `added` |
+| **Storage: Remove from List** | Take items out of a list. Removing something that isn't there isn't an error. | `value`, `length`, `removed` |
+| **Storage: Remove** | Delete a key. | `removed` |
+
+## Scope
+
+Every action has a **Scope** that decides where the key lives:
+
+- **This workflow**, the default, keeps the key private to the workflow that wrote it, so two workflows can both use `cursor` without colliding.
+- **This workspace** shares the key with every workflow. Pick it only when two workflows need to read the same value.
+
+## Keys and values
+
+A key is 1 to 200 characters of letters, digits, and `_ : . -`. It can include a template, so `alerted:{{ trigger.body.group.id }}` makes a separate key for each alert group.
+
+A value you type is stored as text. For a list or an object, type valid JSON with quoted strings, like `["apple","banana"]`. A bare `[apple, banana]` isn't valid JSON, so it's stored as text too.
+
+A value can be up to 64 KB and a list up to 10,000 items, and a workspace holds up to 10,000 keys. At the key limit, a write that adds a key fails, because KloudMate never evicts keys to make room.
+
+## Expiry
+
+**Expires after** takes a duration such as `24h` or `30m`. If you leave it blank, the key lives until something deletes it.
+
+Publishing refuses a typed value that isn't a duration, such as `30 min`. KloudMate checks a value built from a template when the step runs, and the step fails if the value isn't a duration.
+
+Only the actions that can create a key have this field. Leaving it blank never removes an expiry that an earlier write set.
+
+An expired key counts as missing right away. The Storage tab lists it as **Expired** until a daily cleanup removes it.
+
+## Examples
+
+Each example uses readable step ids, such as `steps.cursor`, to show the shape. In the builder, pick references from the variable picker instead of typing them.
+
+### Notify once a day per alert group
+
+An alert group can start many runs while it's open. To notify about it only once a day, use a marker key that expires on its own:
+
+1. Add a **Storage: Get** step with the key `alerted:{{ trigger.body.group.id }}`. Its `found` output is `false` the first time that group arrives.
+2. Add a **Branch** whose condition checks that the step's `found` equals `false`.
+3. In the **Then** block, send the notification. Then add a **Storage: Put** step for the same key, with **Expires after** set to `24h`.
+
+**Storage: Add to List** has a **Skip if already there** option that checks for the item and adds it in one operation. Its `added` output is `true` only for the run that added the item. When two runs can race, use it instead of the read-then-write pattern above.
+
+### Continue from where the last run stopped
+
+A scheduled workflow that processes new items, such as events from an API, can keep a cursor so that each run handles only what's new:
+
+1. Add a **Storage: Get** step, `cursor`, for the key `last_run`. Set its **Default** to a start time, such as `2026-01-01T00:00:00Z`, for the first run.
+2. Fetch only the newer items, for example with an **HTTP Request** to `https://api.example.com/events?since={{ steps.cursor.output.value }}`.
+3. Process the items.
+4. Add a **Storage: Put** step for `last_run` with the value `{{ trigger.schedule.fired_at }}`, the time this run was scheduled.
+
+If a run fails before the last step, the cursor doesn't move, so the next run picks up the same items again. For a complete example, see [Trigger a workflow when a new email is received in Gmail](/guides/workflows/trigger-on-new-gmail/).
+
+### Stop repeating a fix that isn't working
+
+A workflow that restarts a service can count its restarts, and hand over to a person when restarting doesn't help:
+
+1. Add a **Storage: Increment** step, `restarts`, for the key `restarts:{{ trigger.inputs.host }}`, with **Expires after** set to `1h`.
+2. Add a **Branch** whose condition checks that `steps.restarts.output.value` is greater than `3`.
+3. In the **Then** block, ask a person to look, for example with an **Approval** or a Jira **Create Issue** step. In the **Else** block, restart the service with **Run Command on Host**.
+
+Each increment restarts the expiry, so the count clears after an hour without a restart.
+
+### Collect results and report them later
+
+A workflow that fixes hosts can build a list for a daily report that another workflow sends:
+
+1. In the workflow that fixes hosts, add a **Storage: Add to List** step with **Scope** set to **This workspace**, the key `fixed_hosts`, and the host as the **Item**.
+2. In a second workflow on a daily **Schedule**, add a **Storage: Get** step for `fixed_hosts`, email the list, then clear it with **Storage: Remove**.
+
+Both workflows use the key, so its **Scope** must be **This workspace**.
+
+### Pause automation during maintenance
+
+One workflow can switch the others off for a maintenance window:
+
+1. Create a workflow with a **Manual / API** trigger and a **Storage: Put** step with **Scope** set to **This workspace**, the key `maintenance`, the value `true`, and **Expires after** set to the length of the window, such as `2h`.
+2. Start each workflow you want to pause with a **Storage: Get** step for `maintenance`, followed by a **Branch** that checks whether `found` is `true`. Put the rest of the workflow in the **Else** block.
+
+When the key expires, the workflows go back to normal on their own.
+
+### Act on a resource only once in a while
+
+The [Send alerts to your workflows](../alert-router/#how-the-cooldown-works) template uses **Storage: Increment** as a cooldown, so it acts on a resource once and then leaves it alone while it keeps alerting.
+
+## View and delete stored keys
+
+**Workflows → Storage** lists every key in the workspace with its scope, type, size, expiry, and when it last changed. The list doesn't show values. To read one, open **View value** on its row.
+
+
+
+It's the only place to remove a key by hand. A row's menu deletes that key, or clears every key a given workflow wrote.
+
+:::note
+Storage holds data that runs wrote, not configuration you set. Exports never include it, and you can delete a key even while workflows use it. [Workspace variables](../variables/) work the other way on both counts.
+:::
+
+## Related
+
+- [Variables](../variables/) for values you set rather than values a run writes.
+- [The action catalog](../actions/) for the rest of the palette.
diff --git a/src/content/docs/docs/workflows/templating.mdx b/src/content/docs/docs/workflows/templating.mdx
new file mode 100644
index 00000000..16cf8270
--- /dev/null
+++ b/src/content/docs/docs/workflows/templating.mdx
@@ -0,0 +1,103 @@
+---
+title: "Templating"
+description: "Use the trigger payload, an earlier step's output, and workspace variables in any field that accepts a template, and format values with Liquid."
+sidebar:
+ label: "Templating"
+ order: 7
+---
+
+Most fields on a step accept `{{ }}` templates, and every template can reference the same data. Each field that accepts templates has a variable picker. After you test a step, the picker shows a real example value next to every path that step produces.
+
+## What a template can reference
+
+| Reference | What it holds |
+|---|---|
+| `{{ trigger.* }}` | The trigger payload. Its shape depends on the trigger. See [Triggers](../triggers/). |
+| `{{ steps..output.* }}` | What an earlier step returned. |
+| `{{ steps..error }}` | The error message, when that step is set to continue on failure. |
+| `{{ item }}` and `{{ item_index }}` | The current element inside a loop, and its position. |
+| `{{ vars. }}` | A workspace constant. See [Variables](../variables/). |
+| `{{ secrets. }}` | A workspace secret. See [Variables](../variables/). |
+
+The builder generates a step's id when you add it, and renaming the step doesn't change it. Don't type these references. Pick them from the variable picker, which lists steps under the names you gave them and inserts the right id. Examples on these pages use readable ids such as `steps.lookup` to show the shape.
+
+## Write a template
+
+Templates use [Liquid](https://liquidjs.com/tutorials/intro-to-liquid.html). `{{ }}` outputs a value, `{% %}` runs logic such as an `if` or a `for` loop, and a filter after a `|` changes a value before it's output. The standard filters and tags all work, apart from tags that load another file, such as `include`. The [LiquidJS filter reference](https://liquidjs.com/filters/overview.html) and [tag reference](https://liquidjs.com/tags/overview.html) list them with examples.
+
+A template string can be up to 8192 characters long.
+
+Mix references with plain text:
+
+```liquid
+Bearer {{ secrets.stripe_key }}
+```
+
+When a field might be missing, give it a fallback with `default`:
+
+```liquid
+Service: {{ trigger.body.group.labels.service | default: "unknown" }}
+```
+
+Show different text depending on a condition:
+
+```liquid
+{{ trigger.body.group.title }}: {% if trigger.body.group.severity == "sev1" %}page the on-call engineer.{% else %}review it during working hours.{% endif %}
+```
+
+Write a line for each item in a list:
+
+```liquid
+{% for rule in trigger.body.rules %}
+- {{ rule.alarm_name }}: {{ rule.instances.size }} instances
+{% endfor %}
+```
+
+`tz` is a KloudMate filter that shows a timestamp in the timezone you name, or in UTC if you don't name one. With an `opened_at` of `2026-05-23T10:12:00Z`, this prints `Opened 2026-05-23 06:12:00 EDT`:
+
+```liquid
+Opened {{ trigger.body.group.opened_at | tz: "America/New_York" }}
+```
+
+Turn a comma-separated constant into a list for a JSON field:
+
+```liquid
+{{ vars.regions | split: "," }}
+```
+
+## How values render
+
+- **A field is a template only when it contains `{{`.** A value made only of `{% %}` tags is used exactly as written. To choose a value with tags alone, assign it and output it at the end, as in `{% if x %}{% assign p = "High" %}{% else %}{% assign p = "Low" %}{% endif %}{{ p }}`.
+- **A lone reference keeps its type.** When a field holds one reference and nothing else, such as `{{ trigger.body }}` or `{{ item.pods }}`, the step gets the value itself: an object stays an object, and a list stays a list. Anything else renders as text, with an object or a list inside it written out as JSON.
+- **A loop's Items takes a plain reference.** A filter turns the list into text, and a [loop](../control-flow/#loop-for-each) fails when **Items** isn't a list.
+- **`0` and empty text count as false.** `{% if count %}` is false when `count` is `0`, and `default` replaces `0` as well as a missing value. To check whether a value is set at all, compare it with `nil`, as in `{% if count != nil %}`.
+- **Write an array position with a dot**, as in `trigger.body.rules.0.instances.0.labels`. `[0]` also works in a template on an action's field, but condition fields and a loop's **Items** read `rules[0]` as a key name. Publishing refuses `[0]` there and suggests the dot form.
+- **Put a key with spaces or punctuation in quoted brackets**, as in `{{ trigger.body["Alert Name"] }}`. This works in condition fields too.
+- **Publishing parses every template.** A syntax error or a filter that doesn't exist stops the publish, and the error names the field.
+
+The variable picker writes array positions and quoted keys in the right form for you.
+
+## Escape values in an email
+
+**Send Email** accepts HTML in its **Message**, and anyone who has a workflow's webhook URL can write the payload. A payload value placed straight into the message can add markup of its own, such as a link that points somewhere else. Pass every value you didn't write yourself through `escape`, including payload fields, command output, resource names, and error messages:
+
+```liquid
+Alert: {{ trigger.body.group.title | escape }}
+Labels: {{ trigger.body.group.labels | json | escape }}
+```
+
+For a list or an object, add `json` before `escape`. Without it, `escape` prints an object as `[object Object]`.
+
+`escape` changes only HTML characters (`<`, `>`, `&`, and quotes), so Markdown in the value, including a link, still renders. The subject line is plain text and needs no escaping, and KloudMate escapes the messages of Approval and Collect Input steps for you.
+
+## Unresolved references
+
+An unknown reference renders empty rather than failing. The Test panel lists every path that resolved to nothing under **Unresolved variables**, and the run timeline records the same list per step, so check both. The list includes a reference with a `default` too, so you can see when it used the fallback.
+
+Publishing does reject a reference to a step that doesn't exist, and names the step.
+
+## Related
+
+- [Variables](../variables/) for the workspace constants and secrets that `vars` and `secrets` read.
+- [Test a workflow](../test-a-workflow/) for getting real values into the variable picker.
+- [The action catalog](../actions/) for which fields accept templates.
diff --git a/src/content/docs/docs/workflows/test-a-workflow.mdx b/src/content/docs/docs/workflows/test-a-workflow.mdx
new file mode 100644
index 00000000..6032ddcd
--- /dev/null
+++ b/src/content/docs/docs/workflows/test-a-workflow.mdx
@@ -0,0 +1,99 @@
+---
+title: "Test a workflow"
+description: "Run one step at a time against real data, capture a real trigger payload, and run the whole draft before you publish it."
+sidebar:
+ label: "Test a workflow"
+ order: 9
+---
+
+You can't build a step on `{{ steps.lookup.output.instance_id }}` until you know what the lookup returns. Run a step, check its output, then build the next step against those real values.
+
+## Test one step
+
+Open a step's **Test** tab and click **Run test**. **Input** shows the payload the action received after its templates rendered, so you can check whether a template resolved the way you expected. **Output** is what the step returned.
+
+The result is one of these:
+
+- **The step ran and returned.** Both panes fill in, and the output becomes the step's sample.
+- **The step ran and failed.** The panel shows the provider's own error.
+- **The step needs a real run.** **Run Command on Host** doesn't run its command from the Test tab. It shows the shape of its output with empty values instead, and saves that as the sample. Use [Test run](#test-the-whole-workflow) to run the command for real.
+- **The test couldn't run at all.** Nothing ran, and the step isn't marked as failed. Try again.
+
+If any template resolved to nothing, the panel lists the paths under **Unresolved variables**. Fix those before you publish. Publishing catches a reference to a step that doesn't exist, but not a wrong path under a step that does, because a template that renders empty is still valid.
+
+:::caution
+A test runs the real action. Posting a Slack message posts it, and creating a Jira issue creates it.
+:::
+
+Secrets never show in the input. A connection's credential stays on the server, and a credential-looking value you typed into a header shows as `••••`.
+
+### Saved samples
+
+A successful test saves its output automatically. The next step's variable picker offers that saved output, and the next step's test runs against it, so testing a later step doesn't re-run an earlier step that sends or writes something.
+
+Each step keeps its last three outputs, and you can switch which one the picker uses.
+
+A sample's timestamp shows when that exact payload was first seen, not when you last clicked Test. If a test returns the same bytes again, the sample keeps its older time.
+
+
+
+To try a path your real data doesn't reach, click the pencil icon (**Edit sample**). Then change the saved output by hand. A sample must be a JSON object, up to 1024 KB.
+
+Samples exist only for building. A real run never uses them.
+
+### Test a control-flow step
+
+A branch, loop, or wait has nothing to execute, so its test works from the saved samples instead. It resolves the loop's item list, matches the branch's condition, and computes the wait's duration. A **Wait until** shows when it would check and whether its condition holds now, without running the check block.
+
+Testing a **Route** checks every path's condition against the samples, shows whether each one matched, and names the path that would run. It doesn't run that path's steps.
+
+**AI Route** is the exception. It calls the model and shows which route it would take, and why.
+
+**Parallel** and **Call Workflow** report the shape they'd produce without starting anything. A Call Workflow preview returns the sample of the called workflow's last step, so you see what the call returns. That sample exists after the called workflow has been tested, on its own or through a **Test run** of a workflow that calls it.
+
+### Test an Approval or Collect Input step
+
+Testing an **Approval** or **Collect Input** step renders its message and emails a clearly marked test to the people named on it, with no working link. If nobody is named yet, no email goes out.
+
+- **An Approval test** returns `approved: true`, with `decidedBy` set to the first approver, or to you if nobody is named yet. To build the path for a rejection or a timeout, edit the sample.
+- **A Collect Input test** opens the form itself, so you can type the values that later steps read, then click **Save sample**.
+
+## Capture a trigger sample
+
+A trigger sample gives `{{ trigger.* }}` a real payload to test against. How you capture one depends on the trigger:
+
+| Trigger | How to get a sample |
+|---|---|
+| **Manual** and **Form** | Fill in **Test input values** and click **Test trigger**. |
+| **Schedule** | Click **Test trigger**. You get the real next fire time and its timezone-local parts. |
+| **Webhook** | Send a request to the webhook URL, then click **Reload**. |
+| **App event**, polled | Click **Test trigger**. Pick from the app's five newest items, listed newest first as **Result 1** onward. |
+| **App event**, pushed | Click **Test trigger**. You get the last delivery this workflow ran on, or KloudMate starts listening for the next one. |
+
+
+
+### Capture a webhook sample
+
+Send a request to the URL, then click **Reload**. You can capture a sample on a draft as well as a live workflow, so you don't need to publish first.
+
+An alert-driven workflow gets its sample the same way. Point a webhook notification channel at the URL and attach the channel to a routing rule. The next alert notification then becomes the sample, under `trigger.body`.
+
+### Capture a pushed app event
+
+KloudMate can't fetch a pushed event on demand. When the workflow hasn't run on one yet, **Test trigger** waits for the next delivery and tells you what to do in the app, for example mentioning the bot in a Slack channel. If nothing arrives before the window closes, click **Listen again**.
+
+## Test the whole workflow
+
+**Test run** runs the draft end to end through the real engine. Every step's input and output shows in the run timeline, as in a live run, so use it to see how the steps fit together rather than how a single step behaves. The draft must pass the builder's structure checks before it can run.
+
+`{{ trigger.* }}` comes from the trigger sample you saved, so capture a real payload once and replay it as often as you like. When there's a history of captured payloads, pick the one to replay under **Run against**. For a manual or form trigger, fill in the **Trigger inputs** instead.
+
+A test run does everything a real run does. It runs **Run Command on Host**, waits for real approvals and form submissions, waits out delays, and calls the published version of any workflow it calls. Run history marks it **Test**, so you can tell it apart from real traffic.
+
+A test run also saves each step's output as that step's sample, including the steps of the workflows it calls, so you don't have to test them one by one. The new samples appear the next time you open the builder.
+
+## Related
+
+- [Build a workflow](../build-a-workflow/) for publishing after the tests pass.
+- [Run history](../run-history/) for reading a run after it happened.
+- [Templating](../templating/) for what the picker offers.
diff --git a/src/content/docs/docs/workflows/triggers.mdx b/src/content/docs/docs/workflows/triggers.mdx
new file mode 100644
index 00000000..0678b119
--- /dev/null
+++ b/src/content/docs/docs/workflows/triggers.mdx
@@ -0,0 +1,142 @@
+---
+title: "Triggers"
+description: "Start a workflow on demand, on a schedule, from an inbound webhook, from a public form, or from an event in a connected app."
+sidebar:
+ label: "Triggers"
+ order: 3
+---
+
+A trigger is the event that starts a workflow. Every workflow has exactly one, and it's part of the published version. Changing it is a draft edit like any other, so the old trigger keeps working until you publish.
+
+
+
+## Manual / API
+
+Starts when someone clicks **Run**, or when an API client starts a run. Use it while you're building, for one-off remediations, and for anything a person decides to start.
+
+Define the workflow's **Input parameters** here. Each has a name, a type, and an optional default:
+
+| Type | What it takes |
+|---|---|
+| **Text** | A single line of text. |
+| **Long text** | Several lines of text. |
+| **Email** | An email address, checked when the run starts. |
+| **Number** | A number. |
+| **Checkbox** | True or false. |
+| **Choice** | One of a list of options you set. Turn on **Allow multiple selections** to accept several, which arrive as a list. |
+
+Mark a parameter required to refuse any run that leaves it out. KloudMate also refuses a value of the wrong type, such as text in a **Number** or an option that isn't on a **Choice** list.
+
+Those parameters become the form someone fills in when they start the run, and steps read them as `{{ trigger.inputs. }}`.
+
+:::note
+The **Run** button appears only for a manual or form trigger. To start a scheduled or webhook workflow by hand, open one of its past runs and click **Run again**, or call the API.
+:::
+
+## Schedule
+
+Starts on a recurring schedule, in the timezone you pick. Use it for a nightly report, a weekly cleanup, or any check that has to happen whether or not anything alerted.
+
+**Repeat** takes an interval in minutes, hourly, daily, weekly, monthly, or a raw cron expression. Turn off **Run on weekends** on a daily schedule to run Monday through Friday only.
+
+As you edit, the panel previews the next few fire times, so you can check a cron expression against real dates before you publish.
+
+Before you rely on a schedule:
+
+- **A schedule can't fire faster than once a minute.** Publishing rejects an interval or a cron expression that asks for more.
+- **A scheduled run is skipped while the previous one is still going.** Run history records the skip as its own row, so you can see a workflow that keeps overrunning its interval.
+- **Missed fire times become one catch-up run.** If you disable a workflow over a weekend, enabling it again doesn't produce a burst of runs.
+
+Steps read the fire time as `{{ trigger.schedule.fired_at }}`, and its parts in your timezone as `{{ trigger.schedule.date }}`, `{{ trigger.schedule.hour }}`, `{{ trigger.schedule.weekday }}`, and so on.
+
+## Webhook
+
+Starts when something calls the workflow's own URL. Open the trigger panel to copy it. Use it for anything that can send an HTTP request, including KloudMate alerts.
+
+The URL accepts both `POST` and `GET`. A `POST` carries JSON in the body, up to 256 KB. A `GET` passes its data in the query string, for a sender that can only open a URL.
+
+Steps read the body, query, headers, and method separately:
+
+```text
+{{ trigger.method }}
+{{ trigger.query.page }}
+{{ trigger.body.event }}
+{{ trigger.headers.x-github-event }}
+```
+
+KloudMate strips any authorization header the sender supplies before the delivery reaches templates or samples.
+
+### Filter webhook deliveries
+
+**Conditions** checks each delivery against paths such as `body.event`, `query.source`, `headers.x-github-event`, or `method`. A delivery that doesn't match is acknowledged but starts no run. Leave it empty to run on every delivery.
+
+### Ignore duplicate deliveries
+
+Point **Deduplication key** at an id the sender provides, such as `{{ body.id }}`. Two deliveries with the same key value start only one run, and the second gets a success response. If you leave the key empty, every delivery starts its own run.
+
+A key that comes out empty counts as unset. If you point it at a field the sender sometimes leaves out, those deliveries each start their own run instead of counting as duplicates.
+
+### Rotate the webhook URL
+
+**Rotate** replaces the URL, and the old one stops working immediately. Update your senders first. Afterwards the panel lists any notification channel that still holds the old URL, so you can point each one at the new URL. Rotating also changes the public form URL.
+
+### Start a workflow from an alert
+
+There's no separate alert trigger. Alerts arrive at the webhook URL:
+
+1. Create a webhook [notification channel](../../platform/settings/notification-channels/) pointing at the workflow's URL.
+2. Attach that channel to a [routing rule](../../alerts/routing-rules/), the same way you'd attach Slack or email.
+
+The trigger panel names the notification channel that sends alerts to this URL, and warns when a channel still points at an older URL. When no channel does yet, **Run this workflow when an alert fires** links to both steps.
+
+The channel posts the alert group payload as the request body, so steps read `{{ trigger.body.group.title }}`, `{{ trigger.body.group.labels.* }}`, and the rest. `{{ trigger.body.event }}` holds the event type: `opened`, `appended`, `repeat`, `resolved`, or `rca_completed`.
+
+Watch for both of these:
+
+- **Silences apply.** A silenced group never reaches the channel, so it never starts a run.
+- **A group can start more than one run.** `appended` arrives every time an alert joins an active group, and `repeat` arrives as a reminder on the routing rule's [repeat interval](../../alerts/routing-rules/#repeat-interval) while the group stays open. A routing rule can't filter by event type, so filter in the trigger: match on `body.event` equal to `opened` to take the group once.
+
+To act on the resources in an alert group, start from the [Send alerts to your workflows](../alert-router/) template.
+
+## Form submission
+
+Starts when someone submits the workflow's public form. Use it to let people request something that runs under your control, such as a sandbox reset. Copy the page's address from **Public form URL**.
+
+The fields are the workflow's own input parameters, so they work exactly as they do for a manual run. The trigger also sets the **Form title**, a **Description** shown under it, and the **Success message** shown after a submission. Each submission starts its own run.
+
+Turn on **Sign-in required** to limit the form to members of this workspace. A signed-out visitor signs in first, through SSO if your organization uses it, and then lands back on the form. The form refuses anyone signed in who isn't a member. Steps then read who submitted it from `{{ trigger.submitted_by }}`, with `id`, `name`, and `email`. On an open form, that field isn't set.
+
+A form page loads only when the workflow is published and enabled. A draft form returns a 404, unlike a draft webhook, which still accepts deliveries so you can capture a sample before publishing.
+
+Rotating the webhook URL also changes the form's URL.
+
+## App event
+
+Starts on an event in a connected app. Pick the app's event in the trigger menu, for example **Bot is mentioned** under Slack, then pick the **Account** the events come from.
+
+The account has to be a [connection](/platform/settings/connections/) with the **Workflows** capability. The picker lists every connection for the app and flags one without it as **Not enabled for workflows**, and publishing refuses a trigger that uses one. To fix it, add **Workflows** to the connection under **Settings → Connections**. If the connection is then marked **Needs reconnect**, reconnect it.
+
+| App | Events |
+|---|---|
+| Slack | Bot is mentioned, New message in a channel, New direct message to the bot, Reaction added |
+| Jira Cloud | New issue, Issue updated, New comment, Status changed |
+
+Slack pushes its events. KloudMate polls Jira for new ones, and the first poll only records the current state, so a project full of existing issues doesn't fire a run for each one.
+
+KloudMate deduplicates events on the app's own item id, so a redelivery starts one run.
+
+An app-event trigger stops in any of these cases:
+
+- **The connection stopped working for workflows.** It was switched off, lost the **Workflows** capability, or shows **Revoked** or **Degraded** under **Settings → Connections**. Fix the connection there.
+- **The app refused the credential**, for example because the access was revoked. Reconnect the connection.
+- **The trigger hit its hourly cap.** The workflow started more than 200 runs from this trigger within an hour. Run history records each refused event as a skipped run, and after three the trigger stops.
+
+After you fix the cause, switch the workflow off and on again to restart a stopped trigger.
+
+Steps read the event as `{{ trigger.event.* }}`, alongside `{{ trigger.provider }}` and `{{ trigger.trigger_key }}`. The payload has the app's own format, so capture a real one before you build steps on it. See [Test a workflow](../test-a-workflow/).
+
+## Related
+
+- [Test a workflow](../test-a-workflow/) for capturing a real trigger payload.
+- [Templating](../templating/) for using `{{ trigger.* }}` in a field.
+- [Limits](../limits/) for the rate limits on the webhook and form URLs.
diff --git a/src/content/docs/docs/workflows/variables.mdx b/src/content/docs/docs/workflows/variables.mdx
new file mode 100644
index 00000000..cca9d275
--- /dev/null
+++ b/src/content/docs/docs/workflows/variables.mdx
@@ -0,0 +1,92 @@
+---
+title: "Variables"
+description: "Set a constant or a secret once in the workspace, and refer to it by name from every workflow that needs it."
+sidebar:
+ label: "Variables"
+ order: 7.5
+---
+
+Variables hold configuration you set once and reuse across workflows, such as a channel id, a list of regions, or an API token. Open **Workflows → Variables** and click **Add variable**.
+
+Pick **Constant** or **Secret** when you create one. The kind decides how you reference the value, and how KloudMate stores and reads it:
+
+| | Constant | Secret |
+|---|---|---|
+| Reference | `{{ vars. }}` | `{{ secrets. }}` |
+| Shown in the list | Yes | No |
+| In run history | The real value | `secret: ` |
+| Read | Once per run, then frozen | Per step, so a rotation mid-run takes effect on the next step |
+
+A name uses letters, digits, and underscores, starts with a letter or an underscore, and is up to 64 characters long. `STRIPE_KEY` and `slack_channel` are both fine. A workspace holds up to 200 variables.
+
+Reference a variable from any field that accepts a template, for example `Bearer {{ secrets.stripe_key }}` in an HTTP header. See [Templating](../templating/).
+
+
+
+The list shows who set each variable, when its **value** last changed, and, for a secret, when a run last used it. To check whether a secret was rotated after an incident, read the value-changed column. Editing a description doesn't change it.
+
+A variable holds a value you set. If a run needs to write a value for later runs, use [Storage](../storage/) instead.
+
+## Examples
+
+### Reuse an address in every workflow
+
+Store the on-call team's email address as a constant named `oncall_email`, and set **To** to `{{ vars.oncall_email }}` on every **Send Email** step. When the address changes, you update the variable instead of every workflow. The same works for any value you type into many workflows.
+
+### Call an API with a token
+
+For a service that has no provider of its own in KloudMate, store its API token as a secret, for example `inventory_api_token`. Then set an **HTTP Request** step's **Authentication** to **Bearer token**, with the token set to `{{ secrets.inventory_api_token }}`. The token never appears in the workflow definition or in run history.
+
+If the service supports OAuth 2.0, use an [OAuth 2.0 connection](/platform/settings/connections/providers/#tools-and-custom-endpoints) instead, so KloudMate refreshes the token for you.
+
+### Run the same workflow in staging and production
+
+Create the same variable names in each workspace, with each workspace's own values, and use them in steps instead of typing the values in. For example, an **HTTP Request** step can call:
+
+```liquid
+{{ vars.api_base_url }}/hosts/{{ trigger.inputs.host }}
+```
+
+When you [export a workflow](../build-a-workflow/#export-and-import) from staging and import it into production, it uses production's `api_base_url`, because the import matches variables by name and kind.
+
+### Tune a value in one place
+
+Keep a number you might adjust in a constant, and use it in every workflow that needs it. For example, a **Run Command on Host** step can list the largest directories under `/var`, with `report_top_n` deciding how many:
+
+```liquid
+du -xh --max-depth=2 /var | sort -rh | head -{{ vars.report_top_n }}
+```
+
+## What a secret protects against
+
+A secret never enters a workflow definition or run history. KloudMate substitutes it into the step's inputs at the last moment, after everything has been recorded, so run detail shows `secret: stripe_key` where the value would be. If a step's output comes back carrying the secret, KloudMate rewrites it to `••••` before the output is recorded or read downstream, and the step still succeeds.
+
+This keeps the value out of run history and exports, and away from someone looking over your shoulder. It doesn't protect against someone who can edit workflows, because they can send a secret to their own server with an HTTP step. A [connection](/platform/settings/connections/) has the same limit, which is why both need the **Developer** role.
+
+KloudMate doesn't mask values shorter than 8 characters in step output, so don't rely on the masking for a short secret.
+
+## Rotate, rename, or delete a variable
+
+**Rotate** a secret by editing it and typing a new value. To keep the stored secret, leave the value blank.
+
+The first time you **Rename** a variable, KloudMate refuses and lists the workflows that use it. Confirm to go ahead. A template refers to a variable by name, so renaming it breaks every template that uses the old name, including the ones in published versions, which can't be rewritten.
+
+KloudMate refuses to **Delete** a variable while any workflow still uses it, and lists those workflows so you can fix them. There's no confirmation to override it.
+
+**You can't convert a variable to the other kind.** Turning a constant into a secret would move its reference from `vars` to `secrets`, so every `{{ vars.x }}` in the workspace would start resolving empty. Delete it and create the other kind.
+
+## Reveal a secret
+
+**Reveal value** shows the plaintext to whoever created the secret, or to an organization **Owner**. Other users with the **Developer** role can't reveal it, even though they can rotate or delete it. KloudMate records each reveal and who made it.
+
+## Missing variables
+
+Publishing fails if a workflow uses a secret the workspace doesn't have, and the error names the key. A constant that doesn't exist resolves empty, like any other unknown reference, so check **Unresolved variables** in the Test panel.
+
+When you [import a workflow](../build-a-workflow/#export-and-import), the import matches each variable it uses by name and kind in the destination workspace.
+
+## Related
+
+- [Templating](../templating/) for using a variable in a field.
+- [Storage](../storage/) for values a run writes, rather than values you set.
+- [Connections](/platform/settings/connections/) for account credentials, such as an AWS role or a Slack install.
diff --git a/src/content/docs/guides/index.mdx b/src/content/docs/guides/index.mdx
index 2226792d..737d7565 100644
--- a/src/content/docs/guides/index.mdx
+++ b/src/content/docs/guides/index.mdx
@@ -19,6 +19,41 @@ Instrument LLM and agent applications and send their traces to KloudMate.
- [Instrument a Python LLM App with OpenLLMetry](/guides/llm/instrument-python-app-with-openllmetry/)
+## Workflow Guides
+
+Build workflows that automate work across KloudMate and the tools your teams use. Each guide includes a workflow you can import and adapt.
+
+### Site reliability
+
+- [Grant temporary SSH access to a server, with approval](/guides/workflows/temporary-ssh-access/)
+- [Stop development instances at night](/guides/workflows/stop-dev-instances-at-night/)
+- [Patch hosts one at a time](/guides/workflows/patch-hosts-one-at-a-time/)
+- [Post a weekly reliability report with AI](/guides/workflows/weekly-reliability-report/)
+
+### Developer teams
+
+- [Deploy from Slack, with approval for production](/guides/workflows/deploy-from-slack/)
+- [Triage new Jira bugs with AI](/guides/workflows/triage-jira-bugs/)
+- [Tell the author when their build fails](/guides/workflows/notify-author-of-failed-build/)
+- [Summarize a Slack thread with AI](/guides/workflows/summarize-slack-thread/)
+
+### Company processes
+
+- [Onboard a new hire](/guides/workflows/onboard-a-new-hire/)
+- [Offboard a leaver](/guides/workflows/offboard-a-leaver/)
+- [Remind owners before a license or contract renews](/guides/workflows/renewal-reminders/)
+
+### Help desk and support
+
+- [Run an IT help desk in Slack](/guides/workflows/it-help-desk-in-slack/)
+- [Answer common questions in a help channel](/guides/workflows/answer-common-questions/)
+- [Send requests to the right team with AI Route](/guides/workflows/route-requests-with-ai/)
+- [Escalate urgent support tickets to engineering](/guides/workflows/escalate-urgent-tickets/)
+
+### Connect other services
+
+- [Trigger a workflow when a new email is received in Gmail](/guides/workflows/trigger-on-new-gmail/)
+
## OpenTelemetry Instrumentation Tutorials
Step-by-step tutorials on how to instrument sample applications from scratch using OpenTelemetry zero-code and SDK instrumentation.
diff --git a/src/content/docs/guides/workflows/_meta.json b/src/content/docs/guides/workflows/_meta.json
new file mode 100644
index 00000000..6dfd97a2
--- /dev/null
+++ b/src/content/docs/guides/workflows/_meta.json
@@ -0,0 +1,4 @@
+{
+ "label": "Workflows",
+ "order": 5
+}
diff --git a/src/content/docs/guides/workflows/answer-common-questions.mdx b/src/content/docs/guides/workflows/answer-common-questions.mdx
new file mode 100644
index 00000000..96512220
--- /dev/null
+++ b/src/content/docs/guides/workflows/answer-common-questions.mdx
@@ -0,0 +1,307 @@
+---
+title: "Answer common questions in a help channel"
+description: "Answer repeat questions in a Slack help channel with answers your IT team wrote, and open a Jira ticket for every other request."
+sidebar:
+ order: 13
+---
+
+Most questions in an IT help channel are ones the team has answered many times before: how to connect to the VPN, how to reset a password, which Wi-Fi network to use. This guide builds a workflow that answers those in a thread, with answers your team wrote and keeps up to date. Any other request gets a 👀 reaction, so the person knows it was seen, and a Jira ticket for the IT team to pick up.
+
+An **AI Extract** step sorts each message into a topic from a fixed list. The answers themselves are text your team wrote and saved as [variables](/workflows/variables/), so the model never writes an answer.
+
+The finished workflow has these steps:
+
+```text
+Slack: New message in a channel
+└─ A new message? Branch
+ └─ Then
+ ├─ What is it about? AI Extract
+ └─ Which answer? Route
+ ├─ vpn Answer the VPN question
+ ├─ password_reset Answer the password question
+ ├─ wifi Answer the Wi-Fi question
+ ├─ new_software Answer the software question
+ └─ other_request React, look up who asked,
+ open a ticket, and reply
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Slack connection and a Jira Cloud connection, both with the **Workflows** capability. Invite the KloudMate bot to your help channel.
+- You need a Jira project for help desk tickets.
+- Each message in the channel uses workflow credits, and the **AI Extract** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Write the answers
+
+Open **Workflows → Variables**, and create a **Constant** for each answer. The value is the message the bot posts, in Slack's formatting, so a link is written as ``:
+
+| Name | Example value |
+|---|---|
+| `answer_vpn` | `To connect to the VPN, open the VPN app and sign in with your work account. ` |
+| `answer_password_reset` | `Reset your password at . You'll need your phone for the verification code.` |
+| `answer_wifi` | `Connect to the network named Corp, and sign in with your work account. Guests use Corp-Guest.` |
+| `answer_new_software` | `Request new software in the . Approved requests are installed within a day.` |
+
+Also create a **Constant** named `jira_url` with your Jira site's address, such as `https://acme.atlassian.net`, if you don't have it already.
+
+Keeping the answers in variables means the IT team can update an answer, for example when the VPN app changes, without editing the workflow. A value can hold up to 8 KB of text, over several lines.
+
+## Step 2: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On the trigger, pick your Slack connection under **Account**, and pick your help channel under **Channel**.
+ - On every Slack step, pick your Slack connection.
+ - On **Open a ticket**, pick your Jira connection, the **Site**, your help desk project, and an issue type such as **Task**.
+
+```yaml
+kind: workflow
+uid: guide-answer-help-channel-questions
+spec:
+ name: Answer common questions in the help channel
+ description: Answers common IT questions in the help channel with a saved answer, and opens a Jira ticket for any other request.
+ definition:
+ schema_version: 1
+ trigger:
+ type: integration_event
+ config:
+ connection_id:
+ $input: slack
+ trigger_key: slack.new_message_in_channel
+ props:
+ ignore_bots: true
+ steps:
+ - id: new_message
+ type: branch
+ display_name: A new message?
+ if:
+ all:
+ - field: trigger.event.thread_ts
+ op: is_empty
+ then:
+ - id: topic
+ type: action
+ action: ai.extract
+ display_name: What is it about?
+ with:
+ prompt: Someone posted in the IT help channel. Decide what the message is about. Use other_request for a request for help that none of the other topics covers, and not_a_request for a message that doesn't ask for help, such as a thank-you. If you're not sure, use other_request.
+ source: "{{ trigger.event.text }}"
+ output_fields:
+ - name: topic
+ type: enum
+ options: [vpn, password_reset, wifi, new_software, other_request, not_a_request]
+ - name: title
+ type: string
+ description: A title of up to 10 words for a help desk ticket
+ - id: answer
+ type: route
+ display_name: Which answer?
+ routes:
+ - label: vpn
+ if:
+ all:
+ - field: steps.topic.output.topic
+ op: eq
+ value: vpn
+ steps:
+ - id: answer_vpn
+ type: action
+ action: slack.post_message
+ display_name: Answer the VPN question
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{{ trigger.event.ts }}"
+ text: "{{ vars.answer_vpn }}\n\nIf this doesn't solve it, reply in this thread and someone from IT will help."
+ - label: password_reset
+ if:
+ all:
+ - field: steps.topic.output.topic
+ op: eq
+ value: password_reset
+ steps:
+ - id: answer_password_reset
+ type: action
+ action: slack.post_message
+ display_name: Answer the password question
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{{ trigger.event.ts }}"
+ text: "{{ vars.answer_password_reset }}\n\nIf this doesn't solve it, reply in this thread and someone from IT will help."
+ - label: wifi
+ if:
+ all:
+ - field: steps.topic.output.topic
+ op: eq
+ value: wifi
+ steps:
+ - id: answer_wifi
+ type: action
+ action: slack.post_message
+ display_name: Answer the Wi-Fi question
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{{ trigger.event.ts }}"
+ text: "{{ vars.answer_wifi }}\n\nIf this doesn't solve it, reply in this thread and someone from IT will help."
+ - label: new_software
+ if:
+ all:
+ - field: steps.topic.output.topic
+ op: eq
+ value: new_software
+ steps:
+ - id: answer_new_software
+ type: action
+ action: slack.post_message
+ display_name: Answer the software question
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{{ trigger.event.ts }}"
+ text: "{{ vars.answer_new_software }}\n\nIf this doesn't solve it, reply in this thread and someone from IT will help."
+ - label: other_request
+ if:
+ all:
+ - field: steps.topic.output.topic
+ op: eq
+ value: other_request
+ steps:
+ - id: seen
+ type: action
+ action: slack.add_reaction
+ display_name: React so they know it's seen
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ ts: "{{ trigger.event.ts }}"
+ emoji: eyes
+ - id: who
+ type: action
+ action: slack.lookup_user
+ display_name: Look up who asked
+ connection_id:
+ $input: slack
+ with:
+ user: "{{ trigger.event.user }}"
+ - id: ticket
+ type: action
+ action: jira.create_issue
+ display_name: Open a ticket
+ connection_id:
+ $input: jira
+ with:
+ summary: "{{ steps.topic.output.title }}"
+ description: |-
+ *From:* {{ steps.who.output.user.real_name }} ({{ steps.who.output.user.profile.email }}), in the Slack help channel
+ {quote}{{ trigger.event.text }}{quote}
+ fields:
+ labels: [helpdesk]
+ - id: reply
+ type: action
+ action: slack.post_message
+ display_name: Reply with the ticket
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{{ trigger.event.ts }}"
+ text: "I've opened <{{ vars.jira_url }}/browse/{{ steps.ticket.output.key }}|{{ steps.ticket.output.key }}> for this, and the IT team will pick it up there."
+inputs:
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+ jira_url:
+ kind: variable
+ name: jira_url
+ type: constant
+ answer_vpn:
+ kind: variable
+ name: answer_vpn
+ type: constant
+ answer_password_reset:
+ kind: variable
+ name: answer_password_reset
+ type: constant
+ answer_wifi:
+ kind: variable
+ name: answer_wifi
+ type: constant
+ answer_new_software:
+ kind: variable
+ name: answer_new_software
+ type: constant
+```
+
+## How each step works
+
+### The trigger
+
+The **New message in a channel** trigger runs for every message posted in your help channel. **Ignore messages from bots** is on, so the bot's own answers don't start a run.
+
+Replies in a thread start a run too. A **Branch** checks that `trigger.event.thread_ts` **is empty**, so only a new message goes further. Slack sets `thread_ts` only on a reply, and the workflow leaves conversations in threads to people.
+
+### What is it about?
+
+An **AI Extract** step reads `{{ trigger.event.text }}` and returns a topic from a fixed list, with a title in case it becomes a ticket:
+
+```json
+[
+ { "name": "topic", "type": "enum", "options": ["vpn", "password_reset", "wifi", "new_software", "other_request", "not_a_request"] },
+ { "name": "title", "type": "string", "description": "A title of up to 10 words for a help desk ticket" }
+]
+```
+
+The **Instruction** tells the model to use `other_request` when it isn't sure, so a doubtful message goes to a person rather than getting the wrong answer. `not_a_request` covers messages such as a thank-you, which need nothing.
+
+### Which answer?
+
+A **Route** has a path for each topic, and each path runs when `steps.topic.output.topic` **equals** its name. A topic's path posts a reply in a thread on the message, with **Reply to thread** set to `{{ trigger.event.ts }}` and the answer from its variable:
+
+```liquid
+{{ vars.answer_vpn }}
+
+If this doesn't solve it, reply in this thread and someone from IT will help.
+```
+
+The `other_request` path does more:
+
+1. A Slack **Add Reaction** step reacts to the message with 👀.
+2. A Slack **Look Up User** step finds the person's name and email address.
+3. A Jira **Create Issue** step opens a ticket, with the title from the model, and the person and their message in the description.
+4. A Slack **Post Message** step replies in the thread with a link to the ticket.
+
+The Route has no **Otherwise**, so a message with the topic `not_a_request` runs no path, and the workflow does nothing with it.
+
+## Step 3: Test and publish
+
+1. Open the trigger's **Test** tab and click **Test trigger**. Then post a question in the help channel, such as `How do I connect to the VPN?`. The message becomes the trigger's sample.
+2. Test **What is it about?** from its **Test** tab to see the topic the model picked.
+3. Click **Publish**. The first publish also switches the workflow on.
+
+Then post a few different questions in the channel, and check that each one gets the right answer or a ticket. To see which topic a message got, open its run from **Workflows → Runs**.
+
+## Adapt the workflow
+
+- **Add an answer.** Create a variable for it, add its topic to the `topic` options in **What is it about?**, and add a Route path for it with a reply step that uses the variable.
+- **Answer in several channels.** Leave **Channel** set to one channel, and import the workflow again with a different `uid` for each channel. Each copy can use the same answer variables.
+- **Only react, without a ticket.** Remove **Look up who asked**, **Open a ticket**, and **Reply with the ticket** from the `other_request` path, for a team that works from the channel itself.
+
+## Related
+
+- [AI Extract](/workflows/actions/#ai-extract) for output fields.
+- [Variables](/workflows/variables/) for constants.
+- [Run an IT help desk in Slack](/guides/workflows/it-help-desk-in-slack/) for tickets that people open by messaging the bot directly.
diff --git a/src/content/docs/guides/workflows/deploy-from-slack.mdx b/src/content/docs/guides/workflows/deploy-from-slack.mdx
new file mode 100644
index 00000000..e22618e6
--- /dev/null
+++ b/src/content/docs/guides/workflows/deploy-from-slack.mdx
@@ -0,0 +1,386 @@
+---
+title: "Deploy from Slack, with approval for production"
+description: "Mention the bot in Slack to start a GitHub Actions deploy. AI Extract reads the request, a production deploy waits for an approval, and the workflow reports the result in the thread."
+sidebar:
+ order: 5
+---
+
+This guide builds a deploy command for Slack. Someone mentions the KloudMate bot with a request such as "deploy api to staging", and the workflow starts your GitHub Actions deploy workflow, follows the run, and replies in the thread when it finishes. A deploy to production waits until an approver says yes.
+
+The request is ordinary text, so an **AI Extract** step reads it and picks the service and the environment from fixed lists. The rest of the workflow works only with those values, never with the message itself.
+
+The finished workflow has these steps:
+
+```text
+Slack: Bot is mentioned
+├─ Read the request AI Extract
+└─ Is it a deploy request? Branch
+ ├─ Then
+ │ ├─ Look up who asked Slack: Look Up User
+ │ ├─ Production? Branch
+ │ │ └─ Then: say it needs approval, then Approval
+ │ └─ OK to deploy? Branch
+ │ ├─ Then
+ │ │ ├─ Start the deploy workflow HTTP Request
+ │ │ ├─ Say the deploy started Slack: Post Message
+ │ │ ├─ Wait for the deploy to finish Wait until
+ │ │ └─ Report the result Slack: Post Message
+ │ └─ Else: say nobody approved
+ └─ Else: explain how to ask
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Slack connection with the **Workflows** capability, and a channel for deploy requests. Invite the KloudMate bot to that channel.
+- You need a GitHub repository where you can add a workflow, and permission to create a fine-grained personal access token for it.
+- Each run uses workflow credits, and the **AI Extract** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Add a deploy workflow in GitHub
+
+The workflow starts a GitHub Actions workflow named `deploy.yml`, and passes it the service and the environment. Add this file to your repository's default branch as `.github/workflows/deploy.yml`, and replace the last step with your own deploy commands:
+
+```yaml
+name: Deploy
+on:
+ workflow_dispatch:
+ inputs:
+ service:
+ type: choice
+ options: [api, web, worker]
+ environment:
+ type: choice
+ options: [staging, production]
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: ./scripts/deploy.sh "${{ inputs.service }}" "${{ inputs.environment }}"
+```
+
+If you already have a deploy workflow, add the `workflow_dispatch` trigger with these two inputs to it instead.
+
+## Step 2: Save the repository and a token as variables
+
+1. In GitHub, create a [fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) with access to the repository, and the **Actions** repository permission set to **Read and write**.
+2. In KloudMate, open **Workflows → Variables**, and create a **Secret** named `github_token` with the token as its value.
+3. Create a **Constant** named `github_repo` with the repository's owner and name, such as `acme/platform`.
+
+The token lets the workflow start runs and read their status, and nothing else in the repository.
+
+## Step 3: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On the trigger, pick your Slack connection under **Account**, and set **Only in channel** to your deploy channel.
+ - On every Slack step, pick your Slack connection.
+ - On **Approve the production deploy**, add the people who can approve a production deploy under **Approvers**.
+
+```yaml
+kind: workflow
+uid: guide-deploy-from-slack
+spec:
+ name: Deploy from Slack
+ description: When someone mentions the bot with a deploy request, starts the GitHub Actions deploy workflow, asks for approval before a production deploy, and reports the result in the thread.
+ definition:
+ schema_version: 1
+ trigger:
+ type: integration_event
+ config:
+ connection_id:
+ $input: slack
+ trigger_key: slack.app_mention
+ props: {}
+ steps:
+ - id: parse
+ type: action
+ action: ai.extract
+ display_name: Read the request
+ with:
+ prompt: The message asks a bot to deploy a service to an environment. Extract the service and the environment. If the message doesn't ask for a deploy, set intent to other and leave the rest out.
+ source: "{{ trigger.event.text }}"
+ output_fields:
+ - name: intent
+ type: enum
+ options: [deploy, other]
+ - name: service
+ type: enum
+ options: [api, web, worker]
+ required: false
+ - name: environment
+ type: enum
+ options: [staging, production]
+ required: false
+ - id: understood
+ type: branch
+ display_name: Is it a deploy request?
+ if:
+ all:
+ - field: steps.parse.output.intent
+ op: eq
+ value: deploy
+ - field: steps.parse.output.service
+ op: is_not_empty
+ - field: steps.parse.output.environment
+ op: is_not_empty
+ then:
+ - id: who
+ type: action
+ action: slack.lookup_user
+ display_name: Look up who asked
+ connection_id:
+ $input: slack
+ with:
+ user: "{{ trigger.event.user }}"
+ - id: is_production
+ type: branch
+ display_name: Production?
+ if:
+ all:
+ - field: steps.parse.output.environment
+ op: eq
+ value: production
+ then:
+ - id: say_waiting
+ type: action
+ action: slack.post_message
+ display_name: Say it needs approval
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{% assign thread = trigger.event.thread_ts | default: trigger.event.ts %}{{ thread }}"
+ text: ":raised_hand: A production deploy of {{ steps.parse.output.service }} needs an approval. I've asked for one."
+ - id: approve
+ type: approval
+ display_name: Approve the production deploy
+ approvers: []
+ timeout: 30m
+ on_timeout: continue
+ message: "{{ steps.who.output.user.real_name }} asked in Slack to deploy {{ steps.parse.output.service }} to production."
+ - id: go
+ type: branch
+ display_name: OK to deploy?
+ if:
+ any:
+ - field: steps.parse.output.environment
+ op: eq
+ value: staging
+ - field: steps.approve.output.approved
+ op: is_not_empty
+ then:
+ - id: dispatch
+ type: action
+ action: http.request
+ display_name: Start the deploy workflow
+ with:
+ method: POST
+ url: "https://api.github.com/repos/{{ vars.github_repo }}/actions/workflows/deploy.yml/dispatches"
+ authType: bearer
+ auth:
+ token: "{{ secrets.github_token }}"
+ headers:
+ Accept: application/vnd.github+json
+ X-GitHub-Api-Version: "2022-11-28"
+ body_type: json
+ body:
+ ref: main
+ inputs:
+ service: "{{ steps.parse.output.service }}"
+ environment: "{{ steps.parse.output.environment }}"
+ return_run_details: true
+ - id: say_started
+ type: action
+ action: slack.post_message
+ display_name: Say the deploy started
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{% assign thread = trigger.event.thread_ts | default: trigger.event.ts %}{{ thread }}"
+ text: ":rocket: Deploying {{ steps.parse.output.service }} to {{ steps.parse.output.environment }} for <@{{ trigger.event.user }}>. <{{ steps.dispatch.output.body.html_url }}|Follow the run>."
+ - id: wait_run
+ type: wait_until
+ display_name: Wait for the deploy to finish
+ timeout: 30m
+ on_error: continue
+ check:
+ - id: run
+ type: action
+ action: http.request
+ display_name: Read the run
+ with:
+ method: GET
+ url: "https://api.github.com/repos/{{ vars.github_repo }}/actions/runs/{{ steps.dispatch.output.body.workflow_run_id }}"
+ authType: bearer
+ auth:
+ token: "{{ secrets.github_token }}"
+ headers:
+ Accept: application/vnd.github+json
+ X-GitHub-Api-Version: "2022-11-28"
+ until:
+ all:
+ - field: steps.run.output.body.status
+ op: eq
+ value: completed
+ - id: say_result
+ type: action
+ action: slack.post_message
+ display_name: Report the result
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{% assign thread = trigger.event.thread_ts | default: trigger.event.ts %}{{ thread }}"
+ text: "{% assign r = steps.run.output.body %}{% if r.status != 'completed' %}:hourglass: The deploy is still running after 30 minutes.{% elsif r.conclusion == 'success' %}:white_check_mark: {{ steps.parse.output.service }} is deployed to {{ steps.parse.output.environment }}.{% else %}:x: The deploy finished with the result {{ r.conclusion }}.{% endif %} <{{ steps.dispatch.output.body.html_url }}|Open the run>."
+ else:
+ - id: say_expired
+ type: action
+ action: slack.post_message
+ display_name: Say nobody approved
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{% assign thread = trigger.event.thread_ts | default: trigger.event.ts %}{{ thread }}"
+ text: "Nobody approved the production deploy within 30 minutes, so nothing was deployed. Mention me again to ask once more."
+ else:
+ - id: say_help
+ type: action
+ action: slack.post_message
+ display_name: Explain how to ask
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{% assign thread = trigger.event.thread_ts | default: trigger.event.ts %}{{ thread }}"
+ text: "I can deploy api, web, or worker to staging or production. For example: deploy api to staging."
+inputs:
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+ github_repo:
+ kind: variable
+ name: github_repo
+ type: constant
+ github_token:
+ kind: variable
+ name: github_token
+ type: secret
+```
+
+## How each step works
+
+### The trigger
+
+The **Bot is mentioned** trigger runs when someone mentions the bot in a channel. **Only in channel** limits it to your deploy channel, so a mention anywhere else doesn't start a deploy. The event carries the message as `trigger.event.text`, the person as `trigger.event.user`, and the message's channel and timestamp.
+
+### Read the request
+
+An **AI Extract** step reads `{{ trigger.event.text }}` and returns whether it's a deploy request, the service, and the environment. **Output fields** limits each one to a fixed list, so the model can only answer with a service and an environment that you allow:
+
+```json
+[
+ { "name": "intent", "type": "enum", "options": ["deploy", "other"] },
+ { "name": "service", "type": "enum", "options": ["api", "web", "worker"], "required": false },
+ { "name": "environment", "type": "enum", "options": ["staging", "production"], "required": false }
+]
+```
+
+The message is text that anyone in the channel can write, which is why the workflow uses **AI Extract** rather than **AI Prompt**. A message that tries to steer the model still can't produce a service or an environment outside these lists. See [AI Extract](/workflows/actions/#ai-extract).
+
+A **Branch** then checks that `intent` **equals** `deploy`, and that `service` and `environment` **are not empty**. For anything else, the **Else** block replies with an example of how to ask.
+
+### Look up who asked
+
+A Slack **Look Up User** step turns `{{ trigger.event.user }}` into a person, so the approval email can name the requester as `{{ steps.who.output.user.real_name }}` instead of a Slack user ID.
+
+### Approve a production deploy
+
+A **Branch** checks whether `environment` **equals** `production`. If it does, the workflow replies in the thread that the deploy needs an approval, and an **Approval** step emails the approvers. **Timeout** is `30m`, and **On timeout** is **Continue**.
+
+The next **Branch**, **OK to deploy?**, uses **Match any**, so it continues when either condition holds:
+
+- `steps.parse.output.environment` **equals** `staging`, or
+- `steps.approve.output.approved` **is not empty**, which is true only after an approval.
+
+A staging deploy never reaches the approval, so it goes straight through. A production deploy whose approval timed out takes the **Else** block, which replies that nobody approved it. If an approver rejects the deploy, the run ends there.
+
+### Start the deploy workflow
+
+An **HTTP Request** step sends a `POST` to GitHub's workflow dispatch endpoint:
+
+```liquid
+https://api.github.com/repos/{{ vars.github_repo }}/actions/workflows/deploy.yml/dispatches
+```
+
+**Authentication** is **Bearer token**, with the token set to `{{ secrets.github_token }}`, so the token never appears in the workflow or in run history. The **Headers** set `Accept` to `application/vnd.github+json` and `X-GitHub-Api-Version` to `2022-11-28`. The JSON body picks the branch and passes the two inputs:
+
+```json
+{
+ "ref": "main",
+ "inputs": {
+ "service": "{{ steps.parse.output.service }}",
+ "environment": "{{ steps.parse.output.environment }}"
+ },
+ "return_run_details": true
+}
+```
+
+`return_run_details` asks GitHub to return the new run's ID and its web address, as `workflow_run_id` and `html_url`. The next steps use both.
+
+### Reply in the thread
+
+Every reply is a Slack **Post Message** step with **Channel** set to `{{ trigger.event.channel }}`, and **Reply to thread** set to this template:
+
+```liquid
+{% assign thread = trigger.event.thread_ts | default: trigger.event.ts %}{{ thread }}
+```
+
+If the mention was already a reply in a thread, Slack sends `thread_ts`, and the workflow replies in that thread. Otherwise, it starts a thread on the mention. The `assign` form picks one without leaving an unresolved reference behind when `thread_ts` isn't there.
+
+The first reply names the service, the environment, and the person who asked, with a link to the run in GitHub. Slack's link syntax is ``, as in `<{{ steps.dispatch.output.body.html_url }}|Follow the run>`.
+
+### Wait for the deploy to finish
+
+A **Wait until** step reads the run from GitHub, with an **HTTP Request** step in its check block:
+
+```liquid
+https://api.github.com/repos/{{ vars.github_repo }}/actions/runs/{{ steps.dispatch.output.body.workflow_run_id }}
+```
+
+It checks until `steps.run.output.body.status` **equals** `completed`, for up to `30m`. The imported step has `on_error: continue`, so a deploy that takes longer than that still gets a reply. The builder keeps this setting but has no field for it, so if you build the workflow by hand, a deploy that runs past the timeout fails the run instead.
+
+### Report the result
+
+The last reply reads the run's `status` and `conclusion`, and says whether the deploy succeeded, failed, or is still running:
+
+```liquid
+{% assign r = steps.run.output.body %}{% if r.status != 'completed' %}:hourglass: The deploy is still running after 30 minutes.{% elsif r.conclusion == 'success' %}:white_check_mark: {{ steps.parse.output.service }} is deployed to {{ steps.parse.output.environment }}.{% else %}:x: The deploy finished with the result {{ r.conclusion }}.{% endif %}
+```
+
+## Step 4: Test and publish
+
+1. Open the trigger's **Test** tab and click **Test trigger**. Then mention the bot in your deploy channel with a staging deploy, such as `@KloudMate deploy api to staging`. The mention becomes the trigger's sample.
+2. Test **Read the request** from its **Test** tab to see what the model extracted.
+3. Click **Test run**. It runs every step for real, so it starts the staging deploy and replies in the thread.
+4. Click **Publish**. The first publish also switches the workflow on.
+
+Try a production deploy the same way when you're ready, and approve it from the email.
+
+## Adapt the workflow
+
+- **Deploy other services.** Add each service to the `service` options in **Read the request**, and to the `options` of the `service` input in `deploy.yml`.
+- **Limit who can deploy.** Add a condition to **Is it a deploy request?** that `trigger.event.user` is **in** a list of Slack user IDs.
+- **Deploy a branch other than main.** Change `ref` in the body of **Start the deploy workflow**.
+- **Use another CI system.** Replace the two HTTP Request steps with the equivalent calls to your CI system's API, such as starting a pipeline and reading its status.
+
+## Related
+
+- [AI Extract](/workflows/actions/#ai-extract) for output fields and why they're safe with untrusted text.
+- [Approvals and collected input](/workflows/approvals-and-input/) for approval timeouts.
+- [Wait until](/workflows/control-flow/#wait-until) for how often it checks.
+- [Variables](/workflows/variables/) for secrets such as the GitHub token.
diff --git a/src/content/docs/guides/workflows/escalate-urgent-tickets.mdx b/src/content/docs/guides/workflows/escalate-urgent-tickets.mdx
new file mode 100644
index 00000000..6133c58f
--- /dev/null
+++ b/src/content/docs/guides/workflows/escalate-urgent-tickets.mdx
@@ -0,0 +1,332 @@
+---
+title: "Escalate urgent support tickets to engineering"
+description: "Receive urgent tickets from your help desk on a webhook, decide with AI whether each one is a product bug, and open a Jira bug for the owning team with a post in its Slack channel and a note back on the ticket."
+sidebar:
+ order: 15
+---
+
+When a customer reports something urgent, support needs to know quickly whether it's a bug that engineering has to fix, and engineering needs the details without asking support to copy them over. This guide builds a workflow that your help desk calls for every urgent ticket. An **AI Extract** step decides whether the ticket reports a product bug, and which part of the product it's about. For a bug, the workflow:
+
+- opens a Jira bug with the customer's report,
+- posts it in the owning team's Slack channel, and
+- adds a private note to the support ticket with a link to the bug.
+
+An urgent ticket that isn't a bug, such as a question, goes to a channel for your support leads instead.
+
+The finished workflow has these steps:
+
+```text
+Webhook: an urgent ticket from the help desk
+├─ Is it a product bug? AI Extract
+└─ A product bug? Branch
+ ├─ Then
+ │ ├─ Open a bug Jira: Create Issue
+ │ ├─ Which team? Route
+ │ │ ├─ payments Post in the payments channel
+ │ │ ├─ mobile_app Post in the mobile channel
+ │ │ └─ Otherwise Post in the engineering triage channel
+ │ └─ Add a note to the ticket HTTP Request to the help desk
+ └─ Else: Post in the support leads' channel
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Jira Cloud connection and a Slack connection, both with the **Workflows** capability. Invite the KloudMate bot to each channel the workflow posts in.
+- Your help desk must be able to call a webhook when a ticket matches a rule, with a JSON body you write. To add the note to the ticket, it also needs an API with a token.
+- Each urgent ticket uses workflow credits, and the **AI Extract** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Save the variables
+
+Open **Workflows → Variables**, and create these:
+
+| Name | Kind | Value |
+|---|---|---|
+| `jira_url` | **Constant** | Your Jira site's address, such as `https://acme.atlassian.net`. You might have it from another guide already. |
+| `helpdesk_api_url` | **Constant** | The base address of your help desk's API, such as `https://support.example.com/api/v1`. |
+| `helpdesk_api_token` | **Secret** | An API token for your help desk, allowed to add notes to tickets. |
+
+## Step 2: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On **Open a bug**, pick your Jira connection, the **Site**, your engineering project, and the issue type **Bug**.
+ - On every Slack step, pick your Slack connection and the channel it names.
+ - On **Add a note to the ticket**, change the URL and the body to match your help desk's API for adding an internal note. See [Add a note to the ticket](#add-a-note-to-the-ticket).
+
+```yaml
+kind: workflow
+uid: guide-escalate-urgent-tickets
+spec:
+ name: Escalate urgent support tickets
+ description: When the help desk reports an urgent ticket, decides with AI whether it's a product bug. A bug gets a Jira issue, a post in the owning team's Slack channel, and a note on the ticket. Anything else goes to the support leads' channel.
+ definition:
+ schema_version: 1
+ trigger:
+ type: webhook
+ config:
+ dedup_key: "{{ body.ticket.id }}"
+ conditions:
+ all:
+ - field: body.ticket.priority
+ op: eq
+ value: urgent
+ steps:
+ - id: triage
+ type: action
+ action: ai.extract
+ display_name: Is it a product bug?
+ with:
+ prompt: A customer filed this support ticket. Decide whether it reports a bug in our product, as opposed to a question, a request, or a problem on the customer's side. If it's a bug, decide which part of the product it's in, and summarize it for an engineer in one or two sentences.
+ source: "{{ trigger.body.ticket }}"
+ output_fields:
+ - name: kind
+ type: enum
+ options: [bug, not_a_bug]
+ - name: area
+ type: enum
+ options: [payments, mobile_app, other]
+ description: payments is billing, checkout, and invoices. mobile_app is the iOS and Android apps.
+ - name: summary
+ type: string
+ - id: is_bug
+ type: branch
+ display_name: A product bug?
+ if:
+ all:
+ - field: steps.triage.output.kind
+ op: eq
+ value: bug
+ then:
+ - id: bug
+ type: action
+ action: jira.create_issue
+ display_name: Open a bug
+ connection_id:
+ $input: jira
+ with:
+ summary: "Customer bug: {{ trigger.body.ticket.subject }}"
+ description: |-
+ {{ steps.triage.output.summary }}
+
+ *Support ticket:* {{ trigger.body.ticket.url }}
+ *Area:* {{ steps.triage.output.area }}
+ {quote}{{ trigger.body.ticket.description }}{quote}
+ fields:
+ priority:
+ name: Highest
+ labels: [customer-escalation, "{{ steps.triage.output.area }}"]
+ - id: team
+ type: route
+ display_name: Which team?
+ routes:
+ - label: payments
+ if:
+ all:
+ - field: steps.triage.output.area
+ op: eq
+ value: payments
+ steps:
+ - id: post_payments
+ type: action
+ action: slack.post_message
+ display_name: Post in the payments channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":rotating_light: Urgent customer bug in payments: <{{ vars.jira_url }}/browse/{{ steps.bug.output.key }}|{{ steps.bug.output.key }}> {{ steps.triage.output.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}"
+ - label: mobile_app
+ if:
+ all:
+ - field: steps.triage.output.area
+ op: eq
+ value: mobile_app
+ steps:
+ - id: post_mobile
+ type: action
+ action: slack.post_message
+ display_name: Post in the mobile channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":rotating_light: Urgent customer bug in the mobile app: <{{ vars.jira_url }}/browse/{{ steps.bug.output.key }}|{{ steps.bug.output.key }}> {{ steps.triage.output.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}"
+ else:
+ - id: post_triage
+ type: action
+ action: slack.post_message
+ display_name: Post in the engineering triage channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":rotating_light: Urgent customer bug that needs an owner: <{{ vars.jira_url }}/browse/{{ steps.bug.output.key }}|{{ steps.bug.output.key }}> {{ steps.triage.output.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}"
+ - id: note
+ type: action
+ action: http.request
+ display_name: Add a note to the ticket
+ on_error: continue
+ with:
+ method: POST
+ url: "{{ vars.helpdesk_api_url }}/tickets/{{ trigger.body.ticket.id }}/notes"
+ authType: bearer
+ auth:
+ token: "{{ secrets.helpdesk_api_token }}"
+ body_type: json
+ body:
+ private: true
+ body: "Engineering is tracking this as {{ steps.bug.output.key }}: {{ vars.jira_url }}/browse/{{ steps.bug.output.key }}"
+ else:
+ - id: post_support
+ type: action
+ action: slack.post_message
+ display_name: Post in the support leads' channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":bell: Urgent ticket that isn't a product bug: <{{ trigger.body.ticket.url }}|{{ trigger.body.ticket.subject | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}>"
+inputs:
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+ jira_url:
+ kind: variable
+ name: jira_url
+ type: constant
+ helpdesk_api_url:
+ kind: variable
+ name: helpdesk_api_url
+ type: constant
+ helpdesk_api_token:
+ kind: variable
+ name: helpdesk_api_token
+ type: secret
+```
+
+## Step 3: Send urgent tickets from your help desk
+
+1. In KloudMate, open the workflow's trigger and copy the webhook URL.
+2. In your help desk, create a webhook that posts to that URL, with a JSON body.
+3. Create a rule that calls the webhook when a ticket is created with the priority **Urgent**.
+4. Write the body in this shape, using your help desk's placeholders for each value:
+
+```json
+{
+ "ticket": {
+ "id": "48213",
+ "subject": "Charged twice for one order",
+ "description": "When I pay with a saved card, the page spins and I'm charged twice.",
+ "priority": "urgent",
+ "requester": "jane@customer.example",
+ "url": "https://support.example.com/tickets/48213"
+ }
+}
+```
+
+The workflow reads these field names. If your help desk names the priority differently, change the value in the trigger's condition to match.
+
+:::caution
+Anyone with the webhook URL can send the workflow a ticket, and every ticket runs the AI step and can open a Jira issue. Keep the URL private. If it leaks, [rotate it](/workflows/triggers/#rotate-the-webhook-url) and update the webhook in your help desk.
+:::
+
+## How each step works
+
+### The trigger
+
+The **Webhook** trigger's **Conditions** accept a delivery only when `body.ticket.priority` **equals** `urgent`, so a rule that sends more than it should still starts no extra runs. **Deduplication key** is `{{ body.ticket.id }}`. A help desk that sends the same ticket twice, for example when it retries a delivery, then starts only one run.
+
+### Is it a product bug?
+
+An **AI Extract** step reads the whole ticket, with **Source** set to `{{ trigger.body.ticket }}`, and returns these fields:
+
+```json
+[
+ { "name": "kind", "type": "enum", "options": ["bug", "not_a_bug"] },
+ { "name": "area", "type": "enum", "options": ["payments", "mobile_app", "other"], "description": "payments is billing, checkout, and invoices. mobile_app is the iOS and Android apps." },
+ { "name": "summary", "type": "string" }
+]
+```
+
+The ticket is text a customer wrote, so the workflow uses **AI Extract**, which can only return these fields, and `kind` and `area` only from their lists. See [AI Extract](/workflows/actions/#ai-extract). The `description` on `area` tells the model what each part of your product covers, so edit it to match your teams.
+
+A **Branch** then checks whether `kind` **equals** `bug`.
+
+### Open a bug
+
+A Jira **Create Issue** step opens the bug. Its description starts with the model's summary for an engineer, and then quotes the customer's own report, so nothing is lost in the summary:
+
+```liquid
+{{ steps.triage.output.summary }}
+
+*Support ticket:* {{ trigger.body.ticket.url }}
+*Area:* {{ steps.triage.output.area }}
+{quote}{{ trigger.body.ticket.description }}{quote}
+```
+
+Its **Extra fields** set the priority to `Highest`, and add two labels, `customer-escalation` and the area, so the bugs are easy to find on a board:
+
+```json
+{
+ "priority": { "name": "Highest" },
+ "labels": ["customer-escalation", "{{ steps.triage.output.area }}"]
+}
+```
+
+### Which team?
+
+A **Route** posts the bug in the owning team's Slack channel. The `payments` path runs when `steps.triage.output.area` **equals** `payments`, and the `mobile_app` path when it equals `mobile_app`. **Otherwise** posts in an engineering triage channel, for a bug that no team obviously owns.
+
+The model wrote the summary from text a customer sent, so the posts escape `&`, `<`, and `>` in it, which Slack would otherwise read as special syntax:
+
+```liquid
+{{ steps.triage.output.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}
+```
+
+### Add a note to the ticket
+
+An **HTTP Request** step tells the support agent that engineering has the bug, by adding a private note to the ticket with the Jira link. **Authentication** is **Bearer token**, with the token set to `{{ secrets.helpdesk_api_token }}`.
+
+The URL and the body in the YAML are an example of the usual shape:
+
+```liquid
+{{ vars.helpdesk_api_url }}/tickets/{{ trigger.body.ticket.id }}/notes
+```
+
+```json
+{
+ "private": true,
+ "body": "Engineering is tracking this as {{ steps.bug.output.key }}: {{ vars.jira_url }}/browse/{{ steps.bug.output.key }}"
+}
+```
+
+Every help desk names these differently, so replace them with the endpoint and the fields from your help desk's API documentation. If your help desk uses OAuth, use an [OAuth 2.0 connection](/platform/settings/connections/providers/#tools-and-custom-endpoints) instead of a token.
+
+Under **Settings**, **On failure** is **Go to the next step**. The bug is already in Jira and the team has been told, so a failed note doesn't fail the run.
+
+### Not a bug
+
+The **Else** block posts the ticket in your support leads' channel, with a link to it, so someone can answer an urgent question quickly.
+
+## Step 4: Test and publish
+
+1. With the workflow still a draft, create an urgent test ticket in your help desk. The webhook accepts deliveries for a draft and saves the latest one as the trigger's sample.
+2. Open the trigger's **Test** tab and click **Reload** to see the delivery. Check that the field names match the ones in [Step 3](#step-3-send-urgent-tickets-from-your-help-desk).
+3. Test **Is it a product bug?** from its **Test** tab to see what the model decided.
+4. Click **Test run**. It opens a Jira bug, posts in Slack, and adds the note to the test ticket.
+5. Delete the test bug in Jira, and close the test ticket. Then click **Publish**. The first publish also switches the workflow on.
+
+## Adapt the workflow
+
+- **Add a team.** Add an option to `area`, with a line about it in the `description`, and a Route path that posts in the team's channel. To send the bug to the team's own Jira project, move **Open a bug** into each path, with that project picked.
+- **Escalate high-priority tickets too.** Change the trigger's condition to **in**, with the values `urgent` and `high`.
+- **Link duplicate reports.** Before **Open a bug**, add a Jira **Search Issues** step that looks for open bugs with the label `customer-escalation` and a similar summary, and comment on an existing bug instead of opening another.
+
+## Related
+
+- [Webhook](/workflows/triggers/#webhook) for conditions and deduplication.
+- [AI Extract](/workflows/actions/#ai-extract) for output fields.
+- [HTTP Request](/workflows/actions/#http-request) for calling your help desk's API.
diff --git a/src/content/docs/guides/workflows/it-help-desk-in-slack.mdx b/src/content/docs/guides/workflows/it-help-desk-in-slack.mdx
new file mode 100644
index 00000000..e570ed0f
--- /dev/null
+++ b/src/content/docs/guides/workflows/it-help-desk-in-slack.mdx
@@ -0,0 +1,362 @@
+---
+title: "Run an IT help desk in Slack"
+description: "Turn direct messages to the bot into Jira tickets, add replies in the ticket's Slack thread to the ticket, and tell the requester in the same thread when the ticket is resolved."
+sidebar:
+ order: 12
+---
+
+This guide builds a help desk that people use from Slack. Someone sends the KloudMate bot a direct message describing their problem, and the workflow opens a ticket in your IT project in Jira and replies with a link to it. Anything they reply in that thread is added to the ticket as a comment. When the IT team moves the ticket to **Done**, the requester gets a message in the same thread.
+
+The requester never has to open Jira, and the IT team works entirely in Jira.
+
+| Workflow | Trigger | What it does |
+|---|---|---|
+| **Open help desk tickets from Slack** | Slack **New direct message to the bot** | Opens a ticket for a new message, or comments on the ticket for a reply in its thread. |
+| **Tell requesters when their ticket is resolved** | Jira **Status changed** | Replies in the ticket's Slack thread when it moves to **Done**. |
+
+The workflows find each other's data through [Storage](/workflows/storage/) keys that the first workflow writes for each ticket:
+
+| Key | Holds | Used to |
+|---|---|---|
+| `helpdesk-thread:` | The ticket key, such as `IT-311` | Find the ticket for a reply in a thread |
+| `helpdesk:` | The Slack channel and message timestamp | Find the thread when the ticket is resolved |
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Slack connection and a Jira Cloud connection, both with the **Workflows** capability.
+- You need a Jira project for help desk tickets. This guide uses the key `IT`.
+- Each ticket uses workflow credits, and the **AI Extract** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Save your Jira address as a variable
+
+The replies in Slack link to each ticket. Open **Workflows → Variables** and create a **Constant** named `jira_url`, with your Jira site's address as its value, such as `https://acme.atlassian.net`. If you followed another guide that uses it, it's already there.
+
+## Step 2: Import the workflows
+
+Import each workflow on its own. For each one, open **Workflows**, click **Import**, paste the YAML, click **Import**, and then click **Open workflow** to set what the import couldn't.
+
+### Open help desk tickets from Slack
+
+```yaml
+kind: workflow
+uid: guide-helpdesk-from-slack
+spec:
+ name: Open help desk tickets from Slack
+ description: Turns a direct message to the bot into a Jira ticket in the IT project, and adds replies in the ticket's Slack thread to the ticket as comments.
+ definition:
+ schema_version: 1
+ trigger:
+ type: integration_event
+ config:
+ connection_id:
+ $input: slack
+ trigger_key: slack.new_direct_message
+ props: {}
+ steps:
+ - id: who
+ type: action
+ action: slack.lookup_user
+ display_name: Look up who wrote
+ connection_id:
+ $input: slack
+ with:
+ user: "{{ trigger.event.user }}"
+ - id: is_reply
+ type: branch
+ display_name: A reply in a ticket's thread?
+ if:
+ all:
+ - field: trigger.event.thread_ts
+ op: is_not_empty
+ then:
+ - id: thread_ticket
+ type: action
+ action: store.get
+ display_name: Find the thread's ticket
+ with:
+ scope: workspace
+ key: "helpdesk-thread:{{ trigger.event.thread_ts }}"
+ - id: known_thread
+ type: branch
+ display_name: Is it a ticket's thread?
+ if:
+ all:
+ - field: steps.thread_ticket.output.value
+ op: is_not_empty
+ then:
+ - id: add_comment
+ type: action
+ action: jira.add_comment
+ display_name: Add the reply to the ticket
+ connection_id:
+ $input: jira
+ with:
+ issue_key: "{{ steps.thread_ticket.output.value }}"
+ body: |-
+ {{ steps.who.output.user.real_name }} replied in Slack:
+ {quote}{{ trigger.event.text }}{quote}
+ - id: ack_reply
+ type: action
+ action: slack.add_reaction
+ display_name: React to the reply
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ ts: "{{ trigger.event.ts }}"
+ emoji: white_check_mark
+ else:
+ - id: ticket
+ type: action
+ action: ai.extract
+ display_name: Read the request
+ with:
+ prompt: Someone asks the IT help desk for help. Write a short title for the ticket, and pick its category and priority.
+ source: "{{ trigger.event.text }}"
+ output_fields:
+ - name: title
+ type: string
+ description: A title of up to 10 words, such as "Can't connect to the VPN from home"
+ - name: category
+ type: enum
+ options: [access, hardware, software, network, other]
+ - name: priority
+ type: enum
+ options: [High, Medium]
+ description: High if the person can't work at all, Medium for anything else
+ - id: create
+ type: action
+ action: jira.create_issue
+ display_name: Open the ticket
+ connection_id:
+ $input: jira
+ with:
+ summary: "{{ steps.ticket.output.title }}"
+ description: |-
+ *From:* {{ steps.who.output.user.real_name }} ({{ steps.who.output.user.profile.email }}), in Slack
+ {quote}{{ trigger.event.text }}{quote}
+ fields:
+ labels: [helpdesk, "{{ steps.ticket.output.category }}"]
+ priority:
+ name: "{{ steps.ticket.output.priority }}"
+ - id: remember_ticket
+ type: action
+ action: store.put
+ display_name: Remember the Slack thread for the ticket
+ with:
+ scope: workspace
+ key: "helpdesk:{{ steps.create.output.key }}"
+ value:
+ channel: "{{ trigger.event.channel }}"
+ ts: "{{ trigger.event.ts }}"
+ ttl: 90d
+ - id: remember_thread
+ type: action
+ action: store.put
+ display_name: Remember the ticket for the thread
+ with:
+ scope: workspace
+ key: "helpdesk-thread:{{ trigger.event.ts }}"
+ value: "{{ steps.create.output.key }}"
+ ttl: 90d
+ - id: reply
+ type: action
+ action: slack.post_message
+ display_name: Reply with the ticket
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.channel }}"
+ thread_ts: "{{ trigger.event.ts }}"
+ text: "Thanks, I've opened <{{ vars.jira_url }}/browse/{{ steps.create.output.key }}|{{ steps.create.output.key }}> for you: {{ steps.ticket.output.title | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}. Reply in this thread to add anything, and I'll message you here when it's resolved."
+inputs:
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+ jira_url:
+ kind: variable
+ name: jira_url
+ type: constant
+```
+
+After you import it:
+
+- On the trigger, pick your Slack connection under **Account**.
+- On every Slack step, pick your Slack connection.
+- On every Jira step, pick your Jira connection and the **Site**. On **Open the ticket**, also pick your help desk project and an issue type, such as **Task**.
+
+### Tell requesters when their ticket is resolved
+
+```yaml
+kind: workflow
+uid: guide-helpdesk-resolved
+spec:
+ name: Tell requesters when their ticket is resolved
+ description: When a help desk ticket moves to Done, replies in the Slack thread where it was requested.
+ definition:
+ schema_version: 1
+ trigger:
+ type: integration_event
+ config:
+ connection_id:
+ $input: jira
+ trigger_key: jira.status_changed
+ props:
+ jql: project = IT AND status = Done
+ steps:
+ - id: moved_to_done
+ type: branch
+ display_name: Moved to Done?
+ if:
+ all:
+ - field: trigger.event.to.name
+ op: eq
+ value: Done
+ then:
+ - id: thread
+ type: action
+ action: store.get
+ display_name: Find the Slack thread
+ with:
+ scope: workspace
+ key: "helpdesk:{{ trigger.event.issue.key }}"
+ - id: from_slack
+ type: branch
+ display_name: Was it requested in Slack?
+ if:
+ all:
+ - field: steps.thread.output.value
+ op: is_not_empty
+ then:
+ - id: tell
+ type: action
+ action: slack.post_message
+ display_name: Reply in the thread
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ steps.thread.output.value.channel }}"
+ thread_ts: "{{ steps.thread.output.value.ts }}"
+ text: ":white_check_mark: <{{ vars.jira_url }}/browse/{{ trigger.event.issue.key }}|{{ trigger.event.issue.key }}> is resolved. If you still need help, send me a new message."
+ - id: forget_ticket
+ type: action
+ action: store.remove
+ display_name: Forget the ticket's thread
+ with:
+ scope: workspace
+ key: "helpdesk:{{ trigger.event.issue.key }}"
+ - id: forget_thread
+ type: action
+ action: store.remove
+ display_name: Forget the thread's ticket
+ with:
+ scope: workspace
+ key: "helpdesk-thread:{{ steps.thread.output.value.ts }}"
+inputs:
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+ jira_url:
+ kind: variable
+ name: jira_url
+ type: constant
+```
+
+After you import it:
+
+- On the trigger, pick your Jira connection under **Account**, and pick the **Site**. If your project key isn't `IT`, change it in **Narrow with JQL**.
+- On **Reply in the thread**, pick your Slack connection.
+
+## How the ticket workflow works
+
+### The trigger and the requester
+
+The **New direct message to the bot** trigger runs for every message someone sends the bot, including replies in a thread. The bot's own messages don't start a run.
+
+A Slack **Look Up User** step turns `{{ trigger.event.user }}` into the person's name and email address, for the ticket's description and for comments.
+
+### A new message or a reply?
+
+A **Branch** checks whether `trigger.event.thread_ts` **is not empty**. Slack sets `thread_ts` only on a reply in a thread, so a new message takes the **Else** block and becomes a ticket.
+
+### Open the ticket
+
+An **AI Extract** step reads the message and returns a title, a category, and a priority:
+
+```json
+[
+ { "name": "title", "type": "string", "description": "A title of up to 10 words, such as \"Can't connect to the VPN from home\"" },
+ { "name": "category", "type": "enum", "options": ["access", "hardware", "software", "network", "other"] },
+ { "name": "priority", "type": "enum", "options": ["High", "Medium"], "description": "High if the person can't work at all, Medium for anything else" }
+]
+```
+
+Anyone in your Slack workspace can write the message, so the workflow uses **AI Extract**, which can only return these fields. See [AI Extract](/workflows/actions/#ai-extract).
+
+A Jira **Create Issue** step opens the ticket, with the title as its summary, and the requester and their message in its description. `{quote}` is Jira's markup for a quoted block. Its **Extra fields** add two labels, `helpdesk` and the category, and set the priority:
+
+```json
+{
+ "labels": ["helpdesk", "{{ steps.ticket.output.category }}"],
+ "priority": { "name": "{{ steps.ticket.output.priority }}" }
+}
+```
+
+### Remember the thread and the ticket
+
+**Storage: Put** steps, with **Scope** set to **This workspace**, link the Slack message and the ticket in both directions:
+
+- `helpdesk:{{ steps.create.output.key }}` holds the message's channel and timestamp, as an object: `{"channel": "{{ trigger.event.channel }}", "ts": "{{ trigger.event.ts }}"}`.
+- `helpdesk-thread:{{ trigger.event.ts }}` holds the ticket key.
+
+Both keys expire after `90d`, so a ticket that stays open longer loses its link to Slack.
+
+The timestamp is stored inside an object on purpose. A Slack timestamp such as `1790800000.000300` looks like a number, and inside an object it stays text, exactly as Slack sent it. Slack needs it character for character to find the thread.
+
+### Reply with the ticket
+
+A Slack **Post Message** step replies in a thread on the requester's message, with **Channel** set to `{{ trigger.event.channel }}` and **Reply to thread** set to `{{ trigger.event.ts }}`. The reply links to the ticket. The title came from the model, which read text that someone else wrote, so the reply escapes `&`, `<`, and `>` in it, which Slack would otherwise read as special syntax.
+
+### Add a reply to the ticket
+
+For a reply in a thread, a **Storage: Get** step looks up `helpdesk-thread:{{ trigger.event.thread_ts }}`, where `thread_ts` is the timestamp of the message that started the thread. If the thread belongs to a ticket, a Jira **Add Comment** step adds the reply to the ticket, and a Slack **Add Reaction** step reacts to the reply with ✅, so the requester knows it arrived. A reply in any other thread is ignored.
+
+## How the resolved workflow works
+
+The Jira **Status changed** trigger runs when an issue moves to another status. **Narrow with JQL** is `project = IT AND status = Done`, which limits it to help desk tickets that are now in **Done**.
+
+The JQL checks the issue's status when KloudMate looks, not the status the issue moved to. An issue that moved through two statuses between two checks produces an event for each move. So a **Branch** checks that `trigger.event.to.name` **equals** `Done`, and the workflow replies only once.
+
+A **Storage: Get** step then looks up `helpdesk:{{ trigger.event.issue.key }}`. A ticket that someone opened in Jira has no key, so the workflow stops there. For a ticket from Slack, a Slack **Post Message** step replies in the original thread, with **Channel** and **Reply to thread** read from the stored object. Two **Storage: Remove** steps then delete both keys, since the ticket is done.
+
+## Step 3: Test and publish
+
+1. Publish both workflows. The first publish also switches each one on.
+2. In Slack, send the bot a direct message, such as `My second monitor stopped working`. The bot replies with a ticket link within a few seconds.
+3. Reply in that thread. The reply appears as a comment on the ticket, and the bot reacts to it with ✅.
+4. Move the ticket to **Done** in Jira. Within a couple of minutes, the bot replies in the thread that it's resolved.
+
+To see what a run did, open it from **Workflows → Runs**.
+
+## Adapt the workflow
+
+- **Send urgent requests to a channel.** In the ticket workflow, add a **Branch** after **Open the ticket** that checks whether `steps.ticket.output.priority` **equals** `High`, and posts the ticket in your IT team's channel.
+- **Use your own categories.** Change the `category` options in **Read the request**. The labels follow automatically.
+- **Reopen instead of starting over.** In the resolved workflow's reply, invite the requester to reply in the thread. Then, in the ticket workflow, add a Jira **Transition Issue** step after **Add the reply to the ticket** that moves the ticket back to **In Progress**, and stop removing the keys in the resolved workflow.
+
+## Related
+
+- [Storage](/workflows/storage/) for workspace-scoped keys.
+- [Triggers](/workflows/triggers/#app-event) for Slack and Jira events.
+- [Actions](/workflows/actions/#slack) for the Slack and Jira steps.
diff --git a/src/content/docs/guides/workflows/notify-author-of-failed-build.mdx b/src/content/docs/guides/workflows/notify-author-of-failed-build.mdx
new file mode 100644
index 00000000..4893aaf2
--- /dev/null
+++ b/src/content/docs/guides/workflows/notify-author-of-failed-build.mdx
@@ -0,0 +1,173 @@
+---
+title: "Tell the author when their build fails"
+description: "Receive GitHub Actions run events on a webhook, and send the author of a failed build a direct message in Slack with a link to the run."
+sidebar:
+ order: 7
+---
+
+This guide builds a workflow that tells developers about their own failed builds. When a GitHub Actions run fails, GitHub calls the workflow's webhook, and the workflow finds the author of the commit in Slack by their email address and sends them a direct message with a link to the run. Nobody else gets notified, so a busy team channel doesn't fill up with other people's failures.
+
+If the author isn't in your Slack workspace, the workflow posts in a team channel instead, so the failure doesn't go unnoticed.
+
+The finished workflow has these steps:
+
+```text
+Webhook: a GitHub Actions run failed
+├─ Find the author in Slack Slack: Find User by Email
+└─ Found them? Branch
+ ├─ Then: Message the author Slack: Send Direct Message
+ └─ Else: Post in the team channel Slack: Post Message
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Slack connection with the **Workflows** capability. Invite the KloudMate bot to the channel for failures whose author isn't in Slack.
+- You need admin access to the GitHub repository, to add a webhook.
+- Developers commit with the same email address they use in Slack. GitHub's private `noreply` addresses don't match anyone in Slack, so commits made with one go to the team channel.
+
+## Step 1: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**. On the three Slack steps, pick your Slack connection, and on **Post in the team channel**, also pick the channel.
+
+```yaml
+kind: workflow
+uid: guide-notify-author-of-failed-build
+spec:
+ name: Tell the author when their build fails
+ description: When a GitHub Actions run fails, sends the author of the commit a direct message in Slack with a link to the run.
+ definition:
+ schema_version: 1
+ trigger:
+ type: webhook
+ config:
+ dedup_key: "{{ body.workflow_run.id }}:{{ body.workflow_run.run_attempt }}"
+ conditions:
+ all:
+ - field: headers.x-github-event
+ op: eq
+ value: workflow_run
+ - field: body.action
+ op: eq
+ value: completed
+ - field: body.workflow_run.conclusion
+ op: eq
+ value: failure
+ steps:
+ - id: find
+ type: action
+ action: slack.find_user_by_email
+ display_name: Find the author in Slack
+ on_error: continue
+ connection_id:
+ $input: slack
+ with:
+ email: "{{ trigger.body.workflow_run.head_commit.author.email }}"
+ - id: found
+ type: branch
+ display_name: Found them?
+ if:
+ all:
+ - field: steps.find.output.user.id
+ op: is_not_empty
+ then:
+ - id: dm
+ type: action
+ action: slack.send_direct_message
+ display_name: Message the author
+ connection_id:
+ $input: slack
+ with:
+ user: "{{ steps.find.output.user.id }}"
+ text: ":x: {{ trigger.body.workflow_run.name }} failed on {{ trigger.body.workflow_run.head_branch | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }} in {{ trigger.body.repository.full_name }}, for your commit {{ trigger.body.workflow_run.head_sha | slice: 0, 7 }}. <{{ trigger.body.workflow_run.html_url }}|Open the run>"
+ else:
+ - id: post
+ type: action
+ action: slack.post_message
+ display_name: Post in the team channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":x: {{ trigger.body.workflow_run.name }} failed on {{ trigger.body.workflow_run.head_branch | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }} in {{ trigger.body.repository.full_name }}, for a commit by {{ trigger.body.workflow_run.head_commit.author.name | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}. <{{ trigger.body.workflow_run.html_url }}|Open the run>"
+inputs:
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+```
+
+## Step 2: Add a webhook in GitHub
+
+1. In KloudMate, open the workflow's trigger and copy the webhook URL.
+2. In the GitHub repository, open **Settings → Webhooks** and click **Add webhook**.
+3. Set **Payload URL** to the webhook URL, and **Content type** to `application/json`.
+4. Under **Which events would you like to trigger this webhook?**, choose **Let me select individual events**, clear **Pushes**, and select **Workflow runs**.
+5. Click **Add webhook**.
+
+GitHub sends a `ping` event right away, to check the URL. The workflow ignores it, because its trigger conditions only match failed runs.
+
+:::caution
+KloudMate doesn't check GitHub's webhook secret, so anyone who has the webhook URL can send the workflow a request. Keep the URL private. If it leaks, [rotate it](/workflows/triggers/#rotate-the-webhook-url) and update the webhook in GitHub.
+:::
+
+## How each step works
+
+### The trigger
+
+GitHub sends a `workflow_run` event when a run is requested, when it starts, and when it completes. The trigger's **Conditions** keep only failed runs, and every other delivery is acknowledged without starting a run:
+
+| Field | Condition | Value |
+|---|---|---|
+| `headers.x-github-event` | **equals** | `workflow_run` |
+| `body.action` | **equals** | `completed` |
+| `body.workflow_run.conclusion` | **equals** | `failure` |
+
+**Deduplication key** is `{{ body.workflow_run.id }}:{{ body.workflow_run.run_attempt }}`. GitHub retries a delivery it thinks failed, and the key makes a retried delivery start no second run. It includes the attempt number, so when someone re-runs a failed job and it fails again, the author hears about it again.
+
+### Find the author in Slack
+
+A Slack **Find User by Email** step looks up `{{ trigger.body.workflow_run.head_commit.author.email }}`, the email address of the commit's author.
+
+If no Slack user has that address, Slack returns an error and the step fails. Under **Settings**, **On failure** is **Go to the next step**, so the run carries on without the user instead of failing.
+
+### Found them?
+
+A **Branch** checks that `steps.find.output.user.id` **is not empty**. A step that failed and carried on has no output, so the condition is false when the lookup failed.
+
+When the author was found, a Slack **Send Direct Message** step messages them. **User id** is `{{ steps.find.output.user.id }}`, because Slack sends direct messages by user ID. The message names the workflow, the branch, the repository, and the first 7 characters of the commit, which `slice: 0, 7` takes from the full commit hash:
+
+```liquid
+{{ trigger.body.workflow_run.head_sha | slice: 0, 7 }}
+```
+
+Otherwise, a Slack **Post Message** step posts the same details in the team channel, with the author's name.
+
+The branch name and the author's name come from the commit, so the messages escape `&`, `<`, and `>` in them, which Slack would otherwise read as special syntax:
+
+```liquid
+{{ trigger.body.workflow_run.head_branch | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}
+```
+
+## Step 3: Test and publish
+
+1. With the workflow still a draft, make a build fail on a test branch, or re-run a failed run in GitHub. The webhook accepts deliveries for a draft and saves the latest one as the trigger's sample.
+2. Open the trigger's **Test** tab and click **Reload** to see the delivery.
+3. Test **Find the author in Slack** from its **Test** tab, to check that the author's email address matches a Slack user.
+4. Click **Publish**. The first publish also switches the workflow on.
+
+From then on, every failed run in the repository sends one message. To see a delivery that didn't start a run, check **Recent Deliveries** on the webhook in GitHub.
+
+## Adapt the workflow
+
+- **Only watch some workflows.** Add a condition on `body.workflow.path`, such as **equals** `.github/workflows/ci.yml`, so failures in other workflows don't notify anyone.
+- **Only watch the main branch.** Add a condition that `body.workflow_run.head_branch` **equals** `main`, and point **Message the author** at the team channel instead, because a broken main branch affects everyone.
+- **Tell the author when it's fixed.** Import the YAML again with a different `uid`, so it creates a second workflow. In the copy, set the last condition to `success` and change the message.
+- **Watch every repository.** Add the webhook to the GitHub organization instead of one repository, under the organization's **Settings → Webhooks**.
+
+## Related
+
+- [Webhook](/workflows/triggers/#webhook) for conditions, deduplication, and rotating the URL.
+- [Test a workflow](/workflows/test-a-workflow/#capture-a-webhook-sample) for capturing a webhook sample.
+- [Actions](/workflows/actions/#slack) for the Slack steps.
diff --git a/src/content/docs/guides/workflows/offboard-a-leaver.mdx b/src/content/docs/guides/workflows/offboard-a-leaver.mdx
new file mode 100644
index 00000000..22fc78ac
--- /dev/null
+++ b/src/content/docs/guides/workflows/offboard-a-leaver.mdx
@@ -0,0 +1,263 @@
+---
+title: "Offboard a leaver"
+description: "After an approval, suspend a leaver's Google Workspace account, reassign their open Jira issues to their manager, and email a record of every change."
+sidebar:
+ order: 10
+---
+
+This guide builds an offboarding request that someone in IT or a manager submits on a person's last day. After an approver agrees, the workflow suspends the person's Google Workspace account, which blocks their access to mail and files, and reassigns their open Jira issues to their manager. It then emails the manager and the person who asked a record of what changed, and of what still needs doing by hand.
+
+Google Workspace has no built-in action in KloudMate, so the workflow calls Google's Admin SDK API through an [OAuth 2.0 connection](/platform/settings/connections/providers/#tools-and-custom-endpoints), the same way the [Gmail guide](/guides/workflows/trigger-on-new-gmail/) calls Gmail.
+
+The finished workflow has these steps:
+
+```text
+Form: Offboard someone who is leaving
+├─ Approve the offboarding Approval
+├─ Offboard in each tool Parallel
+│ ├─ Suspend the Google account HTTP Request to Google
+│ └─ Find the leaver in Jira Jira: Find User
+│ └─ Do they have a Jira account? Branch
+│ ├─ Find the manager in Jira Jira: Find User
+│ ├─ Find their open issues Jira: Search Issues
+│ └─ For each open issue Loop: assign it to the manager
+└─ Email what changed Send Email
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Jira Cloud connection with the **Workflows** capability.
+- You need a Google Workspace administrator account that can manage users, and a Google Cloud project where you can create an OAuth client.
+- The people who submit the form need to be members of your KloudMate workspace, because the form has **Sign-in required** turned on.
+
+## Step 1: Connect Google as an administrator
+
+1. In the [Google Cloud console](https://console.cloud.google.com/), enable the **Admin SDK API**, and create an OAuth client the way the Gmail guide describes in [Step 1: Create an OAuth client in Google Cloud](/guides/workflows/trigger-on-new-gmail/#step-1-create-an-oauth-client-in-google-cloud). Make the consent screen **Internal** to your organization, so its tokens don't expire after 7 days.
+2. In KloudMate, open **Workflows → Connections**, click **Connect**, and pick **OAuth 2.0**. Name the connection `Google Admin`, and fill in the form:
+
+ | Field | Value |
+ |---|---|
+ | **Grant type** | **Authorization code** |
+ | **Token URL** | `https://oauth2.googleapis.com/token` |
+ | **Authorize URL** | `https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent` |
+ | **Scopes** | `https://www.googleapis.com/auth/admin.directory.user` |
+ | **Client ID** and **Client secret** | The values from your OAuth client. |
+ | **Send client credentials** | **In the body** |
+
+3. Click **Connect**, and sign in to Google as the administrator.
+
+The connection acts as that administrator, so the workflow can suspend any user in your Google Workspace organization. Only people with the **Developer** role can use or change the connection in KloudMate.
+
+## Step 2: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On **Suspend the Google account**, pick your `Google Admin` connection.
+ - On every Jira step, pick your Jira connection and the **Site**.
+ - On **Approve the offboarding**, add the people who can approve an offboarding under **Approvers**.
+
+```yaml
+kind: workflow
+uid: guide-offboard-a-leaver
+spec:
+ name: Offboard a leaver
+ description: After an approval, suspends the leaver's Google Workspace account, reassigns their open Jira issues to their manager, and emails a record of what changed.
+ definition:
+ schema_version: 1
+ trigger:
+ type: form
+ config:
+ title: Offboard someone who is leaving
+ description: Submit this on the person's last day. Their Google Workspace account is suspended as soon as the request is approved.
+ require_login: true
+ success_message: The request is waiting for approval. You'll get an email when the offboarding is done.
+ inputs:
+ - name: email
+ type: email
+ required: true
+ - name: manager_email
+ type: email
+ required: true
+ - name: notes
+ type: textarea
+ required: false
+ steps:
+ - id: approve
+ type: approval
+ display_name: Approve the offboarding
+ approvers: []
+ timeout: 24h
+ on_timeout: fail
+ message: |-
+ {{ trigger.submitted_by.name }} asks to offboard {{ trigger.inputs.email }}. Their Google Workspace account will be suspended, and their open Jira issues reassigned to {{ trigger.inputs.manager_email }}.
+
+ {% assign notes = trigger.inputs.notes | default: "none" %}Notes: {{ notes }}
+ - id: offboard
+ type: parallel
+ display_name: Offboard in each tool
+ branches:
+ - - id: suspend
+ type: action
+ action: http.request
+ display_name: Suspend the Google account
+ on_error: continue
+ connection_id:
+ $input: google_admin
+ with:
+ authType: connection
+ method: PUT
+ url: "https://admin.googleapis.com/admin/directory/v1/users/{{ trigger.inputs.email | url_encode }}"
+ body_type: json
+ body:
+ suspended: true
+ - - id: find_leaver
+ type: action
+ action: jira.find_user
+ display_name: Find the leaver in Jira
+ connection_id:
+ $input: jira
+ with:
+ query: "{{ trigger.inputs.email }}"
+ - id: in_jira
+ type: branch
+ display_name: Do they have a Jira account?
+ if:
+ all:
+ - field: steps.find_leaver.output.users.0.accountId
+ op: is_not_empty
+ then:
+ - id: find_manager
+ type: action
+ action: jira.find_user
+ display_name: Find the manager in Jira
+ connection_id:
+ $input: jira
+ with:
+ query: "{{ trigger.inputs.manager_email }}"
+ - id: open_issues
+ type: action
+ action: jira.search_issues
+ display_name: Find their open issues
+ connection_id:
+ $input: jira
+ with:
+ jql: "assignee = {{ steps.find_leaver.output.users.0.accountId }} AND resolution = Unresolved"
+ max_results: 100
+ fields: summary
+ - id: reassign
+ type: loop
+ display_name: For each open issue
+ items: "{{ steps.open_issues.output.issues }}"
+ concurrency: 5
+ each:
+ - id: assign
+ type: action
+ action: jira.assign_issue
+ display_name: Assign it to the manager
+ on_error: continue
+ connection_id:
+ $input: jira
+ with:
+ issue_key: "{{ item.key }}"
+ account_id: "{{ steps.find_manager.output.users.0.accountId }}"
+ - id: report
+ type: action
+ action: email.send
+ display_name: Email what changed
+ with:
+ to: "{{ trigger.inputs.manager_email }}, {{ trigger.submitted_by.email }}"
+ subject: "Offboarding for {{ trigger.inputs.email }} is done"
+ html: |-
+ {% assign suspended = steps.suspend.output.body.suspended %}{% assign why = steps.suspend.error %}{% assign moved = steps.reassign.output.iterations | default: 0 %}{{ steps.approve.output.decidedBy.name | escape }} approved offboarding {{ trigger.inputs.email | escape }}.
+
+ - Google Workspace: {% if suspended %}the account is suspended.{% else %}the account wasn't suspended. {{ why | escape }}{% endif %}
+ - Jira: {{ moved }} open issues were reassigned to {{ trigger.inputs.manager_email | escape }}.
+
+ Still to do by hand: deactivate their Slack account, and collect their equipment.
+inputs:
+ google_admin:
+ kind: connection
+ name: Google Admin
+ type: oauth2
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+```
+
+## How each step works
+
+### The form and the approval
+
+The **Form submission** trigger asks for the leaver's work `email`, their `manager_email`, and optional `notes`. **Sign-in required** is on, so the approval request can name the person who submitted it, as `{{ trigger.submitted_by.name }}`.
+
+Suspending an account can't wait for someone to notice a mistake, so an **Approval** step asks a second person first. Its message says exactly what will happen to whom. If the approver rejects the request, or nobody answers within 24 hours, the run ends without changing anything.
+
+### Suspend the Google account
+
+An **HTTP Request** step with **Authentication** set to **OAuth 2.0 connection** sends a `PUT` to Google's Directory API:
+
+```liquid
+https://admin.googleapis.com/admin/directory/v1/users/{{ trigger.inputs.email | url_encode }}
+```
+
+Its JSON body is `{"suspended": true}`. Google changes only the fields in the body, so the rest of the account stays as it was. A suspended user can't use Gmail, Drive, or other Google Workspace services, and new mail to them is blocked. Their data stays in place, people they shared files with keep their access, and the account still counts toward your Google bill until you delete it.
+
+`url_encode` makes the address safe to put in a URL, by turning the `@` into `%40`.
+
+### Reassign their open Jira issues
+
+The Jira branch of the **Parallel** step runs at the same time as the Google branch:
+
+1. A Jira **Find User** step finds the leaver by email. A **Branch** checks that it found an account, so someone who never used Jira is skipped.
+2. Another **Find User** step finds the manager.
+3. A Jira **Search Issues** step finds the leaver's open issues, with this **JQL**:
+
+ ```liquid
+ assignee = {{ steps.find_leaver.output.users.0.accountId }} AND resolution = Unresolved
+ ```
+
+4. A **Loop** assigns each issue to the manager. **Run at once** is `5`, because the issues don't depend on each other.
+
+A search returns at most 100 issues. Someone with more than that keeps the rest, and you can run the workflow again to move the next 100.
+
+### Carry on when a tool fails
+
+**Suspend the Google account** and **Assign it to the manager** both have **On failure** set to **Go to the next step**, under **Settings**. If Google refuses the request or one issue can't be reassigned, the other changes still happen, and the email reports the failure.
+
+### Email what changed
+
+A **Send Email** step emails the manager and the person who submitted the form, with a line for each tool. It reads the results of the steps that ran in the parallel branches:
+
+```liquid
+{% assign suspended = steps.suspend.output.body.suspended %}{% assign why = steps.suspend.error %}{% assign moved = steps.reassign.output.iterations | default: 0 %}
+```
+
+Some of these values exist only in some runs. `steps.suspend.error` is set only when the Google request failed, and `steps.reassign` only when the leaver had a Jira account. Reading them with `assign` keeps a missing one from showing up as an unresolved reference in run history. When the request failed, the email includes Google's error, such as a missing permission.
+
+The email also lists what the workflow can't do. KloudMate's Slack actions can't deactivate a user, so a Slack admin does that by hand.
+
+## Step 3: Test and publish
+
+Test with an account you can afford to lose, such as a test user in your Google Workspace:
+
+1. Click **Publish**. The first publish also switches the workflow on, and the form goes live.
+2. Open the trigger, copy the **Public form URL**, and submit the form for the test user, with your own address as the manager.
+3. Approve the request from the email.
+4. Check that the test user is suspended in the Google Admin console, and read the email the workflow sent.
+
+To undo the test, unsuspend the user in the Google Admin console.
+
+## Adapt the workflow
+
+- **Transfer their files.** Add an HTTP Request step that calls Google's Data Transfer API to move the leaver's Drive files to their manager. Add the scope `https://www.googleapis.com/auth/admin.datatransfer` to the connection, and click **Reconnect** on it.
+- **Offboard from another identity provider.** Replace **Suspend the Google account** with a call to your provider's API, through an OAuth 2.0 connection or with a token stored as a [secret](/workflows/variables/).
+- **Open a ticket for the manual steps.** Add a Jira **Create Issue** step after the email, for the Slack account and the equipment, and assign it to your IT team.
+
+## Related
+
+- [Approvals and collected input](/workflows/approvals-and-input/) for the approval email.
+- [Parallel](/workflows/control-flow/#parallel) for running independent branches together.
+- [HTTP Request](/workflows/actions/#http-request) for calling an API through an OAuth 2.0 connection.
diff --git a/src/content/docs/guides/workflows/onboard-a-new-hire.mdx b/src/content/docs/guides/workflows/onboard-a-new-hire.mdx
new file mode 100644
index 00000000..52c8356a
--- /dev/null
+++ b/src/content/docs/guides/workflows/onboard-a-new-hire.mdx
@@ -0,0 +1,439 @@
+---
+title: "Onboard a new hire"
+description: "Turn an HR form into an onboarding issue in Jira with tasks for IT, facilities, and the manager, then invite the new hire to their team's Slack channels on their first day."
+sidebar:
+ order: 9
+---
+
+This guide automates the routine part of onboarding. When HR submits a form for a new hire, the workflow opens an onboarding issue in Jira with a task each for IT, facilities, and the hiring manager, and emails the manager a link to it. On the new hire's start date, a second workflow invites them to the company and team channels in Slack and sends them a welcome message.
+
+The two workflows run weeks apart, and a single run can wait for 24 hours at most. So the form workflow saves the new hire under their start date in [Storage](/workflows/storage/), and the daily workflow reads the list for today.
+
+| Workflow | Trigger | What it does |
+|---|---|---|
+| **Start onboarding for a new hire** | **Form submission** | Opens the Jira issue and its tasks, emails the manager, and adds the new hire to the list for their start date. |
+| **Welcome new hires on their start date** | **Schedule**, daily at 09:00 | Invites the people who start today to Slack channels, and welcomes them. |
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Jira Cloud connection and a Slack connection, both with the **Workflows** capability.
+- Pick a Jira project for onboarding issues, such as your IT or HR project. The project needs an issue type for the onboarding issue, such as **Task**, and a subtask type, usually called **Sub-task** or **Subtask**.
+- Invite the KloudMate bot to the Slack channels that new hires join: one for the whole company, and one for each team.
+- The people who submit the form need to be members of your KloudMate workspace, because the form has **Sign-in required** turned on.
+
+## Step 1: Save your Jira address as a variable
+
+The email to the manager links to the onboarding issue. Open **Workflows → Variables** and create a **Constant** named `jira_url`, with your Jira site's address as its value, such as `https://acme.atlassian.net`. If you followed another guide that uses it, it's already there.
+
+## Step 2: Import the form workflow
+
+1. Copy the YAML below. To use your own teams, change the `options` of the `team` input.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and on every Jira step, pick your Jira connection and the **Site**. Then set the project and the issue type:
+ - On **Open the onboarding issue**, pick the project and an issue type such as **Task**.
+ - On the three task steps, pick the same project and its subtask type.
+
+```yaml
+kind: workflow
+uid: guide-start-onboarding
+spec:
+ name: Start onboarding for a new hire
+ description: From an HR form, opens an onboarding issue in Jira with tasks for IT, facilities, and the manager, and schedules the new hire's Slack welcome for their start date.
+ definition:
+ schema_version: 1
+ trigger:
+ type: form
+ config:
+ title: Onboard a new hire
+ description: "Fill this in when someone accepts an offer. Enter start_date as YYYY-MM-DD, up to 90 days ahead."
+ require_login: true
+ success_message: Thanks. The onboarding tasks are in Jira, and the manager gets an email with the details.
+ inputs:
+ - name: name
+ type: string
+ required: true
+ - name: email
+ type: email
+ required: true
+ - name: manager_email
+ type: email
+ required: true
+ - name: team
+ type: enum
+ required: true
+ options: [Engineering, Sales, Support]
+ - name: start_date
+ type: string
+ required: true
+ steps:
+ - id: check_date
+ type: branch
+ display_name: Is the start date a date?
+ if:
+ all:
+ - field: trigger.inputs.start_date
+ op: regex
+ value: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
+ then:
+ - id: parent
+ type: action
+ action: jira.create_issue
+ display_name: Open the onboarding issue
+ connection_id:
+ $input: jira
+ with:
+ summary: "Onboard {{ trigger.inputs.name }} ({{ trigger.inputs.team }}), starting {{ trigger.inputs.start_date }}"
+ description: |-
+ *New hire:* {{ trigger.inputs.name }} ({{ trigger.inputs.email }})
+ *Team:* {{ trigger.inputs.team }}
+ *Manager:* {{ trigger.inputs.manager_email }}
+ *Start date:* {{ trigger.inputs.start_date }}
+ - id: tasks
+ type: parallel
+ display_name: Create the onboarding tasks
+ branches:
+ - - id: task_it
+ type: action
+ action: jira.create_issue
+ display_name: "Task for IT: laptop and accounts"
+ connection_id:
+ $input: jira
+ with:
+ summary: "Set up a laptop and accounts for {{ trigger.inputs.name }}"
+ fields:
+ parent:
+ key: "{{ steps.parent.output.key }}"
+ - - id: task_facilities
+ type: action
+ action: jira.create_issue
+ display_name: "Task for facilities: desk and badge"
+ connection_id:
+ $input: jira
+ with:
+ summary: "Prepare a desk and a badge for {{ trigger.inputs.name }}"
+ fields:
+ parent:
+ key: "{{ steps.parent.output.key }}"
+ - - id: find_manager
+ type: action
+ action: jira.find_user
+ display_name: Find the manager in Jira
+ connection_id:
+ $input: jira
+ with:
+ query: "{{ trigger.inputs.manager_email }}"
+ - id: task_manager
+ type: action
+ action: jira.create_issue
+ display_name: "Task for the manager: first week"
+ connection_id:
+ $input: jira
+ with:
+ summary: "Plan the first week for {{ trigger.inputs.name }}"
+ fields:
+ parent:
+ key: "{{ steps.parent.output.key }}"
+ assignee:
+ accountId: "{{ steps.find_manager.output.users.0.accountId }}"
+ - id: schedule_welcome
+ type: action
+ action: store.add_to_list
+ display_name: Schedule the Slack welcome
+ with:
+ scope: workspace
+ key: "onboarding:{{ trigger.inputs.start_date }}"
+ item:
+ name: "{{ trigger.inputs.name }}"
+ email: "{{ trigger.inputs.email }}"
+ team: "{{ trigger.inputs.team }}"
+ manager_email: "{{ trigger.inputs.manager_email }}"
+ skip_if_present: true
+ ttl: 90d
+ - id: tell_manager
+ type: action
+ action: email.send
+ display_name: Email the manager
+ with:
+ to: "{{ trigger.inputs.manager_email }}"
+ subject: "Onboarding for {{ trigger.inputs.name }} is set up"
+ html: |-
+ {{ trigger.inputs.name | escape }} starts on {{ trigger.inputs.start_date | escape }}. Their onboarding tasks are in {{ steps.parent.output.key }} , including one for you to plan their first week.
+
+ On their start date, KloudMate invites them to the team's Slack channels and sends them a welcome message.
+ else:
+ - id: tell_bad_date
+ type: action
+ action: email.send
+ display_name: Ask for the date again
+ with:
+ to: "{{ trigger.submitted_by.email }}"
+ subject: "Onboarding for {{ trigger.inputs.name }} needs a start date"
+ html: "{{ trigger.inputs.start_date | escape }} isn't a date in the form YYYY-MM-DD, such as 2026-11-02, so nothing was set up. Submit the form again with the start date in that form."
+inputs:
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+ jira_url:
+ kind: variable
+ name: jira_url
+ type: constant
+```
+
+## Step 3: Import the daily workflow
+
+1. Copy the YAML below. If you changed the teams in the form, change the Route paths in **Which team?** to match.
+2. Import it the same way.
+3. On every Slack step, pick your Slack connection. On each **Invite to …** step, pick the channel it names.
+4. On the trigger, set **Timezone** to your office's timezone, such as `Europe/London`.
+
+```yaml
+kind: workflow
+uid: guide-welcome-new-hires
+spec:
+ name: Welcome new hires on their start date
+ description: Every morning, invites the people who start today to their team's Slack channels and sends them a welcome message.
+ definition:
+ schema_version: 1
+ trigger:
+ type: schedule
+ config:
+ mode: daily
+ hour: 9
+ minute: 0
+ run_on_weekends: true
+ timezone: UTC
+ steps:
+ - id: today
+ type: action
+ action: store.get
+ display_name: Who starts today?
+ with:
+ scope: workspace
+ key: "onboarding:{{ trigger.schedule.date }}"
+ - id: anyone
+ type: branch
+ display_name: Anyone starting?
+ if:
+ all:
+ - field: steps.today.output.value
+ op: is_not_empty
+ then:
+ - id: each_hire
+ type: loop
+ display_name: For each new hire
+ items: "{{ steps.today.output.value }}"
+ concurrency: 1
+ each:
+ - id: find
+ type: action
+ action: slack.find_user_by_email
+ display_name: Find them in Slack
+ on_error: continue
+ connection_id:
+ $input: slack
+ with:
+ email: "{{ item.email }}"
+ - id: in_slack
+ type: branch
+ display_name: Do they have Slack yet?
+ if:
+ all:
+ - field: steps.find.output.user.id
+ op: is_not_empty
+ then:
+ - id: invite_all
+ type: action
+ action: slack.invite_to_channel
+ display_name: Invite to the company channel
+ on_error: continue
+ connection_id:
+ $input: slack
+ with:
+ users: "{{ steps.find.output.user.id }}"
+ - id: team_channel
+ type: route
+ display_name: Which team?
+ routes:
+ - label: engineering
+ if:
+ all:
+ - field: item.team
+ op: eq
+ value: Engineering
+ steps:
+ - id: invite_engineering
+ type: action
+ action: slack.invite_to_channel
+ display_name: Invite to the engineering channel
+ on_error: continue
+ connection_id:
+ $input: slack
+ with:
+ users: "{{ steps.find.output.user.id }}"
+ - label: sales
+ if:
+ all:
+ - field: item.team
+ op: eq
+ value: Sales
+ steps:
+ - id: invite_sales
+ type: action
+ action: slack.invite_to_channel
+ display_name: Invite to the sales channel
+ on_error: continue
+ connection_id:
+ $input: slack
+ with:
+ users: "{{ steps.find.output.user.id }}"
+ - label: support
+ if:
+ all:
+ - field: item.team
+ op: eq
+ value: Support
+ steps:
+ - id: invite_support
+ type: action
+ action: slack.invite_to_channel
+ display_name: Invite to the support channel
+ on_error: continue
+ connection_id:
+ $input: slack
+ with:
+ users: "{{ steps.find.output.user.id }}"
+ - id: welcome
+ type: action
+ action: slack.send_direct_message
+ display_name: Send a welcome message
+ connection_id:
+ $input: slack
+ with:
+ user: "{{ steps.find.output.user.id }}"
+ text: ":wave: Welcome, {{ item.name | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}! You're now in your team's channels. Your manager has a plan for your first week, and the IT team has your laptop ready."
+ else:
+ - id: tell_manager
+ type: action
+ action: email.send
+ display_name: Tell the manager
+ with:
+ to: "{{ item.manager_email }}"
+ subject: "{{ item.name }} doesn't have a Slack account yet"
+ html: "{{ item.name | escape }} starts today, but no Slack account uses {{ item.email | escape }}, so KloudMate couldn't invite them to the team's channels. Ask IT to create the account, then invite them by hand."
+ - id: done
+ type: action
+ action: store.remove
+ display_name: Clear today's list
+ with:
+ scope: workspace
+ key: "onboarding:{{ trigger.schedule.date }}"
+inputs:
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+```
+
+## How the form workflow works
+
+### The form
+
+The **Form submission** trigger asks for the new hire's `name`, `email`, and `team`, their `manager_email`, and their `start_date`. A **Choice** input lists the teams, so the value always matches one of the Route paths in the daily workflow.
+
+There's no date input type, so `start_date` is **Text**. A **Branch** checks that it **matches regex** `^[0-9]{4}-[0-9]{2}-[0-9]{2}$`, the form `2026-11-02`, because the daily workflow looks the new hire up by a date in exactly that form. If the date doesn't match, the **Else** block emails the person who submitted the form, at `{{ trigger.submitted_by.email }}`, and nothing is created.
+
+### Open the onboarding issue
+
+A Jira **Create Issue** step opens one issue for the whole onboarding, with the details in its description. Jira reads `*text*` in a description as bold:
+
+```liquid
+*New hire:* {{ trigger.inputs.name }} ({{ trigger.inputs.email }})
+*Team:* {{ trigger.inputs.team }}
+*Manager:* {{ trigger.inputs.manager_email }}
+*Start date:* {{ trigger.inputs.start_date }}
+```
+
+### Create the onboarding tasks
+
+A **Parallel** step creates the three tasks at the same time, one in each branch. Each task is a Jira **Create Issue** step with the subtask issue type, and its **Extra fields** set the onboarding issue as its parent:
+
+```json
+{ "parent": { "key": "{{ steps.parent.output.key }}" } }
+```
+
+The manager's branch looks the manager up with Jira **Find User** first, and assigns them the task by adding `"assignee": { "accountId": "{{ steps.find_manager.output.users.0.accountId }}" }` to **Extra fields**. The tasks for IT and facilities stay unassigned, for those teams to pick up from the project.
+
+### Schedule the Slack welcome
+
+A **Storage: Add to List** step adds the new hire to a list under their start date, with **Scope** set to **This workspace** so the daily workflow can read it. The key is `onboarding:{{ trigger.inputs.start_date }}`, so everyone who starts on the same day ends up in the same list. The **Item** is an object:
+
+```json
+{
+ "name": "{{ trigger.inputs.name }}",
+ "email": "{{ trigger.inputs.email }}",
+ "team": "{{ trigger.inputs.team }}",
+ "manager_email": "{{ trigger.inputs.manager_email }}"
+}
+```
+
+**Skip if already there** is on, so a form submitted twice by mistake doesn't welcome someone twice. **Expires after** is `90d`, which removes a list that nothing reads, for example after a start date moved. That also limits how far ahead the form works: a start date more than 90 days away expires before the day comes.
+
+### Email the manager
+
+A **Send Email** step sends the manager a link to the onboarding issue. Values from the form go through `escape`, so the email shows them as plain text. See [Escape values in an email](/workflows/templating/#escape-values-in-an-email).
+
+## How the daily workflow works
+
+### Who starts today?
+
+A **Storage: Get** step reads `onboarding:{{ trigger.schedule.date }}`. `trigger.schedule.date` is the run's date in the trigger's timezone, in the same `2026-11-02` form as the key, so the step finds the list for today. **Run on weekends** stays on, for anyone who starts on a weekend.
+
+A **Branch** stops there when the list **is empty**, which is most days.
+
+### For each new hire
+
+A **Loop** runs once for each new hire on today's list, with **Items** set to `{{ steps.today.output.value }}`. Inside it, `item` is one new hire, so `item.email` is their address and `item.team` their team.
+
+A Slack **Find User by Email** step looks the new hire up in Slack. IT might not have created their account yet, so under **Settings**, **On failure** is **Go to the next step**, and a **Branch** checks whether a user came back.
+
+For a new hire who has an account:
+
+1. A Slack **Invite to Channel** step invites them to the company channel.
+2. A **Route** invites them to their team's channel. Each path runs when `item.team` **equals** its team's name.
+3. A Slack **Send Direct Message** step welcomes them.
+
+Slack refuses to invite someone who is already in a channel, and returns an error. The invite steps have **On failure** set to **Go to the next step**, so someone who joined a channel early still gets the rest of their welcome.
+
+For a new hire without an account, a **Send Email** step tells their manager, so someone can invite them by hand.
+
+### Clear today's list
+
+After the loop, a **Storage: Remove** step deletes today's list, since nothing needs it again.
+
+## Step 4: Test and publish
+
+The daily workflow finds a new hire only on their start date, so test it with a start date that matches its trigger sample:
+
+1. In the daily workflow, open the trigger's **Test** tab and click **Test trigger**. The sample's `date` is the next day the workflow runs.
+2. Publish the form workflow. The first publish also switches it on, and the form goes live.
+3. Open the form workflow's trigger, copy the **Public form URL**, and submit the form for a test hire. Use your own email address as the new hire's and the manager's, and the `date` from step 1 as the start date.
+4. Check that the onboarding issue and its three tasks are in Jira, and that you got the manager's email.
+5. In the daily workflow, click **Test run**. It reads the list for that date, and invites and welcomes you as the test hire.
+6. Publish the daily workflow.
+
+Delete the test issues in Jira afterwards. The test run already removed the test hire's list from Storage.
+
+## Adapt the workflow
+
+- **Add a task.** Add a branch to **Create the onboarding tasks** with another Jira **Create Issue** step, such as a task for payroll.
+- **Let HR submit without a KloudMate account.** Turn off **Sign-in required** on the form trigger, and keep the form's address private. Then change **To** on **Ask for the date again** to your HR team's address, because an open form doesn't know who submitted it.
+- **Send a welcome email as well.** Add a **Send Email** step to the daily workflow's loop, to `{{ item.email }}`, with links to your handbook and the tools the team uses.
+- **Invite people to more channels.** Add an **Invite to Channel** step to a team's path in **Which team?**, with **On failure** set to **Go to the next step**, like the others.
+
+## Related
+
+- [Storage](/workflows/storage/) for lists and expiry.
+- [Triggers](/workflows/triggers/#form-submission) for form fields and **Sign-in required**.
+- [Parallel](/workflows/control-flow/#parallel) and [Loop](/workflows/control-flow/#loop-for-each) for the steps that run the tasks and the new hires.
diff --git a/src/content/docs/guides/workflows/patch-hosts-one-at-a-time.mdx b/src/content/docs/guides/workflows/patch-hosts-one-at-a-time.mdx
new file mode 100644
index 00000000..7310b8d3
--- /dev/null
+++ b/src/content/docs/guides/workflows/patch-hosts-one-at-a-time.mdx
@@ -0,0 +1,357 @@
+---
+title: "Patch hosts one at a time"
+description: "Upgrade the packages on a list of Linux hosts through the KloudMate agent, one host after another. Each host reboots if an update needs it, and the rollout stops at the first host that doesn't come back healthy."
+sidebar:
+ order: 3
+---
+
+This guide builds a rolling patch: you give it a list of Linux hosts, and it upgrades their packages one host at a time through the KloudMate agent. A host reboots when an update needs it, and the next host starts only after the previous one is back and has no failed services. If a host fails, the rollout stops there, so a bad update takes down one host instead of the whole fleet.
+
+The rollout is two workflows. **Patch one host** does the work on a single host. **Patch hosts one at a time** reads your list and calls **Patch one host** for each host in turn:
+
+```text
+Patch hosts one at a time Manual / API trigger
+├─ Read the host list Run Code
+├─ For each host Loop, one at a time
+│ └─ Patch this host Call Workflow: Patch one host
+└─ Email the result Send Email
+
+Patch one host Manual / API trigger
+├─ Find the host's agent Check a Host Agent
+└─ Is the agent reporting? Branch
+ ├─ Then
+ │ ├─ Upgrade packages Run Command on Host
+ │ ├─ Reboot if an update needs it Run Command on Host
+ │ ├─ Rebooting? Branch
+ │ │ └─ Then: wait for the reboot, and until the agent is back
+ │ └─ Check that nothing failed to start Run Command on Host
+ └─ Else: stop, the host can't be patched
+```
+
+**Patch one host** is also useful on its own, when you want to patch a single host by hand.
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- Install the [KloudMate agent](/kloudmate-agent/installation/linux-agent/) on each host, and [allow runbook scripts](/workflows/actions/#allow-runbook-scripts-on-a-host) on each one.
+- The hosts use `apt`, `dnf`, or `yum` to manage packages, and `systemd`.
+- Patch hosts that can go down one at a time without an outage, such as web servers behind a load balancer.
+
+## Step 1: Import and publish Patch one host
+
+Import this workflow first, because the rollout calls it and can only call a published workflow:
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, then click **Publish**.
+
+```yaml
+kind: workflow
+uid: guide-patch-one-host
+spec:
+ name: Patch one host
+ description: Upgrades a host's packages through its KloudMate agent, reboots it if an update needs a reboot, and checks that nothing failed to start.
+ definition:
+ schema_version: 1
+ trigger:
+ type: manual
+ config: {}
+ inputs:
+ - name: host
+ type: string
+ required: true
+ steps:
+ - id: agent
+ type: action
+ action: km.host_agent
+ display_name: Find the host's agent
+ with:
+ hostname: "{{ trigger.inputs.host }}"
+ - id: ready
+ type: branch
+ display_name: Is the agent reporting?
+ if:
+ all:
+ - field: steps.agent.output.status
+ op: eq
+ value: reachable
+ then:
+ - id: upgrade
+ type: action
+ action: agent.run_script
+ display_name: Upgrade packages
+ timeout: 15m
+ with:
+ agent_id: "{{ steps.agent.output.agent_id }}"
+ timeout_seconds: 720
+ commands: |-
+ set -e
+ if command -v apt-get >/dev/null 2>&1; then
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get update -q
+ apt-get upgrade -y -q
+ else
+ dnf upgrade -y -q || yum update -y -q
+ fi
+ - id: reboot
+ type: action
+ action: agent.run_script
+ display_name: Reboot if an update needs it
+ timeout: 5m
+ with:
+ agent_id: "{{ steps.agent.output.agent_id }}"
+ commands: |-
+ if [ -f /var/run/reboot-required ] || { command -v needs-restarting >/dev/null 2>&1 && ! needs-restarting -r >/dev/null 2>&1; }; then
+ shutdown -r +1 "Rebooting after package updates"
+ echo "rebooting"
+ else
+ echo "no reboot needed"
+ fi
+ - id: rebooting
+ type: branch
+ display_name: Rebooting?
+ if:
+ all:
+ - field: steps.reboot.output.stdout
+ op: contains
+ value: rebooting
+ then:
+ - id: settle
+ type: wait
+ display_name: Wait for the reboot
+ duration: 3m
+ - id: back
+ type: wait_until
+ display_name: Wait until the agent reports again
+ timeout: 15m
+ check:
+ - id: agent_again
+ type: action
+ action: km.host_agent
+ display_name: Check the agent
+ with:
+ hostname: "{{ trigger.inputs.host }}"
+ until:
+ all:
+ - field: steps.agent_again.output.seconds_since_checkin
+ op: lt
+ value: 90
+ - id: health
+ type: action
+ action: agent.run_script
+ display_name: Check that nothing failed to start
+ timeout: 5m
+ with:
+ agent_id: "{{ steps.agent.output.agent_id }}"
+ commands: |-
+ failed=$(systemctl list-units --state=failed --no-legend --plain)
+ if [ -n "$failed" ]; then
+ echo "$failed"
+ exit 1
+ fi
+ echo "all units running"
+ else:
+ - id: not_ready
+ type: action
+ action: code.run
+ display_name: Stop, the host can't be patched
+ with:
+ input:
+ host: "{{ trigger.inputs.host }}"
+ status: "{{ steps.agent.output.status }}"
+ source: |-
+ function main(input) {
+ throw new Error(input.host + " can't be patched: its agent status is " + input.status + ".");
+ }
+inputs: {}
+```
+
+## Step 2: Import Patch hosts one at a time
+
+Import the rollout the same way. The import finds **Patch one host** by its `uid` and fills in the **Call Workflow** step for you:
+
+```yaml
+kind: workflow
+uid: guide-patch-hosts-one-at-a-time
+spec:
+ name: Patch hosts one at a time
+ description: Patches a list of hosts one after another, and stops at the first host that fails.
+ definition:
+ schema_version: 1
+ trigger:
+ type: manual
+ config: {}
+ inputs:
+ - name: hosts
+ type: textarea
+ required: true
+ - name: notify_email
+ type: email
+ required: true
+ steps:
+ - id: split
+ type: action
+ action: code.run
+ display_name: Read the host list
+ with:
+ input:
+ text: "{{ trigger.inputs.hosts }}"
+ source: |-
+ function main(input) {
+ const hosts = String(input.text || "")
+ .split(/[\s,]+/)
+ .filter((name) => name !== "");
+ return { hosts: hosts };
+ }
+ - id: each_host
+ type: loop
+ display_name: For each host
+ items: "{{ steps.split.output.output.hosts }}"
+ concurrency: 1
+ each:
+ - id: patch
+ type: call_workflow
+ display_name: Patch this host
+ workflow_id:
+ $input: patch_one_host
+ with:
+ host: "{{ item }}"
+ - id: done
+ type: action
+ action: email.send
+ display_name: Email the result
+ with:
+ to: "{{ trigger.inputs.notify_email }}"
+ subject: The patch rollout finished
+ html: "These hosts are patched, and none of them has a failed service: {{ steps.split.output.output.hosts | join: ', ' | escape }}."
+inputs:
+ patch_one_host:
+ kind: workflow
+ uid: guide-patch-one-host
+ name: Patch one host
+```
+
+Publish it when you're ready. Its trigger is **Manual / API**, so it only runs when someone clicks **Run**.
+
+## How each step works
+
+### Find the host's agent
+
+A **Check a Host Agent** step looks up the agent by **Host name**, `{{ trigger.inputs.host }}`. The name must match the host name the agent registered with, as shown under **Settings → Agents**.
+
+A **Branch** then checks that `steps.agent.output.status` **equals** `reachable`, which means the agent checked in within the last 5 minutes. For any other status, such as `unreachable` or `not_found`, the **Else** block runs a **Run Code** step that throws an error:
+
+```js
+function main(input) {
+ throw new Error(input.host + " can't be patched: its agent status is " + input.status + ".");
+}
+```
+
+The error fails the run with a message that names the host and the reason, and a failed call stops the rollout. Without the check, a command for a host that's down would wait until the step timed out.
+
+### Upgrade packages
+
+A **Run Command on Host** step runs the upgrade on the host, with **Host** set to `{{ steps.agent.output.agent_id }}` from the lookup:
+
+```sh
+set -e
+if command -v apt-get >/dev/null 2>&1; then
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get update -q
+ apt-get upgrade -y -q
+else
+ dnf upgrade -y -q || yum update -y -q
+fi
+```
+
+The lines run as one shell script. `set -e` makes the script stop, and the step fail, at the first command that fails, instead of carrying on after a failed `apt-get update`.
+
+An upgrade can take several minutes, so the step allows for it in two places. **Command timeout (seconds)** is `720`, which stops the upgrade on the host after 12 minutes. The step's **Timeout**, under **Settings**, is `15m`, which also covers the time the agent takes to pick up the command and report back.
+
+### Reboot if an update needs it
+
+A second **Run Command on Host** step checks whether the host needs a reboot. On Debian and Ubuntu, an update that needs one creates `/var/run/reboot-required`. On Red Hat-based systems, `needs-restarting -r` exits with a non-zero code when a reboot is needed:
+
+```sh
+if [ -f /var/run/reboot-required ] || { command -v needs-restarting >/dev/null 2>&1 && ! needs-restarting -r >/dev/null 2>&1; }; then
+ shutdown -r +1 "Rebooting after package updates"
+ echo "rebooting"
+else
+ echo "no reboot needed"
+fi
+```
+
+`shutdown -r +1` schedules the reboot a minute later instead of rebooting at once. That gives the agent time to report the step's result before the host goes down.
+
+### Wait for the reboot
+
+A **Branch** checks whether the output, `steps.reboot.output.stdout`, **contains** `rebooting`. If it does, a **Wait / Delay** step waits 3 minutes for the reboot to happen.
+
+A **Wait until** step then checks the agent with **Check a Host Agent** until `steps.agent_again.output.seconds_since_checkin` is **less than** `90`. The agent checks in every 60 seconds, so a check-in less than 90 seconds old means the host is back up. The step's **Timeout** is `15m`. If the host isn't back by then, the step fails, and so does the rollout.
+
+### Check that nothing failed to start
+
+The last **Run Command on Host** step lists the `systemd` units that are in the failed state:
+
+```sh
+failed=$(systemctl list-units --state=failed --no-legend --plain)
+if [ -n "$failed" ]; then
+ echo "$failed"
+ exit 1
+fi
+echo "all units running"
+```
+
+If any unit failed, the script prints them and exits with `1`, which fails the step. The failed units appear in the step's output in run history.
+
+### Read the host list
+
+In the rollout, the `hosts` input is **Long text**. A **Run Code** step turns it into a list, splitting it at line breaks, spaces, and commas, so you can paste one host name per line or a comma-separated list:
+
+```js
+function main(input) {
+ const hosts = String(input.text || "")
+ .split(/[\s,]+/)
+ .filter((name) => name !== "");
+ return { hosts: hosts };
+}
+```
+
+Its **Input** is `{"text": "{{ trigger.inputs.hosts }}"}`, and the list comes back as `steps.split.output.output.hosts`.
+
+### For each host
+
+A **Loop** runs a **Call Workflow** step for each host, with **Items** set to `{{ steps.split.output.output.hosts }}` and **Run at once** at `1`. The call's **Inputs** pass the host as `host`, which **Patch one host** reads as `{{ trigger.inputs.host }}`.
+
+With **Run at once** at `1`, the loop waits for each call to finish before it starts the next one, and a call that fails stops the loop. The rollout then fails, and run history shows which host failed and why.
+
+A **Wait until** can't go inside a loop, which is why the steps for one host are a workflow of their own. See [Wait until](/workflows/control-flow/#wait-until).
+
+### Email the result
+
+When every host is done, a **Send Email** step emails the address in the `notify_email` input, and lists the hosts it patched.
+
+## Step 3: Try it on one host
+
+1. Open **Patch one host**, click **Run**, and enter a host that can go down for a few minutes.
+2. Open the run from **Workflows → Runs**, and follow the steps as they run. A host that reboots shows **Waiting** while the workflow waits for it.
+3. When that works, run **Patch hosts one at a time** with two hosts, one per line, and your email address.
+
+A run patches at most 100 hosts, which is the most one loop handles.
+
+:::caution
+Every run changes the hosts for real. Try it on hosts you can afford to lose for a few minutes before you run it across a fleet.
+:::
+
+## Adapt the workflow
+
+- **Ask for approval first.** Add an **Approval** step before **For each host** in the rollout, with a message that lists `{{ steps.split.output.output.hosts | join: ", " }}`.
+- **Check your application too.** Add a command to **Check that nothing failed to start** that calls your service's health endpoint on the host, such as `curl -fsS http://localhost:8080/health`.
+- **Install security updates only.** On Red Hat-based systems, change the upgrade line to `dnf upgrade --security -y -q`.
+- **Patch on a schedule.** Change the rollout's trigger to a monthly **Schedule**, and keep the host names in a [constant](/workflows/variables/) as a comma-separated list. Then set the **Input** of **Read the host list** to `{"text": "{{ vars.patch_hosts }}"}`, and **To** on **Email the result** to your team's address.
+
+## Related
+
+- [Run Command on Host](/workflows/actions/#run-command-on-host) for how commands and timeouts work.
+- [Control flow](/workflows/control-flow/) for Loop, Call Workflow, and Wait until.
+- [Run Code](/workflows/actions/#run-code) for the sandbox the host list runs in.
diff --git a/src/content/docs/guides/workflows/renewal-reminders.mdx b/src/content/docs/guides/workflows/renewal-reminders.mdx
new file mode 100644
index 00000000..9a43bcd1
--- /dev/null
+++ b/src/content/docs/guides/workflows/renewal-reminders.mdx
@@ -0,0 +1,271 @@
+---
+title: "Remind owners before a license or contract renews"
+description: "Read a Google Sheet of software licenses and contracts every weekday, email each owner 30 days and 7 days before a renewal, and open a Jira issue to decide whether to renew."
+sidebar:
+ order: 11
+---
+
+Software licenses and contracts renew on their own unless someone cancels them in time. This guide builds a workflow that reads your list of renewals from a Google Sheet every weekday. When a renewal is 30 days away, it emails the owner and opens a Jira issue to decide whether to renew. When it's 7 days away, it emails the owner again.
+
+The list stays in a spreadsheet that anyone can update, and the workflow keeps track of the reminders it has sent in [Storage](/workflows/storage/), so each owner gets each reminder once.
+
+The finished workflow has these steps:
+
+```text
+Schedule: weekdays at 09:00
+├─ Read the renewals sheet HTTP Request to Google Sheets
+├─ Find the renewals that are due Run Code
+└─ For each renewal that's due Loop
+ ├─ Count reminders at this stage Storage: Increment
+ ├─ First reminder at this stage? Branch
+ │ └─ Then: Email the owner Send Email
+ ├─ Count issues for this renewal Storage: Increment
+ └─ No issue yet? Branch
+ └─ Then: Open a renewal issue Jira: Create Issue
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Google connection that can read Google Sheets. If you followed [Trigger a workflow when a new email is received in Gmail](/guides/workflows/trigger-on-new-gmail/), your `Google` connection already can. Otherwise, create one the way that guide describes, with the scope `https://www.googleapis.com/auth/spreadsheets.readonly`.
+- You need a Jira Cloud connection with the **Workflows** capability, and a project for renewal decisions.
+
+## Step 1: Set up the sheet
+
+Create a Google Sheet with one row per license or contract, and these columns, starting in column A:
+
+| Name | Owner | Renews on | Cost |
+|---|---|---|---|
+| Design tool | `design-lead@example.com` | `2026-11-30` | $9,000 a year |
+| CRM | `sales-ops@example.com` | `2027-01-15` | $48,000 a year |
+
+Keep the header in row 1. Write each date as `2026-11-30`, year first. If you type dates another way, format the column with **Format → Number → Custom date and time** as `yyyy-mm-dd`. **Cost** is optional.
+
+Copy the sheet's ID from its URL. It's the part between `/d/` and `/edit` in `https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit`. Then open **Workflows → Variables** in KloudMate, and create a **Constant** named `renewals_sheet_id` with the ID as its value.
+
+## Step 2: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On **Read the renewals sheet**, pick your Google connection.
+ - On **Open a renewal issue**, pick your Jira connection, the **Site**, the project, and an issue type such as **Task**.
+ - On the trigger, set **Timezone** to your team's timezone, such as `America/Chicago`.
+
+```yaml
+kind: workflow
+uid: guide-renewal-reminders
+spec:
+ name: Remind owners before a renewal
+ description: Every weekday, reads a Google Sheet of licenses and contracts, emails each owner 30 days and 7 days before a renewal, and opens a Jira issue to decide on the renewal.
+ definition:
+ schema_version: 1
+ trigger:
+ type: schedule
+ config:
+ mode: daily
+ hour: 9
+ minute: 0
+ run_on_weekends: false
+ timezone: UTC
+ steps:
+ - id: sheet
+ type: action
+ action: http.request
+ display_name: Read the renewals sheet
+ connection_id:
+ $input: google
+ with:
+ authType: connection
+ method: GET
+ url: "https://sheets.googleapis.com/v4/spreadsheets/{{ vars.renewals_sheet_id }}/values/A2:D500"
+ - id: due
+ type: action
+ action: code.run
+ display_name: Find the renewals that are due
+ with:
+ input:
+ rows: "{{ steps.sheet.output.body.values }}"
+ today: "{{ trigger.schedule.date }}"
+ source: |-
+ function main(input) {
+ const day = 24 * 60 * 60 * 1000;
+ const today = Date.parse(input.today);
+ const due = [];
+ const skipped = [];
+ for (const row of input.rows || []) {
+ const [name, owner, date, cost] = row;
+ if (!name || !owner || !/^\d{4}-\d{2}-\d{2}$/.test(date || "")) {
+ if (name) skipped.push(name);
+ continue;
+ }
+ const days = Math.round((Date.parse(date) - today) / day);
+ if (days < 0 || days > 30) continue;
+ const stage = days <= 7 ? "7" : "30";
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
+ due.push({ name: name, owner: owner, date: date, cost: cost || "", days: days, stage: stage, id: slug + ":" + date });
+ }
+ return { due: due, skipped: skipped };
+ }
+ - id: each_renewal
+ type: loop
+ display_name: For each renewal that's due
+ items: "{{ steps.due.output.output.due }}"
+ concurrency: 1
+ each:
+ - id: reminders
+ type: action
+ action: store.increment
+ display_name: Count reminders at this stage
+ with:
+ scope: workflow
+ key: "reminded:{{ item.id }}:{{ item.stage }}"
+ by: 1
+ ttl: 60d
+ - id: first_reminder
+ type: branch
+ display_name: First reminder at this stage?
+ if:
+ all:
+ - field: steps.reminders.output.value
+ op: lt
+ value: 2
+ then:
+ - id: email_owner
+ type: action
+ action: email.send
+ display_name: Email the owner
+ with:
+ to: "{{ item.owner }}"
+ subject: "{{ item.name }} renews on {{ item.date }}"
+ html: |-
+ **{{ item.name | escape }}** renews on {{ item.date | escape }}{% if item.cost != "" %}, for {{ item.cost | escape }}{% endif %}. You're listed as its owner.
+
+ If you still need it, check the renewal terms now. If you don't, cancel it before {{ item.date | escape }} so it doesn't renew.
+ - id: issues
+ type: action
+ action: store.increment
+ display_name: Count issues for this renewal
+ with:
+ scope: workflow
+ key: "issue:{{ item.id }}"
+ by: 1
+ ttl: 60d
+ - id: first_issue
+ type: branch
+ display_name: No issue yet?
+ if:
+ all:
+ - field: steps.issues.output.value
+ op: lt
+ value: 2
+ then:
+ - id: open_issue
+ type: action
+ action: jira.create_issue
+ display_name: Open a renewal issue
+ connection_id:
+ $input: jira
+ with:
+ summary: "Decide on the renewal of {{ item.name }} by {{ item.date }}"
+ description: "*Owner:* {{ item.owner }}\n*Renews on:* {{ item.date }}\n*Cost:* {{ item.cost | default: 'not listed' }}"
+inputs:
+ google:
+ kind: connection
+ name: Google
+ type: oauth2
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+ renewals_sheet_id:
+ kind: variable
+ name: renewals_sheet_id
+ type: constant
+```
+
+## How each step works
+
+### Read the renewals sheet
+
+An **HTTP Request** step reads the sheet through your Google connection, with **Authentication** set to **OAuth 2.0 connection**:
+
+```liquid
+https://sheets.googleapis.com/v4/spreadsheets/{{ vars.renewals_sheet_id }}/values/A2:D500
+```
+
+`A2:D500` skips the header row, and covers up to 499 renewals. Google returns the rows as `body.values`, a list with one list of cells per row. It leaves out empty rows at the end, and empty cells at the end of a row.
+
+### Find the renewals that are due
+
+A **Run Code** step works out which renewals are 30 days away or less. Its **Input** passes the rows and the run's date:
+
+```json
+{
+ "rows": "{{ steps.sheet.output.body.values }}",
+ "today": "{{ trigger.schedule.date }}"
+}
+```
+
+`trigger.schedule.date` is the run's date in the trigger's timezone, in the same `2026-11-30` form as the sheet. The code compares each row's date with it:
+
+```js
+function main(input) {
+ const day = 24 * 60 * 60 * 1000;
+ const today = Date.parse(input.today);
+ const due = [];
+ const skipped = [];
+ for (const row of input.rows || []) {
+ const [name, owner, date, cost] = row;
+ if (!name || !owner || !/^\d{4}-\d{2}-\d{2}$/.test(date || "")) {
+ if (name) skipped.push(name);
+ continue;
+ }
+ const days = Math.round((Date.parse(date) - today) / day);
+ if (days < 0 || days > 30) continue;
+ const stage = days <= 7 ? "7" : "30";
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
+ due.push({ name: name, owner: owner, date: date, cost: cost || "", days: days, stage: stage, id: slug + ":" + date });
+ }
+ return { due: due, skipped: skipped };
+}
+```
+
+Each renewal that's due gets a `stage`: `30` from 30 days out, and `7` in the last week. It also gets an `id` built from its name and date, such as `design-tool:2026-11-30`, because a Storage key can't hold spaces. A row without an owner, or with a date in another form, goes into `skipped`, which the step's output shows in run history.
+
+### Send each reminder once
+
+A **Loop** runs for each renewal in `steps.due.output.output.due`.
+
+A **Storage: Increment** step adds 1 to a counter under `reminded:{{ item.id }}:{{ item.stage }}`, and returns the new count. The first time the workflow sees a renewal at a stage, the count is `1`. A **Branch** checks that the count is **less than** `2`, and only then emails the owner. On the days after, the count is higher, so the owner isn't emailed again until the renewal reaches the next stage and gets a new counter.
+
+A second counter, `issue:{{ item.id }}`, works the same way for the Jira issue, so each renewal gets one issue, whichever stage the workflow first sees it at.
+
+The counters have **Scope** set to **This workflow**, and **Expires after** set to `60d`, so they clean themselves up after the renewal has passed. Checking the count keeps a reminder from going out twice even when the workflow runs again the same day. A run that fails before it sends a reminder, however, leaves the count at `1`, so that reminder isn't sent.
+
+### Email the owner and open the issue
+
+A **Send Email** step emails the owner, and passes values from the sheet through `escape` so they show up as written. See [Escape values in an email](/workflows/templating/#escape-values-in-an-email).
+
+A Jira **Create Issue** step opens the renewal decision, with the owner, the date, and the cost in its description. Assign the issue type and project that your finance or IT team works from.
+
+## Step 3: Test and publish
+
+1. Add a test row to the sheet with a renewal date 20 days from today, and your own email address as the owner.
+2. Open the trigger's **Test** tab and click **Test trigger**, then test **Read the renewals sheet** and **Find the renewals that are due** from their **Test** tabs. The output of **Find the renewals that are due** lists the test row under `due`.
+3. Click **Test run**. It emails you and opens a Jira issue for the test row.
+4. Delete the test row and the Jira issue. Then open **Workflows → Storage**, and delete the two keys the test run created, `reminded:…` and `issue:…`.
+5. Click **Publish**. The first publish also switches the workflow on.
+
+## Adapt the workflow
+
+- **Remind at other times.** Change the `stage` rule in **Find the renewals that are due**, for example to add a stage at 60 days with `days <= 60`, and change `days > 30` to match.
+- **Remind a team as well as the owner.** Add a **To** address, separated by a comma, such as `{{ item.owner }}, finance@example.com`.
+- **Post in Slack instead.** Replace **Email the owner** with a Slack **Post Message** step in your finance channel.
+- **Read a longer sheet.** Change `A2:D500` in the URL. A loop handles up to 100 renewals in one run, which is plenty for the renewals due in any 30 days.
+
+## Related
+
+- [Run Code](/workflows/actions/#run-code) for the sandbox the date check runs in.
+- [Storage](/workflows/storage/) for counters and expiry.
+- [Trigger a workflow when a new email is received in Gmail](/guides/workflows/trigger-on-new-gmail/) for creating a Google connection.
diff --git a/src/content/docs/guides/workflows/route-requests-with-ai.mdx b/src/content/docs/guides/workflows/route-requests-with-ai.mdx
new file mode 100644
index 00000000..919b71b2
--- /dev/null
+++ b/src/content/docs/guides/workflows/route-requests-with-ai.mdx
@@ -0,0 +1,200 @@
+---
+title: "Send requests to the right team with AI Route"
+description: "Collect requests through one internal form, let AI Route pick the team that handles each one, email that team, and tell the requester where the request went."
+sidebar:
+ order: 14
+---
+
+People rarely know which team handles what. This guide builds one form for every internal request, such as a broken laptop, a parking space, or an expense question. An **AI Route** step reads the request and picks the team that handles it, the workflow emails that team, and the requester gets an email saying where the request went. A request that doesn't clearly belong to one team goes to your office manager to pass on.
+
+**AI Route** decides from descriptions you write, such as "Facilities: desks, parking, and repairs in the office", instead of from rules. Nobody has to list every word that means "laptop", and a new kind of request usually finds its team without a change to the workflow.
+
+The finished workflow has these steps:
+
+```text
+Form: Ask for help
+├─ Which team handles it? AI Route
+│ ├─ it Email IT
+│ ├─ facilities Email facilities
+│ ├─ people Email the people team
+│ ├─ finance Email finance
+│ └─ unsure (the fallback route) Email the office manager
+└─ Tell the requester where it went Send Email
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- The people who submit the form need to be members of your KloudMate workspace, because the form has **Sign-in required** turned on.
+- Each request uses workflow credits, and the **AI Route** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Import the workflow
+
+1. Copy the YAML below, and replace the example addresses, such as `it@example.com`, with your teams' addresses.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and read each route's description. Change them to match what your teams handle.
+
+```yaml
+kind: workflow
+uid: guide-route-requests-with-ai
+spec:
+ name: Send requests to the right team
+ description: Reads a request from an internal form, lets AI pick the team that handles it, emails that team, and tells the requester where it went.
+ definition:
+ schema_version: 1
+ trigger:
+ type: form
+ config:
+ title: Ask for help
+ description: Describe what you need in your own words, and we'll send it to the team that handles it.
+ require_login: true
+ success_message: Thanks. We've sent your request to the team that handles it, and emailed you which one.
+ inputs:
+ - name: request
+ type: textarea
+ required: true
+ steps:
+ - id: team
+ type: ai_branch
+ display_name: Which team handles it?
+ instruction: "{{ trigger.inputs.request }}"
+ default_route: unsure
+ routes:
+ - label: it
+ description: IT. Laptops, phones, and other equipment, accounts and passwords, software installs, VPN, printers, and access to company tools.
+ steps:
+ - id: email_it
+ type: action
+ action: email.send
+ display_name: Email IT
+ with:
+ to: it@example.com
+ subject: "Request from {{ trigger.submitted_by.name }}"
+ html: "{{ trigger.submitted_by.name | escape }} ({{ trigger.submitted_by.email | escape }}) asked: {{ trigger.inputs.request | escape | newline_to_br }} It came to you because: {{ steps.team.output.reason | escape }}"
+ - label: facilities
+ description: Facilities. Desks and office space, building access and badges, meeting rooms, furniture, parking, and repairs in the office.
+ steps:
+ - id: email_facilities
+ type: action
+ action: email.send
+ display_name: Email facilities
+ with:
+ to: facilities@example.com
+ subject: "Request from {{ trigger.submitted_by.name }}"
+ html: "{{ trigger.submitted_by.name | escape }} ({{ trigger.submitted_by.email | escape }}) asked: {{ trigger.inputs.request | escape | newline_to_br }} It came to you because: {{ steps.team.output.reason | escape }}"
+ - label: people
+ description: The people team. Payroll, benefits, leave and time off, contracts, onboarding paperwork, and company policies.
+ steps:
+ - id: email_people
+ type: action
+ action: email.send
+ display_name: Email the people team
+ with:
+ to: people@example.com
+ subject: "Request from {{ trigger.submitted_by.name }}"
+ html: "{{ trigger.submitted_by.name | escape }} ({{ trigger.submitted_by.email | escape }}) asked: {{ trigger.inputs.request | escape | newline_to_br }} It came to you because: {{ steps.team.output.reason | escape }}"
+ - label: finance
+ description: Finance. Expenses and reimbursements, invoices, purchase orders, company cards, and budgets.
+ steps:
+ - id: email_finance
+ type: action
+ action: email.send
+ display_name: Email finance
+ with:
+ to: finance@example.com
+ subject: "Request from {{ trigger.submitted_by.name }}"
+ html: "{{ trigger.submitted_by.name | escape }} ({{ trigger.submitted_by.email | escape }}) asked: {{ trigger.inputs.request | escape | newline_to_br }} It came to you because: {{ steps.team.output.reason | escape }}"
+ - label: unsure
+ description: Anything that doesn't clearly belong to one of the other teams, a request that needs several teams, or a message that isn't a request.
+ steps:
+ - id: email_office
+ type: action
+ action: email.send
+ display_name: Email the office manager
+ with:
+ to: office@example.com
+ subject: "Request from {{ trigger.submitted_by.name }}"
+ html: "{{ trigger.submitted_by.name | escape }} ({{ trigger.submitted_by.email | escape }}) asked: {{ trigger.inputs.request | escape | newline_to_br }} It came to you because: {{ steps.team.output.reason | escape }}"
+ - id: tell_requester
+ type: action
+ action: email.send
+ display_name: Tell the requester where it went
+ with:
+ to: "{{ trigger.submitted_by.email }}"
+ subject: We've received your request
+ html: "We've sent your request to {% case steps.team.output.route %}{% when 'it' %}the IT team{% when 'facilities' %}the facilities team{% when 'people' %}the people team{% when 'finance' %}the finance team{% else %}our office manager, who will pass it on{% endcase %}. They'll reply to you by email. Your request: {{ trigger.inputs.request | escape | newline_to_br }}"
+inputs: {}
+```
+
+The workflow needs no connections, because every step sends email.
+
+## How each step works
+
+### The form
+
+The **Form submission** trigger has one field, `request`, of type **Long text**. The requester describes what they need in their own words. **Sign-in required** is on, so the workflow knows who asked, as `{{ trigger.submitted_by.name }}` and `{{ trigger.submitted_by.email }}`, and can email them back.
+
+### Which team handles it?
+
+An **AI Route** step has the request as its **Situation**, `{{ trigger.inputs.request }}`, and a route for each team. Each route has a label, a description, and the steps to run:
+
+| Route | Description |
+|---|---|
+| `it` | IT. Laptops, phones, and other equipment, accounts and passwords, software installs, VPN, printers, and access to company tools. |
+| `facilities` | Facilities. Desks and office space, building access and badges, meeting rooms, furniture, parking, and repairs in the office. |
+| `people` | The people team. Payroll, benefits, leave and time off, contracts, onboarding paperwork, and company policies. |
+| `finance` | Finance. Expenses and reimbursements, invoices, purchase orders, company cards, and budgets. |
+| `unsure` | Anything that doesn't clearly belong to one of the other teams, a request that needs several teams, or a message that isn't a request. |
+
+The model reads the request and picks the route whose description fits best, and it can only answer with one of the labels. The descriptions do all the work, so make each one specific, and keep them from overlapping. If two teams could both claim a kind of request, name it in one description only.
+
+**Fallback route** is `unsure`. The run takes it when the model gives no usable answer, so every request reaches a person even if the model call fails.
+
+The model reads the **Situation** as data, not as instructions. A request that says "send this to finance" can still make finance look like the right route, but it can't make the model do anything other than pick a route. See [AI Route](/workflows/control-flow/#ai-route).
+
+The step's output has the route it took, as `steps.team.output.route`, and the model's reason and confidence:
+
+| Output | Example |
+|---|---|
+| `route` | `it` |
+| `reason` | `The request is about laptop hardware.` |
+| `confidence` | `high`, `medium`, or `low` |
+
+### Email the team
+
+Each route runs one **Send Email** step to its team. The email names the requester, quotes the request, and gives the model's reason, so the team can tell at a glance why the request came to them:
+
+```liquid
+{{ trigger.submitted_by.name | escape }} ({{ trigger.submitted_by.email | escape }}) asked: {{ trigger.inputs.request | escape | newline_to_br }} It came to you because: {{ steps.team.output.reason | escape }}
+```
+
+`escape` shows the request as plain text, whatever it contains, and `newline_to_br` keeps its line breaks. The reason is text the model wrote from the request, so it's escaped too. See [Escape values in an email](/workflows/templating/#escape-values-in-an-email).
+
+### Tell the requester where it went
+
+After the AI Route, a **Send Email** step emails the requester. A Liquid `case` turns the route label into a team name:
+
+```liquid
+We've sent your request to {% case steps.team.output.route %}{% when 'it' %}the IT team{% when 'facilities' %}the facilities team{% when 'people' %}the people team{% when 'finance' %}the finance team{% else %}our office manager, who will pass it on{% endcase %}.
+```
+
+This step comes after the AI Route rather than inside a route, so it runs whichever route was taken.
+
+## Step 2: Test and publish
+
+1. Open the trigger's **Test** tab, type a request under **Test input values**, such as `My laptop battery lasts an hour`, and click **Test trigger**.
+2. Test **Which team handles it?** from its **Test** tab. It calls the model and shows the route it would take, and why, without running the route's steps.
+3. Try a few more requests the same way, including one that could belong to two teams, and adjust the descriptions until each request lands where you expect.
+4. Click **Publish**. The first publish also switches the workflow on, and the form goes live. Share the **Public form URL** from the trigger with your colleagues.
+
+## Adapt the workflow
+
+- **Open a ticket instead of an email.** Replace a team's **Send Email** step with a Jira **Create Issue** step in that team's project.
+- **Ask a person when the model isn't sure.** Inside each team's route, add a **Branch** that checks whether `steps.team.output.confidence` **equals** `low`, and also emails the office manager.
+- **Add a team.** Add a route with a label, a description, and a **Send Email** step, and add its name to the `case` in **Tell the requester where it went**. An AI Route holds up to 8 routes.
+
+## Related
+
+- [AI Route](/workflows/control-flow/#ai-route) for routes, descriptions, and the fallback route.
+- [Route](/workflows/control-flow/#route) for choosing a path by a condition instead of a model.
+- [Answer common questions in a help channel](/guides/workflows/answer-common-questions/) for sorting messages with **AI Extract** instead.
diff --git a/src/content/docs/guides/workflows/stop-dev-instances-at-night.mdx b/src/content/docs/guides/workflows/stop-dev-instances-at-night.mdx
new file mode 100644
index 00000000..7b7279e2
--- /dev/null
+++ b/src/content/docs/guides/workflows/stop-dev-instances-at-night.mdx
@@ -0,0 +1,468 @@
+---
+title: "Stop development instances at night"
+description: "Stop the EC2 instances tagged as development every weekday evening, and start them again the next weekday morning. A Slack reaction on the evening warning keeps them running for the night."
+sidebar:
+ order: 2
+---
+
+This guide builds a schedule that stops your development EC2 instances every weekday evening and starts them again the next weekday morning. EC2 bills a running instance by the second, so a development instance that sits idle overnight and at weekends costs money for nothing. A stopped instance keeps everything on its EBS volumes.
+
+Half an hour before the instances stop, the workflow posts a warning in Slack. If someone is still working, they react to the warning with 📌 (`:pushpin:`), and the instances keep running that night.
+
+The work is split across these workflows:
+
+| Workflow | Trigger | What it does |
+|---|---|---|
+| **Stop development instances at night** | **Schedule**, weekdays at 18:30 | Posts the warning, waits 30 minutes, and stops the running development instances, unless someone reacted to the warning. |
+| **Keep development instances running tonight** | Slack **Reaction added** | Records that someone reacted to tonight's warning. |
+| **Start development instances in the morning** | **Schedule**, weekdays at 08:00 | Starts the instances that the evening workflow stopped. |
+
+The workflows pass information to each other through [Storage](/workflows/storage/) keys with **Scope** set to **This workspace**:
+
+| Key | Written by | Read by |
+|---|---|---|
+| `dev_warning:` | The evening workflow, when it posts the warning | The reaction workflow, to check that a reaction is on tonight's warning |
+| `keep_dev_running` | The reaction workflow | The evening workflow, before it stops anything |
+| `dev_stopped_instances` | The evening workflow | The morning workflow |
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need an [AWS connection](/platform/settings/connections/connect-an-account/) with the **Workflows** capability, in the account that runs the instances.
+- You need a Slack connection with the **Workflows** capability, and a channel for the warnings. Invite the KloudMate bot to that channel.
+- Tag each development instance with the key `Environment` and the value `development`. The workflows don't touch instances without this tag.
+
+## Step 1: Let the AWS connection stop and start the instances
+
+Add this policy to the IAM role that your AWS connection uses. Replace `ACCOUNT_ID` with your AWS account ID:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "ec2:DescribeInstances",
+ "Resource": "*"
+ },
+ {
+ "Effect": "Allow",
+ "Action": ["ec2:StopInstances", "ec2:StartInstances"],
+ "Resource": "arn:aws:ec2:*:ACCOUNT_ID:instance/*",
+ "Condition": {
+ "StringEquals": { "aws:ResourceTag/Environment": "development" }
+ }
+ }
+ ]
+}
+```
+
+The condition limits stopping and starting to instances tagged as development, so the connection can't stop a production instance even if a step asks it to.
+
+If the instances' EBS volumes are encrypted with a customer managed KMS key, the role also needs `kms:CreateGrant` on that key. Without it, an instance goes back to **Stopped** a few seconds after the morning workflow starts it.
+
+## Step 2: Import the workflows
+
+Import each workflow on its own. For each one, open **Workflows**, click **Import**, paste the YAML, click **Import**, and then click **Open workflow** to set what the import couldn't.
+
+### Stop development instances at night
+
+```yaml
+kind: workflow
+uid: guide-stop-dev-instances
+spec:
+ name: Stop development instances at night
+ description: Every weekday evening, warns in Slack and then stops the running EC2 instances tagged Environment=development, unless someone asked to keep them running.
+ definition:
+ schema_version: 1
+ trigger:
+ type: schedule
+ config:
+ mode: daily
+ hour: 18
+ minute: 30
+ run_on_weekends: false
+ timezone: UTC
+ steps:
+ - id: find
+ type: action
+ action: aws.executeApi
+ display_name: Find running development instances
+ connection_id:
+ $input: aws
+ with:
+ service: ec2
+ action: DescribeInstances
+ params:
+ Filters:
+ - Name: tag:Environment
+ Values: [development]
+ - Name: instance-state-name
+ Values: [running]
+ - id: ids
+ type: action
+ action: transform
+ display_name: List their instance IDs
+ with:
+ from: "{{ steps.find.output.result }}"
+ path: $.Reservations[*].Instances[*].InstanceId
+ - id: any_running
+ type: branch
+ display_name: Anything running?
+ if:
+ all:
+ - field: steps.ids.output.value
+ op: is_not_empty
+ then:
+ - id: warn
+ type: action
+ action: slack.post_message
+ display_name: Warn the channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":crescent_moon: In 30 minutes, these development instances stop for the night: {{ steps.ids.output.value | join: ', ' }}. React to this message with :pushpin: to keep them running tonight."
+ - id: remember_warning
+ type: action
+ action: store.put
+ display_name: Remember the warning message
+ with:
+ scope: workspace
+ key: "dev_warning:{{ steps.warn.output.ts }}"
+ value: "true"
+ ttl: 30m
+ - id: wait
+ type: wait
+ display_name: Wait 30 minutes
+ duration: 30m
+ - id: keep
+ type: action
+ action: store.get
+ display_name: Did anyone ask to keep them running?
+ with:
+ scope: workspace
+ key: keep_dev_running
+ - id: kept
+ type: branch
+ display_name: Keep them running?
+ if:
+ all:
+ - field: steps.keep.output.value
+ op: is_not_empty
+ then:
+ - id: say_kept
+ type: action
+ action: slack.post_message
+ display_name: Say they keep running
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ steps.warn.output.channel }}"
+ thread_ts: "{{ steps.warn.output.ts }}"
+ text: "Development instances keep running tonight, as <@{{ steps.keep.output.value.user }}> asked."
+ else:
+ - id: remember_stopped
+ type: action
+ action: store.put
+ display_name: Remember which instances stop
+ with:
+ scope: workspace
+ key: dev_stopped_instances
+ value: "{{ steps.ids.output.value }}"
+ ttl: 4d
+ - id: stop
+ type: action
+ action: aws.executeApi
+ display_name: Stop the instances
+ connection_id:
+ $input: aws
+ with:
+ service: ec2
+ action: StopInstances
+ params:
+ InstanceIds: "{{ steps.ids.output.value }}"
+ - id: say_stopped
+ type: action
+ action: slack.post_message
+ display_name: Say they stopped
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ steps.warn.output.channel }}"
+ thread_ts: "{{ steps.warn.output.ts }}"
+ text: "Stopped the development instances. They start again on the next weekday morning."
+inputs:
+ aws:
+ kind: connection
+ name: AWS
+ type: aws
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+```
+
+After you import it:
+
+- On **Find running development instances** and **Stop the instances**, pick your AWS connection.
+- On the Slack steps, pick your Slack connection. On **Warn the channel**, also pick the channel.
+- On the trigger, set **Timezone** to your team's timezone, such as `America/New_York`.
+
+### Keep development instances running tonight
+
+```yaml
+kind: workflow
+uid: guide-keep-dev-instances-running
+spec:
+ name: Keep development instances running tonight
+ description: When someone reacts with a pushpin to tonight's shutdown warning, keeps the development instances running until morning.
+ definition:
+ schema_version: 1
+ trigger:
+ type: integration_event
+ config:
+ connection_id:
+ $input: slack
+ trigger_key: slack.reaction_added
+ props:
+ emoji: pushpin
+ steps:
+ - id: warning
+ type: action
+ action: store.get
+ display_name: Is it tonight's warning?
+ with:
+ scope: workspace
+ key: "dev_warning:{{ trigger.event.item.ts }}"
+ - id: on_warning
+ type: branch
+ display_name: Reacted to the warning?
+ if:
+ all:
+ - field: steps.warning.output.value
+ op: is_not_empty
+ then:
+ - id: keep
+ type: action
+ action: store.put
+ display_name: Keep them running tonight
+ with:
+ scope: workspace
+ key: keep_dev_running
+ value:
+ user: "{{ trigger.event.user }}"
+ ttl: 12h
+ - id: confirm
+ type: action
+ action: slack.post_message
+ display_name: Confirm in the thread
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.item.channel }}"
+ thread_ts: "{{ trigger.event.item.ts }}"
+ text: "OK <@{{ trigger.event.user }}>, development instances keep running tonight."
+inputs:
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+```
+
+After you import it:
+
+- On the trigger, pick your Slack connection under **Account**, and set **Only in channel** to the channel that gets the warnings.
+- On **Confirm in the thread**, pick your Slack connection.
+
+### Start development instances in the morning
+
+```yaml
+kind: workflow
+uid: guide-start-dev-instances
+spec:
+ name: Start development instances in the morning
+ description: Every weekday morning, starts the development instances that the evening workflow stopped.
+ definition:
+ schema_version: 1
+ trigger:
+ type: schedule
+ config:
+ mode: daily
+ hour: 8
+ minute: 0
+ run_on_weekends: false
+ timezone: UTC
+ steps:
+ - id: stopped
+ type: action
+ action: store.get
+ display_name: Which instances stopped last night?
+ with:
+ scope: workspace
+ key: dev_stopped_instances
+ - id: any_stopped
+ type: branch
+ display_name: Anything to start?
+ if:
+ all:
+ - field: steps.stopped.output.value
+ op: is_not_empty
+ then:
+ - id: find
+ type: action
+ action: aws.executeApi
+ display_name: Find the ones still stopped
+ connection_id:
+ $input: aws
+ with:
+ service: ec2
+ action: DescribeInstances
+ params:
+ Filters:
+ - Name: instance-id
+ Values: "{{ steps.stopped.output.value }}"
+ - Name: instance-state-name
+ Values: [stopped]
+ - id: ids
+ type: action
+ action: transform
+ display_name: List their instance IDs
+ with:
+ from: "{{ steps.find.output.result }}"
+ path: $.Reservations[*].Instances[*].InstanceId
+ - id: any_left
+ type: branch
+ display_name: Any still stopped?
+ if:
+ all:
+ - field: steps.ids.output.value
+ op: is_not_empty
+ then:
+ - id: start
+ type: action
+ action: aws.executeApi
+ display_name: Start the instances
+ connection_id:
+ $input: aws
+ with:
+ service: ec2
+ action: StartInstances
+ params:
+ InstanceIds: "{{ steps.ids.output.value }}"
+ - id: say_started
+ type: action
+ action: slack.post_message
+ display_name: Say they started
+ connection_id:
+ $input: slack
+ with:
+ text: ":sunrise: Started these development instances: {{ steps.ids.output.value | join: ', ' }}."
+ - id: forget
+ type: action
+ action: store.remove
+ display_name: Forget last night's list
+ with:
+ scope: workspace
+ key: dev_stopped_instances
+inputs:
+ aws:
+ kind: connection
+ name: AWS
+ type: aws
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+```
+
+After you import it:
+
+- On **Find the ones still stopped** and **Start the instances**, pick your AWS connection.
+- On **Say they started**, pick your Slack connection and the channel.
+- On the trigger, set **Timezone** to the same timezone as the evening workflow.
+
+All three workflows find the instances in the AWS connection's default region. For another region, set **Region** on every AWS step.
+
+## How the workflows work
+
+### Find the running instances
+
+An **AWS API Call** step calls **EC2 → DescribeInstances** with two filters, so it returns only running instances tagged as development:
+
+```json
+[
+ { "Name": "tag:Environment", "Values": ["development"] },
+ { "Name": "instance-state-name", "Values": ["running"] }
+]
+```
+
+EC2 returns instances grouped into reservations. A **Transform** step collects the instance IDs from every reservation into one list, with **JSONPath** set to `$.Reservations[*].Instances[*].InstanceId`. A **Branch** then checks that the list **is not empty**, so on an evening when nothing is running, the workflow stops there without posting anything.
+
+### Warn, then wait
+
+**Warn the channel** posts the list of instance IDs and asks anyone who needs them to react with 📌.
+
+Slack identifies a message by its timestamp, which the post returns as `ts`. A **Storage: Put** step saves a key that contains it, `dev_warning:{{ steps.warn.output.ts }}`, with **Expires after** set to `30m`. The reaction workflow looks for this key to tell tonight's warning apart from any other message, and the expiry stops a late reaction from counting after the instances have stopped.
+
+A **Wait / Delay** step then waits 30 minutes.
+
+### Keep them running tonight
+
+The reaction workflow starts on Slack's **Reaction added** event, with **Only this emoji** set to `pushpin`. The event carries the reacted message's timestamp as `trigger.event.item.ts`, so a **Storage: Get** step looks up `dev_warning:{{ trigger.event.item.ts }}`. The key exists only when the reaction is on tonight's warning, and only for 30 minutes after it was posted.
+
+When the key exists, a **Storage: Put** step saves `keep_dev_running`, holding the person who reacted:
+
+```json
+{ "user": "{{ trigger.event.user }}" }
+```
+
+**Expires after** is `12h`, so the key is gone by the next evening. A reply in the warning's thread confirms the request.
+
+### Stop the instances
+
+After the wait, the evening workflow reads `keep_dev_running`. If the key exists, it replies in the warning's thread and names who asked, using Slack's mention syntax, `<@{{ steps.keep.output.value.user }}>`.
+
+Otherwise, it saves the list of instance IDs to `dev_stopped_instances` for the morning, and calls **EC2 → StopInstances** with **InstanceIds** set to `{{ steps.ids.output.value }}`. A field that holds a single reference receives the list itself, not text. See [How values render](/workflows/templating/#how-values-render).
+
+The list expires after 4 days, so the list from Friday evening is still there on Monday morning.
+
+### Start them in the morning
+
+The morning workflow reads `dev_stopped_instances`. Before it starts anything, it calls **DescribeInstances** again, filtered by those instance IDs and by the state `stopped`:
+
+```json
+[
+ { "Name": "instance-id", "Values": "{{ steps.stopped.output.value }}" },
+ { "Name": "instance-state-name", "Values": ["stopped"] }
+]
+```
+
+This skips an instance that someone started by hand or terminated overnight. **StartInstances** fails for the whole list if any instance in it no longer exists, so checking first keeps one missing instance from stopping the rest.
+
+It then starts the instances, posts which ones started, and removes `dev_stopped_instances`.
+
+## Step 3: Test and publish
+
+1. In **Stop development instances at night**, test **Find running development instances** and **List their instance IDs** from their **Test** tabs. Neither one changes anything, and the output shows which instances the workflow would stop.
+2. Publish all three workflows. The first publish also switches each one on.
+3. On the first evening, check the warning in Slack, and react to it with 📌 to try the reaction workflow. The instances then keep running that night.
+
+To follow what happened, open **Workflows → Runs**. See [Run history](/workflows/run-history/).
+
+:::caution
+A **Test run** of the evening or morning workflow runs every step for real. It posts the warning, and it stops or starts your development instances.
+:::
+
+## Adapt the workflow
+
+- **Use your own tag.** Change the tag filter on **Find running development instances** and the condition in the IAM policy, for example to `Stage` = `dev`.
+- **Change the hours.** Set the time on each trigger. To keep the instances off at weekends, leave **Run on weekends** off on both schedules.
+- **Give more notice.** Change **Wait 30 minutes**, and set **Expires after** on **Remember the warning message** to the same duration.
+- **Stop databases too.** Add AWS API Call steps for **RDS → StopDBInstance** and **StartDBInstance**, and add both actions to the IAM policy. AWS starts a stopped RDS instance again after 7 days, which a nightly schedule never reaches.
+
+## Related
+
+- [Storage](/workflows/storage/) for workspace-scoped keys and their expiry.
+- [Triggers](/workflows/triggers/#app-event) for Slack events such as **Reaction added**.
+- [Schedule](/workflows/triggers/#schedule) for time zones and weekday schedules.
+- [AWS API Call](/workflows/actions/#aws-api-call) for the services a step can call.
diff --git a/src/content/docs/guides/workflows/summarize-slack-thread.mdx b/src/content/docs/guides/workflows/summarize-slack-thread.mdx
new file mode 100644
index 00000000..4bd46f2b
--- /dev/null
+++ b/src/content/docs/guides/workflows/summarize-slack-thread.mdx
@@ -0,0 +1,158 @@
+---
+title: "Summarize a Slack thread with AI"
+description: "React to any message in a Slack thread with 📝, and the workflow reads the whole thread and replies with a summary of its decisions, open questions, and action items."
+sidebar:
+ order: 8
+---
+
+Long Slack threads are where decisions get made and then lost. This guide builds a workflow that summarizes a thread on request: anyone reacts to a message in the thread with 📝, and an **AI Prompt** step reads the whole thread and replies in it with the decisions, the open questions, and who agreed to do what. The owner of each action item is mentioned, so they get a notification.
+
+The finished workflow has these steps:
+
+```text
+Slack: Reaction added, 📝 only
+├─ Read the thread Slack: Get Thread Replies
+└─ Is it a thread? Branch
+ └─ Then
+ ├─ Summarize the thread AI Prompt
+ └─ Reply with the summary Slack: Post Message
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Slack connection with the **Workflows** capability. The workflow works in every channel the KloudMate bot is in, so invite the bot to the channels where you want summaries.
+- Each summary uses workflow credits, and the **AI Prompt** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**. On the trigger, pick your Slack connection under **Account**, and on the two Slack steps, pick your Slack connection.
+
+```yaml
+kind: workflow
+uid: guide-summarize-slack-thread
+spec:
+ name: Summarize a Slack thread
+ description: When someone reacts to a message in a thread with a memo emoji, reads the whole thread and replies in it with a summary of the decisions, open questions, and action items.
+ definition:
+ schema_version: 1
+ trigger:
+ type: integration_event
+ config:
+ connection_id:
+ $input: slack
+ trigger_key: slack.reaction_added
+ props:
+ emoji: memo
+ steps:
+ - id: thread
+ type: action
+ action: slack.get_thread_replies
+ display_name: Read the thread
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.item.channel }}"
+ ts: "{{ trigger.event.item.ts }}"
+ limit: 200
+ - id: has_replies
+ type: branch
+ display_name: Is it a thread?
+ if:
+ all:
+ - field: steps.thread.output.messages.1
+ op: is_not_empty
+ then:
+ - id: summary
+ type: action
+ action: ai.prompt
+ display_name: Summarize the thread
+ with:
+ prompt: |-
+ Summarize this Slack thread for someone who wasn't part of it. Use Slack formatting, and keep it under 150 words, with these sections:
+ *Decisions*: what the thread agreed on.
+ *Open questions*: what is still undecided.
+ *Action items*: who does what. Name each owner as they appear below, in the form <@USER_ID>.
+ Leave out a section that would be empty. Don't add anything the thread doesn't say.
+
+ The thread, oldest message first. Each message starts with its author:
+ {% for m in steps.thread.output.messages %}
+ <@{{ m.user }}>: {{ m.text }}
+ {% endfor %}
+ - id: reply
+ type: action
+ action: slack.post_message
+ display_name: Reply with the summary
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ trigger.event.item.channel }}"
+ thread_ts: "{{ steps.thread.output.messages.0.ts }}"
+ text: ":memo: *Summary, as <@{{ trigger.event.user }}> asked*\n{{ steps.summary.output.text | replace: ': {{ m.text }}
+{% endfor %}
+```
+
+Each message starts with its author's user ID in Slack's mention syntax, `<@U02ABCDEF>`. The prompt asks the model to name the owner of each action item the same way, so the owners are mentioned when the summary is posted, and Slack shows their names.
+
+The prompt also tells the model not to add anything the thread doesn't say. A summary that invents a decision is worse than no summary, so check a few summaries against their threads when you start.
+
+**AI Prompt** reads the messages as part of its instructions, so whoever writes in the thread can steer the summary. That's acceptable here, because the people in the thread are your colleagues, and the summary only goes back into the same thread. For text from outside your company, use **AI Extract** instead. See [AI Prompt](/workflows/actions/#ai-prompt).
+
+### Reply with the summary
+
+A Slack **Post Message** step replies in the thread, with **Reply to thread** set to `{{ steps.thread.output.messages.0.ts }}`, the first message of the thread. The reply names who asked for the summary:
+
+```liquid
+:memo: *Summary, as <@{{ trigger.event.user }}> asked*
+{{ steps.summary.output.text | replace: '` or ``, even if a message in the thread tricked the model into writing one. User mentions, which start with `<@`, still work.
+
+## Step 2: Test and publish
+
+1. Open the trigger's **Test** tab and click **Test trigger**. Then react with 📝 to a message in a thread in a channel the bot is in. The reaction becomes the trigger's sample.
+2. Test **Read the thread** from its **Test** tab, then test **Summarize the thread** to see the summary without posting it.
+3. Click **Publish**. The first publish also switches the workflow on.
+
+## Adapt the workflow
+
+- **Save the summary in Jira.** For threads about a Jira issue, add an **AI Extract** step that returns the issue key mentioned in the thread, and a Jira **Add Comment** step that posts the summary on that issue.
+- **Summarize only some channels.** Import the workflow once for each channel, each with a different `uid`, and set **Only in channel** on each trigger.
+- **Write a different summary.** Change the sections in the prompt, for example to a timeline for an incident thread, or to a list of open questions only.
+
+## Related
+
+- [AI Prompt](/workflows/actions/#ai-prompt) and [AI Extract](/workflows/actions/#ai-extract) for when to use each one.
+- [Templating](/workflows/templating/#write-a-template) for `for` loops in a template.
+- [Triggers](/workflows/triggers/#app-event) for Slack events.
diff --git a/src/content/docs/guides/workflows/temporary-ssh-access.mdx b/src/content/docs/guides/workflows/temporary-ssh-access.mdx
new file mode 100644
index 00000000..775e279a
--- /dev/null
+++ b/src/content/docs/guides/workflows/temporary-ssh-access.mdx
@@ -0,0 +1,371 @@
+---
+title: "Grant temporary SSH access to a server, with approval"
+description: "Let a workspace member request SSH access to a bastion host through a form. After an on-call lead approves, the workflow opens the host's security group to their IP address, and closes it again after 1, 4, or 8 hours."
+sidebar:
+ order: 1
+---
+
+This guide builds a workflow for just-in-time SSH access to a bastion host on AWS. An engineer requests access through a form, an on-call lead approves the request by email, and the workflow adds an inbound rule for the engineer's IP address to the bastion's security group. When the time the engineer asked for is up, the workflow removes the rule again, so access never stays open because someone forgot to close it.
+
+The workflow also posts each change to a Slack channel. That gives you a record of who had access, from which address, for how long, and who approved it.
+
+The finished workflow has these steps:
+
+```text
+Form: Request SSH access to the bastion
+└─ Is it an IPv4 address? Branch
+ ├─ Then
+ │ ├─ Approve the access Approval
+ │ └─ Approved in time? Branch
+ │ ├─ Then
+ │ │ ├─ Open SSH to the address AWS API Call
+ │ │ ├─ Post in the audit channel Slack: Post Message
+ │ │ ├─ Tell the requester access is open Send Email
+ │ │ ├─ How long? Route, then Wait
+ │ │ ├─ Close SSH again AWS API Call
+ │ │ └─ Reply in the audit thread Slack: Post Message
+ │ └─ Else: tell the requester nobody approved
+ └─ Else: tell the requester the address is wrong
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need an [AWS connection](/platform/settings/connections/connect-an-account/) with the **Workflows** capability, in the account that holds the bastion.
+- You need a Slack connection with the **Workflows** capability, and a channel for the access records. Invite the KloudMate bot to that channel.
+- Copy the ID of the bastion's security group, such as `sg-0123456789abcdef0`, from the EC2 console.
+
+## Step 1: Let the AWS connection change the security group
+
+Add this policy to the IAM role that your AWS connection uses. Replace `REGION`, `ACCOUNT_ID`, and the security group ID with your own values:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:AuthorizeSecurityGroupIngress",
+ "ec2:RevokeSecurityGroupIngress"
+ ],
+ "Resource": "arn:aws:ec2:REGION:ACCOUNT_ID:security-group/sg-0123456789abcdef0"
+ }
+ ]
+}
+```
+
+The policy lets the workflow add and remove inbound rules on that one security group, and nothing else.
+
+## Step 2: Save the security group ID as a variable
+
+Open **Workflows → Variables** and click **Add variable**. Create a **Constant** named `bastion_security_group`, with the security group ID as its value.
+
+The workflow reads the ID from this variable. To point it at another security group later, you change the variable instead of editing the workflow.
+
+## Step 3: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On **Open SSH to the address** and **Close SSH again**, pick your AWS connection.
+ - On **Post in the audit channel** and **Reply in the audit thread**, pick your Slack connection. On **Post in the audit channel**, also pick the channel.
+ - On **Approve the access**, add the people who can approve a request under **Approvers**.
+
+```yaml
+kind: workflow
+uid: guide-temporary-ssh-access
+spec:
+ name: Temporary SSH access to the bastion
+ description: After an approval, opens SSH on the bastion to one IP address for 1, 4, or 8 hours, then closes it again.
+ definition:
+ schema_version: 1
+ trigger:
+ type: form
+ config:
+ title: Request SSH access to the bastion
+ description: "Access opens after an on-call lead approves it, and closes on its own. For ip_address, enter the public IPv4 address you'll connect from. To find it, run: curl https://checkip.amazonaws.com"
+ require_login: true
+ success_message: Your request is waiting for approval. You'll get an email when access opens.
+ inputs:
+ - name: ip_address
+ type: string
+ required: true
+ - name: duration
+ type: enum
+ required: true
+ options: [1 hour, 4 hours, 8 hours]
+ - name: reason
+ type: textarea
+ required: true
+ steps:
+ - id: check_ip
+ type: branch
+ display_name: Is it an IPv4 address?
+ if:
+ all:
+ - field: trigger.inputs.ip_address
+ op: regex
+ value: '^([0-9]{1,3}\.){3}[0-9]{1,3}$'
+ then:
+ - id: approve
+ type: approval
+ display_name: Approve the access
+ approvers: []
+ timeout: 1h
+ on_timeout: continue
+ message: |-
+ {{ trigger.submitted_by.name }} ({{ trigger.submitted_by.email }}) asks for SSH access to the bastion from {{ trigger.inputs.ip_address }} for {{ trigger.inputs.duration }}.
+
+ Reason: {{ trigger.inputs.reason }}
+ - id: approved
+ type: branch
+ display_name: Approved in time?
+ if:
+ all:
+ - field: steps.approve.output.approved
+ op: is_not_empty
+ then:
+ - id: open
+ type: action
+ action: aws.executeApi
+ display_name: Open SSH to the address
+ connection_id:
+ $input: aws
+ with:
+ service: ec2
+ action: AuthorizeSecurityGroupIngress
+ params:
+ GroupId: "{{ vars.bastion_security_group }}"
+ IpPermissions:
+ - IpProtocol: tcp
+ FromPort: 22
+ ToPort: 22
+ IpRanges:
+ - CidrIp: "{{ trigger.inputs.ip_address }}/32"
+ Description: "Temporary access for {{ trigger.submitted_by.email }}"
+ - id: announce
+ type: action
+ action: slack.post_message
+ display_name: Post in the audit channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":unlock: Opened SSH on the bastion to {{ trigger.inputs.ip_address }} for {{ trigger.submitted_by.name }}, for {{ trigger.inputs.duration }}. Approved by {{ steps.approve.output.decidedBy.name }}."
+ - id: tell_open
+ type: action
+ action: email.send
+ display_name: Tell the requester access is open
+ with:
+ to: "{{ trigger.submitted_by.email }}"
+ subject: Your SSH access to the bastion is open
+ html: "{{ steps.approve.output.decidedBy.name | escape }} approved your request. The bastion accepts SSH from {{ trigger.inputs.ip_address | escape }} for {{ trigger.inputs.duration }}, and then closes to that address on its own."
+ - id: how_long
+ type: route
+ display_name: How long?
+ routes:
+ - label: one_hour
+ if:
+ all:
+ - field: trigger.inputs.duration
+ op: eq
+ value: 1 hour
+ steps:
+ - id: wait_1h
+ type: wait
+ display_name: Wait 1 hour
+ duration: 1h
+ - label: four_hours
+ if:
+ all:
+ - field: trigger.inputs.duration
+ op: eq
+ value: 4 hours
+ steps:
+ - id: wait_4h
+ type: wait
+ display_name: Wait 4 hours
+ duration: 4h
+ else:
+ - id: wait_8h
+ type: wait
+ display_name: Wait 8 hours
+ duration: 8h
+ - id: close
+ type: action
+ action: aws.executeApi
+ display_name: Close SSH again
+ connection_id:
+ $input: aws
+ with:
+ service: ec2
+ action: RevokeSecurityGroupIngress
+ params:
+ GroupId: "{{ vars.bastion_security_group }}"
+ SecurityGroupRuleIds:
+ - "{{ steps.open.output.result.SecurityGroupRules.0.SecurityGroupRuleId }}"
+ - id: announce_closed
+ type: action
+ action: slack.post_message
+ display_name: Reply in the audit thread
+ connection_id:
+ $input: slack
+ with:
+ channel: "{{ steps.announce.output.channel }}"
+ thread_ts: "{{ steps.announce.output.ts }}"
+ text: ":lock: Closed SSH to {{ trigger.inputs.ip_address }}."
+ else:
+ - id: tell_expired
+ type: action
+ action: email.send
+ display_name: Tell the requester nobody approved
+ with:
+ to: "{{ trigger.submitted_by.email }}"
+ subject: Your SSH access request expired
+ html: "Nobody approved your request for SSH access from {{ trigger.inputs.ip_address | escape }} within an hour, so the bastion stays closed to it. Submit the form again if you still need access."
+ else:
+ - id: tell_bad_ip
+ type: action
+ action: email.send
+ display_name: Tell the requester the address is wrong
+ with:
+ to: "{{ trigger.submitted_by.email }}"
+ subject: Your SSH access request needs an IPv4 address
+ html: "{{ trigger.inputs.ip_address | escape }} isn't an IPv4 address such as 203.0.113.7, so nothing was opened. Submit the form again with the address you'll connect from."
+inputs:
+ aws:
+ kind: connection
+ name: AWS
+ type: aws
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+ bastion_security_group:
+ kind: variable
+ name: bastion_security_group
+ type: constant
+```
+
+The security group must be in the AWS connection's default region. If it's in another region, set **Region** on both AWS steps.
+
+## How each step works
+
+### The form
+
+The **Form submission** trigger has **Sign-in required** turned on, so only members of your workspace can submit it. Steps read who submitted it from `{{ trigger.submitted_by.name }}` and `{{ trigger.submitted_by.email }}`, which means the requester doesn't type their own name, and can't type someone else's.
+
+The form's fields are the workflow's input parameters:
+
+| Field | Type | What it's for |
+|---|---|---|
+| `ip_address` | **Text** | The public IPv4 address the requester connects from. |
+| `duration` | **Choice** | How long access stays open: `1 hour`, `4 hours`, or `8 hours`. |
+| `reason` | **Long text** | Why they need access. It appears in the approval email. |
+
+### Is it an IPv4 address?
+
+A **Branch** checks that `trigger.inputs.ip_address` **matches regex** `^([0-9]{1,3}\.){3}[0-9]{1,3}$`, which accepts four numbers separated by dots, such as `203.0.113.7`. If the requester typed anything else, such as a range or a host name, the **Else** block emails them and the run ends without asking anyone to approve it.
+
+### Approve the access
+
+An **Approval** step emails the approvers a link to approve or reject the request. Its **Message** names the requester, the address, the duration, and the reason:
+
+```liquid
+{{ trigger.submitted_by.name }} ({{ trigger.submitted_by.email }}) asks for SSH access to the bastion from {{ trigger.inputs.ip_address }} for {{ trigger.inputs.duration }}.
+
+Reason: {{ trigger.inputs.reason }}
+```
+
+**Timeout** is `1h`, because a request that nobody looks at within an hour is usually stale. **On timeout** is **Continue**, so a timed-out request goes on to the next step instead of failing the run.
+
+If an approver rejects the request, the run ends as **Rejected** and none of the later steps run.
+
+### Approved in time?
+
+A **Branch** checks that `steps.approve.output.approved` **is not empty**. An approval sets it to `true`, and a timeout leaves it empty. When the request timed out, the **Else** block emails the requester that nobody approved it.
+
+### Open SSH to the address
+
+An **AWS API Call** step calls **EC2 → AuthorizeSecurityGroupIngress**. Its **Parameters** set **GroupId** to `{{ vars.bastion_security_group }}` and **IpPermissions** to one rule for TCP port 22:
+
+```json
+[
+ {
+ "IpProtocol": "tcp",
+ "FromPort": 22,
+ "ToPort": 22,
+ "IpRanges": [
+ {
+ "CidrIp": "{{ trigger.inputs.ip_address }}/32",
+ "Description": "Temporary access for {{ trigger.submitted_by.email }}"
+ }
+ ]
+ }
+]
+```
+
+`/32` limits the rule to the one address. The rule's description names the requester, so anyone who looks at the security group in the EC2 console can tell who the rule is for.
+
+AWS returns the new rule in `result.SecurityGroupRules`, including its ID, which **Close SSH again** uses to remove it.
+
+### Post in the audit channel
+
+A Slack **Post Message** step posts who got access, from which address, for how long, and who approved it. `{{ steps.approve.output.decidedBy.name }}` is the approver.
+
+The message leaves out the requester's reason. Slack reads some text in a message as a mention, so free text from a form could notify a whole channel. The reason stays in the approval email and in run history instead.
+
+### Tell the requester access is open
+
+A **Send Email** step emails the requester at `{{ trigger.submitted_by.email }}`. Values from the form are passed through `escape`, so the email shows them as plain text. See [Escape values in an email](/workflows/templating/#escape-values-in-an-email).
+
+### How long?
+
+A **Route** picks the wait that matches the requester's choice. The `one_hour` path runs when `trigger.inputs.duration` **equals** `1 hour`, and the `four_hours` path when it equals `4 hours`. **Otherwise** covers `8 hours`.
+
+Each path holds a **Wait / Delay** step. **Wait for** takes a fixed duration, such as `4h`, rather than a template, which is why the workflow chooses between steps instead of reading the duration from the form. A wait of a minute or longer doesn't count toward the [runs a workspace can have running at the same time](/workflows/limits/#run-limits), so a run that waits for 8 hours doesn't hold anything up.
+
+### Close SSH again
+
+An **AWS API Call** step calls **EC2 → RevokeSecurityGroupIngress** with the rule's ID:
+
+```liquid
+{{ steps.open.output.result.SecurityGroupRules.0.SecurityGroupRuleId }}
+```
+
+Removing the rule by its ID removes exactly the rule this run added. If the same address also has a permanent rule on the security group, that rule stays.
+
+### Reply in the audit thread
+
+A Slack **Post Message** step replies in the thread of the first message. **Channel** is `{{ steps.announce.output.channel }}` and **Reply to thread** is `{{ steps.announce.output.ts }}`, both from Slack's response to the first post, so the opening and the closing of each request stay together.
+
+## Step 4: Publish and try it
+
+The form works only when the workflow is published, so try it on the live form:
+
+1. Click **Publish**. The first publish also switches the workflow on.
+2. Open the trigger and copy the **Public form URL**. Open it, sign in if you're asked to, and request access for your own IP address, with **duration** set to `1 hour`.
+3. Approve the request from the email that the approvers receive.
+4. Check that the security group has a new inbound rule for your address, and that you can connect over SSH.
+
+An hour later, the workflow removes the rule and replies in the Slack thread. To see the whole cycle without waiting an hour, set **Wait 1 hour** to `2m` while you try it, and set it back to `1h` afterwards. Publish after each change.
+
+To follow a request, open **Workflows → Runs**. A run shows **Waiting approval** until someone decides, and **Waiting** while the access is open. See [Run history](/workflows/run-history/).
+
+:::caution
+If you cancel a run while it waits, the rule stays on the security group. Remove it by hand in the EC2 console. Rules that the workflow adds have a description that starts with `Temporary access for`.
+:::
+
+## Adapt the workflow
+
+- **Open another port.** Change `FromPort` and `ToPort` on **Open SSH to the address**, for example to `5432` for PostgreSQL. **Close SSH again** removes the rule by its ID, so it needs no change.
+- **Offer other durations.** Add an option to the `duration` input, then add a Route path with a matching condition and a **Wait / Delay** step. **Wait for** can be up to `24h`.
+- **Protect more than one server.** Add a **Choice** input that names the servers, and a Route that picks the security group for each one. Keep each security group ID in its own variable, and add each group to the IAM policy.
+- **Send the record somewhere else.** Replace the two Slack steps with a Microsoft Teams message or an email to your security team, using the same text.
+
+## Related
+
+- [Approvals and collected input](/workflows/approvals-and-input/) for the approval email and its timeout.
+- [Triggers](/workflows/triggers/#form-submission) for the form trigger and **Sign-in required**.
+- [AWS API Call](/workflows/actions/#aws-api-call) for the services a step can call.
+- [Variables](/workflows/variables/) for constants such as the security group ID.
diff --git a/src/content/docs/guides/workflows/triage-jira-bugs.mdx b/src/content/docs/guides/workflows/triage-jira-bugs.mdx
new file mode 100644
index 00000000..75b12ec2
--- /dev/null
+++ b/src/content/docs/guides/workflows/triage-jira-bugs.mdx
@@ -0,0 +1,304 @@
+---
+title: "Triage new Jira bugs with AI"
+description: "Read each new bug report with AI Extract, set its Jira priority, assign it to the lead of the team that owns it, and post it in that team's Slack channel."
+sidebar:
+ order: 6
+---
+
+This guide builds a first pass of bug triage. When someone files a bug in your Jira project, the workflow reads the report, decides which team owns it and what priority it needs, and then:
+
+- sets the issue's priority in Jira,
+- adds a comment that explains the decision,
+- assigns the issue to the owning team's lead, and
+- posts the bug in that team's Slack channel.
+
+When the report doesn't say enough to decide, the workflow posts it in a triage channel for a person to look at instead.
+
+The finished workflow has these steps:
+
+```text
+Jira: New issue
+├─ Read the bug Jira: Get Issue
+├─ Triage the bug AI Extract
+├─ Set the priority Jira: Update Issue
+├─ Comment with the triage Jira: Add Comment
+└─ Which team? Route
+ ├─ frontend
+ │ ├─ Find the frontend lead Jira: Find User
+ │ ├─ Assign to the frontend lead Jira: Assign Issue
+ │ └─ Post in the frontend channel Slack: Post Message
+ ├─ backend: the same three steps for the backend team
+ └─ Otherwise: ask for a person to triage it
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Jira Cloud connection and a Slack connection, both with the **Workflows** capability. Invite the KloudMate bot to each team's Slack channel, and to a channel for bugs that need a person.
+- Note your Jira project key, such as `APP`, and the email address of each team's lead.
+- Each run uses workflow credits, and the **AI Extract** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Save your Jira address as a variable
+
+The Slack messages link to each bug. Open **Workflows → Variables** and create a **Constant** named `jira_url`, with your Jira site's address as its value, such as `https://acme.atlassian.net`.
+
+## Step 2: Import the workflow
+
+1. Copy the YAML below, and make these changes:
+ - In the trigger's `jql`, replace `APP` with your project key.
+ - Replace `frontend-lead@example.com` and `backend-lead@example.com` with your team leads' addresses.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On the trigger, pick your Jira connection under **Account**, and pick the **Site**.
+ - On every Jira step, pick your Jira connection and the **Site**.
+ - On every Slack step, pick your Slack connection and the channel.
+
+```yaml
+kind: workflow
+uid: guide-triage-jira-bugs
+spec:
+ name: Triage new Jira bugs
+ description: For each new bug, reads the report with AI, sets its priority, assigns it to the owning team's lead, comments with the triage, and posts it in the team's Slack channel.
+ definition:
+ schema_version: 1
+ trigger:
+ type: integration_event
+ config:
+ connection_id:
+ $input: jira
+ trigger_key: jira.issue_created
+ props:
+ jql: project = APP AND issuetype = Bug
+ steps:
+ - id: issue
+ type: action
+ action: jira.get_issue
+ display_name: Read the bug
+ connection_id:
+ $input: jira
+ with:
+ issue_key: "{{ trigger.event.key }}"
+ - id: triage
+ type: action
+ action: ai.extract
+ display_name: Triage the bug
+ with:
+ prompt: This is a new bug report for a web application. Decide which part of the product it's about, and which Jira priority it needs. Explain your decision in one sentence.
+ source:
+ summary: "{{ steps.issue.output.fields.summary }}"
+ description: "{{ steps.issue.output.fields.description }}"
+ output_fields:
+ - name: component
+ type: enum
+ options: [frontend, backend, unknown]
+ description: frontend is the web UI, backend is the API and data
+ - name: priority
+ type: enum
+ options: [Highest, High, Medium, Low]
+ description: Highest if users can't use the product or lose data, High if a feature is broken with no workaround, Medium if there's a workaround, Low for anything cosmetic
+ - name: reason
+ type: string
+ - id: set_priority
+ type: action
+ action: jira.update_issue
+ display_name: Set the priority
+ connection_id:
+ $input: jira
+ with:
+ issue_key: "{{ trigger.event.key }}"
+ fields:
+ priority:
+ name: "{{ steps.triage.output.priority }}"
+ - id: comment
+ type: action
+ action: jira.add_comment
+ display_name: Comment with the triage
+ connection_id:
+ $input: jira
+ with:
+ issue_key: "{{ trigger.event.key }}"
+ body: "Triaged automatically: priority *{{ steps.triage.output.priority }}*, component *{{ steps.triage.output.component }}*. {{ steps.triage.output.reason }}"
+ - id: team
+ type: route
+ display_name: Which team?
+ routes:
+ - label: frontend
+ if:
+ all:
+ - field: steps.triage.output.component
+ op: eq
+ value: frontend
+ steps:
+ - id: find_frontend_lead
+ type: action
+ action: jira.find_user
+ display_name: Find the frontend lead
+ connection_id:
+ $input: jira
+ with:
+ query: frontend-lead@example.com
+ - id: assign_frontend
+ type: action
+ action: jira.assign_issue
+ display_name: Assign to the frontend lead
+ connection_id:
+ $input: jira
+ with:
+ issue_key: "{{ trigger.event.key }}"
+ account_id: "{{ steps.find_frontend_lead.output.users.0.accountId }}"
+ - id: post_frontend
+ type: action
+ action: slack.post_message
+ display_name: Post in the frontend channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":beetle: New {{ steps.triage.output.priority }} priority bug for frontend: <{{ vars.jira_url }}/browse/{{ trigger.event.key }}|{{ trigger.event.key }}> {{ steps.issue.output.fields.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}"
+ - label: backend
+ if:
+ all:
+ - field: steps.triage.output.component
+ op: eq
+ value: backend
+ steps:
+ - id: find_backend_lead
+ type: action
+ action: jira.find_user
+ display_name: Find the backend lead
+ connection_id:
+ $input: jira
+ with:
+ query: backend-lead@example.com
+ - id: assign_backend
+ type: action
+ action: jira.assign_issue
+ display_name: Assign to the backend lead
+ connection_id:
+ $input: jira
+ with:
+ issue_key: "{{ trigger.event.key }}"
+ account_id: "{{ steps.find_backend_lead.output.users.0.accountId }}"
+ - id: post_backend
+ type: action
+ action: slack.post_message
+ display_name: Post in the backend channel
+ connection_id:
+ $input: slack
+ with:
+ text: ":beetle: New {{ steps.triage.output.priority }} priority bug for backend: <{{ vars.jira_url }}/browse/{{ trigger.event.key }}|{{ trigger.event.key }}> {{ steps.issue.output.fields.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}"
+ else:
+ - id: post_unsorted
+ type: action
+ action: slack.post_message
+ display_name: Ask for a person to triage it
+ connection_id:
+ $input: slack
+ with:
+ text: ":thinking_face: New bug that needs a person to triage it: <{{ vars.jira_url }}/browse/{{ trigger.event.key }}|{{ trigger.event.key }}> {{ steps.issue.output.fields.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}"
+inputs:
+ jira:
+ kind: connection
+ name: Jira
+ type: jira
+ slack:
+ kind: connection
+ name: Slack
+ type: slack
+ jira_url:
+ kind: variable
+ name: jira_url
+ type: constant
+```
+
+## How each step works
+
+### The trigger
+
+The Jira **New issue** trigger runs once for each new issue. **Narrow with JQL** is `project = APP AND issuetype = Bug`, so only new bugs in that project start a run. KloudMate checks Jira for new issues every couple of minutes, so a run starts shortly after the bug is filed, not at the same moment.
+
+The event carries the new issue's key as `trigger.event.key`, with a few common fields, but not the description.
+
+### Read the bug
+
+A Jira **Get Issue** step reads the whole issue, with **Issue key** set to `{{ trigger.event.key }}`. The description is in `steps.issue.output.fields.description`.
+
+### Triage the bug
+
+An **AI Extract** step reads the summary and the description, which its **Source** passes as one object:
+
+```json
+{
+ "summary": "{{ steps.issue.output.fields.summary }}",
+ "description": "{{ steps.issue.output.fields.description }}"
+}
+```
+
+**Output fields** asks for the component and the priority from fixed lists, and a one-sentence reason:
+
+```json
+[
+ { "name": "component", "type": "enum", "options": ["frontend", "backend", "unknown"], "description": "frontend is the web UI, backend is the API and data" },
+ { "name": "priority", "type": "enum", "options": ["Highest", "High", "Medium", "Low"], "description": "Highest if users can't use the product or lose data, High if a feature is broken with no workaround, Medium if there's a workaround, Low for anything cosmetic" },
+ { "name": "reason", "type": "string" }
+]
+```
+
+The `description` of each field tells the model what the options mean for your product, so edit them to match how your teams split the work.
+
+Anyone who can file a bug writes the text the model reads, which is why the workflow uses **AI Extract**. Whatever the report says, `component` and `priority` can only come back as one of the listed options. See [AI Extract](/workflows/actions/#ai-extract).
+
+### Set the priority and comment
+
+A Jira **Update Issue** step sets the priority, with **Fields** set to `{"priority": {"name": "{{ steps.triage.output.priority }}"}}`. The priority options are Jira's default priority names. If your site uses others, use those in **Output fields** instead.
+
+A Jira **Add Comment** step records the decision on the issue, so everyone who opens the bug can see why it was routed where it was:
+
+```liquid
+Triaged automatically: priority *{{ steps.triage.output.priority }}*, component *{{ steps.triage.output.component }}*. {{ steps.triage.output.reason }}
+```
+
+Jira reads `*text*` in a comment as bold.
+
+### Which team?
+
+A **Route** picks a path by `steps.triage.output.component`. The `frontend` path runs when it **equals** `frontend`, and the `backend` path when it equals `backend`. **Otherwise** runs for `unknown`.
+
+Each team's path has these steps:
+
+1. A Jira **Find User** step looks up the team lead by email.
+2. A Jira **Assign Issue** step assigns the bug to the lead, with **Assignee** set to `{{ steps.find_frontend_lead.output.users.0.accountId }}`. Jira assigns issues by account ID, not by email, which is why the lookup comes first.
+3. A Slack **Post Message** step posts the bug in the team's channel, with a link to it.
+
+**Otherwise** only posts the bug in the triage channel. It doesn't assign it to anyone.
+
+### Keep Slack messages safe
+
+The Slack messages include the bug's summary, which the person who filed the bug wrote. Slack gives some text in a message a special meaning, so a summary that contains `` would notify everyone in the channel. The messages escape `&`, `<`, and `>`, the characters Slack treats specially:
+
+```liquid
+{{ steps.issue.output.fields.summary | replace: '&', '&' | replace: '<', '<' | replace: '>', '>' }}
+```
+
+The summary then shows up as written, and mentions nobody.
+
+## Step 3: Test and publish
+
+1. Open the trigger's **Test** tab and click **Test trigger**, then pick a recent bug as the sample.
+2. Test **Read the bug** and **Triage the bug** from their **Test** tabs. The triage output shows what the model decided, without changing anything in Jira.
+3. Click **Test run** to run every step for real on that bug. It changes the bug's priority, comments on it, assigns it, and posts in Slack.
+4. Click **Publish**. The first publish also switches the workflow on.
+
+From then on, each new bug in the project gets a run. To see what the workflow decided for a bug, open the run from **Workflows → Runs**.
+
+## Adapt the workflow
+
+- **Add a team.** Add an option to `component` in **Output fields**, with a line about it in the `description`. Then add a Route path with a matching condition and the team's three steps.
+- **Keep a person in the loop.** Remove **Set the priority** and the assign steps, and keep the comment. The team then sees the suggestion and decides for themselves.
+- **Triage other issue types.** Change the trigger's JQL, for example to `project = APP AND issuetype in (Bug, Task)`.
+- **Label the bug instead of assigning it.** Replace the assign steps with a Jira **Update Issue** step that sets `{"labels": ["team-frontend"]}`, for teams that pull work from a board.
+
+## Related
+
+- [Triggers](/workflows/triggers/#app-event) for Jira events and how often KloudMate checks for them.
+- [Actions](/workflows/actions/#jira-cloud) for the Jira steps.
+- [Route](/workflows/control-flow/#route) for choosing between paths.
diff --git a/src/content/docs/guides/workflows/trigger-on-new-gmail.mdx b/src/content/docs/guides/workflows/trigger-on-new-gmail.mdx
new file mode 100644
index 00000000..461f0c5b
--- /dev/null
+++ b/src/content/docs/guides/workflows/trigger-on-new-gmail.mdx
@@ -0,0 +1,270 @@
+---
+title: "Trigger a workflow when a new email is received in Gmail"
+description: "Run workflow steps for each new email in a Gmail inbox, using an OAuth 2.0 connection to read the mail and Storage to track the last check. The example adds each email to a Google Sheet."
+sidebar:
+ order: 16
+---
+
+This guide builds a workflow that runs its steps for each new email in a Gmail inbox. KloudMate has no Gmail trigger, so a **Schedule** trigger checks the inbox every 5 minutes, and the workflow then handles each email that arrived since the last check.
+
+As the example, the workflow adds a row to a Google Sheet for each email, with its date, sender, subject, and a short preview. You can replace that step with anything else, such as a Slack message or a Jira issue.
+
+There are no built-in Gmail or Google Sheets actions either, so the workflow calls their APIs directly. You can use the same approach with any service that has an HTTP API:
+
+- An **OAuth 2.0 connection** holds your Google credential, and KloudMate refreshes its access token for you.
+- **HTTP Request** steps call the Gmail and Google Sheets APIs through that connection.
+- **Storage** remembers when the workflow last checked, so each run looks only at new mail.
+
+The finished workflow has these steps:
+
+```text
+Schedule: every 5 minutes
+├─ Last check time Storage: Get
+├─ New inbox mail HTTP Request to Gmail
+├─ Any new mail? Branch
+│ └─ For each message Loop
+│ ├─ Read the message HTTP Request to Gmail
+│ └─ Add a row HTTP Request to Google Sheets
+└─ Remember this check Storage: Put
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- Create a Google Sheet to write to, and copy its ID from the sheet's URL. The ID is the part between `/d/` and `/edit` in `https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit`.
+- You need a Google Cloud project where you can create an OAuth client.
+
+## Step 1: Create an OAuth client in Google Cloud
+
+In the [Google Cloud console](https://console.cloud.google.com/), in the project you'll use:
+
+1. Enable the **Gmail API** and the **Google Sheets API**.
+2. Set up the OAuth consent screen. If the app is external and its publishing status is **Testing**, add your Google account as a test user.
+3. Create an OAuth client ID with the application type **Web application**.
+4. Under **Authorized redirect URIs**, add this URI:
+
+ ```text
+ https://api.kloudmate.com/integrations/oauth/callback
+ ```
+
+5. Copy the client ID and the client secret.
+
+For more detail on each step, see Google's guide to [creating OAuth credentials](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred).
+
+:::caution
+While an external app's publishing status is **Testing**, Google expires its refresh tokens after 7 days, and the KloudMate connection then needs a reconnect. To avoid that, publish the app, or make it internal to your Google Workspace organization.
+:::
+
+## Step 2: Connect your Google account
+
+1. In KloudMate, open **Workflows → Connections**, click **Connect**, and pick **OAuth 2.0**.
+2. Name the connection `Google`, and fill in the form:
+
+ | Field | Value |
+ |---|---|
+ | **Grant type** | **Authorization code** |
+ | **Token URL** | `https://oauth2.googleapis.com/token` |
+ | **Authorize URL** | `https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent` |
+ | **Scopes** | `https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/spreadsheets` |
+ | **Client ID** and **Client secret** | The values from Step 1. |
+ | **Send client credentials** | **In the body** |
+
+3. Click **Connect**. In the Google window that opens, choose your account and approve the access.
+
+The connection then shows **Verified**. Its scopes let it read your mail and edit your sheets, and nothing else.
+
+Keep `?access_type=offline&prompt=consent` at the end of the **Authorize URL**. Without it, Google issues no refresh token, and the connection stops working after an hour.
+
+## Step 3: Import the workflow
+
+The quickest way to build the workflow is to import it:
+
+1. Copy the YAML below, and replace `YOUR_SPREADSHEET_ID` with your sheet's ID.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**. On **New inbox mail**, **Read the message**, and **Add a row to the sheet**, pick your `Google` connection.
+
+The import can't match a connection from another workspace, so it lists those steps under **Set these before publishing** until you pick the connection.
+
+```yaml
+kind: workflow
+uid: guide-gmail-to-google-sheet
+spec:
+ name: Gmail to Google Sheet
+ description: Every 5 minutes, adds a row to a Google Sheet for each new email in the Gmail inbox.
+ definition:
+ schema_version: 1
+ trigger:
+ type: schedule
+ config:
+ mode: interval
+ every_minutes: 5
+ timezone: UTC
+ steps:
+ - id: since
+ type: action
+ action: store.get
+ display_name: Last check time
+ with:
+ scope: workflow
+ key: gmail_after
+ default: "{{ trigger.schedule.fired_at | default: 'now' | date: '%s' | minus: 900 }}"
+ - id: list
+ type: action
+ action: http.request
+ display_name: New inbox mail
+ connection_id:
+ $input: google
+ with:
+ authType: connection
+ method: GET
+ url: "https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=20&q={{ 'in:inbox after:' | append: steps.since.output.value | url_encode }}"
+ - id: any_new
+ type: branch
+ display_name: Any new mail?
+ if:
+ all:
+ - field: steps.list.output.body.messages
+ op: is_not_empty
+ then:
+ - id: each_msg
+ type: loop
+ display_name: For each message
+ items: steps.list.output.body.messages
+ each:
+ - id: msg
+ type: action
+ action: http.request
+ display_name: Read the message
+ connection_id:
+ $input: google
+ with:
+ authType: connection
+ method: GET
+ url: "https://gmail.googleapis.com/gmail/v1/users/me/messages/{{ item.id }}?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date"
+ - id: append
+ type: action
+ action: http.request
+ display_name: Add a row to the sheet
+ connection_id:
+ $input: google
+ with:
+ authType: connection
+ method: POST
+ url: "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SPREADSHEET_ID/values/A1:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS"
+ body_type: json
+ body:
+ values:
+ - - "{{ steps.msg.output.body.payload.headers | where: 'name', 'Date' | map: 'value' | first }}"
+ - "{{ steps.msg.output.body.payload.headers | where: 'name', 'From' | map: 'value' | first }}"
+ - "{{ steps.msg.output.body.payload.headers | where: 'name', 'Subject' | map: 'value' | first }}"
+ - "{{ steps.msg.output.body.snippet }}"
+ - id: advance
+ type: action
+ action: store.put
+ display_name: Remember this check
+ with:
+ scope: workflow
+ key: gmail_after
+ value: "{{ trigger.schedule.fired_at | default: 'now' | date: '%s' }}"
+inputs:
+ google:
+ kind: connection
+ name: Google
+ type: oauth2
+```
+
+To build the workflow by hand instead, create it with a **Schedule** trigger, set **Repeat** to **Every N minutes (interval)** with `5`, and add the steps in the order below. The YAML uses readable step ids such as `since` and `list`, but the builder generates its own ids, so pick each reference from the variable picker instead of copying it.
+
+## How each step works
+
+### Last check time
+
+A **Storage: Get** step reads the key `gmail_after`, with **Scope** set to **This workflow**. The key holds a Unix timestamp in seconds, the format that Gmail's `after:` search expects.
+
+On the first run the key doesn't exist yet, so the step returns its **Default**, which starts the search 15 minutes (900 seconds) before this run:
+
+```liquid
+{{ trigger.schedule.fired_at | default: 'now' | date: '%s' | minus: 900 }}
+```
+
+`date: '%s'` turns the run's scheduled time into Unix seconds, and `default: 'now'` uses the current time if the run has no scheduled time.
+
+### New inbox mail
+
+An **HTTP Request** step with **Authentication** set to **OAuth 2.0 connection** and your `Google` connection sends a `GET` to Gmail's message list:
+
+```liquid
+https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=20&q={{ 'in:inbox after:' | append: steps.since.output.value | url_encode }}
+```
+
+The `q` parameter takes a Gmail search, so this asks for inbox messages newer than the last check. `url_encode` makes the search text safe to put in a URL. Gmail returns the ids of up to 20 matching messages in `body.messages`.
+
+### Any new mail?
+
+A **Branch** checks that `steps.list.output.body.messages` **is not empty**. Gmail leaves `messages` out of its response when nothing matches, so a run with no new mail goes straight to the last step.
+
+### For each message
+
+A **Loop** with **Items** set to `steps.list.output.body.messages` runs the next two steps once for each message. **Run at once** stays at 1, so the workflow adds the messages one at a time.
+
+### Read the message
+
+An **HTTP Request** step sends a `GET` for one message. `format=metadata` and the `metadataHeaders` parameters ask Gmail for only the `Date`, `From`, and `Subject` headers, along with a short `snippet` of the body:
+
+```liquid
+https://gmail.googleapis.com/gmail/v1/users/me/messages/{{ item.id }}?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date
+```
+
+`{{ item.id }}` is the current message's id from the list.
+
+### Add a row to the sheet
+
+An **HTTP Request** step sends a `POST` to the Google Sheets API, which adds a row below the existing rows on the sheet's first tab. **Body type** is **JSON**, and the body holds one row with four cells:
+
+```json
+{
+ "values": [[
+ "{{ steps.msg.output.body.payload.headers | where: 'name', 'Date' | map: 'value' | first }}",
+ "{{ steps.msg.output.body.payload.headers | where: 'name', 'From' | map: 'value' | first }}",
+ "{{ steps.msg.output.body.payload.headers | where: 'name', 'Subject' | map: 'value' | first }}",
+ "{{ steps.msg.output.body.snippet }}"
+ ]]
+}
+```
+
+Gmail returns the headers as a list of `name` and `value` pairs. `where` keeps the header with the name you want, `map: 'value'` takes its value, and `first` turns the one-item list into text. For more on these filters, see [Templating](/workflows/templating/).
+
+### Remember this check
+
+A **Storage: Put** step saves this run's scheduled time to `gmail_after`, so the next run starts its search from there:
+
+```liquid
+{{ trigger.schedule.fired_at | default: 'now' | date: '%s' }}
+```
+
+It's the last step on purpose. If a run fails partway through, `gmail_after` keeps its old value, and the next run searches the same period again, so no message is skipped.
+
+## Step 4: Test and publish
+
+1. Open the trigger's **Test** tab and click **Test trigger** to capture a sample with a real fire time.
+2. Click **Test run**. It runs every step for real, so any mail from the last 15 minutes becomes rows in your sheet.
+3. Delete the rows the test added from your sheet. Then open **Workflows → Storage** and delete the `gmail_after` key that the test run saved, so the first scheduled run starts fresh, from 15 minutes before it runs.
+4. Click **Publish**. The first publish also switches the workflow on, and it runs every 5 minutes from then on.
+
+To see what each run did, open **Workflows → Runs**. See [Run history](/workflows/run-history/).
+
+## Adapt the workflow
+
+- **Do something else with each email.** Replace **Add a row to the sheet** with any other step, such as Slack **Post Message** or Jira **Create Issue**. The step can read the email's details from `steps.msg.output.body`, the same way the sheet row does.
+- **Only react to some emails.** Narrow the Gmail search in the **New inbox mail** URL. For example, change `'in:inbox after:'` to `'in:inbox from:alerts@example.com after:'`, and the workflow sees only mail from that sender.
+- **Handle more mail per run.** Each run lists at most 20 messages. If more can arrive in 5 minutes, raise `maxResults`, up to 100, which is the most one loop handles.
+- **Avoid duplicate rows.** If a run fails after adding some rows, the next run adds those messages again. To skip them, keep a marker key for each message id, such as `added:{{ item.id }}`, the same way as in [Notify once a day per alert group](/workflows/storage/#notify-once-a-day-per-alert-group).
+- **Call another Google API.** Add its scope to the connection's **Scopes**, then click **Reconnect** on the connection, so Google asks you to approve the new access.
+- **Use another service.** Any API that supports OAuth 2.0 works the same way: create an OAuth client with the service, register the redirect URI, create an OAuth 2.0 connection with the service's token and authorize URLs, and call the API from **HTTP Request** steps.
+
+## Related
+
+- [Connect an account](/platform/settings/connections/connect-an-account/) for OAuth 2.0 connections.
+- [HTTP Request](/workflows/actions/#http-request) for the step's fields.
+- [Storage](/workflows/storage/) for the cursor pattern this workflow uses.
+- [Test a workflow](/workflows/test-a-workflow/) for testing steps against real data.
diff --git a/src/content/docs/guides/workflows/weekly-reliability-report.mdx b/src/content/docs/guides/workflows/weekly-reliability-report.mdx
new file mode 100644
index 00000000..39efd5f7
--- /dev/null
+++ b/src/content/docs/guides/workflows/weekly-reliability-report.mdx
@@ -0,0 +1,227 @@
+---
+title: "Post a weekly reliability report with AI"
+description: "Every Monday, read the past week's traffic, errors, open incidents, and SLOs from the KloudMate MCP server, and post a report in Slack that AI Prompt writes from them."
+sidebar:
+ order: 4
+---
+
+This guide builds a report that writes itself every Monday. The workflow collects the past week's numbers from your KloudMate workspace, and an **AI Prompt** step turns them into a short report for the engineering channel: the busiest services and their error rates, the incidents that are still open, and the SLOs that are running out of error budget.
+
+The numbers come from the [KloudMate MCP server](/kloudmate-assistant/mcp-server/), the same server that coding tools such as Claude Code use to query KloudMate. A workflow connects to it as a **Custom MCP Server**, and **MCP Tool Call** steps call its tools the way an agent would. Each step calls one tool that you pick, with the arguments you set, so the workflow asks the same questions every week.
+
+The finished workflow has these steps:
+
+```text
+Schedule: Mondays at 09:00
+├─ Collect the week's data Parallel
+│ ├─ Requests and latency by service MCP Tool Call
+│ ├─ Errors by service MCP Tool Call
+│ ├─ Open incidents MCP Tool Call
+│ └─ SLOs MCP Tool Call
+├─ Write the report AI Prompt
+└─ Post the report Slack: Post Message
+```
+
+## Before you start
+
+- You need the **Developer** role in a KloudMate workspace whose plan includes workflows.
+- You need a Slack connection with the **Workflows** capability, and a channel for the report. Invite the KloudMate bot to that channel.
+- Each run uses workflow credits, and the **AI Prompt** step uses more. See [Check workflow usage](/workflows/run-history/#check-workflow-usage).
+
+## Step 1: Connect the KloudMate MCP server
+
+1. Create a [personal API key](/platform/settings/api-keys/#a-personal-key) and copy it. The workflow reads whatever the key's owner can read, so create it from an account that can see every service you want in the report.
+2. Copy the ID of this workspace from its URL in the KloudMate app. It starts with `ws_`.
+3. Open **Workflows → Connections**, click **Connect**, and pick **Custom MCP Server**. Name the connection `KloudMate MCP`, and fill in the form:
+
+ | Field | Value |
+ |---|---|
+ | **Server URL** | `https://api.kloudmate.com/mcp` |
+ | **Transport** | **Streamable HTTP** |
+ | **Authentication** | **Bearer token**, with your personal API key as the token |
+ | **Extra headers** | `{"x-workspace-id": "YOUR_WORKSPACE_ID"}` |
+
+4. Click **Connect**.
+
+The personal key works for every workspace its owner belongs to, and `x-workspace-id` picks the one the tools read. KloudMate stores the key encrypted with the connection, so it never appears in the workflow or in run history.
+
+## Step 2: Import the workflow
+
+1. Copy the YAML below.
+2. Open **Workflows**, click **Import**, paste the YAML, and click **Import**.
+3. Click **Open workflow**, and set these before you publish:
+ - On the four MCP Tool Call steps, pick your `KloudMate MCP` connection.
+ - On **Post the report**, pick your Slack connection and the channel.
+ - On the trigger, set **Timezone** to your team's timezone, such as `Europe/Berlin`.
+
+```yaml
+kind: workflow
+uid: guide-weekly-reliability-report
+spec:
+ name: Weekly reliability report
+ description: Every Monday, reads the past week's traffic, errors, open incidents, and SLOs from the KloudMate MCP server, and posts a report that AI writes from them.
+ definition:
+ schema_version: 1
+ trigger:
+ type: schedule
+ config:
+ mode: weekly
+ weekday: 1
+ hour: 9
+ minute: 0
+ timezone: UTC
+ steps:
+ - id: collect
+ type: parallel
+ display_name: Collect the week's data
+ branches:
+ - - id: traffic
+ type: action
+ action: mcp.call
+ display_name: Requests and latency by service
+ connection_id:
+ $input: kloudmate_mcp
+ with:
+ tool: get_service_requests_count_and_avg_latency
+ arguments:
+ from: now-7d
+ to: now
+ - - id: errors
+ type: action
+ action: mcp.call
+ display_name: Errors by service
+ connection_id:
+ $input: kloudmate_mcp
+ with:
+ tool: get_service_error_count
+ arguments:
+ from: now-7d
+ to: now
+ - - id: incidents
+ type: action
+ action: mcp.call
+ display_name: Open incidents
+ connection_id:
+ $input: kloudmate_mcp
+ with:
+ tool: search_incidents
+ arguments:
+ limit: 50
+ - - id: slos
+ type: action
+ action: mcp.call
+ display_name: SLOs
+ connection_id:
+ $input: kloudmate_mcp
+ with:
+ tool: search_slos
+ arguments:
+ enabled: true
+ limit: 50
+ - id: write
+ type: action
+ action: ai.prompt
+ display_name: Write the report
+ with:
+ prompt: |-
+ Write this week's reliability report for the engineering team, as a Slack message. Use Slack formatting: *bold* for headings and short bullet lists. Keep it under 250 words, and don't include links.
+
+ Include these sections:
+ 1. Traffic and errors: the five busiest services, with their request count and error rate. Error rate is the service's errors divided by its requests, as a percentage.
+ 2. Open incidents: how many are open, and the three highest-severity ones by title.
+ 3. SLOs at risk: every SLO that is breached or has less than 25% of its error budget left.
+
+ Use only the numbers in the data below. If a section has no data, say so in one line. Latency in the data is in nanoseconds, so show it in milliseconds.
+
+ Requests and average latency by service:
+ {{ steps.traffic.output.result }}
+
+ Errors by service:
+ {{ steps.errors.output.result }}
+
+ Open incidents:
+ {{ steps.incidents.output.result }}
+
+ SLOs:
+ {{ steps.slos.output.result }}
+ - id: post
+ type: action
+ action: slack.post_message
+ display_name: Post the report
+ connection_id:
+ $input: slack
+ with:
+ text: "{{ steps.write.output.text | replace: '` from notifying the whole channel if the model ever writes it:
+
+```liquid
+{{ steps.write.output.text | replace: '