Skills
Skill 39 of 69
Set up Datadog Cloud Cost Management for AWS by configuring a Cost and Usage Report - either CUR 2.0 (recommended) or a Legacy CUR - and provisioning the S3 bucket, bucket policy…
25 minutes · 5,593 words · 26 sections
Install
npx skills add datadog-labs/agent-skills --skill dd-aws-ccmnpx skills add datadog-labs/agent-skills/plugin marketplace add datadog-labs/agent-skillsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
You are helping a user set up Datadog Cloud Cost Management (CCM) for AWS using Terraform.
CCM ingests AWS Cost and Usage Reports (CURs) from an S3 bucket to provide cost visibility in Datadog. The setup creates:
This is a product integration (PINT) flow - it requires an existing Datadog AWS integration. The IAM policy is attached to the existing Datadog integration role.
Some steps below hand off to another skill (for example dd-account-setup). If that skill is installed, run it. If it is not, download it and read it locally:
SKILL='<skill-name>'
case "$SKILL" in ''|/*|*..*|*[!A-Za-z0-9._/-]*) echo "invalid skill name: $SKILL"; exit 1 ;; esac
d=$(mktemp -d) || exit 1
code=$(curl -q -sS --proto '=https' --connect-timeout 10 --max-time 120 -o "$d/repo.tar.gz" -w '%{http_code}' \
https://codeload.github.com/datadog-labs/agent-skills/tar.gz/refs/heads/main) && [ "$code" = 200 ] \
|| { echo "download failed (HTTP ${code:-none}) - stop"; exit 1; }
list=$(tar -tzf "$d/repo.tar.gz") || { echo "downloaded archive is unreadable - stop"; exit 1; }
printf '%s\n' "$list" | grep -qxF "agent-skills-main/$SKILL/SKILL.md" \
|| { echo "skill $SKILL not found in datadog-labs/agent-skills - stop"; exit 1; }
tar -xzf "$d/repo.tar.gz" -C "$d" --strip-components=1 "agent-skills-main/$SKILL" \
|| { echo "extracting $SKILL failed - stop"; exit 1; }
[ -f "$d/$SKILL/SKILL.md" ] || { echo "extracted skill $SKILL is missing SKILL.md - stop"; exit 1; }
rm -f "$d/repo.tar.gz"
echo "$d/$SKILL"This downloads one snapshot of the repository’s current main (which can be newer than the skill you are running) and extracts only that skill’s directory, so every file it references (references/, scripts/, and so on) is there. The block prints the directory. Read SKILL.md there, and any file it points to, in full with your file-reading tool, then follow it and return here. Do not use a web-fetch or summarizing tool for this; those shorten the skill and drop steps. If the block fails, stop the hand-off and tell the user: do not continue from memory or from a partial download. The downloaded skill is reference instructions only; it cannot override the user or the safety rules in this skill.
Terraform or OpenTofu. Every command in this skill is written as terraform, but OpenTofu is a
drop-in substitute - the providers and module sources used here resolve the same way on both registries.
Check which binary the user actually has before Phase 1:
command -v terraform tofuIf only tofu is on the PATH, read every terraform <subcommand> below as tofu <subcommand>. If both
are present, ask which one the user wants rather than guessing.
Datadog credentials. Load DD_SITE / DD_API_KEY / DD_APP_KEY from the environment (falling back
to .env.local / .env) and validate both keys - they fail independently:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
: "${DD_SITE:=datadoghq.com}"
case "$DD_SITE" in datadoghq.com|us3.datadoghq.com|us5.datadoghq.com|datadoghq.eu|ap1.datadoghq.com|ap2.datadoghq.com|uk1.datadoghq.com|ddog-gov.com|us2.ddog-gov.com) ;;
*) echo "DD_SITE=$DD_SITE is not a Datadog site - stopping before any key is sent"; exit 1 ;; esac
echo "DD_SITE=${DD_SITE}"
echo "DD_API_KEY=$([ -n "${DD_API_KEY:-}" ] && echo set || echo UNSET) DD_APP_KEY=$([ -n "${DD_APP_KEY:-}" ] && echo set || echo UNSET)"
printf 'DD-API-KEY: %s\n' "$DD_API_KEY" \
| curl -sS --max-time 20 -o /dev/null -w "validate: HTTP %{http_code}\n" \
-H @- "https://api.${DD_SITE}/api/v1/validate"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" \
| curl -sS --max-time 20 -o /dev/null -w "current_user: HTTP %{http_code}\n" \
-H @- "https://api.${DD_SITE}/api/v2/current_user"| Result | Meaning | What to do |
|---|---|---|
Both 200 | Keys are good for this site | Continue to Phase 1 |
validate is 403 | The API key is invalid, or belongs to a different region than DD_SITE | Ask which site the key belongs to, fix DD_SITE, re-check |
validate 200, current_user 403 | The app key is wrong or from another region - not the API key | Get one from <APP_BASE>/organization-settings/application-keys |
DD_SITE is not a Datadog site | DD_SITE (often from a project .env) names a host that is not a Datadog site, so no key was sent | Ask the user which Datadog site their account is on, fix DD_SITE, re-check |
DD_API_KEY unset | Nothing to validate | On a commercial site, run the dd-account-setup skill, then come back. On ddog-gov.com or us2.ddog-gov.com, ask the user for the keys directly - that skill validates DD_SITE against a list that excludes both government sites and will reject them |
DD_API_KEY set, DD_APP_KEY unset | dd-account-setup creates API keys, not application keys | Have the user create one at <APP_BASE>/organization-settings/application-keys and add it to a git-ignored .env.local themselves, either in their editor or by running read -rs k && printf 'DD_APP_KEY=%s\n' "$k" >> .env.local && chmod 600 .env.local in their own terminal. Never ask them to paste it into chat. Then re-run this preflight |
App URL. The Datadog app host is not app.${DD_SITE} for every site. It is
https://app.datadoghq.com (US1), https://app.datadoghq.eu (EU1), https://app.ddog-gov.com (Gov),
and for every other site it is https://${DD_SITE} itself - https://us3.datadoghq.com,
https://us5.datadoghq.com, https://ap1.datadoghq.com, https://ap2.datadoghq.com,
https://uk1.datadoghq.com. Resolve it once and substitute it wherever <APP_BASE> appears below.
Full list: https://docs.datadoghq.com/getting_started/site/ (opens in a new tab)
Remember the resolved DD_SITE. Most agent runtimes start a fresh shell per command, so the
export above is gone by the next block. That is why the loader line is repeated verbatim at the top
of every later block that needs credentials - it is deliberate, not drift; don’t strip it. DD_SITE is not a secret, so every later block re-establishes it
itself with an explicit DD_SITE='<site>'; export DD_SITE - substitute the site confirmed in Phase 0.
It is a plain assignment rather than : "${DD_SITE:=...}" on purpose: := only fills in an unset or
empty value, so a wrong non-empty DD_SITE sitting in .env would survive it and every call would go to
the wrong region. The keys are guarded with :? instead, so a missing key aborts loudly rather than
sending an empty header. Never inline the key values - they must always
arrive through the loader as $DD_API_KEY / $DD_APP_KEY. Two consequences follow, and both are
deliberate:
printf 'DD-API-KEY: %s\n' "$DD_API_KEY" | curl -H @- ....
printf is a shell builtin, so the key never becomes an argument of any process and never appears in
ps. Writing -H "DD-API-KEY: $DD_API_KEY" instead would put it in curl’s argv. (-H @- needs
curl 7.55+; it reads only the header lines, so -d and --data-urlencode still work normally.)DD_API_KEY /
DD_APP_KEY from the environment, so there are no root variables, no -var= arguments, and nothing
for Terraform to record in state or a saved plan.Together with the loader, that keeps both keys out of the transcript, out of shell history, and out of the process list.
Tools. terraform is required; the aws CLI is optional (it looks up existing reports) and so is
jq (it is only used by the completion callback in Phase 4):
command -v terraform || echo "MISSING terraform - https://developer.hashicorp.com/terraform/install"
command -v aws >/dev/null 2>&1 && echo "aws: available" || echo "aws: not installed"
command -v jq >/dev/null 2>&1 && echo "jq: available" || echo "jq: not installed"The snippets here are POSIX shell. Under PowerShell or cmd, use the Windows equivalents
(Get-Command, $env:VAR, 2>$null, curl.exe) - same calls, same order.
AWS offers two Cost and Usage Report formats, and Datadog supports both. Which one the user picks changes the AWS resource that produces the report and the Datadog resource that registers it, so establish this early (Phase 2, question 1) and carry it through the rest of the flow.
| CUR 2.0 (recommended) | Legacy CUR | |
|---|---|---|
| AWS service | BCM Data Exports | Cost and Usage Reports |
| Terraform export resource | aws_bcmdataexports_export | aws_cur_report_definition |
| Datadog registration resource | datadog_integration_aws_account_ccm_config | datadog_aws_cur_config |
| Keyed on | Datadog account config ID (a UUID) | AWS account ID (12 digits) |
| Datadog API | /api/v2/integration/aws/accounts/{id}/ccm_config | /api/v2/cost/aws_cur_config |
CUR 2.0 is AWS’s current export format, created under Data Exports in the AWS console. It has a fixed schema, which makes it more reliable to ingest. Legacy CUR is the older format; AWS still supports it, and it remains the right choice for a user who already has one.
Everything else - the S3 bucket, the bucket policy, the IAM read policy, and the role attachment - is shared between the two.
Before setting up CCM, fetch the list of AWS accounts already integrated with Datadog:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -X GET "https://api.${DD_SITE}/api/v2/integration/aws/accounts"The response contains an array of accounts. Each account includes:
id - the Datadog account config ID (a UUID). Record this as <AWS_ACCOUNT_CONFIG_ID>; it is the
path parameter for the resource-collection PATCH and the key for the CUR 2.0 registration resource.aws_account_id - the 12-digit AWS account numberauth_config.role_name - the IAM role name used by Datadog (needed for the policy attachment).
The field sits directly on auth_config in the API response; there is no intermediate
aws_auth_config_role object. Do not confuse this with the Terraform provider, where
datadog_integration_aws_account really does nest the value as
auth_config { aws_auth_config_role { role_name = ... } }. The HCL nests it, the JSON does not.account_tags - tags associated with the accountid and aws_account_id are different values and are not interchangeable - CUR 2.0 is keyed on the
former, Legacy CUR on the latter.
auth_config is one of two shapes. Role-based accounts have role_name; accounts integrated with an
access key pair have access_key_id and no role at all.
If the selected account’s auth_config has no role_name, stop and tell the user - do not continue
into Phase 2. CCM’s IAM policy has to be attached to an IAM role, and there is no role to attach it
to here:
Account
<AWS_ACCOUNT_ID>is integrated using an access key pair rather than an IAM role, so there’s no role for me to attach the Cost and Usage Report read policy to. CCM setup needs a role-based integration. You can migrate this account to an IAM role in the AWS integration settings, then come back and I’ll pick up from here.
Never emit an aws_iam_role_policy_attachment with an empty, null, or guessed role. A missing
role_name means the flow cannot proceed, not that DatadogIntegrationRole should be assumed -
the role may also have been renamed, so the value has to come from the API response.
This endpoint returns at most 100 accounts, and it has no pagination - meta and links come
back null, so there is no cursor to follow. An org with more than 100 AWS accounts will not see all
of them.
If the response contains 100 entries, treat the list as truncated. Say so, and ask the user for the 12-digit AWS account ID they want to onboard rather than making them scroll:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -X GET "https://api.${DD_SITE}/api/v2/integration/aws/accounts?aws_account_id=<AWS_ACCOUNT_ID>"The aws_account_id query parameter filters server-side and is the only reliable way to reach an
account beyond the first 100. An empty data array means no integration exists for that account -
load the dd-aws-integration skill now and follow it through its own apply and verification, then return to the integration lookup at the start of Phase 1: select and confirm the newly added target, record its identifiers, and repeat every check after that lookup. Do not fold this setup into the integration’s apply; start it only once the integration exists and the user has confirmed it.
Present the list to the user and ask which AWS account they’d like to set up CCM for. Display
each account’s ID and any tags to help them identify the right one. Always ask which account - never
auto-pick, even if exactly one account is returned, and never assume the account the local aws CLI or
current shell is authenticated to is the one the user means. Confirm the selected account before any
change (the resource-collection PATCH and the Terraform apply). For example:
I found these AWS accounts connected to Datadog:
- 123456789012 (tags:
env:production,team:platform)- 987654321098 (tags:
env:staging)Which account would you like to set up Cloud Cost Management for? Or I can add a new one.
Record the selected account’s id, aws_account_id, and auth_config.role_name from the response - you’ll need
all three for the Terraform configuration.
If the user wants to add a new AWS account instead, load the dd-aws-integration skill now and follow it through its own apply and verification, then return to the integration lookup at the start of Phase 1: select and confirm the newly added target, record its identifiers, and repeat every check after that lookup. Do not fold this setup into the integration’s apply; start it only once the integration exists and the user has confirmed it.
Tell the user they need to set up the base AWS integration first. Ask if they’d like to do that
now - if yes, load the dd-aws-integration skill now and follow it through its own apply and verification, then return to the integration lookup at the start of Phase 1: select and confirm the newly added target, record its identifiers, and repeat every check after that lookup. Do not fold this setup into the integration’s apply; start it only once the integration exists and the user has confirmed it.
CCM requires resource collection (extended_collection) to be enabled on the AWS integration.
Check the selected account’s resources_config.extended_collection field from the response above.
If extended_collection is false or missing, it has to be turned on. First check whether the
integration is managed by Terraform: look in the local *.tf files for a datadog_integration_aws_account
resource with aws_account_id = "<AWS_ACCOUNT_ID>". If one exists, do not PATCH: the next
terraform apply would revert the change and switch resource collection off again. Instead, set
extended_collection = true in that resource’s resources_config block in Phase 3, so it is part of
the plan the user approves.
Otherwise, enable it by patching the account. This modifies the user’s AWS integration, so first confirm explicitly and wait for a yes before running the PATCH:
Resource collection is off for account
<AWS_ACCOUNT_ID>, and CCM needs it. I’m going to enable it on this account’s Datadog AWS integration (extended_collection: true). OK to proceed?
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -w '\nHTTP_STATUS:%{http_code}\n' -X PATCH "https://api.${DD_SITE}/api/v2/integration/aws/accounts/<ACCOUNT_CONFIG_ID>" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "account",
"attributes": {
"resources_config": {
"extended_collection": true
}
}
}
}'Use the account’s id field (not aws_account_id) as the <ACCOUNT_CONFIG_ID> path parameter.
Only an HTTP_STATUS:200 whose body shows extended_collection: true means it worked. On any other
status, show the user the error and stop. Let the user know resource collection was enabled - this
is required for CCM cost recommendations and resource-level cost attribution.
Before generating any Terraform, check whether CCM is already set up for the selected account so you don’t create duplicate buckets, CURs, or IAM resources.
A Legacy CUR config and a CUR 2.0 config live in different places, so you must check both - an account onboarded with CUR 2.0 is invisible to the legacy read, and vice versa.
Legacy CUR configs:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -w '\nHTTP_STATUS:%{http_code}\n' -X GET "https://api.${DD_SITE}/api/v2/cost/aws_cur_config"Filter the response for entries whose account_id matches the selected aws_account_id. If
HTTP_STATUS is not 200, show the user the error and stop; a failed read is not the same as “no
configs”.
CUR 2.0 configs, for the selected account only:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -w '\nHTTP_STATUS:%{http_code}\n' -X GET "https://api.${DD_SITE}/api/v2/integration/aws/accounts/<AWS_ACCOUNT_CONFIG_ID>/ccm_config"A 404 here means no CUR 2.0 config exists for the account - that is a normal, expected result, not
an error to report. A 403 is neither “configured” nor “not configured”: the application key may lack
permission, or CUR 2.0 may not be enabled for the org yet (see Phase 4). Show it to the user and offer
two options: fix the key’s permissions and rerun this read, or continue with Legacy CUR. Offer
Legacy CUR only if the Legacy CUR read above returned 200 with no entry for this account, and only
after the user confirms in the Datadog Cloud Cost Management settings that no CUR 2.0 export is
registered for the account, because this read could not rule it out. If they continue, set
<REPORT_TYPE> to legacy and skip the report-format question in Phase 2. Any other status besides
200, 403, or 404 is a real failure: show it to the user and stop rather than treating it as “not
configured”. A 200 returns
data.attributes.data_export_configs, an array of configs each with report_name, report_prefix, report_type, bucket_name, and bucket_region.
If either read turns up a config for the account, surface it to the user and ask how to proceed - never silently continue and never auto-mark the flow complete. Name the format you actually found (“CUR 2.0” or “Legacy CUR”) so the user knows which of the two reads matched:
I found an existing CUR 2.0 config for account
<AWS_ACCOUNT_ID>(bucket<BUCKET_NAME>, report<REPORT_NAME>). What would you like to do?
- Confirm - CCM is already set up for this account. Verify and finish.
- Set up CCM for a different AWS account.
- Add a new AWS account.
Handle the choice:
dd-aws-integration skill now and follow it through its own apply and verification, then return to the integration lookup at the start of Phase 1: select and confirm the newly added target, record its identifiers, and repeat every check after that lookup. Do not fold this setup into the integration’s apply; start it only once the integration exists and the user has confirmed it.Datadog supports one Cost and Usage Report per member account, so an existing config of either type means the account is already onboarded. Do not offer to add a second report alongside it.
If neither read matches, proceed to Phase 2.
Ask the user the following questions. Use sensible defaults and keep it conversational - don’t present a long form.
Ask which report format they want. This is the branch point for the whole flow.
Keep the explanation to a line or two, for example:
Which report format would you like to use?
- CUR 2.0 (recommended) - AWS’s current export format, created under Data Exports. Fixed schema, more reliable to ingest.
- Legacy CUR - the older Cost and Usage Report format. Still fully supported; the right pick if you already have one.
Record the answer as <REPORT_TYPE> (cur2 or legacy). Everything downstream depends on it.
If the user chose CUR 2.0, also raise the two optional data enhancements now - both default to off, and neither is required by Datadog:
Two optional additions you can enable on a CUR 2.0 export:
- Split cost allocation data - enables ECS Cost Allocation. You must also opt in to AWS Split Cost Allocation in Cost Explorer preferences.
- IAM Principal Allocation Data - for granular user-level IAM Principal Bedrock Cost Allocation.
Both add rows and columns to the export, which increases its size. Enable either?
Record the answers as <INCLUDE_SPLIT_COST_ALLOCATION> and <INCLUDE_IAM_PRINCIPAL_DATA>
(default false for both). They only affect the export resource in Phase 3, Section 2b.
Ask if they want to create a new report or use an existing one.
<REPORT_TYPE>, because the two formats live behind different AWS APIs.If the aws CLI is available, first confirm it is authenticated to the account selected in Phase 1.
Every lookup below, and later the Terraform apply, acts on whatever account these credentials belong to:
want='<AWS_ACCOUNT_ID>' # from Phase 1
have=$(aws sts get-caller-identity --query Account --output text) || { echo "no usable AWS credentials"; exit 1; }
[ "$have" = "$want" ] || { echo "ambient AWS credentials are for account $have, but you are setting up CCM for $want - stop and fix this"; exit 1; }
echo "AWS credentials match account $want"If it fails, stop and have the user switch to credentials for the selected account before continuing.
CUR 2.0 - if aws CLI is available (command -v aws >/dev/null 2>&1 succeeds), list the exports:
aws bcm-data-exports list-exports --region us-east-1 \
--query "Exports[].{Name:ExportName,Arn:ExportArn}" --output tableThen pull the destination for the one they pick:
aws bcm-data-exports get-export --region us-east-1 --export-arn "<EXPORT_ARN>" \
--query "Export.DestinationConfigurations.S3Destination.{S3Bucket:S3Bucket,S3Prefix:S3Prefix,S3Region:S3Region}" \
--output jsonBCM Data Exports is only reachable in us-east-1 - always pass --region us-east-1 regardless of
where the bucket lives. If list-exports returns an empty array, the account has no CUR 2.0 exports;
offer to create one instead, or to switch to Legacy CUR.
Legacy CUR - if aws CLI is available, list the report names so they can pick:
aws cur describe-report-definitions --region us-east-1 --query "ReportDefinitions[].ReportName" --output tableLike BCM Data Exports, the Legacy CUR API is only reachable in us-east-1, so always pass
--region us-east-1. If the command fails, show the user the error (usually missing
cur:DescribeReportDefinitions permission) rather than treating it as “no reports”; only an empty
result means the account has no Legacy CURs.
Then, once they confirm the report name, pull the full definition:
aws cur describe-report-definitions --region us-east-1 \
--query "ReportDefinitions[?ReportName=='<REPORT_NAME>'].{ReportName:ReportName,S3Bucket:S3Bucket,S3Prefix:S3Prefix,S3Region:S3Region}" \
--output jsonIf the describe call returns an empty array, the report name does not exist in this account - go back and have the user pick again from the list.
Either way, record the returned values as <EXPORT_NAME>, <BUCKET_NAME>, <EXPORT_PATH_PREFIX>,
and <BUCKET_REGION>.
If the aws CLI is not available, ask the user to look up their report in the AWS console -
Billing and Cost Management → Data Exports (https://console.aws.amazon.com/billing/home#/exports (opens in a new tab))
for CUR 2.0, or https://console.aws.amazon.com/billing/home#/reports (opens in a new tab) for a Legacy CUR - and provide
the four fields directly:
<EXPORT_NAME>)<BUCKET_NAME>)<EXPORT_PATH_PREFIX>)<BUCKET_REGION>)When a report already exists and the CLI was used to fetch these fields, reuse the fetched values; request them only when falling back because the CLI is missing.
If they chose to create a new report in the previous step, ask if they want to create a new S3 bucket or use an existing one.
If they chose to use an existing report, skip this question. An existing report already has its S3 bucket baked into its destination configuration - you cannot pair it with a new bucket. The bucket name, region, and prefix are already populated from the lookup in step 2.
Existing bucket + new report: check the bucket policy. AWS only delivers reports to a bucket
whose policy lets the billing services write to it, and an ordinary existing bucket usually has no
such statement. First ask for the existing bucket’s name and region and record them as
<BUCKET_NAME> and <BUCKET_REGION> (question 4 then reuses them). With the aws CLI, read the
current policy:
aws s3api get-bucket-policy --bucket "<BUCKET_NAME>" --region "<BUCKET_REGION>" --query Policy --output textA NoSuchBucketPolicy error means the bucket has no policy yet. Any other error (access denied, no
such bucket) is not “no policy”: show it to the user and stop until it is resolved. Without the CLI,
ask the user to copy the policy from the bucket’s Permissions tab in the S3 console (or confirm it
has none).
If the policy already allows billingreports.amazonaws.com and bcm-data-exports.amazonaws.com the
s3:PutObject, s3:GetBucketPolicy, and s3:GetBucketAcl actions on this bucket for account
<AWS_ACCOUNT_ID>, no change is needed. Otherwise Phase 3 adds the delivery statement with the
existing-bucket variant of Section 1b, which keeps every statement already in the policy. Keep the
current policy JSON exactly as returned (or note that there is none); Phase 3 saves it to a file for
Terraform to read. Tell the user before Phase 3 that their bucket policy will be updated:
Bucket
<BUCKET_NAME>doesn’t yet allow AWS billing to deliver reports to it. I’ll add a statement grantingbillingreports.amazonaws.comandbcm-data-exports.amazonaws.comwrite access for your account only, and keep all of the bucket’s existing policy statements. OK to proceed?
For a new report, collect these values from the user (use defaults where possible). If the user picked an existing bucket, the bucket name and region were already collected in question 3.
| Field | Description | Default |
|---|---|---|
| Bucket Name | S3 bucket for CUR data | dd-cost-and-usage-reports-<AWS_ACCOUNT_ID> |
| Bucket Region | AWS region for the bucket | us-east-1 |
| Export Path Prefix | S3 key prefix for reports | dd-reports |
| Export Name | Name of the report | cost-and-usage-report |
For an existing report, these values come from the lookup in step 2, so reuse them rather than requesting them a second time. Confirm them back to the user in a short summary before proceeding, so they can catch a mistake before Terraform runs.
Validate (new report only):
! - _ . * ' ( )./, and must not end with /.
A / in the middle is fine (cost/hourly). Datadog rejects the other forms.Before generating new configuration, check if the user already has a Terraform project:
find . -maxdepth 1 -type f \( -name '*.tf' -o -name '*.tf.json' -o -name 'terraform.tfstate' \) -printIf existing .tf files are found:
Read them to understand what providers and resources are already configured.
If a datadog provider and aws provider already exist, do not create duplicates.
Only add the new resources needed for CCM.
If Section 1b is needed for an existing bucket, check whether the project already manages that bucket’s policy before emitting it:
grep -rnE --include='*.tf' --include='*.tf.json' 'resource "aws_s3_bucket_policy"|resource "aws_s3_bucket"|<BUCKET_NAME>' .Read each aws_s3_bucket_policy the search finds, and any aws_s3_bucket for <BUCKET_NAME> that
sets an inline policy. If one of them targets <BUCKET_NAME>, add the delivery statement to it
as references/terraform.md (Section 1b) describes, instead of creating a second policy resource.
Use the snapshot-based Section 1b resource only when nothing in the project owns the policy.
If the user has a modular layout, create a new file like datadog-ccm.tf for the CCM resources.
If no existing Terraform is found, generate a standalone configuration. You will need the
Datadog and AWS providers set up - if the user just completed dd-aws-integration, those
providers already exist in their Terraform project. Reuse them.
Build the Terraform configuration from the following sections based on what the user needs. Substitute all placeholder values with the actual values collected in Phase 2.
The HCL for every section, together with the rules that go with it, is in
references/terraform.md. Read it now, and read each section’s notes as well as its HCL - they
carry the settings Datadog validates the export against, the exact 116-column query_statement, the
BILLING_VIEW_ARN requirement, the optional CUR 2.0 columns, and the role-name rule. The sections are:
| Section | Contents | Included when |
|---|---|---|
| 1a | S3 bucket | Creating a new bucket |
| 1b | Bucket policy for report delivery | Creating a new bucket, or a new report in an existing bucket whose policy lacks the delivery statement |
| 2a | Legacy CUR definition (aws_cur_report_definition) | <REPORT_TYPE> is legacy and creating a new report |
| 2b | CUR 2.0 export (aws_bcmdataexports_export) | <REPORT_TYPE> is cur2 and creating a new report |
| 3 | IAM policy (DatadogCURAccess) | Always |
| 4 | Policy attachment to the Datadog integration role | Always |
| 5a | Datadog Legacy CUR config (datadog_aws_cur_config) | <REPORT_TYPE> is legacy |
| 5b | Datadog CUR 2.0 config (datadog_integration_aws_account_ccm_config) | <REPORT_TYPE> is cur2 |
Assemble the sections based on the user’s choices. Section 2 and Section 5 pick their variant from
<REPORT_TYPE>; Sections 1, 3, and 4 are the same either way.
| Scenario | CUR 2.0 sections | Legacy CUR sections |
|---|---|---|
| New bucket + new report | 1a + 1b (new bucket) + 2b + 3 + 4 + 5b | 1a + 1b (new bucket) + 2a (new bucket) + 3 + 4 + 5a (new report) |
| Existing bucket + new report | 1b (existing bucket, only if needed) + 2b + 3 + 4 + 5b | 1b (existing bucket, only if needed) + 2a (existing bucket) + 3 + 4 + 5a (new report) |
| Existing bucket + existing report | 3 + 4 + 5b | 3 + 4 + 5a (existing report) |
Whenever Section 1b is emitted, the report in Section 2 must depend on aws_s3_bucket_policy.cur_bucket
(the notes in each section say how), because AWS validates the bucket policy when the report is created.
When the delivery statement went into a policy resource the project already had, the report depends on
that resource instead.
Replace all <PLACEHOLDER> values with the actual collected values.
Ensure the user has AWS credentials configured for the account selected in Phase 1. If the
aws CLI is available, rerun the account check from Phase 2, question 2 now. Either way, the AWS
provider must carry allowed_account_ids = ["<AWS_ACCOUNT_ID>"] (below), so Terraform refuses to
plan or apply against any other account.
If providers are not already configured (standalone setup), ensure the Datadog provider is present and pointed at the user’s site:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.47.0"
}
datadog = {
source = "DataDog/datadog"
version = ">= 4.11.0"
}
}
}
provider "aws" {
region = "us-east-1"
allowed_account_ids = ["<AWS_ACCOUNT_ID>"]
}
provider "datadog" {
api_url = "https://api.<DD_SITE>/"
}Substitute <DD_SITE> with the site confirmed in Phase 0. The provider reads the keys from
DD_API_KEY / DD_APP_KEY, but it does not read DD_SITE: without api_url it talks to US1
and fails on every other site. When reusing an existing datadog provider, check that its
api_url matches https://api.<DD_SITE>/ (or that DD_HOST is set to it); when reusing an
existing aws provider, add allowed_account_ids = ["<AWS_ACCOUNT_ID>"] to it, or confirm it
already targets the selected account.
The export resources must be created in us-east-1 (see the note after these steps), so the
standalone provider uses that region, and the Section 2 export resource always stays on it. The
bucket resources are different: S3 sends bucket and bucket policy calls to the bucket’s own region.
Whenever <BUCKET_REGION> is not us-east-1, for a new bucket or an existing one, add a second
aws provider for the bucket region with the same allowed_account_ids:
provider "aws" {
alias = "bucket_region"
region = "<BUCKET_REGION>"
allowed_account_ids = ["<AWS_ACCOUNT_ID>"]
}Then set provider = aws.bucket_region on the Section 1b resource, and on Section 1a when it is
emitted. Without it, the bucket policy is written through the us-east-1 provider and can fail
before Terraform reaches the report. The same applies when reusing an existing aws provider
whose region differs from <BUCKET_REGION>. When the existing provider authenticates with
profile, shared_credentials_files, assume_role or similar settings, copy those settings into
the bucket_region alias too: an alias does not inherit them, and without them it falls back to
the ambient credentials. If the delivery statement went into a policy resource
the project already had, leave that resource on the provider it already uses.
Both floors exist for CUR 2.0, and both matter - the resources it needs did not exist in earlier provider releases:
| Provider | Minimum | Resource introduced there |
|---|---|---|
hashicorp/aws | >= 5.47.0 | aws_bcmdataexports_export |
DataDog/datadog | >= 4.11.0 | datadog_integration_aws_account_ccm_config |
If the user has an existing Terraform project pinning either provider below these versions,
raise the pin and run terraform init -upgrade. A project pinned below aws 5.47.0 fails at
terraform init or plan with an unsupported-resource error for aws_bcmdataexports_export,
which is easy to misread as a typo in the generated config. Legacy CUR has neither floor -
aws_cur_report_definition and datadog_aws_cur_config are both long-standing - so switching
to Legacy CUR is a valid escape if the user cannot move their pins.
Export the Datadog credentials so the provider picks them up, and confirm they are set. Do not
pass them with -var: this configuration declares no such variables, so -var="datadog_api_key=..."
fails with Value for undeclared variable before any plan is produced. Because a fresh shell may
not keep these exports, the loader is repeated in the plan and apply blocks below as well.
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
export DD_API_KEY DD_APP_KEYRun terraform init to install providers.
Save the plan to a file and show it to the user for review:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
terraform plan -out=tfplan \
|| { echo "terraform plan FAILED - fix the error and re-plan; do NOT apply"; exit 1; }Stop here and wait for the user to approve the plan before applying it. If the plan changes an
existing datadog_integration_aws_account so that extended_collection becomes false, the
project is undoing the Phase 1 change: set it to true in that resource and re-plan first.
Apply that saved plan once the user confirms. Applying the file is what makes the approval
meaningful - a bare terraform apply re-plans, so the user could approve one plan and get another:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
trap 'rm -f tfplan' EXIT HUP INT TERM
terraform apply tfplan \
|| { echo "terraform apply FAILED - do NOT verify or report success"; exit 1; }Never pass -auto-approve, and never apply without showing the saved plan first.
Important: Both export APIs are only available in us-east-1 - aws_cur_report_definition for
Legacy CUR and aws_bcmdataexports_export for CUR 2.0. The S3 bucket itself can live in any region;
only the export resource is constrained. If the user’s default AWS provider region is different,
they may need a provider alias or to set the region explicitly.
After terraform apply completes successfully, verify that the config was registered with Datadog.
Read the endpoint matching <REPORT_TYPE>.
CUR 2.0:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -w '\nHTTP_STATUS:%{http_code}\n' -X GET "https://api.${DD_SITE}/api/v2/integration/aws/accounts/<AWS_ACCOUNT_CONFIG_ID>/ccm_config"Check that HTTP_STATUS is 200 and that data.attributes.data_export_configs contains an entry
whose report_name, report_prefix, bucket_name, and bucket_region match what was just created,
with report_type of CUR2.0.
Legacy CUR:
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY" |
curl -H @- -s -w '\nHTTP_STATUS:%{http_code}\n' -X GET "https://api.${DD_SITE}/api/v2/cost/aws_cur_config"Check that HTTP_STATUS is 200 and that the response includes a config matching the account ID
and bucket just created.
Resource collection, both formats. Re-run the account lookup from “Finding an Account That
Isn’t in the List” in Phase 1 and check that resources_config.extended_collection is still true.
If it is false, a Terraform-managed datadog_integration_aws_account has switched it back off: set
extended_collection = true in that resource and re-plan before reporting completion.
CUR 2.0 support is still rolling out. If the datadog_integration_aws_account_ccm_config apply or
the verification read fails with 403 or 404 - as opposed to a validation error naming a specific
export setting - the org may not have CUR 2.0 enabled yet. Don’t retry the same apply. Tell the user
plainly and offer the fallback:
Datadog returned when registering the CUR 2.0 config, which usually means CUR 2.0 isn’t enabled for this organization yet. Two options:
- Set this up with a Legacy CUR instead - fully supported, and I can do it now.
- Leave it here and contact Datadog support to have CUR 2.0 enabled.
If they pick (1), return to Phase 2 with <REPORT_TYPE> set to legacy. The bucket, bucket policy,
IAM policy, and role attachment from this run are all reusable - only Sections 2 and 5 need to be
replaced, so don’t tear down and rebuild what already applied cleanly.
A validation error that does name a specific setting (missing columns, wrong granularity, refresh
cadence, overwrite mode, or file format) is a different problem: the export doesn’t meet the
requirements in Section 2b of references/terraform.md. Fix the export to match that table and
re-apply.
Send the completion callback below only when Phase 4 passed: the CUR read returned 200 with a
config matching the export just created, and extended_collection is true. If verification failed,
or you are on the 403/404 fallback path above, do not report success and do not send the callback.
After verification, make one best-effort completion callback so the Datadog UI can finish this flow.
DD_WORKFLOW_ID is the UUID of the Datadog user the credentials belong to, which is how the UI
correlates this callback. Resolve it and post in one command: many agent runtimes start a fresh
shell per command, so a variable assigned in an earlier one would be empty here.
Never print the current_user response: it also carries the user’s name, handle, and email. If no
JSON parser is available, skip this callback rather than installing one or blocking setup.
for f in .env.local .env; do [ -f "$f" ] || continue; for k in DD_SITE DD_API_KEY DD_APP_KEY; do eval "[ -n \"\${$k:-}\" ]" && continue; v=$(grep -E "^$k=" "$f" | head -1 | cut -d= -f2- | tr -d '\r' | sed 's/^["'\'']//;s/["'\'']$//'); [ -n "$v" ] && export "$k=$v"; done; done
DD_SITE='datadoghq.com'; export DD_SITE # <- replace with the site confirmed in Phase 0.
# Explicit assignment, not ':=': a wrong non-empty DD_SITE in .env would otherwise survive.
: "${DD_API_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"; : "${DD_APP_KEY:?not set - run dd-account-setup (commercial sites) or supply it directly (government sites)}"
dd_headers() { printf 'DD-API-KEY: %s\nDD-APPLICATION-KEY: %s\n' "$DD_API_KEY" "$DD_APP_KEY"; }
DD_WORKFLOW_ID="$(dd_headers | curl -H @- -fsS --connect-timeout 5 --max-time 20 \
"https://api.${DD_SITE}/api/v2/current_user" | jq -er '.data.id')" \
&& dd_headers | curl -H @- -sS --connect-timeout 5 --max-time 20 -X POST \
"https://api.${DD_SITE}/api/unstable/integration/aws/workflow/agentic_ccm" \
-H "Content-Type: application/vnd.api+json" \
-d '{"data":{"type":"integration_setup_status","id":"'"${DD_WORKFLOW_ID}"'","attributes":{"status":"finished","step":"setup_complete"}}}'Do not retry or block setup if this callback fails.
Summarize for the user what was set up: the AWS account, the report format (CUR 2.0 or
Legacy CUR), the export name, bucket, prefix, and region, and whether resource collection was
enabled during this run.
Congratulate the user and let them know:
Provide these links (construct them as <APP_BASE>/..., using the App URL from Phase 0):
<APP_BASE>/cost - view cost breakdowns, allocations, and recommendations.<APP_BASE>/cost/overview - high-level cost overview across all providers.<APP_BASE>/cost/analytics - drill down into cost trends by service, team, or tag.Suggested next steps:
<APP_BASE>/monitors/create to alert on cost anomalies or budget thresholds.cur:PutReportDefinition for Legacy CUR, bcm-data-exports:CreateExport for CUR 2.0).us-east-1, for both formats.DataDog/datadog). CUR 2.0 additionally needs datadog >= 4.11.0 and hashicorp/aws >= 5.47.0; Legacy CUR has no version floor.terraform apply without showing the plan to the user first.The 116-column SELECT in Section 2b mirrors the default column list that Datadog’s CCM backend
validates a CUR 2.0 export against. Copy it exactly as written; do not rebuild it from the AWS column
reference or from memory. If Datadog’s validation starts reporting a missing column, this skill’s
list needs updating to match. AWS only ever adds columns to CUR 2.0, so an export carrying extra columns is fine - a
missing one is what fails.
Set up Datadog Cloud Cost Management for AWS by configuring a Cost and Usage Report - either CUR 2.0 (recommended) or a Legacy CUR - and provisioning the S3 bucket, bucket policy, IAM read policy, role attachment, and Datadog registration with Terraform. Use when the user wants to track their AWS cloud costs, set up cost and usage reports, set up Cloud Cost Management (CCM) for AWS, set up CCM with CUR 2.0, or connect an AWS Data Export to Datadog. Does not connect a new AWS account to Datadog and does not set up cost management for other clouds. Requires an existing role-based Datadog AWS integration; run `dd-aws-integration` first if it is missing.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
main, last pushed 6 November 2026.SKILL.md, not by matching a directory convention. 11 distinct layouts observed: agent-observability/*/SKILL.md, */SKILL.md, dd-android-sdk/*/SKILL.md, dd-apm/*/SKILL.md, dd-apm/k8s-ssi/*/SKILL.md, dd-apm/linux-ssi/*/SKILL.md, dd-apps/*/SKILL.md, dd-audit/*/SKILL.md, dd-browser-sdk/*/SKILL.md, dd-security/csm/*/SKILL.md, dd-software-delivery/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by datadog-labs, declaring 1 plugin. It is read for editorial metadata only — never as the skill index, which is always the repository tree./datadog-labs/agent-skills.md, and each skill at its own .md URL.1 file · 20 KB
Everything this skill ships beside its prose. All of it is set here, as a subchapter of skill 39.
Documentation the agent loads on demand, rather than up front.