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

Managing Dynatrace Automation Workflows as Code with dtctl and GitHub Copilot

Georgi_V
Visitor

Managing Dynatrace Automation Workflows as Code with dtctl and GitHub Copilot

Infrastructure as Code for Dynatrace Workflows — a field guide from the trenches


Why Workflows as Code?

Dynatrace Automation Workflows are powerful — but clicking through the UI to build and maintain them doesn't scale. Once you have more than a handful of workflows, you want:

  • Version control — know who changed what and when
  • Reproducibility — deploy the same workflow reliably, every time
  • Code review — catch logic errors before they hit production
  • Copilot assistance — let AI help you author complex JavaScript tasks and DQL queries

This guide walks through the full cycle: using dtctl (the Dynatrace CLI) together with GitHub Copilot to author, deploy, and version-control Dynatrace Workflows as YAML files.


Prerequisites

  • dtctl installed and configured (~/.config/dtctl/config) with a valid context pointing to your Dynatrace environment
  • A Git repository to store your workflow YAML files
  • GitHub Copilot active in VS Code

Verify your context is working:

dtctl get workflows | head -5

The Workflow Cycle

Copilot generates YAML → dtctl apply → test in DT UI → git commit → PR

Simple. Let's walk through each step.


Step 1 — Export an Existing Workflow as a Reference

Before writing from scratch, export a working workflow to understand the schema:

# List all workflows
dtctl get workflows

# Export a specific workflow to YAML
dtctl get workflow <workflow-id> -o yaml

The exported YAML is the ground truth for the API schema. Use it as a starting point and ask Copilot to adapt it to your use case. Any workflows already living in your repo are equally good templates.


Step 2 — Author a New Workflow with Copilot

When prompting Copilot, be specific about three things:

  • Trigger type: davis-problem, davis-event, or schedule
  • Tasks: JavaScript logic, DQL queries, ServiceNow calls, email notifications
  • Conditions: when tasks should run or be skipped

Here are the three trigger types with correct YAML structures — including some non-obvious gotchas I hit along the way.

Davis Problem trigger

trigger:
  eventTrigger:
    filterQuery: >-
      event.kind == "DAVIS_PROBLEM" AND event.status == "ACTIVE"
      AND matchesValue(event.status_transition, {"CREATED", "UPDATED", "REOPENED"})
      AND matchesPhrase(affected_entity_names, "MY_HOST")
    isActive: true
    triggerConfiguration:
      type: davis-problem
      value:
        analysisReady: false
        categories:
          custom: true
        customFilter: 'matchesPhrase(affected_entity_names, "MY_HOST")'
        entityTags: {}
        entityTagsMatch: "all"
        onProblemClose: false
        problemOpenDuration: null
        severityThreshold: null
        triggerOn: null
        triggerOnUpdateFields: []
    uniqueExpression: '{{ event()["event.id"] }}-{{ "open" if event()["event.status"] == "ACTIVE" else "resolved" }}-{{ event()["dt.davis.last_reopen_timestamp"] }}'

Davis Event trigger (e.g. PGI_CRASHED_INFO)

trigger:
  eventTrigger:
    filterQuery: >-
      event.type == "PGI_CRASHED_INFO"
      AND matchesPhrase(event.description, "MyProcess")
      AND dt.entity.host == "HOST-XXXXXXXXXXXXXXXX"
    isActive: true
    triggerConfiguration:
      type: davis-event
      value:
        entityTags: {}
        entityTagsMatch: "all"
        customFilter: 'event.type == "PGI_CRASHED_INFO" AND matchesPhrase(event.description, "MyProcess")'
    uniqueExpression: '{{ event()["event.id"] }}-{{ event()["event.start"] }}'
Gotcha: For davis-event, the value object only accepts entityTags, entityTagsMatch, and customFilter. Fields like eventName, problemState, and eventTypes are not valid and will cause a 400 error. Copilot sometimes generates these — always validate with dtctl apply --debug.

Schedule (CRON) trigger

trigger:
  schedule:
    filterParameters: {}
    inputs: {}
    isActive: true
    nextExecution: null
    rule: null
    timezone: America/Toronto
    trigger:
      type: cron
      cron: "5 20 * * 0"   # Sunday 20:05 local time
Gotcha: The cron expression goes in the nested trigger.cron field — not in rule. The rule field must be null or a valid UUID. Setting it to a cron string causes a 400 error.

A Complete Workflow Example

Here's a full, deployable workflow that ties everything together. It listens for a process crash event, suppresses the alert during a known maintenance window, then creates a ServiceNow incident via a shared workflow.

title: HealthCheck - MyService - Process Crash Alert - MYSERVER01
isDeployed: true
description: >
  Creates a ServiceNow incident when MyService.exe crashes on MYSERVER01.
  Suppresses alerting during the Sunday 20:00-20:05 scheduled maintenance window.
actor: <your-actor-id>
owner: <your-owner-id>
ownerType: USER
isPrivate: false
schemaVersion: 4
trigger:
  eventTrigger:
    filterQuery: >-
      event.type == "PGI_CRASHED_INFO"
      AND matchesPhrase(event.description, "MyService")
      AND dt.entity.host == "HOST-XXXXXXXXXXXXXXXX"
    isActive: true
    triggerConfiguration:
      type: davis-event
      value:
        entityTags: {}
        entityTagsMatch: "all"
        customFilter: 'event.type == "PGI_CRASHED_INFO" AND matchesPhrase(event.description, "MyService") AND dt.entity.host == "HOST-XXXXXXXXXXXXXXXX"'
    uniqueExpression: '{{ event()["event.id"] }}-{{ event()["event.start"] }}'
type: STANDARD
hourlyExecutionLimit: 1000
tasks:
  check_maintenance_window:
    action: dynatrace.automations:run-javascript
    description: >
      Returns inMaintenanceWindow=true if current time falls inside the Sunday
      20:00-20:05 scheduled restart window. Handles DST by checking both UTC offsets.
    input:
      script: |
        export default async function () {
          const now = new Date();
          const utcDay  = now.getUTCDay();
          const utcHour = now.getUTCHours();
          const utcMin  = now.getUTCMinutes();

          const inSummerWindow = utcDay === 1 && utcHour === 0 && utcMin < 5;
          const inWinterWindow = utcDay === 1 && utcHour === 1 && utcMin < 5;

          const inMaintenanceWindow = inSummerWindow || inWinterWindow;
          console.log(`UTC day=${utcDay} hour=${utcHour} min=${utcMin} | inMaintenanceWindow=${inMaintenanceWindow}`);
          return { inMaintenanceWindow };
        }
    name: check_maintenance_window
    position:
      x: 0
      "y": 1
    predecessors: []

  prepare_snow_payload:
    action: dynatrace.automations:run-javascript
    active: true
    conditions:
      custom: '{{ result("check_maintenance_window").inMaintenanceWindow == false }}'
      else: STOP
      states:
        check_maintenance_window: OK
    description: >
      Builds the ServiceNow incident payload. Only runs when the crash occurs
      outside the scheduled maintenance window.
    input:
      script: |
        import { execution } from '@dynatrace-sdk/automation-utils';

        export default async function ({ execution_id }) {
          const ex = await execution(execution_id);
          const ev = ex.params.event || {};

          const payload = {
            serviceNowPayload: {
              cmdb_ci_input:     "MYSERVER01",
              assignment_group:  "My Team Name",
              u_impacted_system: "My Application",
              caller_id:         "dynatrace",
              category:          "Software",
              subcategory:       "Application",
              contact_type:      "Alert",
              correlation_id:    `DT_${ev["event.id"] || ""}`,
              short_description: "MyService crashed on MYSERVER01",
              description: [
                `Dynatrace detected a process crash on MYSERVER01.`,
                `Process  : MyService.exe`,
                `Time     : ${ev["event.start"] || new Date().toISOString()}`,
                `Event ID : ${ev["event.id"] || ""}`,
                `The service may have restarted automatically.`
              ].join("\n"),
              priority:          "3",
              state:             "1",
              environment:       "Production",
              environment_type:  "PRD",
              options: { search_cmdb_ci_by_fqdn: "N" }
            },
            original_event: ev
          };

          return payload;
        }
    name: prepare_snow_payload
    position:
      x: 0
      "y": 2
    predecessors:
      - check_maintenance_window

  create_snow_incident:
    action: dynatrace.automations:run-workflow
    active: true
    conditions:
      states:
        prepare_snow_payload: OK
    description: Calls the shared ServiceNow Incident Creation Workflow.
    input:
      workflowId: <your-snow-workflow-id>
      workflowInput: '{{ result("prepare_snow_payload") }}'
    name: create_snow_incident
    position:
      x: 0
      "y": 3
    predecessors:
      - prepare_snow_payload

Three tasks, one conditional gate:

  1. check_maintenance_window — always runs; returns a boolean
  2. prepare_snow_payload — runs only when inMaintenanceWindow == false; stops the chain otherwise
  3. create_snow_incident — delegates to the shared SNOW workflow, passing the prepared payload

Save this as HealthCheck-MyService-ProcessCrash-MYSERVER01.yaml and deploy with dtctl apply -f.


Step 3 — Deploy with dtctl apply

cd ~/projects/dynatrace-workflows

# Create a new workflow (auto-generates an ID)
dtctl apply -f MyWorkflow.yaml

# Stamp the generated ID back into the file for future updates
dtctl apply -f MyWorkflow.yaml --write-id

# Update an existing workflow (file must already contain the id field)
dtctl apply -f MyWorkflow.yaml

The output tells you whether the workflow was created or updated. Use --write-id on first deploy so the file becomes self-contained for all future updates.


Step 4 — Diagnose 400 Errors

When dtctl apply returns a 400, run with --debug to see the full API error body:

dtctl apply -f MyWorkflow.yaml --debug 2>&1 | grep -A 10 '"error"'

The response includes a details object that pinpoints the exact invalid field:

{
  "trigger": {
    "eventTrigger": {
      "triggerConfiguration": [
        "davis-event -> value -> eventName: Extra inputs are not permitted"
      ]
    }
  }
}

This tells you exactly which field to remove or fix. Much faster than guessing.


Step 5 — Test the Workflow

Workflows with event triggers can't be tested by injecting synthetic events via the Events API — PGI_CRASHED_INFO and similar types are OneAgent-generated. Use the manual run feature in the Dynatrace UI instead:

  1. Go to Automations → Workflows → open your workflow
  2. Click Run (top right)
  3. Paste a mock event context JSON:
{
  "event.type": "PGI_CRASHED_INFO",
  "event.description": "Process MyService.exe has crashed",
  "event.start": "2026-01-01T13:00:00.000Z",
  "event.id": "test-event-001",
  "dt.entity.host": "HOST-XXXXXXXXXXXXXXXX"
}
  1. Verify each task ran successfully and check that any downstream integrations (SNOW, email, etc.) fired correctly
Note: Manual test runs do not create Dynatrace problems or inject real events. They only execute the workflow tasks with the provided mock context.

Step 6 — Commit to Git

cd ~/projects/my-dynatrace-repo

git add dynatrace-workflows/
git commit -m "Add/update workflow: <description>"

# Use a feature branch — main typically requires PR + CI checks
git checkout -b feature/my-workflow-change
git push -u origin feature/my-workflow-change

Then open a PR. Treat workflow YAML the same as application code — review it, scan it, merge it.


Bonus: Calling a Shared Workflow (ServiceNow Integration Pattern)

A pattern that works well at scale is a shared integration workflow that handles ServiceNow incident creation, with individual alert workflows delegating to it. This avoids duplicating SNOW logic across dozens of workflows and gives you a single place to update the integration.

Preparing the ServiceNow payload (JavaScript task)

const ev = execution.params.event ?? {};

const payload = {
  serviceNowPayload: {
    cmdb_ci_input:     "MY-HOSTNAME",
    assignment_group:  "My Team Name",
    u_impacted_system: "My Application",
    caller_id:         "dynatrace",
    category:          "Software",
    subcategory:       "Application",
    contact_type:      "Alert",
    correlation_id:    `DT_${ev["event.id"]}`,
    short_description: "Alert: process crash detected on MY-HOSTNAME",
    description:       `Event ID: ${ev["event.id"]}\nHost: ${ev["dt.entity.host"]}`,
    priority:          "2",
    state:             "1",
    environment:       "Production",
    environment_type:  "PRD",
    options: {
      search_cmdb_ci_by_fqdn: "N"
    }
  },
  original_event: ev
};

return payload;

Calling the shared SNOW workflow from YAML

create_snow_incident:
  action: dynatrace.automations:run-workflow
  input:
    workflowId: <your-snow-workflow-id>
    workflowInput: '{{ result("prepare_snow_payload") }}'

Common Gotchas at a Glance

Issue Cause Fix

400 on davis-event triggerInvalid fields in value (e.g. eventName, problemState)Only use entityTags, entityTagsMatch, customFilter
400 on schedule triggerCron string in rule fieldPut cron in trigger.cron, set rule: null
Push rejected to mainBranch protection / CI requiredUse feature branch + PR
PGI_CRASHED_INFO not triggeringINFO-level event, not a Davis ProblemUse davis-event trigger type, not davis-problem
contains() not allowed in filterQueryfilterQuery uses a DQL matcher subset, not full DQLUse matchesPhrase() instead

Quick Reference: Useful dtctl Commands

# List all workflows
dtctl get workflows

# Export a workflow to YAML
dtctl get workflow <id> -o yaml > my-workflow.yaml

# Deploy / update
dtctl apply -f my-workflow.yaml

# Deploy and stamp ID into file
dtctl apply -f my-workflow.yaml --write-id

# Debug a failed deploy
dtctl apply -f my-workflow.yaml --debug 2>&1 | grep -A 20 '"error"'

# View execution history
dtctl get workflow-executions --workflow <id>

# Get task result from a specific execution
dtctl get wfe-task-result <execution-id> <task-name>

Wrapping Up

The dtctl + Copilot combination makes managing Dynatrace Workflows as code genuinely practical. Copilot handles the boilerplate YAML and JavaScript task logic; dtctl apply --debug gives you precise, field-level error feedback when the API rejects something; and Git gives you the full audit trail.

The main things to remember:

  • Export a working workflow first — it's the best schema reference you have
  • Use --write-id on first deploy — keeps the file self-contained for future updates
  • davis-event trigger value is more restrictive than it looks — let --debug be your guide
  • Manual run in the UI is your best testing tool for event-triggered workflows

Happy automating. If you've hit other gotchas or have workflow patterns worth sharing, drop them in the comments.

0 REPLIES 0

Featured Posts