AWS Bedrock AgentCore FinOps AI agents

AWS Bedrock AgentCore: agent cannot read cost data (AccessDenied or empty results)

Fix a FinOps agent on Bedrock AgentCore that returns AccessDenied or empty results when reading cost data, by diagnosing the Cost Explorer IAM action, the CUR plus Athena query path, and management-versus-member account scope.

Jerzy Kopaczewski ·
A FinOps agent on Amazon Bedrock AgentCore is only as good as its view of the bill. When the agent returns AccessDenied or - more confusingly - a successful call with empty cost data, the problem is almost always on the read path: the Cost Explorer IAM action, the CUR plus Athena query route, or the account the agent is reading from. This runbook separates the three and fixes each.

This runbook covers the read side - the agent cannot see cost data at all. For the write side, when the agent sees the data but is denied acting on a finding, see AWS FinOps agent: AccessDenied on a cost-optimisation action. For where this sits in the agent design, see AWS FinOps agent: cost, function, and runbooks. To design the read layer and account scope, book a consulting session.

Symptoms

The failure shows up in one of two shapes. Identify yours first - a hard denial and an empty-but-successful result have different causes.

# 1. Hard IAM denial on the cost API
An error occurred (AccessDeniedException) when calling the GetCostAndUsage
operation: User: arn:aws:sts::...:assumed-role/agentcore-exec-role is not
authorized to perform: ce:GetCostAndUsage

# 2. Success, but no data (the quietly misleading case)
{ "ResultsByTime": [] }
# ...or an Athena query over the CUR returning zero rows / "table not found".

Observable impact:

  • The agent runs, but every cost summary is empty or errors - no trends, no anomalies, no recommendations
  • Other AgentCore functions (runtime, memory, gateway) work; only the cost-data read fails
  • In an Organizations setup, the agent sees its own account’s spend but nothing for the rest of the org

Cause

Cost data on AWS is reachable by two different routes, and a FinOps agent typically uses one or both: the Cost Explorer API (ce:*, aggregated, near-real-time) and the Cost and Usage Report (CUR) queried through Athena (granular, line-item). Each has its own failure mode, and there is a third, account-scope issue that produces the misleading empty result.

  1. Cost Explorer IAM action missing. The execution role lacks ce:GetCostAndUsage (and friends). A hard AccessDeniedException from a ce: call is this.
  2. CUR / Athena path not wired. The agent queries the CUR via Athena, but the CUR is not configured, the Glue catalog table is missing, or the role lacks athena:StartQueryExecution / glue:GetTable / s3:GetObject on the CUR bucket. This shows as “table not found” or an Athena/S3 denial, not a Cost Explorer error.
  3. Wrong account scope (the empty-result trap). The call succeeds but returns nothing because the agent is reading from a member account while the billing data lives in the management (payer) account, or Cost Explorer has simply never been enabled. An empty ResultsByTime with no error is the signature of this.

Common triggers:

  • ce:GetCostAndUsage not on the role - the most common hard denial
  • CUR not set up, or Athena/Glue/S3 permissions missing - the granular path is incomplete
  • Reading a member account - billing visibility lives in the management account or a delegated cost account, not an arbitrary member
  • Cost Explorer never enabled - first-time enablement is required and data backfills with a delay

Fix

Step 1: Tell a hard denial apart from an empty result

Read the response. An AccessDeniedException from a ce: call is a missing IAM action (Step 2). An Athena/Glue/S3 denial or “table not found” is the CUR path (Step 3). A successful call returning empty data is almost always account scope (Step 4), not permissions - do not widen IAM to chase it.

Step 2: Grant the Cost Explorer read actions (scoped to read)

Add the Cost Explorer read actions to the agent’s execution role. These are read-only by nature; keep them that way and do not pair them with any ce: write/modify actions the agent does not need.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "FinOpsAgentCostExplorerRead",
    "Effect": "Allow",
    "Action": [
      "ce:GetCostAndUsage",
      "ce:GetCostForecast",
      "ce:GetDimensionValues",
      "ce:GetAnomalies"
    ],
    "Resource": "*"
  }]
}

ce: actions do not support resource-level ARNs, so Resource: "*" is expected here - the scoping is that these are read-only actions, not that they are resource-bound.

Step 3: Wire the CUR plus Athena path (if the agent uses granular data)

If the agent needs line-item detail, confirm the full chain: a CUR is delivering to an S3 bucket, a Glue catalog table points at it, and the role can run the Athena query and read the results.

  • Confirm the CUR exists and is delivering to its S3 bucket (CUR data lands with a delay of up to 24 hours after first setup)
  • Grant athena:StartQueryExecution, athena:GetQueryResults, glue:GetTable / glue:GetDatabase, and s3:GetObject on the CUR bucket and the Athena query-results bucket
  • Verify the Glue table name the agent queries matches the CUR table actually created

Step 4: Fix account scope - read from the management or delegated cost account

If the call succeeds but returns nothing, the agent is reading from the wrong account. Organisation-wide cost data lives in the management (payer) account or a delegated cost-management account, not an arbitrary member account.

  • Point the agent at the management account, or set up a delegated administrator for Cost Explorer / billing and have the agent assume a role there
  • Confirm Cost Explorer is enabled (first-time enablement is required once per management account) and that enough time has passed for data to populate
  • For a member-account-only agent, accept that it will only ever see that account’s slice - that is correct, not a bug

Validation

Confirm the agent reads non-empty cost data through whichever path it uses.

# Cost Explorer path: run the exact call the agent makes, as its role
aws ce get-cost-and-usage \
  --time-period Start=2026-09-01,End=2026-09-22 \
  --granularity DAILY --metrics "UnblendedCost" \
  --region us-east-1
# Expect: a populated ResultsByTime array, not [] and not AccessDenied.

For the CUR path, run the agent’s Athena query directly and confirm it returns rows. Expected end state: the agent produces a real cost summary with trends and anomalies, reading from the correct (management or delegated) account, with Cost Explorer read-only and no write actions attached.