---
title: "Weekly incident metrics to Microsoft Teams | Grafana Cloud documentation"
description: "Every Monday, pull last week's incidents from the Grafana IRM API, compute counts and time to resolve in Python, and post a scorecard to Microsoft Teams."
---

> For a curated documentation index, see [llms.txt](/llms.txt). For the complete documentation index, see [llms-full.txt](/llms-full.txt).

# Weekly incident metrics to Microsoft Teams

> Note
> 
> Grafana Workflows is currently in [public preview](https://grafana.com/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available.

Every Monday, pull last week’s incidents from the Grafana IRM API, compute counts and time to resolve in Python, and post a scorecard to Microsoft Teams.

This Grafana Workflows blueprint is in the **Reports** category of the Blueprints library.

## Before you begin

To use this blueprint, you need:

- The Grafana IRM Microsoft Teams integration. For details, refer to [Microsoft Teams integration for Grafana IRM](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/integrations/chat-and-collaboration/ms-teams/).
- A workflow secret named `grafana-api-token`.

To create a secret, refer to [Manage secrets](https://grafana.com/docs/grafana-cloud/platform/workflows/manage/secrets).

## Steps

Steps run in order from first to last. A step inside a switch branch runs only when Workflows chooses that branch.

Expand table

| Step                        | ID                | Type                   | Description                                                                                                                                                                                                                                                                                                                                               |
|-----------------------------|-------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Fetch recent incidents      | `fetch-incidents` | `http.call`            | Queries the 100 most recent incidents from the Incident API. Replace `your-stack.grafana.net` with your Grafana Cloud stack, for example `mycompany.grafana.net`. Also create a workflow secret named grafana-api-token that holds a service account token with permission to read incidents. Raise the limit if you have more than 100 incidents a week. |
| Fail if the API call failed | `check-response`  | `filter`               | Fails the run with an error instead of posting an empty report when the API call does not succeed. No changes needed.                                                                                                                                                                                                                                     |
| Compute the metrics         | `compute`         | `code.execute`         | Computes weekly incident metrics and builds the Teams card. Change `lookbackDays` in args, or add your own metrics.                                                                                                                                                                                                                                       |
| Post the scorecard to Teams | `post-scorecard`  | `msteams.message.post` | Posts the metrics card. Pick the Teams team and channel for your weekly ops review.                                                                                                                                                                                                                                                                       |

For the inputs and outputs of each step type, refer to [Step and trigger types reference](https://grafana.com/docs/grafana-cloud/platform/workflows/reference/step-trigger-types).

Before you turn on the workflow, customize any steps that need input from you, such as a channel ID or a schedule name. Each step’s description says what to set.

## Workflow definition

Save this definition as a YAML file to create the workflow with gcx, the Workflows API, or Terraform, or give it to a coding agent as an example. Replace *FOLDER\_UID* with the UID of the folder for the workflow. Blueprints start disabled, so finish customizing the steps before you set `enabled` to `true`. For details, refer to [Manage workflows as code](https://grafana.com/docs/grafana-cloud/platform/workflows/as-code).

YAML ![Copy code to clipboard](https://grafana.com/media/images/icons/icon-copy-small-2.svg) Copy

```yaml
apiVersion: workflows.ext.grafana.app/v1alpha1
kind: Definition
metadata:
  name: weekly-incident-metrics-teams
  annotations:
    grafana.app/folder: <FOLDER_UID>
spec:
  name: Weekly incident metrics to Microsoft Teams
  enabled: false
  startWhen:
    matchingRules: []
    schedules:
      - '0 9 * * 1'
  resources:
    secrets:
      grafanaApiToken: 'secret://grafana-api-token'
  steps:
    - id: fetch-incidents
      type: http.call
      name: Fetch recent incidents
      description: >-
        Queries the 100 most recent incidents from the Incident API. Replace your-stack.grafana.net
        with your Grafana Cloud stack, for example mycompany.grafana.net. Also create a workflow
        secret named grafana-api-token that holds a service account token with permission to read
        incidents. Raise the limit if you have more than 100 incidents a week.
      inputs:
        method: 'POST'
        url: 'https://your-stack.grafana.net/api/plugins/grafana-irm-app/resources/api/v1/IncidentsService.QueryIncidents'
        body: '{"query": {"limit": 100, "orderDirection": "DESC"}}'
        authMethod: 'bearer'
        authSecret: '${resources.secret.grafanaApiToken}'
    - id: check-response
      type: filter
      name: Fail if the API call failed
      description: Fails the run with an error instead of posting an empty report when the API call does not succeed. No changes needed.
      inputs:
        condition: "steps['fetch-incidents'].outputs.responseCode == 200"
        failOnExit: 'true'
    - id: compute
      type: code.execute
      name: Compute the metrics
      description: Computes weekly incident metrics and builds the Teams card. Change lookbackDays in args, or add your own metrics.
      inputs:
        language: 'python'
        args: '{"lookbackDays": 7}'
        code: |
          import json
          from collections import Counter
          from datetime import datetime, timedelta, timezone

          def parse_time(value):
              if not value:
                  return None
              return datetime.fromisoformat(value.replace("Z", "+00:00"))

          body = json.loads(context["steps"]["fetch-incidents"]["outputs"]["responseBody"])
          since = datetime.now(timezone.utc) - timedelta(days=args.get("lookbackDays", 7))

          incidents = [
              i for i in body.get("incidents", [])
              if not i.get("isDrill") and (parse_time(i.get("createdTime")) or since) >= since
          ]

          by_severity = Counter((i.get("severity") or "unknown").lower() for i in incidents)
          resolve_hours = []
          for i in incidents:
              start = parse_time(i.get("incidentStart") or i.get("createdTime"))
              end = parse_time(i.get("closedTime"))
              if start and end:
                  resolve_hours.append((end - start).total_seconds() / 3600)

          still_open = sum(1 for i in incidents if i.get("status") != "resolved")
          mttr = f"{sum(resolve_hours) / len(resolve_hours):.1f} h" if resolve_hours else "n/a"
          severity_facts = [
              {"title": severity.title(), "value": str(count)}
              for severity, count in by_severity.most_common()
          ]

          card = {
              "type": "AdaptiveCard",
              "version": "1.4",
              "body": [
                  {"type": "TextBlock", "text": "Weekly incident metrics", "size": "Large", "weight": "Bolder"},
                  {"type": "TextBlock", "text": f"Since {since:%Y-%m-%d}", "isSubtle": True, "spacing": "None"},
                  {
                      "type": "FactSet",
                      "facts": [
                          {"title": "Incidents", "value": str(len(incidents))},
                          {"title": "Still open", "value": str(still_open)},
                          {"title": "Mean time to resolve", "value": mttr},
                      ],
                  },
                  {"type": "TextBlock", "text": "By severity", "weight": "Bolder"},
                  {"type": "FactSet", "facts": severity_facts or [{"title": "None", "value": "0"}]},
              ],
          }
          print(json.dumps(card))
    - id: post-scorecard
      type: msteams.message.post
      name: Post the scorecard to Teams
      description: Posts the metrics card. Pick the Teams team and channel for your weekly ops review.
      inputs:
        teamID: ''
        channelID: ''
        messageText: 'Weekly incident metrics'
        adaptiveCard: '${steps.compute.outputs.stdout}'
```

## How it works

This section explains when the workflow runs and how its steps work together.

### When it runs

This blueprint runs on the schedule `0 9 * * 1`.

### How the steps fit together

These notes cover how the steps connect and which Workflows behaviors the blueprint relies on.

- Shows the HTTP, Python, and secrets pattern end to end.
- A schedule trigger runs Mondays at 09:00 UTC.
- `http.call` POSTs to the Grafana IRM Incident API (`IncidentsService.QueryIncidents`) with a service account token.
- The token is stored as a workflow secret, declared under `resources.secrets`, and referenced only in `authSecret`, a `secretString` parameter, so it is redacted from run history.
- A filter step with `failOnExit` set to true fails the run loudly when the API does not return 200.
- The Python `code.execute` step reads the response body from context\[“steps”]\[“fetch-incidents”]\[“outputs”], keeps incidents created in the last 7 days, counts them by severity, computes mean time to resolve, and prints a complete Adaptive Card as JSON.
- The sandbox has no network access, which is why the HTTP call happens in its own step.
- The Teams step passes stdout straight to `adaptiveCard`.
- Customize the stack host in the URL, the secret name, the lookback window in args, and the Teams team and channel.

## Next steps

- [Blueprints](https://grafana.com/docs/grafana-cloud/platform/workflows/reference/blueprints)
- [Start from a blueprint](https://grafana.com/docs/grafana-cloud/platform/workflows/build/use-blueprints)
- [Manage workflows as code](https://grafana.com/docs/grafana-cloud/platform/workflows/as-code)
- [Step and trigger types reference](https://grafana.com/docs/grafana-cloud/platform/workflows/reference/step-trigger-types)
