<?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 Propagating host tags to extension entities via Workflow — looking for feedback/suggestions in Automations</title>
    <link>https://community.dynatrace.com/t5/Automations/Propagating-host-tags-to-extension-entities-via-Workflow-looking/m-p/296751#M2547</link>
    <description>&lt;P&gt;&lt;STRONG&gt;Title:&lt;/STRONG&gt; Propagating host tags to extension entities via Workflow — looking for feedback/suggestions&lt;/P&gt;
&lt;HR /&gt;
&lt;P&gt;Hey all — we've put together a workflow to propagate &lt;CODE&gt;patch-group&lt;/CODE&gt; tags from HOST entities down to their extension entities (WMI, SQL, IIS, HyperV, etc.) and wanted to share the approach and get some feedback before we scale it out.&lt;/P&gt;
&lt;P&gt;The goal is to make sure extension entities are included in our maintenance windows during patching — without the tag on the extension entity, they keep alerting even when the host they run on is in a maintenance window. Propagating the tag automatically keeps everything in sync without manual tagging.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;How it works&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;A JS action uses &lt;CODE&gt;queryExecutionClient&lt;/CODE&gt; to query &lt;CODE&gt;dt.system.data_objects&lt;/CODE&gt; to dynamically discover all extension entity types, then loops over each type running this DQL:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;fetch `dt.entity.{entityType}`
| fieldsAdd runs_on, tags
| filter isNotNull(runs_on[`dt.entity.host`])
| fieldsAdd hostId = runs_on[`dt.entity.host`]
| lookup [fetch dt.entity.host | fieldsAdd tags], sourceField: hostId, lookupField: id
| expand tag = lookup.tags
| filter startsWith(tag, "patch-group:")
| fieldsAdd patchGroupValue = replaceString(tag, "patch-group:", "")
| fields entity.name, id, entity.type, hostId, tags, patchGroupValue&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Entity types that don't have a &lt;CODE&gt;runs_on&lt;/CODE&gt; field throw a &lt;CODE&gt;FIELD_DOES_NOT_EXIST&lt;/CODE&gt; error and get skipped. The JS action validates existing tags and only passes entities that actually need updating to a follow-up HTTP action that calls &lt;CODE&gt;POST /api/v2/tags&lt;/CODE&gt;.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;A few things we're curious about&lt;/STRONG&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;Is &lt;CODE&gt;dt.system.data_objects&lt;/CODE&gt; a stable/supported table to use as an entity type catalog, or should we be pulling this differently?&lt;/LI&gt;
&lt;LI&gt;Is there a cleaner way to pre-filter entity types that have a &lt;CODE&gt;runs_on[dt.entity.host]&lt;/CODE&gt; relationship rather than querying each type and catching the error?&lt;/LI&gt;
&lt;LI&gt;Any recommended pattern for tag replacement (delete + add) in a workflow? The tags API only adds, so a changed host tag leaves the old value on the extension entity.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;Happy to share more detail if useful — just looking for any gotchas or better approaches before we push this to production. Thanks!&lt;/P&gt;
&lt;HR /&gt;
&lt;P&gt;&lt;STRONG&gt;Full JS action code&lt;/STRONG&gt;&lt;/P&gt;
&lt;PRE&gt;&lt;CODE class="language-javascript"&gt;import { queryExecutionClient } from '@dynatrace-sdk/client-query';
import { execution } from '@dynatrace-sdk/automation-utils';

export default async function ({ execution_id }) {
  const ex = await execution(execution_id);

  // Configuration — update this value to retarget the workflow
  const SOURCE_TAG_PREFIX = 'patch-group';   // Tag prefix to look for on the host and write to extension entities

  // Step 1: Get all extension entity types from the Dynatrace data object catalog
  const typeResponse = await queryExecutionClient.queryExecute({
    body: {
      query: `fetch dt.system.data_objects
| filter contains(name, "dt.entity.") and contains(name, ":")
| fieldsAdd entityType = replaceString(name, "dt.entity.", "")
| fields name, entityType`,
      requestTimeoutMilliseconds: 60000,
      fetchTimeoutSeconds: 60
    }
  });

  const types = typeResponse.result.records ?? [];
  const output = [];
  const skipped = [];
  let totalFound = 0;
  let totalAlreadyTagged = 0;

  // Step 2: For each entity type, query extension entities that run on a host
  // and have a patch-group tag on the host. Types without a runs_on relationship
  // will throw a DQL error and be skipped gracefully.
  for (const t of types) {
    const entityType = t.entityType;

    try {
      const entityResponse = await queryExecutionClient.queryExecute({
        body: {
          query: `fetch \`dt.entity.${entityType}\`
| fieldsAdd runs_on, tags
| filter isNotNull(runs_on[\`dt.entity.host\`])
| fieldsAdd hostId = runs_on[\`dt.entity.host\`]
| lookup [fetch dt.entity.host | fieldsAdd tags], sourceField: hostId, lookupField: id
| expand tag = lookup.tags
| filter startsWith(tag, "${SOURCE_TAG_PREFIX}:")
| fieldsAdd patchGroupValue = replaceString(tag, "${SOURCE_TAG_PREFIX}:", "")
| fields entity.name, id, entity.type, hostId, tags, patchGroupValue`,
          requestTimeoutMilliseconds: 60000,
          fetchTimeoutSeconds: 60
        }
      });

      const records = entityResponse.result?.records ?? [];
      totalFound += records.length;

      // Step 3: Validate each entity — only add to output if tag is missing or incorrect
      for (const r of records) {
        const existingTags = Array.isArray(r.tags) ? r.tags : [];
        const expectedTag = `${SOURCE_TAG_PREFIX}:${r.patchGroupValue}`;
        const alreadyTagged = existingTags.some(t =&amp;gt; t === expectedTag);

        if (alreadyTagged) {
          totalAlreadyTagged++;
        } else {
          output.push({
            entityId: r.id,
            entityName: r['entity.name'],
            entityType: r['entity.type'],
            tagKey: SOURCE_TAG_PREFIX,
            tagValue: r.patchGroupValue,
            entitySelector: `entityId("${r.id}")`
          });
        }
      }

    } catch (e) {
      // Entity type does not have a runs_on field — skip silently
      skipped.push(entityType);
    }
  }

  // Step 4: Detect and exclude entities where the host has duplicate patch-group tags
  const entityIdCounts = output.reduce((acc, r) =&amp;gt; {
    acc[r.entityId] = (acc[r.entityId] || 0) + 1;
    return acc;
  }, {});

  const duplicateEntities = [...new Set(
    output.filter(r =&amp;gt; entityIdCounts[r.entityId] &amp;gt; 1).map(r =&amp;gt; r.entityId)
  )];

  const cleanOutput = output.filter(r =&amp;gt; entityIdCounts[r.entityId] === 1);

  console.log(`Types processed: ${types.length}, Skipped (no runs_on field): ${skipped.length}`);
  console.log(`Entities found: ${totalFound}, Already tagged: ${totalAlreadyTagged}, Needs update: ${cleanOutput.length}`);

  if (duplicateEntities.length &amp;gt; 0) {
    console.log(`WARNING: ${duplicateEntities.length} entities skipped — host has duplicate patch-group tags`);
    duplicateEntities.forEach(id =&amp;gt; console.log(`  Duplicate: ${id}`));
  }

  // Log each entity queued for tag write
  cleanOutput.forEach(r =&amp;gt; console.log(`Queued: ${r.entityName} (${r.entityId}) [${r.entityType}] → ${r.tagKey}:${r.tagValue}`));

  return { total: cleanOutput.length, duplicates: duplicateEntities.length, records: cleanOutput };
}&lt;/CODE&gt;&lt;/PRE&gt;</description>
    <pubDate>Fri, 27 Mar 2026 09:50:26 GMT</pubDate>
    <dc:creator>VMorrison101</dc:creator>
    <dc:date>2026-03-27T09:50:26Z</dc:date>
    <item>
      <title>Propagating host tags to extension entities via Workflow — looking for feedback/suggestions</title>
      <link>https://community.dynatrace.com/t5/Automations/Propagating-host-tags-to-extension-entities-via-Workflow-looking/m-p/296751#M2547</link>
      <description>&lt;P&gt;&lt;STRONG&gt;Title:&lt;/STRONG&gt; Propagating host tags to extension entities via Workflow — looking for feedback/suggestions&lt;/P&gt;
&lt;HR /&gt;
&lt;P&gt;Hey all — we've put together a workflow to propagate &lt;CODE&gt;patch-group&lt;/CODE&gt; tags from HOST entities down to their extension entities (WMI, SQL, IIS, HyperV, etc.) and wanted to share the approach and get some feedback before we scale it out.&lt;/P&gt;
&lt;P&gt;The goal is to make sure extension entities are included in our maintenance windows during patching — without the tag on the extension entity, they keep alerting even when the host they run on is in a maintenance window. Propagating the tag automatically keeps everything in sync without manual tagging.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;How it works&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;A JS action uses &lt;CODE&gt;queryExecutionClient&lt;/CODE&gt; to query &lt;CODE&gt;dt.system.data_objects&lt;/CODE&gt; to dynamically discover all extension entity types, then loops over each type running this DQL:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;fetch `dt.entity.{entityType}`
| fieldsAdd runs_on, tags
| filter isNotNull(runs_on[`dt.entity.host`])
| fieldsAdd hostId = runs_on[`dt.entity.host`]
| lookup [fetch dt.entity.host | fieldsAdd tags], sourceField: hostId, lookupField: id
| expand tag = lookup.tags
| filter startsWith(tag, "patch-group:")
| fieldsAdd patchGroupValue = replaceString(tag, "patch-group:", "")
| fields entity.name, id, entity.type, hostId, tags, patchGroupValue&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Entity types that don't have a &lt;CODE&gt;runs_on&lt;/CODE&gt; field throw a &lt;CODE&gt;FIELD_DOES_NOT_EXIST&lt;/CODE&gt; error and get skipped. The JS action validates existing tags and only passes entities that actually need updating to a follow-up HTTP action that calls &lt;CODE&gt;POST /api/v2/tags&lt;/CODE&gt;.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;A few things we're curious about&lt;/STRONG&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;Is &lt;CODE&gt;dt.system.data_objects&lt;/CODE&gt; a stable/supported table to use as an entity type catalog, or should we be pulling this differently?&lt;/LI&gt;
&lt;LI&gt;Is there a cleaner way to pre-filter entity types that have a &lt;CODE&gt;runs_on[dt.entity.host]&lt;/CODE&gt; relationship rather than querying each type and catching the error?&lt;/LI&gt;
&lt;LI&gt;Any recommended pattern for tag replacement (delete + add) in a workflow? The tags API only adds, so a changed host tag leaves the old value on the extension entity.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;Happy to share more detail if useful — just looking for any gotchas or better approaches before we push this to production. Thanks!&lt;/P&gt;
&lt;HR /&gt;
&lt;P&gt;&lt;STRONG&gt;Full JS action code&lt;/STRONG&gt;&lt;/P&gt;
&lt;PRE&gt;&lt;CODE class="language-javascript"&gt;import { queryExecutionClient } from '@dynatrace-sdk/client-query';
import { execution } from '@dynatrace-sdk/automation-utils';

export default async function ({ execution_id }) {
  const ex = await execution(execution_id);

  // Configuration — update this value to retarget the workflow
  const SOURCE_TAG_PREFIX = 'patch-group';   // Tag prefix to look for on the host and write to extension entities

  // Step 1: Get all extension entity types from the Dynatrace data object catalog
  const typeResponse = await queryExecutionClient.queryExecute({
    body: {
      query: `fetch dt.system.data_objects
| filter contains(name, "dt.entity.") and contains(name, ":")
| fieldsAdd entityType = replaceString(name, "dt.entity.", "")
| fields name, entityType`,
      requestTimeoutMilliseconds: 60000,
      fetchTimeoutSeconds: 60
    }
  });

  const types = typeResponse.result.records ?? [];
  const output = [];
  const skipped = [];
  let totalFound = 0;
  let totalAlreadyTagged = 0;

  // Step 2: For each entity type, query extension entities that run on a host
  // and have a patch-group tag on the host. Types without a runs_on relationship
  // will throw a DQL error and be skipped gracefully.
  for (const t of types) {
    const entityType = t.entityType;

    try {
      const entityResponse = await queryExecutionClient.queryExecute({
        body: {
          query: `fetch \`dt.entity.${entityType}\`
| fieldsAdd runs_on, tags
| filter isNotNull(runs_on[\`dt.entity.host\`])
| fieldsAdd hostId = runs_on[\`dt.entity.host\`]
| lookup [fetch dt.entity.host | fieldsAdd tags], sourceField: hostId, lookupField: id
| expand tag = lookup.tags
| filter startsWith(tag, "${SOURCE_TAG_PREFIX}:")
| fieldsAdd patchGroupValue = replaceString(tag, "${SOURCE_TAG_PREFIX}:", "")
| fields entity.name, id, entity.type, hostId, tags, patchGroupValue`,
          requestTimeoutMilliseconds: 60000,
          fetchTimeoutSeconds: 60
        }
      });

      const records = entityResponse.result?.records ?? [];
      totalFound += records.length;

      // Step 3: Validate each entity — only add to output if tag is missing or incorrect
      for (const r of records) {
        const existingTags = Array.isArray(r.tags) ? r.tags : [];
        const expectedTag = `${SOURCE_TAG_PREFIX}:${r.patchGroupValue}`;
        const alreadyTagged = existingTags.some(t =&amp;gt; t === expectedTag);

        if (alreadyTagged) {
          totalAlreadyTagged++;
        } else {
          output.push({
            entityId: r.id,
            entityName: r['entity.name'],
            entityType: r['entity.type'],
            tagKey: SOURCE_TAG_PREFIX,
            tagValue: r.patchGroupValue,
            entitySelector: `entityId("${r.id}")`
          });
        }
      }

    } catch (e) {
      // Entity type does not have a runs_on field — skip silently
      skipped.push(entityType);
    }
  }

  // Step 4: Detect and exclude entities where the host has duplicate patch-group tags
  const entityIdCounts = output.reduce((acc, r) =&amp;gt; {
    acc[r.entityId] = (acc[r.entityId] || 0) + 1;
    return acc;
  }, {});

  const duplicateEntities = [...new Set(
    output.filter(r =&amp;gt; entityIdCounts[r.entityId] &amp;gt; 1).map(r =&amp;gt; r.entityId)
  )];

  const cleanOutput = output.filter(r =&amp;gt; entityIdCounts[r.entityId] === 1);

  console.log(`Types processed: ${types.length}, Skipped (no runs_on field): ${skipped.length}`);
  console.log(`Entities found: ${totalFound}, Already tagged: ${totalAlreadyTagged}, Needs update: ${cleanOutput.length}`);

  if (duplicateEntities.length &amp;gt; 0) {
    console.log(`WARNING: ${duplicateEntities.length} entities skipped — host has duplicate patch-group tags`);
    duplicateEntities.forEach(id =&amp;gt; console.log(`  Duplicate: ${id}`));
  }

  // Log each entity queued for tag write
  cleanOutput.forEach(r =&amp;gt; console.log(`Queued: ${r.entityName} (${r.entityId}) [${r.entityType}] → ${r.tagKey}:${r.tagValue}`));

  return { total: cleanOutput.length, duplicates: duplicateEntities.length, records: cleanOutput };
}&lt;/CODE&gt;&lt;/PRE&gt;</description>
      <pubDate>Fri, 27 Mar 2026 09:50:26 GMT</pubDate>
      <guid>https://community.dynatrace.com/t5/Automations/Propagating-host-tags-to-extension-entities-via-Workflow-looking/m-p/296751#M2547</guid>
      <dc:creator>VMorrison101</dc:creator>
      <dc:date>2026-03-27T09:50:26Z</dc:date>
    </item>
  </channel>
</rss>

