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:
tag_key.@@assigndefines the canonical capitalization. Any resource withenvironment(lowercase) is non-compliant because the key doesn't matchEnvironment.tag_value.@@assignis a whitelist — only these values are compliant. Omit to allow any value.enforced_fortakes resource type strings inservice:resource-typeformat. The list of valid values comes from the supported resources for enforcement.
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 tag policies use
@@appendto add allowed values to a parent's list, or@@removeto remove values. - A child policy with
@@assignfor a tag key completely replaces the parent's values for that key. - If no tag policy is attached to any ancestor,
GetEffectivePolicyCommandthrowsEffectivePolicyNotFoundException.
// 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
| Mechanism | Blocks creation? | Reports compliance? | Best for |
|---|---|---|---|
| Tag policy | No — reports only | Yes — via Tagging API | Standardizing key names and allowed values across the org |
AWS Config rule (required-tags) | No — detects after creation | Yes — Config compliance | Detecting missing required tags on specific resource types |
| SCP with Deny + Condition | Yes — rejects the API call | No (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
- AWS Organizations overview — ListAccounts, CreateAccount, MoveAccount — manage accounts and OU hierarchy
- AWS Organizations SCPs — CreatePolicy, AttachPolicy, evaluation logic — restrict what member accounts can do
- AWS Organizations delegated administrators — SecurityHub, Config, GuardDuty — delegate org-wide service administration
- AWS Config rules for MCP servers