Documentationbreadcrumb arrow Grafana Cloudbreadcrumb arrow Platformbreadcrumb arrow Workflowsbreadcrumb arrow Referencebreadcrumb arrow Blueprintsbreadcrumb arrow Weekly incident metrics to Microsoft Teams
Public preview Grafana Cloud
Last reviewed: November 9, 2026

Weekly incident metrics to Microsoft Teams

Note

Grafana Workflows is currently in public preview. 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:

To create a secret, refer to Manage secrets.

Steps

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

StepIDTypeDescription
Fetch recent incidentsfetch-incidentshttp.callQueries 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 failedcheck-responsefilterFails the run with an error instead of posting an empty report when the API call does not succeed. No changes needed.
Compute the metricscomputecode.executeComputes weekly incident metrics and builds the Teams card. Change lookbackDays in args, or add your own metrics.
Post the scorecard to Teamspost-scorecardmsteams.message.postPosts 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.

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.

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