Dynatrace tips
Tips and workarounds from Dynatrace users for Dynatrace users.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

PRO-TIP: Removal of expired maintenance windows with a Workflow

MaximilianoML
Champion

Hi Community,

A small housekeeping tip that can help keep Dynatrace environments cleaner over time: create a reusable Workflow to identify and remove expired Maintenance Windows.

Maintenance windows are stored as Settings objects under the following schema:

builtin:alerting.maintenance-window

The older Maintenance Windows Environment API is deprecated, so the recommended approach is to use the Settings API with the Maintenance Windows schema instead.

Why this is useful

In many environments, maintenance windows are created for one-time changes, patching activities, releases, or incident handling. After some time, these windows may no longer be relevant, but they still remain in the configuration.

This can lead to:

  • A long list of outdated maintenance windows
  • Confusion when reviewing alerting suppression rules
  • Higher risk of reusing or copying old configurations
  • More manual cleanup work for administrators

The idea is to create a generic Workflow that can be imported and reused in any tenant.

Workflow idea

The Workflow should:

  1. Query Settings objects for schema builtin:alerting.maintenance-window
  2. Read the maintenance window schedule
  3. Identify windows where the end date/time is already in the past
  4. Optionally apply a retention period, for example only delete windows expired more than 7 days ago
  5. Run in dryRun mode first
  6. Send an email report when expired maintenance windows are found
  7. Delete only after the output has been validated

Recommended safe behavior

I would recommend starting with this logic:

  • Delete one-time maintenance windows where scheduleType is ONCE and endTime is older than the retention period
  • Keep recurring maintenance windows by default
  • Add an optional flag to include expired recurring windows later
  • Use dryRun=true as the default mode
  • Send a report only when expired maintenance windows are found

This avoids accidentally removing recurring configurations that may still be useful or were intentionally kept for audit reasons.

Required permissions

The Workflow execution context needs permission to read and delete Settings objects.

  • settings:objects:read to list maintenance window settings
  • settings:objects:write to delete expired maintenance window settings
  • email:emails:send if you want the Workflow to send an email report

Deleting a Settings object requires write permission, and deletion cannot be undone, so this should be handled carefully.

JavaScript logic for the Workflow

This can be used as the base logic in a Run JavaScript action inside the Workflow.

import { settingsObjectsClient } from "@dynatrace-sdk/client-classic-environment-v2";

const DRY_RUN = true;
const RETENTION_DAYS = 7;
const DELETE_RECURRING = false;

const SCHEMA_ID = "builtin:alerting.maintenance-window";

function cutoffDate() {
  const date = new Date();
  date.setDate(date.getDate() - RETENTION_DAYS);
  return date;
}

function parseDynatraceLocalDateTime(value) {
  if (!value) return null;

  const parsed = new Date(value);
  return isNaN(parsed.getTime()) ? null : parsed;
}

function parseDynatraceLocalDate(value) {
  if (!value) return null;

  const parsed = new Date(`${value}T23:59:59`);
  return isNaN(parsed.getTime()) ? null : parsed;
}

function getExpiryDate(settingsObject) {
  const value = settingsObject.value || {};
  const schedule = value.schedule || {};
  const scheduleType = schedule.scheduleType;

  if (scheduleType === "ONCE") {
    return parseDynatraceLocalDateTime(schedule.onceRecurrence?.endTime);
  }

  if (
    DELETE_RECURRING &&
    ["DAILY", "WEEKLY", "MONTHLY"].includes(scheduleType)
  ) {
    const recurrence =
      schedule.dailyRecurrence ||
      schedule.weeklyRecurrence ||
      schedule.monthlyRecurrence;

    return parseDynatraceLocalDate(
      recurrence?.recurrenceRange?.scheduleEndDate
    );
  }

  return null;
}

function buildEmailReport(result) {
  const lines = [];

  lines.push("Dynatrace expired maintenance windows report");
  lines.push("");
  lines.push("The maintenance window cleanup Workflow found expired maintenance windows in this tenant.");
  lines.push("");
  lines.push("Summary:");
  lines.push("");
  lines.push(`Total maintenance windows checked: ${result.totalMaintenanceWindows}`);
  lines.push(`Expired maintenance windows found: ${result.expiredEligibleForCleanup}`);
  lines.push(`Dry-run mode: ${result.dryRun}`);
  lines.push(`Deleted maintenance windows: ${result.deleted}`);
  lines.push("");
  lines.push("Details:");

  for (const candidate of result.candidates) {
    lines.push("");
    lines.push(`Maintenance window: ${candidate.summary}`);
    lines.push(`Schedule type: ${candidate.scheduleType}`);
    lines.push(`Expired at: ${candidate.expiryDate}`);
    lines.push(`Object ID: ${candidate.objectId.substring(0, 24)}...`);
  }

  lines.push("");
  lines.push("Note:");
  lines.push("If dry-run mode is true, no maintenance windows were deleted.");
  lines.push("Review the result and change DRY_RUN to false only after validation.");

  return lines.join("\n");
}

async function listMaintenanceWindows() {
  const allObjects = [];

  let response = await settingsObjectsClient.getSettingsObjects({
    schemaIds: SCHEMA_ID,
    fields: "objectId,summary,value,updateToken,schemaId,scope",
    pageSize: 500
  });

  allObjects.push(...(response.items || []));

  while (response.nextPageKey) {
    response = await settingsObjectsClient.getSettingsObjects({
      nextPageKey: response.nextPageKey
    });

    allObjects.push(...(response.items || []));
  }

  return allObjects;
}

async function main() {
  const cutoff = cutoffDate();
  const maintenanceWindows = await listMaintenanceWindows();

  const expired = maintenanceWindows
    .map((mw) => {
      const expiryDate = getExpiryDate(mw);

      return {
        objectId: mw.objectId,
        summary: mw.summary,
        scheduleType: mw.value?.schedule?.scheduleType,
        expiryDate
      };
    })
    .filter((mw) => mw.expiryDate && mw.expiryDate < cutoff);

  console.log(`Found ${maintenanceWindows.length} maintenance windows`);
  console.log(`Found ${expired.length} expired maintenance windows eligible for cleanup`);
  console.log(`Dry run mode: ${DRY_RUN}`);

  for (const mw of expired) {
    console.log(
      `${DRY_RUN ? "[DRY RUN]" : "[DELETE]"} ${mw.objectId} | ${mw.summary} | ${mw.scheduleType} | expired=${mw.expiryDate.toISOString()}`
    );

    if (!DRY_RUN) {
      await settingsObjectsClient.deleteSettingsObjectByObjectId({
        objectId: mw.objectId
      });
    }
  }

  const result = {
    dryRun: DRY_RUN,
    hasFindings: expired.length > 0,
    totalMaintenanceWindows: maintenanceWindows.length,
    expiredEligibleForCleanup: expired.length,
    deleted: DRY_RUN ? 0 : expired.length,
    candidates: expired.map((mw) => ({
      objectId: mw.objectId,
      summary: mw.summary,
      scheduleType: mw.scheduleType,
      expiryDate: mw.expiryDate.toISOString()
    }))
  };

  return {
    ...result,
    emailSubject: `Dynatrace report: ${result.expiredEligibleForCleanup} expired maintenance window(s) found`,
    emailBody: buildEmailReport(result)
  };
}

export default async function () {
  return await main();
}

Optional email report step

After the JavaScript step, add a Condition step so that the email is sent only when expired maintenance windows are found.

{{ result("cleanup_expired_maintenance_windows").hasFindings }}

Replace cleanup_expired_maintenance_windows with the real task name of your JavaScript action.

Then add a Send email action.

Subject:

{{ result("cleanup_expired_maintenance_windows").emailSubject }}

Body:

{{ result("cleanup_expired_maintenance_windows").emailBody }}

Using a plain-text email body avoids the issue where HTML tags are displayed directly in the email.

Suggested Workflow configuration

Use a scheduled trigger, for example weekly or monthly.

Suggested parameters:

dryRun = true
retentionDays = 7
deleteRecurring = false

After validating the Workflow execution result, change:

dryRun = false

Example dry-run result

Found 4 maintenance windows
Found 1 expired maintenance windows eligible for cleanup
Dry run mode: true
[DRY RUN] objectId | DEV Deploy | ONCE | expired=2023-10-25T15:00:00.000Z

This confirms that the Workflow is only identifying the expired maintenance window and is not deleting anything while dryRun mode is enabled.

Important note

I would not recommend deleting recurring maintenance windows on the first version of the Workflow. Start with expired one-time windows only. After the logic is validated in your tenant, you can enable recurring cleanup if that matches your internal process.

Also, because Settings API deletion cannot be undone, keep the first executions in dryRun mode and review the candidate list before enabling deletion.

Final thought

This is a simple cleanup automation, but it helps keep the environment easier to operate and audit. It is also a good example of a reusable Workflow pattern: query the Settings API, evaluate configuration state, run safely in dryRun mode, notify the right team when something is found, and only then apply changes.

Max Lopes
0 REPLIES 0

Featured Posts