28 May 2026 04:20 PM
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.
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:
The idea is to create a generic Workflow that can be imported and reused in any tenant.
The Workflow should:
I would recommend starting with this logic:
This avoids accidentally removing recurring configurations that may still be useful or were intentionally kept for audit reasons.
The Workflow execution context needs permission to read and delete Settings objects.
Deleting a Settings object requires write permission, and deletion cannot be undone, so this should be handled carefully.
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();
}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.
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
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.
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.
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.
Featured Posts