<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic Managing Dynatrace Automation Workflows as Code with dtctl and GitHub Copilot in Dynatrace tips</title>
    <link>https://community.dynatrace.com/t5/Dynatrace-tips/Managing-Dynatrace-Automation-Workflows-as-Code-with-dtctl-and/m-p/302859#M2133</link>
    <description>&lt;H1&gt;Managing Dynatrace Automation Workflows as Code with dtctl and GitHub Copilot&lt;/H1&gt;&lt;P&gt;&lt;EM&gt;Infrastructure as Code for Dynatrace Workflows — a field guide from the trenches&lt;/EM&gt;&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Why Workflows as Code?&lt;/H2&gt;&lt;P&gt;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:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Version control&lt;/STRONG&gt; — know who changed what and when&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Reproducibility&lt;/STRONG&gt; — deploy the same workflow reliably, every time&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Code review&lt;/STRONG&gt; — catch logic errors before they hit production&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Copilot assistance&lt;/STRONG&gt; — let AI help you author complex JavaScript tasks and DQL queries&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;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.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Prerequisites&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;dtctl installed and configured (~/.config/dtctl/config) with a valid context pointing to your Dynatrace environment&lt;/LI&gt;&lt;LI&gt;A Git repository to store your workflow YAML files&lt;/LI&gt;&lt;LI&gt;GitHub Copilot active in VS Code&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Verify your context is working:&lt;/P&gt;&lt;PRE&gt;dtctl get workflows | head -5&lt;/PRE&gt;&lt;HR /&gt;&lt;H2&gt;The Workflow Cycle&lt;/H2&gt;&lt;PRE&gt;Copilot generates YAML → dtctl apply → test in DT UI → git commit → PR&lt;/PRE&gt;&lt;P&gt;Simple. Let's walk through each step.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 1 — Export an Existing Workflow as a Reference&lt;/H2&gt;&lt;P&gt;Before writing from scratch, export a working workflow to understand the schema:&lt;/P&gt;&lt;PRE&gt;# List all workflows
dtctl get workflows

# Export a specific workflow to YAML
dtctl get workflow &amp;lt;workflow-id&amp;gt; -o yaml&lt;/PRE&gt;&lt;P&gt;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.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 2 — Author a New Workflow with Copilot&lt;/H2&gt;&lt;P&gt;When prompting Copilot, be specific about three things:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Trigger type&lt;/STRONG&gt;: davis-problem, davis-event, or schedule&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Tasks&lt;/STRONG&gt;: JavaScript logic, DQL queries, ServiceNow calls, email notifications&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Conditions&lt;/STRONG&gt;: when tasks should run or be skipped&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Here are the three trigger types with correct YAML structures — including some non-obvious gotchas I hit along the way.&lt;/P&gt;&lt;H3&gt;Davis Problem trigger&lt;/H3&gt;&lt;PRE&gt;trigger:
  eventTrigger:
    filterQuery: &amp;gt;-
      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"] }}'&lt;/PRE&gt;&lt;H3&gt;Davis Event trigger (e.g. PGI_CRASHED_INFO)&lt;/H3&gt;&lt;PRE&gt;trigger:
  eventTrigger:
    filterQuery: &amp;gt;-
      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"] }}'&lt;/PRE&gt;&lt;BLOCKQUOTE&gt;&lt;STRONG&gt;Gotcha:&lt;/STRONG&gt; For davis-event, the value object only accepts entityTags, entityTagsMatch, and customFilter. Fields like eventName, problemState, and eventTypes are &lt;STRONG&gt;not&lt;/STRONG&gt; valid and will cause a 400 error. Copilot sometimes generates these — always validate with dtctl apply --debug.&lt;/BLOCKQUOTE&gt;&lt;H3&gt;Schedule (CRON) trigger&lt;/H3&gt;&lt;PRE&gt;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&lt;/PRE&gt;&lt;BLOCKQUOTE&gt;&lt;STRONG&gt;Gotcha:&lt;/STRONG&gt; The cron expression goes in the nested trigger.cron field — &lt;STRONG&gt;not&lt;/STRONG&gt; in rule. The rule field must be null or a valid UUID. Setting it to a cron string causes a 400 error.&lt;/BLOCKQUOTE&gt;&lt;HR /&gt;&lt;H2&gt;A Complete Workflow Example&lt;/H2&gt;&lt;P&gt;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.&lt;/P&gt;&lt;PRE&gt;title: HealthCheck - MyService - Process Crash Alert - MYSERVER01
isDeployed: true
description: &amp;gt;
  Creates a ServiceNow incident when MyService.exe crashes on MYSERVER01.
  Suppresses alerting during the Sunday 20:00-20:05 scheduled maintenance window.
actor: &amp;lt;your-actor-id&amp;gt;
owner: &amp;lt;your-owner-id&amp;gt;
ownerType: USER
isPrivate: false
schemaVersion: 4
trigger:
  eventTrigger:
    filterQuery: &amp;gt;-
      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: &amp;gt;
      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 &amp;amp;&amp;amp; utcHour === 0 &amp;amp;&amp;amp; utcMin &amp;lt; 5;
          const inWinterWindow = utcDay === 1 &amp;amp;&amp;amp; utcHour === 1 &amp;amp;&amp;amp; utcMin &amp;lt; 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: &amp;gt;
      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: &amp;lt;your-snow-workflow-id&amp;gt;
      workflowInput: '{{ result("prepare_snow_payload") }}'
    name: create_snow_incident
    position:
      x: 0
      "y": 3
    predecessors:
      - prepare_snow_payload&lt;/PRE&gt;&lt;P&gt;Three tasks, one conditional gate:&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;&lt;STRONG&gt;check_maintenance_window&lt;/STRONG&gt; — always runs; returns a boolean&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;prepare_snow_payload&lt;/STRONG&gt; — runs only when inMaintenanceWindow == false; stops the chain otherwise&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;create_snow_incident&lt;/STRONG&gt; — delegates to the shared SNOW workflow, passing the prepared payload&lt;/LI&gt;&lt;/OL&gt;&lt;P&gt;Save this as HealthCheck-MyService-ProcessCrash-MYSERVER01.yaml and deploy with dtctl apply -f.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 3 — Deploy with dtctl apply&lt;/H2&gt;&lt;PRE&gt;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&lt;/PRE&gt;&lt;P&gt;The output tells you whether the workflow was &lt;STRONG&gt;created&lt;/STRONG&gt; or &lt;STRONG&gt;updated&lt;/STRONG&gt;. Use --write-id on first deploy so the file becomes self-contained for all future updates.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 4 — Diagnose 400 Errors&lt;/H2&gt;&lt;P&gt;When dtctl apply returns a 400, run with --debug to see the full API error body:&lt;/P&gt;&lt;PRE&gt;dtctl apply -f MyWorkflow.yaml --debug 2&amp;gt;&amp;amp;1 | grep -A 10 '"error"'&lt;/PRE&gt;&lt;P&gt;The response includes a details object that pinpoints the exact invalid field:&lt;/P&gt;&lt;PRE&gt;{
  "trigger": {
    "eventTrigger": {
      "triggerConfiguration": [
        "davis-event -&amp;gt; value -&amp;gt; eventName: Extra inputs are not permitted"
      ]
    }
  }
}&lt;/PRE&gt;&lt;P&gt;This tells you exactly which field to remove or fix. Much faster than guessing.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 5 — Test the Workflow&lt;/H2&gt;&lt;P&gt;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 &lt;STRONG&gt;manual run&lt;/STRONG&gt; feature in the Dynatrace UI instead:&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;Go to &lt;STRONG&gt;Automations → Workflows&lt;/STRONG&gt; → open your workflow&lt;/LI&gt;&lt;LI&gt;Click &lt;STRONG&gt;Run&lt;/STRONG&gt; (top right)&lt;/LI&gt;&lt;LI&gt;Paste a mock event context JSON:&lt;/LI&gt;&lt;/OL&gt;&lt;PRE&gt;{
  "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"
}&lt;/PRE&gt;&lt;OL&gt;&lt;LI&gt;Verify each task ran successfully and check that any downstream integrations (SNOW, email, etc.) fired correctly&lt;/LI&gt;&lt;/OL&gt;&lt;BLOCKQUOTE&gt;&lt;STRONG&gt;Note:&lt;/STRONG&gt; Manual test runs do &lt;STRONG&gt;not&lt;/STRONG&gt; create Dynatrace problems or inject real events. They only execute the workflow tasks with the provided mock context.&lt;/BLOCKQUOTE&gt;&lt;HR /&gt;&lt;H2&gt;Step 6 — Commit to Git&lt;/H2&gt;&lt;PRE&gt;cd ~/projects/my-dynatrace-repo

git add dynatrace-workflows/
git commit -m "Add/update workflow: &amp;lt;description&amp;gt;"

# 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&lt;/PRE&gt;&lt;P&gt;Then open a PR. Treat workflow YAML the same as application code — review it, scan it, merge it.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Bonus: Calling a Shared Workflow (ServiceNow Integration Pattern)&lt;/H2&gt;&lt;P&gt;A pattern that works well at scale is a &lt;STRONG&gt;shared integration workflow&lt;/STRONG&gt; 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.&lt;/P&gt;&lt;H3&gt;Preparing the ServiceNow payload (JavaScript task)&lt;/H3&gt;&lt;PRE&gt;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;&lt;/PRE&gt;&lt;H3&gt;Calling the shared SNOW workflow from YAML&lt;/H3&gt;&lt;PRE&gt;create_snow_incident:
  action: dynatrace.automations:run-workflow
  input:
    workflowId: &amp;lt;your-snow-workflow-id&amp;gt;
    workflowInput: '{{ result("prepare_snow_payload") }}'&lt;/PRE&gt;&lt;HR /&gt;&lt;H2&gt;Common Gotchas at a Glance&lt;/H2&gt;&lt;P&gt;Issue Cause Fix&lt;/P&gt;&lt;TABLE&gt;&lt;TBODY&gt;&lt;TR&gt;&lt;TD&gt;400 on davis-event trigger&lt;/TD&gt;&lt;TD&gt;Invalid fields in value (e.g. eventName, problemState)&lt;/TD&gt;&lt;TD&gt;Only use entityTags, entityTagsMatch, customFilter&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;400 on schedule trigger&lt;/TD&gt;&lt;TD&gt;Cron string in rule field&lt;/TD&gt;&lt;TD&gt;Put cron in trigger.cron, set rule: null&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;Push rejected to main&lt;/TD&gt;&lt;TD&gt;Branch protection / CI required&lt;/TD&gt;&lt;TD&gt;Use feature branch + PR&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;PGI_CRASHED_INFO not triggering&lt;/TD&gt;&lt;TD&gt;INFO-level event, not a Davis Problem&lt;/TD&gt;&lt;TD&gt;Use davis-event trigger type, not davis-problem&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;contains() not allowed in filterQuery&lt;/TD&gt;&lt;TD&gt;filterQuery uses a DQL matcher subset, not full DQL&lt;/TD&gt;&lt;TD&gt;Use matchesPhrase() instead&lt;/TD&gt;&lt;/TR&gt;&lt;/TBODY&gt;&lt;/TABLE&gt;&lt;HR /&gt;&lt;H2&gt;Quick Reference: Useful dtctl Commands&lt;/H2&gt;&lt;PRE&gt;# List all workflows
dtctl get workflows

# Export a workflow to YAML
dtctl get workflow &amp;lt;id&amp;gt; -o yaml &amp;gt; 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&amp;gt;&amp;amp;1 | grep -A 20 '"error"'

# View execution history
dtctl get workflow-executions --workflow &amp;lt;id&amp;gt;

# Get task result from a specific execution
dtctl get wfe-task-result &amp;lt;execution-id&amp;gt; &amp;lt;task-name&amp;gt;&lt;/PRE&gt;&lt;HR /&gt;&lt;H2&gt;Wrapping Up&lt;/H2&gt;&lt;P&gt;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.&lt;/P&gt;&lt;P&gt;The main things to remember:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Export a working workflow first&lt;/STRONG&gt; — it's the best schema reference you have&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Use --write-id on first deploy&lt;/STRONG&gt; — keeps the file self-contained for future updates&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;davis-event trigger value is more restrictive than it looks&lt;/STRONG&gt; — let --debug be your guide&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Manual run in the UI is your best testing tool&lt;/STRONG&gt; for event-triggered workflows&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Happy automating. If you've hit other gotchas or have workflow patterns worth sharing, drop them in the comments.&lt;/P&gt;</description>
    <pubDate>Wed, 05 Aug 2026 00:14:08 GMT</pubDate>
    <dc:creator>Georgi_V</dc:creator>
    <dc:date>2026-08-05T00:14:08Z</dc:date>
    <item>
      <title>Managing Dynatrace Automation Workflows as Code with dtctl and GitHub Copilot</title>
      <link>https://community.dynatrace.com/t5/Dynatrace-tips/Managing-Dynatrace-Automation-Workflows-as-Code-with-dtctl-and/m-p/302859#M2133</link>
      <description>&lt;H1&gt;Managing Dynatrace Automation Workflows as Code with dtctl and GitHub Copilot&lt;/H1&gt;&lt;P&gt;&lt;EM&gt;Infrastructure as Code for Dynatrace Workflows — a field guide from the trenches&lt;/EM&gt;&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Why Workflows as Code?&lt;/H2&gt;&lt;P&gt;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:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Version control&lt;/STRONG&gt; — know who changed what and when&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Reproducibility&lt;/STRONG&gt; — deploy the same workflow reliably, every time&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Code review&lt;/STRONG&gt; — catch logic errors before they hit production&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Copilot assistance&lt;/STRONG&gt; — let AI help you author complex JavaScript tasks and DQL queries&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;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.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Prerequisites&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;dtctl installed and configured (~/.config/dtctl/config) with a valid context pointing to your Dynatrace environment&lt;/LI&gt;&lt;LI&gt;A Git repository to store your workflow YAML files&lt;/LI&gt;&lt;LI&gt;GitHub Copilot active in VS Code&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Verify your context is working:&lt;/P&gt;&lt;PRE&gt;dtctl get workflows | head -5&lt;/PRE&gt;&lt;HR /&gt;&lt;H2&gt;The Workflow Cycle&lt;/H2&gt;&lt;PRE&gt;Copilot generates YAML → dtctl apply → test in DT UI → git commit → PR&lt;/PRE&gt;&lt;P&gt;Simple. Let's walk through each step.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 1 — Export an Existing Workflow as a Reference&lt;/H2&gt;&lt;P&gt;Before writing from scratch, export a working workflow to understand the schema:&lt;/P&gt;&lt;PRE&gt;# List all workflows
dtctl get workflows

# Export a specific workflow to YAML
dtctl get workflow &amp;lt;workflow-id&amp;gt; -o yaml&lt;/PRE&gt;&lt;P&gt;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.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 2 — Author a New Workflow with Copilot&lt;/H2&gt;&lt;P&gt;When prompting Copilot, be specific about three things:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Trigger type&lt;/STRONG&gt;: davis-problem, davis-event, or schedule&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Tasks&lt;/STRONG&gt;: JavaScript logic, DQL queries, ServiceNow calls, email notifications&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Conditions&lt;/STRONG&gt;: when tasks should run or be skipped&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Here are the three trigger types with correct YAML structures — including some non-obvious gotchas I hit along the way.&lt;/P&gt;&lt;H3&gt;Davis Problem trigger&lt;/H3&gt;&lt;PRE&gt;trigger:
  eventTrigger:
    filterQuery: &amp;gt;-
      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"] }}'&lt;/PRE&gt;&lt;H3&gt;Davis Event trigger (e.g. PGI_CRASHED_INFO)&lt;/H3&gt;&lt;PRE&gt;trigger:
  eventTrigger:
    filterQuery: &amp;gt;-
      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"] }}'&lt;/PRE&gt;&lt;BLOCKQUOTE&gt;&lt;STRONG&gt;Gotcha:&lt;/STRONG&gt; For davis-event, the value object only accepts entityTags, entityTagsMatch, and customFilter. Fields like eventName, problemState, and eventTypes are &lt;STRONG&gt;not&lt;/STRONG&gt; valid and will cause a 400 error. Copilot sometimes generates these — always validate with dtctl apply --debug.&lt;/BLOCKQUOTE&gt;&lt;H3&gt;Schedule (CRON) trigger&lt;/H3&gt;&lt;PRE&gt;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&lt;/PRE&gt;&lt;BLOCKQUOTE&gt;&lt;STRONG&gt;Gotcha:&lt;/STRONG&gt; The cron expression goes in the nested trigger.cron field — &lt;STRONG&gt;not&lt;/STRONG&gt; in rule. The rule field must be null or a valid UUID. Setting it to a cron string causes a 400 error.&lt;/BLOCKQUOTE&gt;&lt;HR /&gt;&lt;H2&gt;A Complete Workflow Example&lt;/H2&gt;&lt;P&gt;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.&lt;/P&gt;&lt;PRE&gt;title: HealthCheck - MyService - Process Crash Alert - MYSERVER01
isDeployed: true
description: &amp;gt;
  Creates a ServiceNow incident when MyService.exe crashes on MYSERVER01.
  Suppresses alerting during the Sunday 20:00-20:05 scheduled maintenance window.
actor: &amp;lt;your-actor-id&amp;gt;
owner: &amp;lt;your-owner-id&amp;gt;
ownerType: USER
isPrivate: false
schemaVersion: 4
trigger:
  eventTrigger:
    filterQuery: &amp;gt;-
      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: &amp;gt;
      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 &amp;amp;&amp;amp; utcHour === 0 &amp;amp;&amp;amp; utcMin &amp;lt; 5;
          const inWinterWindow = utcDay === 1 &amp;amp;&amp;amp; utcHour === 1 &amp;amp;&amp;amp; utcMin &amp;lt; 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: &amp;gt;
      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: &amp;lt;your-snow-workflow-id&amp;gt;
      workflowInput: '{{ result("prepare_snow_payload") }}'
    name: create_snow_incident
    position:
      x: 0
      "y": 3
    predecessors:
      - prepare_snow_payload&lt;/PRE&gt;&lt;P&gt;Three tasks, one conditional gate:&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;&lt;STRONG&gt;check_maintenance_window&lt;/STRONG&gt; — always runs; returns a boolean&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;prepare_snow_payload&lt;/STRONG&gt; — runs only when inMaintenanceWindow == false; stops the chain otherwise&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;create_snow_incident&lt;/STRONG&gt; — delegates to the shared SNOW workflow, passing the prepared payload&lt;/LI&gt;&lt;/OL&gt;&lt;P&gt;Save this as HealthCheck-MyService-ProcessCrash-MYSERVER01.yaml and deploy with dtctl apply -f.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 3 — Deploy with dtctl apply&lt;/H2&gt;&lt;PRE&gt;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&lt;/PRE&gt;&lt;P&gt;The output tells you whether the workflow was &lt;STRONG&gt;created&lt;/STRONG&gt; or &lt;STRONG&gt;updated&lt;/STRONG&gt;. Use --write-id on first deploy so the file becomes self-contained for all future updates.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 4 — Diagnose 400 Errors&lt;/H2&gt;&lt;P&gt;When dtctl apply returns a 400, run with --debug to see the full API error body:&lt;/P&gt;&lt;PRE&gt;dtctl apply -f MyWorkflow.yaml --debug 2&amp;gt;&amp;amp;1 | grep -A 10 '"error"'&lt;/PRE&gt;&lt;P&gt;The response includes a details object that pinpoints the exact invalid field:&lt;/P&gt;&lt;PRE&gt;{
  "trigger": {
    "eventTrigger": {
      "triggerConfiguration": [
        "davis-event -&amp;gt; value -&amp;gt; eventName: Extra inputs are not permitted"
      ]
    }
  }
}&lt;/PRE&gt;&lt;P&gt;This tells you exactly which field to remove or fix. Much faster than guessing.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Step 5 — Test the Workflow&lt;/H2&gt;&lt;P&gt;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 &lt;STRONG&gt;manual run&lt;/STRONG&gt; feature in the Dynatrace UI instead:&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;Go to &lt;STRONG&gt;Automations → Workflows&lt;/STRONG&gt; → open your workflow&lt;/LI&gt;&lt;LI&gt;Click &lt;STRONG&gt;Run&lt;/STRONG&gt; (top right)&lt;/LI&gt;&lt;LI&gt;Paste a mock event context JSON:&lt;/LI&gt;&lt;/OL&gt;&lt;PRE&gt;{
  "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"
}&lt;/PRE&gt;&lt;OL&gt;&lt;LI&gt;Verify each task ran successfully and check that any downstream integrations (SNOW, email, etc.) fired correctly&lt;/LI&gt;&lt;/OL&gt;&lt;BLOCKQUOTE&gt;&lt;STRONG&gt;Note:&lt;/STRONG&gt; Manual test runs do &lt;STRONG&gt;not&lt;/STRONG&gt; create Dynatrace problems or inject real events. They only execute the workflow tasks with the provided mock context.&lt;/BLOCKQUOTE&gt;&lt;HR /&gt;&lt;H2&gt;Step 6 — Commit to Git&lt;/H2&gt;&lt;PRE&gt;cd ~/projects/my-dynatrace-repo

git add dynatrace-workflows/
git commit -m "Add/update workflow: &amp;lt;description&amp;gt;"

# 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&lt;/PRE&gt;&lt;P&gt;Then open a PR. Treat workflow YAML the same as application code — review it, scan it, merge it.&lt;/P&gt;&lt;HR /&gt;&lt;H2&gt;Bonus: Calling a Shared Workflow (ServiceNow Integration Pattern)&lt;/H2&gt;&lt;P&gt;A pattern that works well at scale is a &lt;STRONG&gt;shared integration workflow&lt;/STRONG&gt; 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.&lt;/P&gt;&lt;H3&gt;Preparing the ServiceNow payload (JavaScript task)&lt;/H3&gt;&lt;PRE&gt;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;&lt;/PRE&gt;&lt;H3&gt;Calling the shared SNOW workflow from YAML&lt;/H3&gt;&lt;PRE&gt;create_snow_incident:
  action: dynatrace.automations:run-workflow
  input:
    workflowId: &amp;lt;your-snow-workflow-id&amp;gt;
    workflowInput: '{{ result("prepare_snow_payload") }}'&lt;/PRE&gt;&lt;HR /&gt;&lt;H2&gt;Common Gotchas at a Glance&lt;/H2&gt;&lt;P&gt;Issue Cause Fix&lt;/P&gt;&lt;TABLE&gt;&lt;TBODY&gt;&lt;TR&gt;&lt;TD&gt;400 on davis-event trigger&lt;/TD&gt;&lt;TD&gt;Invalid fields in value (e.g. eventName, problemState)&lt;/TD&gt;&lt;TD&gt;Only use entityTags, entityTagsMatch, customFilter&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;400 on schedule trigger&lt;/TD&gt;&lt;TD&gt;Cron string in rule field&lt;/TD&gt;&lt;TD&gt;Put cron in trigger.cron, set rule: null&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;Push rejected to main&lt;/TD&gt;&lt;TD&gt;Branch protection / CI required&lt;/TD&gt;&lt;TD&gt;Use feature branch + PR&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;PGI_CRASHED_INFO not triggering&lt;/TD&gt;&lt;TD&gt;INFO-level event, not a Davis Problem&lt;/TD&gt;&lt;TD&gt;Use davis-event trigger type, not davis-problem&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;contains() not allowed in filterQuery&lt;/TD&gt;&lt;TD&gt;filterQuery uses a DQL matcher subset, not full DQL&lt;/TD&gt;&lt;TD&gt;Use matchesPhrase() instead&lt;/TD&gt;&lt;/TR&gt;&lt;/TBODY&gt;&lt;/TABLE&gt;&lt;HR /&gt;&lt;H2&gt;Quick Reference: Useful dtctl Commands&lt;/H2&gt;&lt;PRE&gt;# List all workflows
dtctl get workflows

# Export a workflow to YAML
dtctl get workflow &amp;lt;id&amp;gt; -o yaml &amp;gt; 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&amp;gt;&amp;amp;1 | grep -A 20 '"error"'

# View execution history
dtctl get workflow-executions --workflow &amp;lt;id&amp;gt;

# Get task result from a specific execution
dtctl get wfe-task-result &amp;lt;execution-id&amp;gt; &amp;lt;task-name&amp;gt;&lt;/PRE&gt;&lt;HR /&gt;&lt;H2&gt;Wrapping Up&lt;/H2&gt;&lt;P&gt;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.&lt;/P&gt;&lt;P&gt;The main things to remember:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Export a working workflow first&lt;/STRONG&gt; — it's the best schema reference you have&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Use --write-id on first deploy&lt;/STRONG&gt; — keeps the file self-contained for future updates&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;davis-event trigger value is more restrictive than it looks&lt;/STRONG&gt; — let --debug be your guide&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Manual run in the UI is your best testing tool&lt;/STRONG&gt; for event-triggered workflows&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Happy automating. If you've hit other gotchas or have workflow patterns worth sharing, drop them in the comments.&lt;/P&gt;</description>
      <pubDate>Wed, 05 Aug 2026 00:14:08 GMT</pubDate>
      <guid>https://community.dynatrace.com/t5/Dynatrace-tips/Managing-Dynatrace-Automation-Workflows-as-Code-with-dtctl-and/m-p/302859#M2133</guid>
      <dc:creator>Georgi_V</dc:creator>
      <dc:date>2026-08-05T00:14:08Z</dc:date>
    </item>
  </channel>
</rss>

