Guide · AWS Organizations · Tag Policies

AWS Organizations Tag Policies from MCP Server — CreatePolicy, GetEffectivePolicy

AWS Organizations tag policies define the allowed capitalization and values for tag keys across your organization. From an MCP server you create tag policies with CreatePolicyCommand{Type: 'TAG_POLICY'}, attach them to OUs and accounts, and retrieve the merged effective policy for any account with GetEffectivePolicyCommand. The critical distinction: tag policies do not block non-compliant tagging — they report compliance. Unlike SCPs, a resource tagged with a non-compliant value is still created successfully; the violation shows up in Resource Groups Tagging API compliance reports, not as an API error.

TL;DR

Enable tag policies on the root with EnablePolicyTypeCommand{PolicyType: 'TAG_POLICY', RootId}. Create a tag policy with CreatePolicyCommand{Type: 'TAG_POLICY', Content: JSON}. The JSON schema defines allowed tag key casings and values per resource type. Attach with AttachPolicyCommand. Retrieve the effective merged policy for an account with GetEffectivePolicyCommand{PolicyType: 'TAG_POLICY', TargetId: accountId}. Report compliance with the Resource Groups Tagging API GetComplianceSummaryCommand.

Enabling tag policies

Tag policies must be explicitly enabled on the root before you can create or attach them. This is a one-time operation per org.

import {
  OrganizationsClient,
  EnablePolicyTypeCommand,
  ListRootsCommand,
} from '@aws-sdk/client-organizations';

const org = new OrganizationsClient({ region: 'us-east-1' });

export async function enableTagPolicies(): Promise<void> {
  // Get the root ID first
  const rootsResponse = await org.send(new ListRootsCommand({}));
  const rootId = rootsResponse.Roots![0].Id!;

  try {
    await org.send(new EnablePolicyTypeCommand({
      RootId: rootId,
      PolicyType: 'TAG_POLICY',
    }));
  } catch (err: any) {
    if (err.name !== 'PolicyTypeAlreadyEnabledException') throw err;
    // Already enabled — no-op
  }
}

Tag policy content schema

Tag policy content is a JSON document with a tags top-level key. Each entry under tags defines a tag key and its constraints. The schema uses @@assign operators to set values and child-OU inheritance operators (@@append, @@remove) to extend or override parent policies.

// Full tag policy example with common patterns
const tagPolicy = {
  tags: {
    // Enforce exact capitalization — 'environment' is non-compliant, 'Environment' is required
    Environment: {
      tag_key: {
        '@@assign': 'Environment',  // exact case requirement
      },
      tag_value: {
        '@@assign': ['development', 'staging', 'production', 'sandbox'],
      },
      // enforced_for: makes non-compliant resources reportable per resource type
      // omitting enforced_for = key/value standard exists but no per-resource compliance
      enforced_for: {
        '@@assign': [
          'ec2:instance',
          'ec2:volume',
          'rds:db',
          's3:bucket',
          'lambda:function',
          'ecs:cluster',
          'ecs:service',
          'eks:cluster',
        ],
      },
    },

    // Owner: free-form value — only standardize the key name casing
    Owner: {
      tag_key: {
        '@@assign': 'Owner',
      },
      // No tag_value restriction — any value is compliant
    },

    // CostCenter: numeric codes only
    CostCenter: {
      tag_key: {
        '@@assign': 'CostCenter',
      },
      tag_value: {
        '@@assign': ['CC-1000', 'CC-1001', 'CC-1002', 'CC-2000', 'CC-2001'],
      },
      enforced_for: {
        '@@assign': ['ec2:instance', 'rds:db', 's3:bucket'],
      },
    },
  },
};

Key schema rules:

Creating and attaching tag policies

import {
  CreatePolicyCommand,
  AttachPolicyCommand,
  ListPoliciesCommand,
} from '@aws-sdk/client-organizations';

export async function createTagPolicy(params: {
  name: string;
  description: string;
  content: object;
}): Promise<string> {  // returns PolicyId
  const response = await org.send(new CreatePolicyCommand({
    Name: params.name,
    Description: params.description,
    Type: 'TAG_POLICY',
    Content: JSON.stringify(params.content),
  }));
  return response.Policy!.PolicySummary!.Id!;
}

export async function attachTagPolicy(
  policyId: string,
  targetId: string,  // root, OU, or account ID
): Promise<void> {
  await org.send(new AttachPolicyCommand({ PolicyId: policyId, TargetId: targetId }));
}

// List all tag policies in the org
export async function listTagPolicies() {
  const policies = [];
  let nextToken: string | undefined;

  do {
    const response = await org.send(new ListPoliciesCommand({
      Filter: 'TAG_POLICY',
      NextToken: nextToken,
    }));
    policies.push(...(response.Policies ?? []));
    nextToken = response.NextToken;
  } while (nextToken);

  return policies;
}

Retrieving the effective tag policy for an account

GetEffectivePolicyCommand returns the merged result of all tag policies that apply to a target — including policies inherited from parent OUs and the root. This is the policy that actually governs tag compliance for that account.

import { GetEffectivePolicyCommand } from '@aws-sdk/client-organizations';

export async function getEffectiveTagPolicy(accountId: string): Promise<object> {
  const response = await org.send(new GetEffectivePolicyCommand({
    PolicyType: 'TAG_POLICY',
    TargetId: accountId,
  }));

  // EffectivePolicy.PolicyContent is the merged JSON string
  const content = response.EffectivePolicy?.PolicyContent;
  return content ? JSON.parse(content) : {};
}

Inheritance merging rules:

// Child OU policy that extends the root policy's Environment values
const sandboxOUTagPolicy = {
  tags: {
    Environment: {
      tag_key: {
        '@@assign': 'Environment',
      },
      tag_value: {
        // Append 'ephemeral' to the parent's allowed values
        '@@append': ['ephemeral'],
      },
    },
  },
};

Checking tag compliance

Tag policy compliance is checked via the Resource Groups Tagging API, not the Organizations API. Use the GetComplianceSummaryCommand from @aws-sdk/client-resource-groups-tagging-api to get an org-wide summary, or GetResourcesCommand with IncludeComplianceDetails: true to get per-resource compliance.

import {
  ResourceGroupsTaggingAPIClient,
  GetComplianceSummaryCommand,
  GetResourcesCommand,
} from '@aws-sdk/client-resource-groups-tagging-api';

// Must use management account (or delegated admin) to query org-wide compliance
const tagging = new ResourceGroupsTaggingAPIClient({ region: 'us-east-1' });

export async function getTagComplianceSummary() {
  const response = await tagging.send(new GetComplianceSummaryCommand({
    GroupBy: ['ACCOUNT', 'REGION', 'RESOURCE_TYPE'],
  }));

  return response.SummaryList?.map(summary => ({
    accountId: summary.AccountId,
    region: summary.Region,
    resourceType: summary.ResourceType,
    complianceStatus: summary.ComplianceStatus,  // 'COMPLIANT' or 'NON_COMPLIANT'
    lastUpdated: summary.LastUpdated,
  }));
}

// Get individual non-compliant resources for a specific account
export async function getNonCompliantResources(accountId?: string) {
  const resources = [];
  let paginationToken: string | undefined;

  do {
    const response = await tagging.send(new GetResourcesCommand({
      IncludeComplianceDetails: true,
      ExcludeCompliantResources: true,
      PaginationToken: paginationToken,
    }));

    resources.push(...(response.ResourceTagMappingList ?? []));
    paginationToken = response.PaginationToken;
  } while (paginationToken);

  return resources.map(r => ({
    arn: r.ResourceARN,
    tags: r.Tags,
    // ComplianceDetails.NoncompliantKeys: tag keys that are non-compliant
    // ComplianceDetails.KeysWithNoncompliantValues: keys with valid key but bad value
    noncompliantKeys: r.ComplianceDetails?.NoncompliantKeys ?? [],
    keysWithBadValues: r.ComplianceDetails?.KeysWithNoncompliantValues ?? [],
    compliant: r.ComplianceDetails?.ComplianceStatus,
  }));
}

GetComplianceSummaryCommand aggregates compliance across the entire org when called from the management account. When called from a member account, it only shows compliance for resources in that account.

Tag policy vs Config rule vs SCP

MechanismBlocks creation?Reports compliance?Best for
Tag policyNo — reports onlyYes — via Tagging APIStandardizing key names and allowed values across the org
AWS Config rule (required-tags)No — detects after creationYes — Config complianceDetecting missing required tags on specific resource types
SCP with Deny + ConditionYes — rejects the API callNo (blocked outright)Absolutely requiring a tag on resource creation (heavy-handed)

Tag policies and Config rules are complementary: tag policies define the vocabulary, Config rules enforce presence. Use both together — tag policies for standardizing values, Config required-tags rules for mandatory key enforcement.

Related pages