cube-js/cube · error · UserError

Can't fetch metrics from Dbt Cloud

Error message

Can't fetch metrics from Dbt Cloud: ${res.errors[0].message}

What it means

loadMetricCubesFromDbtCloud executes a GraphQL query against the Dbt Cloud Discovery API (with a bearer token and jobId). If the response body contains a non-empty errors array, the first error message from the API is surfaced wrapped in this UserError.

Solutions

  1. Verify the Dbt Cloud auth token is valid and has access to the job/account (test with a curl to the Discovery API)
  2. Check the jobId passed in options corresponds to an existing, successful job in the same account
  3. Read the inner errors[0].message for the actual API reason (auth, not found, validation) and fix accordingly
  4. Confirm the Dbt Cloud account/environment has the Discovery API/metrics enabled
  5. Upgrade the extension if the Discovery API schema changed

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch('https://cloud.getdbt.com/api/graphql/v2', {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: 'query { __typename }' })
});
if (!res.ok) throw new Error(`Dbt Cloud token/jobId check failed: ${res.status}`);

Type guard

function hasGraphqlErrors(res) {
  return Array.isArray(res?.errors) && res.errors.length > 0;
}

Try / catch

try {
  await dbt.loadMetricCubesFromDbtCloud(projectPath, { jobId, authToken });
} catch (e) {
  if (e.message.startsWith("Can't fetch metrics from Dbt Cloud")) {
    console.error('Dbt Cloud API error:', e.message); // inspect inner API message
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling loadMetricCubesFromDbtCloud with an invalid/expired Dbt Cloud API token, a jobId that doesn't exist or isn't accessible to the token, a GraphQL query error (e.g. metrics not exposed by the Discovery API for that job/account), or Dbt Cloud-side validation errors.

Common situations: Expired or rotated Dbt Cloud service tokens; wrong jobId in configuration; the job's environment lacking Discovery API access or the metrics feature; account/permission changes; Dbt Cloud API deprecations changing the schema the extension queries.

Understand the failure class

Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.

Related errors


AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02). Data as JSON: /api/errors/f29c64559f26f2a8. Report an issue: GitHub.

Appendix: source

Thrown at packages/cubejs-dbt-schema-extension/src/Dbt.ts:173

      const modelName = match[1];
      metricDef.model = modelName.indexOf('.') !== -1 ? modelName : `model.${metricDef.package_name}.${modelName}`;
    });
    return this.loadMetricCubesFromNormalizedManifest(manifest, manifestPath, options);
  }

  public async loadMetricCubesFromDbtCloud(jobId: string | number, authToken: string, options: DbtLoadOptions): Promise<{ [cubeName: string]: any }> {
    const response = await fetch('https://metadata.cloud.getdbt.com/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Authorization: `Bearer ${authToken}`
      },
      body: JSON.stringify({ query: loadModelsQuery, variables: { jobId } })
    });
    const res = <GraphqlResponse>(await response.json());
    if (res.errors && res.errors.length > 0) {
      throw new UserError(`Can't fetch metrics from Dbt Cloud: ${res.errors[0].message}`);
    }
    const manifest: DbtManifest = {
      metrics: res.data.metrics.map(metricDef => ({
        [metricDef.uniqueId]: {
          unique_id: metricDef.uniqueId,
          name: metricDef.name,
          model: metricDef.model.uniqueId,
          // eslint-disable-next-line camelcase
          package_name: metricDef.packageName,
          type: metricDef.type,
          sql: metricDef.sql,
          dimensions: metricDef.dimensions,
          timestamp: metricDef.timestamp,
        }
      })).reduce((a, b) => ({ ...a, ...b }), {}),
      nodes: res.data.models.map(modelDef => ({
        [modelDef.uniqueId]: {
          database: modelDef.database,

View on GitHub (pinned to 7d981676b3)