> ## Documentation Index
> Fetch the complete documentation index at: https://developer.box.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a contract analytics dashboard with Box Query Insights API

> Use the Box Query Insights API to aggregate metadata across Box content and power dashboards with counts, sums, averages, and grouped breakdowns.

Sales, legal, and procurement teams often store contracts in Box with structured metadata, for example, contract type, value, effective dates, and status. Listing every file to compute totals in your application is slow, expensive, and hard to keep current as content grows.

The [Box Query Insights API](https://developer.box.com/guides/query/insights.md) solves this by returning aggregated metrics directly from Box. You filter items with the same predicate syntax as [Box Query API](https://developer.box.com/guides/query/box-query), optionally group results, and request metrics such as `count`, `sum`, `avg`, `min`, and `max` in a single API call.

This tutorial builds a contract analytics dashboard with total, average, minimum, and maximum contract value for sales contracts in a date range.

> **[Clone the working sample](https://github.com/box-community/query-insights-dashboard)**
>
> Prefer to start from running code? The complete app built in this tutorial is on GitHub. Clone it, add your Box credentials, and run.

## Use cases

The Box Query Insights API fits any workflow where you need summary numbers instead of individual file lists.

| Use case | What you compute | Example metrics |
| - | - | - |
| **Executive dashboard** | Portfolio totals and breakdowns by category | `sum` of contract value grouped by `contractType` |
| **Compliance reporting** | How many documents match a template in a period | `count` with `box:item:created_at` filters |
| **Procurement analytics** | Typical and outlier deal sizes | `avg`, `min`, and `max` on a currency field |
| **Department scorecards** | Metrics scoped to a team folder | `ancestors` set to a department root folder |
| **Data quality checks** | Volume of tagged content before a migration | Empty `metrics` object for default `totalResultCount` |

> [!NOTE]
>
> The Box Query Insights API applies filters first, then grouping, then metric calculation. It does not return individual items. When you need file-level results, use [Box Query API](https://developer.box.com/guides/query/box-query) or [metadata queries](https://developer.box.com/guides/metadata/queries) and compute aggregates in your app, or call both APIs for drill-down views.

## Before you start

You need:

* A free [Box developer account](https://account.box.com/signup/developer).
* A [Box application](https://cloud.app.box.com/developers/console) configured with **Client Credentials Grant (CCG)** authentication.
* The **Read and write all files and folders stored in Box** scope enabled on your app.
* **Client Credentials Grant authorized** for your enterprise in the **Developer Console** and [Admin Console](https://app.box.com/master/settings/openbox).
* Python 3.10 or later.
* Admin permissions to create a metadata template, or an existing template you can use instead. See [Create a metadata template](https://developer.box.com/guides/metadata/templates/create.md).

A free Box developer account gives you a Developer Console, API access, and a test environment for building metadata-driven reporting workflows.

[https://account.box.com/signup/developer](https://account.box.com/signup/developer)
[https://account.box.com/developers/console](https://account.box.com/developers/console)

## Build with an agent

The agent expects a metadata template, a contracts folder, and tagged sample files to already exist in Box. If you don't have them yet, complete the first three steps of [Build by hand](#build-by-hand), through **Create a metadata template and sample data**, and then return here.

Get these values from the sample data generated by the script `setup_test_data.py` or from the metadata template and folder you created:

* **Template key** - from the template you created (for example, `sales`)
* **Folder ID** - from the folder you created (for example, `1234567890`)
* **Enterprise ID** - from the [Developer Console](https://app.box.com/developers/console) (for example, `1234567890`)

Choose your stack and copy the prompt. It already lists these as prerequisites, so the agent reads them from environment variables instead of trying to create them. Replace the `<TEMPLATE_KEY>`, `<FOLDER_ID>`, and `<ENTERPRISE_ID>` placeholders if you want the agent to pre-fill `.env`; otherwise leave them and fill `.env` yourself after scaffolding.

```md Python + Script theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use Python as a command-line script. Target Python 3.11+, use pip and a virtual environment. Install exactly one Box package: boxsdk (do NOT also install box-sdk-gen — both ship the box_sdk_gen module and conflict). Import Box APIs with `from box_sdk_gen import ...`. Also install python-dotenv, and pytest for tests.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

Call the API exactly as the tutorial does: client.query.create_query_insight_v2026_r0(query, metrics), passing CreateQueryInsightV2026R0Query (predicate, params, ancestors with QueryAncestorReferenceV2026R0, group_by with QueryInsightsGroupByV2026R0) and a dict of metric aliases to QueryInsightsMetricDefinitionV2026R0 (its type field takes the QueryInsightsMetricDefinitionV2026R0TypeField enum). The method returns a typed QueryInsightsV2026R0 result, so read entry.type, entry.key, and entry.metrics[alias].values off that model rather than parsing raw JSON. Compare entry.type against QueryInsightEntryV2026R0TypeField. Errors raise BoxAPIError, which carries the status as error.response_info.status_code and the Box code as error.response_info.code. In tests, turn recorded payloads into typed models with box_sdk_gen.deserialize(payload, QueryInsightsV2026R0) instead of hand-building objects; if you also need to raise a BoxAPIError, its RequestInfo and ResponseInfo live in box_sdk_gen.box.errors. Confirm the symbol names against the installed SDK's query manager and schema modules before writing code. Build the client with BoxCCGAuth and CCGConfig as the tutorial shows. Print a console report; do not add a web server.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- box_client.py (CCG client), reports.py (count, value statistics, and grouped queries with response parsing), dashboard.py that prints the report, and pytest.ini with pythonpath = .
- a flat script layout matching the tutorial (no unnecessary packages)
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Add pytest.ini with pythonpath = . so pytest can import reports and box_client from the project root. Without it, pytest fails with ModuleNotFoundError: No module named 'reports'
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md Python + Flask theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use Python with Flask. Target Python 3.11+, use pip and a virtual environment. Install exactly one Box package: boxsdk (do NOT also install box-sdk-gen — both ship the box_sdk_gen module and conflict). Import Box APIs with `from box_sdk_gen import ...`. Also install flask, python-dotenv, and pytest for tests.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

Call the API exactly as the tutorial does: client.query.create_query_insight_v2026_r0(query, metrics), passing CreateQueryInsightV2026R0Query (predicate, params, ancestors with QueryAncestorReferenceV2026R0, group_by with QueryInsightsGroupByV2026R0) and a dict of metric aliases to QueryInsightsMetricDefinitionV2026R0 (its type field takes the QueryInsightsMetricDefinitionV2026R0TypeField enum). The method returns a typed QueryInsightsV2026R0 result, so read entry.type, entry.key, and entry.metrics[alias].values off that model rather than parsing raw JSON. Compare entry.type against QueryInsightEntryV2026R0TypeField. Errors raise BoxAPIError, which carries the status as error.response_info.status_code and the Box code as error.response_info.code. In tests, turn recorded payloads into typed models with box_sdk_gen.deserialize(payload, QueryInsightsV2026R0) instead of hand-building objects; if you also need to raise a BoxAPIError, its RequestInfo and ResponseInfo live in box_sdk_gen.box.errors. Confirm the symbol names against the installed SDK's query manager and schema modules before writing code. Build the client with BoxCCGAuth and CCGConfig as the tutorial shows. Add only a thin Flask layer that returns the aggregates as JSON, and listen on port 5000 so the endpoints can be tested with curl.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- box_client.py, reports.py (the Box query logic), app.py exposing GET /dashboard plus GET /healthz, and pytest.ini with pythonpath = .
- a flat Flask layout; keep the Box modules separate from the HTTP layer
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Add pytest.ini with pythonpath = . so pytest can import reports and box_client from the project root. Without it, pytest fails with ModuleNotFoundError: No module named 'reports'
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md Python + FastAPI theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use Python with FastAPI. Target Python 3.11+, use pip and a virtual environment. Install exactly one Box package: boxsdk (do NOT also install box-sdk-gen — both ship the box_sdk_gen module and conflict). Import Box APIs with `from box_sdk_gen import ...`. Also install fastapi, uvicorn, python-dotenv, and pytest for tests.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

Call the API exactly as the tutorial does: client.query.create_query_insight_v2026_r0(query, metrics), passing CreateQueryInsightV2026R0Query (predicate, params, ancestors with QueryAncestorReferenceV2026R0, group_by with QueryInsightsGroupByV2026R0) and a dict of metric aliases to QueryInsightsMetricDefinitionV2026R0 (its type field takes the QueryInsightsMetricDefinitionV2026R0TypeField enum). The method returns a typed QueryInsightsV2026R0 result, so read entry.type, entry.key, and entry.metrics[alias].values off that model rather than parsing raw JSON. Compare entry.type against QueryInsightEntryV2026R0TypeField. Errors raise BoxAPIError, which carries the status as error.response_info.status_code and the Box code as error.response_info.code. In tests, turn recorded payloads into typed models with box_sdk_gen.deserialize(payload, QueryInsightsV2026R0) instead of hand-building objects; if you also need to raise a BoxAPIError, its RequestInfo and ResponseInfo live in box_sdk_gen.box.errors. Confirm the symbol names against the installed SDK's query manager and schema modules before writing code. Build the client with BoxCCGAuth and CCGConfig as the tutorial shows. Adapt the HTTP layer to FastAPI and uvicorn. The Box Python SDK is synchronous — use plain def route handlers (or run the sync Box calls in a threadpool); do not await sync SDK methods. Listen on port 5000 so the endpoints can be tested with curl.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- box_client.py, reports.py (the Box query logic), a FastAPI app module exposing GET /dashboard plus GET /healthz with Pydantic response models, and pytest.ini with pythonpath = .
- an idiomatic FastAPI layout; keep the Box modules separate from the HTTP layer
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Add pytest.ini with pythonpath = . so pytest can import reports and box_client from the project root. Without it, pytest fails with ModuleNotFoundError: No module named 'reports'
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md TypeScript + Express theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use TypeScript with Express on Node.js 20+. Use npm and exactly one Box package: box-node-sdk@10 (do NOT also install box-typescript-sdk-gen). Import BoxClient, BoxCcgAuth, CcgConfig, and BoxApiError from box-node-sdk. Import Query Insights types and deserializeQueryInsightsV2026R0 from box-node-sdk/schemas — they are not on the package root. Also install express, dotenv, tsx (or tsc), and a test runner such as node:test or vitest.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the TypeScript equivalent of the same call. Call await client.query.createQueryInsightV2026R0({ query, metrics }), where query holds predicate, params, ancestors, and groupBy — note the camelCase groupBy and bucketLimit, unlike the snake_case JSON body. Import BoxClient, BoxCcgAuth, CcgConfig, and BoxApiError from box-node-sdk. Import QueryInsightsV2026R0, QueryInsightsMetricDefinitionV2026R0, QueryInsightsGroupByV2026R0, QueryAncestorReferenceV2026R0, and deserializeQueryInsightsV2026R0 from box-node-sdk/schemas — the package root does not re-export those symbols. Build CcgConfig with camelCase clientId, clientSecret, and enterpriseId. The method returns a typed QueryInsightsV2026R0 result, so read entry.type, entry.key, and entry.metrics[alias].values off that model rather than parsing raw JSON. Errors are BoxApiError; read error.responseInfo.statusCode and error.responseInfo.code. In tests, turn recorded payloads into typed models with deserializeQueryInsightsV2026R0(payload) instead of hand-building objects. Confirm the schema and manager names against the installed SDK's typings before writing code. Build the client with BoxCcgAuth and CcgConfig using the enterprise ID. Use tsconfig module ES2022, moduleResolution bundler, esModuleInterop, and strict. Do not set moduleResolution to node — TypeScript 7 removed it (TS5108). Run with npx tsx app.ts. Listen on port 5000 so the endpoints can be tested with curl.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- boxClient.ts, reports.ts (count, value statistics, and grouped queries with response parsing), app.ts exposing GET /dashboard plus GET /healthz, and tsconfig.json
- a flat Express + TypeScript layout; keep the Box modules separate from the HTTP layer
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Add a test script such as tsx --test tests/*.test.ts so local modules resolve from the project root without extra NODE_PATH flags. If you keep CommonJS output, resolve fixture paths with __dirname, not import.meta.url. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md TypeScript + Fastify theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use TypeScript with Fastify on Node.js 20+. Use npm and exactly one Box package: box-node-sdk@10 (do NOT also install box-typescript-sdk-gen). Import BoxClient, BoxCcgAuth, CcgConfig, and BoxApiError from box-node-sdk. Import Query Insights types and deserializeQueryInsightsV2026R0 from box-node-sdk/schemas — they are not on the package root. Also install fastify, dotenv, tsx (or tsc), and a test runner such as node:test or vitest.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the TypeScript equivalent of the same call. Call await client.query.createQueryInsightV2026R0({ query, metrics }), where query holds predicate, params, ancestors, and groupBy — note the camelCase groupBy and bucketLimit, unlike the snake_case JSON body. Import BoxClient, BoxCcgAuth, CcgConfig, and BoxApiError from box-node-sdk. Import QueryInsightsV2026R0, QueryInsightsMetricDefinitionV2026R0, QueryInsightsGroupByV2026R0, QueryAncestorReferenceV2026R0, and deserializeQueryInsightsV2026R0 from box-node-sdk/schemas — the package root does not re-export those symbols. Build CcgConfig with camelCase clientId, clientSecret, and enterpriseId. The method returns a typed QueryInsightsV2026R0 result, so read entry.type, entry.key, and entry.metrics[alias].values off that model rather than parsing raw JSON. Errors are BoxApiError; read error.responseInfo.statusCode and error.responseInfo.code. In tests, turn recorded payloads into typed models with deserializeQueryInsightsV2026R0(payload) instead of hand-building objects. Confirm the schema and manager names against the installed SDK's typings before writing code. Build the client with BoxCcgAuth and CcgConfig using the enterprise ID. Use tsconfig module ES2022, moduleResolution bundler, esModuleInterop, and strict. Do not set moduleResolution to node — TypeScript 7 removed it (TS5108). Keep the Box modules framework-agnostic and adapt only the HTTP layer to Fastify. Listen on port 5000 so the endpoints can be tested with curl.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- boxClient.ts, reports.ts (the Box query logic), a Fastify app module exposing GET /dashboard plus GET /healthz, and tsconfig.json
- an idiomatic Fastify + TypeScript layout; keep the Box modules separate from the HTTP layer
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Add a test script such as tsx --test tests/*.test.ts so local modules resolve from the project root without extra NODE_PATH flags. If you keep CommonJS output, resolve fixture paths with __dirname, not import.meta.url. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md Node + Express theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use Node.js 20+ with JavaScript (not TypeScript) and Express. Use npm and exactly one Box package: box-node-sdk@10 (do NOT also install box-typescript-sdk-gen). Load BoxClient, BoxCcgAuth, CcgConfig, and BoxApiError with require("box-node-sdk"). Also install express, dotenv, and a test runner such as node:test.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the Node.js equivalent of the same call. Call await client.query.createQueryInsightV2026R0({ query, metrics }), where query holds predicate, params, ancestors, and groupBy — note the camelCase groupBy and bucketLimit, unlike the snake_case JSON body. npm init defaults to CommonJS: write const { BoxClient, BoxCcgAuth, CcgConfig, BoxApiError } = require("box-node-sdk") and const { deserializeQueryInsightsV2026R0 } = require("box-node-sdk/schemas"). Do not write import statements unless you also set "type": "module" in package.json — otherwise Node throws SyntaxError: Cannot use import statement outside a module. The package root does not re-export Query Insights helpers. Build CcgConfig with camelCase clientId, clientSecret, and enterpriseId. Read entry.type, entry.key, and entry.metrics[alias].values off the returned model rather than parsing raw JSON. Errors are BoxApiError; read error.responseInfo.statusCode and error.responseInfo.code. In tests, turn recorded payloads into models with deserializeQueryInsightsV2026R0(payload). Confirm the symbol names against the installed SDK before writing code. Build the client with BoxCcgAuth and CcgConfig using the enterprise ID. Write plain JavaScript with JSDoc comments instead of type annotations, and listen on port 5000 so the endpoints can be tested with curl.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- boxClient.js, reports.js (count, value statistics, and grouped queries with response parsing), and app.js exposing GET /dashboard plus GET /healthz
- a flat Express + JavaScript layout; keep the Box modules separate from the HTTP layer
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Add a test script such as node --test tests/*.test.js so require("../reports") resolves from the tests folder. Use __dirname for fixture paths. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md Node + NestJS theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use Node.js 20+ with NestJS. NestJS projects use TypeScript by default. Use npm and exactly one Box package: box-node-sdk@10 (do NOT also install box-typescript-sdk-gen). Import BoxClient, BoxCcgAuth, CcgConfig, and BoxApiError from box-node-sdk. Import Query Insights types and deserializeQueryInsightsV2026R0 from box-node-sdk/schemas — they are not on the package root. Also install @nestjs/core, @nestjs/common, @nestjs/platform-express, dotenv, and a test runner such as Jest.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the TypeScript equivalent of the same call. Call await client.query.createQueryInsightV2026R0({ query, metrics }), where query holds predicate, params, ancestors, and groupBy — note the camelCase groupBy and bucketLimit, unlike the snake_case JSON body. Import BoxClient, BoxCcgAuth, CcgConfig, and BoxApiError from box-node-sdk. Import QueryInsightsV2026R0, QueryInsightsMetricDefinitionV2026R0, QueryInsightsGroupByV2026R0, QueryAncestorReferenceV2026R0, and deserializeQueryInsightsV2026R0 from box-node-sdk/schemas — the package root does not re-export those symbols. Build CcgConfig with camelCase clientId, clientSecret, and enterpriseId. The method returns a typed QueryInsightsV2026R0 result, so read entry.type, entry.key, and entry.metrics[alias].values off that model rather than parsing raw JSON. Errors are BoxApiError; read error.responseInfo.statusCode and error.responseInfo.code. In tests, turn recorded payloads into typed models with deserializeQueryInsightsV2026R0(payload) instead of hand-building objects. Confirm the schema and manager names against the installed SDK's typings before writing code. Build the client with BoxCcgAuth and CcgConfig using the enterprise ID, and expose it as an injectable provider. Use tsconfig module ES2022, moduleResolution bundler, esModuleInterop, and strict. Do not set moduleResolution to node — TypeScript 7 removed it (TS5108). Listen on port 5000 so the endpoints can be tested with curl.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- a Box client provider, a reports provider (the Box query logic), and a dashboard controller exposing GET /dashboard plus GET /healthz
- an idiomatic NestJS layout (module, controller, providers); keep the Box client and report logic in dedicated providers separate from the HTTP controller
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Configure Jest so Nest providers import without extra path flags. Use moduleResolution bundler so box-node-sdk/schemas resolves. Keep parsing tests as plain unit tests (do not start a Nest testing module that constructs BoxClient) so they pass without BOX_* environment variables. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md Java + Spring Boot theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use Java 17+ with Spring Boot 3. Use Maven or Gradle and exactly one Box artifact: com.box:box-java-sdk version 10 or later (do NOT also add box-java-sdk-gen). Use BoxClient, BoxCCGAuth, CCGConfig, BoxAPIError, and the generated query types from that SDK (packages under com.box.sdkgen.schemas.v2026r0, lowercase r0). Also use spring-boot-starter-web, a test starter such as spring-boot-starter-test, and me.paulschwarz:spring-dotenv so a tutorial .env file is loaded — Spring Boot does not read .env by itself.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the Java equivalent of the same call. Call client.getQuery().createQueryInsightV2026R0(new QueryInsightsRequestBodyV2026R0(query, metrics)). That request body has no builder — pass the query and metrics map to the constructor. Build the query with new QueryInsightsRequestBodyV2026R0QueryField.Builder(predicate).params(...).ancestors(...).groupBy(...).build(). Ancestors are new QueryAncestorReferenceV2026R0(folderId, "folder"). Group-by is new QueryInsightsGroupByV2026R0.Builder(field).bucketLimit(n).build(). Each metric is new QueryInsightsMetricDefinitionV2026R0(QueryInsightsMetricDefinitionV2026R0TypeField.AVG, field) — that class also has no builder. Import these types from their generated packages under com.box.sdkgen.schemas.v2026r0 (lowercase r0), for example com.box.sdkgen.schemas.v2026r0.queryinsightsrequestbodyv2026r0. Entry types are EnumWrapper values: compare entry.getType().getEnumValue() to QueryInsightEntryV2026R0TypeField.OVERALL, GROUP, or OTHER, and read metrics with getMetrics().get(alias).getValues().get(metricType). Build auth with new BoxCCGAuth(new CCGConfig.Builder(clientId, clientSecret).enterpriseId(enterpriseId).build()) and new BoxClient(auth). Errors are BoxAPIError; read getResponseInfo().getStatusCode() and getResponseInfo().getCode(). In tests, deserialize with JsonManager.deserialize(JsonManager.jsonToSerializedData(json), QueryInsightsV2026R0.class) — deserialize takes a JsonNode, not a raw String. Confirm the builder and getter names against the installed SDK before writing code. Build the client with BoxCCGAuth and CCGConfig using the enterprise ID. Catch BoxAPIError and check getResponseInfo().getStatusCode() to distinguish the documented status codes. Adapt the HTTP layer to Spring Web (@RestController) and keep the server on port 5000 (server.port=5000).

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- a Box client configuration/bean, a ReportService with the count, value statistics, and grouped queries, and a dashboard controller exposing GET /dashboard plus GET /healthz
- an idiomatic Spring Boot layout; keep Box client configuration and the report logic in dedicated classes separate from the HTTP controller
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Use the default Maven or Gradle test classpath so ReportService tests compile and run. Keep parsing tests as plain unit tests (do not start a Spring context) so they pass without BOX_* environment variables. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md Java + Quarkus theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use Java 17+ with Quarkus. Use Maven or Gradle and exactly one Box artifact: com.box:box-java-sdk version 10 or later (do NOT also add box-java-sdk-gen). Use BoxClient, BoxCCGAuth, CCGConfig, BoxAPIError, and the generated query types from that SDK (packages under com.box.sdkgen.schemas.v2026r0, lowercase r0). Also use quarkus-rest-jackson (or quarkus-resteasy-jackson) and quarkus-junit5 for tests. Quarkus loads a project .env file natively — keep the tutorial variable names.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the Java equivalent of the same call. Call client.getQuery().createQueryInsightV2026R0(new QueryInsightsRequestBodyV2026R0(query, metrics)). That request body has no builder — pass the query and metrics map to the constructor. Build the query with new QueryInsightsRequestBodyV2026R0QueryField.Builder(predicate).params(...).ancestors(...).groupBy(...).build(). Ancestors are new QueryAncestorReferenceV2026R0(folderId, "folder"). Group-by is new QueryInsightsGroupByV2026R0.Builder(field).bucketLimit(n).build(). Each metric is new QueryInsightsMetricDefinitionV2026R0(QueryInsightsMetricDefinitionV2026R0TypeField.AVG, field) — that class also has no builder. Import these types from their generated packages under com.box.sdkgen.schemas.v2026r0 (lowercase r0), for example com.box.sdkgen.schemas.v2026r0.queryinsightsrequestbodyv2026r0. Entry types are EnumWrapper values: compare entry.getType().getEnumValue() to QueryInsightEntryV2026R0TypeField.OVERALL, GROUP, or OTHER, and read metrics with getMetrics().get(alias).getValues().get(metricType). Build auth with new BoxCCGAuth(new CCGConfig.Builder(clientId, clientSecret).enterpriseId(enterpriseId).build()) and new BoxClient(auth). Errors are BoxAPIError; read getResponseInfo().getStatusCode() and getResponseInfo().getCode(). In tests, deserialize with JsonManager.deserialize(JsonManager.jsonToSerializedData(json), QueryInsightsV2026R0.class) — deserialize takes a JsonNode, not a raw String. Confirm the builder and getter names against the installed SDK before writing code. Build the client with BoxCCGAuth and CCGConfig using the enterprise ID. Catch BoxAPIError and check getResponseInfo().getStatusCode() to distinguish the documented status codes. Adapt the HTTP layer to Quarkus REST and keep the server on port 5000 (quarkus.http.port=5000).

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- a Box client producer/bean, a ReportService with the count, value statistics, and grouped queries, and a JAX-RS resource exposing GET /dashboard plus GET /healthz
- an idiomatic Quarkus layout; keep the Box client and report logic in dedicated beans separate from the JAX-RS resource
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Use the default Maven or Gradle test classpath so ReportService tests compile and run. Keep parsing tests as plain unit tests (do not start a Quarkus test profile that constructs BoxClient) so they pass without BOX_* environment variables. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md .NET + ASP.NET Core theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use .NET 8+ with ASP.NET Core controllers. Use NuGet and exactly one Box package: Box.V2.Core version 10 or later (do NOT also install Box.Sdk.Gen — v10 of Box.V2.Core already ships the generated client). Use BoxClient, BoxCcgAuth, CcgConfig, and BoxApiException from Box.Sdk.Gen. Also add xUnit and DotNetEnv — ASP.NET Core does not load a tutorial .env file by itself.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the .NET equivalent of the same call. Call await client.Query.CreateQueryInsightV2026R0Async(new QueryInsightsRequestBodyV2026R0(query, metrics)). Build the query with new QueryInsightsRequestBodyV2026R0QueryField(predicate) { Parameters = ..., Ancestors = ..., GroupBy = ... }. The predicate params map is Parameters because params is a C# keyword. Group-by is new QueryInsightsGroupByV2026R0(field) { BucketLimit = n }. Each metric is new QueryInsightsMetricDefinitionV2026R0(QueryInsightsMetricDefinitionV2026R0TypeField.Avg, field). Ancestors are new QueryAncestorReferenceV2026R0(folderId, "folder"). Types live in Box.Sdk.Gen and Box.Sdk.Gen.Schemas and ship inside Box.V2.Core — do not add a second Box.Sdk.Gen package. Entry types are StringEnum values: compare entry.Type.Value to QueryInsightEntryV2026R0TypeField.Overall, Group, or Other, and read metrics with entry.Metrics[alias].Values[metricType]. Build auth with new BoxCcgAuth(new CcgConfig(clientId, clientSecret) { EnterpriseId = enterpriseId }) and new BoxClient(auth). The CcgConfig third constructor argument is tokenStorage, not the enterprise ID. Errors are BoxApiException; read ResponseInfo.StatusCode and ResponseInfo.Code. In tests, deserialize with System.Text.Json.JsonSerializer.Deserialize<QueryInsightsV2026R0>(json) — SimpleJsonSerializer is internal. Qualify System.IO.File because Box.Sdk.Gen.Schemas.File conflicts with it. Confirm the property names against the installed SDK before writing code. Build the client with BoxCcgAuth and CcgConfig using the enterprise ID. All SDK calls are async — await them and keep controller actions async. Call DotNetEnv.Env.Load() at startup, bind with builder.WebHost.UseUrls("http://127.0.0.1:5000"), skip UseHttpsRedirection, and run with --no-launch-profile (or edit launchSettings.json). The webapi template otherwise listens on 5020/7060 and enables HTTPS, so curl to port 5000 fails.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- a Box client factory/service, a ReportService with the count, value statistics, and grouped queries, and a dashboard controller exposing GET /dashboard plus GET /healthz
- an idiomatic ASP.NET Core layout; keep the Box client and report logic in dedicated services separate from the HTTP controller
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Reference the app project from the test project so ReportService tests build. Keep parsing tests as plain xUnit tests (do not start a WebApplicationFactory that constructs BoxClient) so they pass without BOX_* environment variables. Copy fixture files to the test output directory. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

```md .NET + Minimal APIs theme={null}
Build a production-minded contract analytics dashboard by following this Box tutorial:
https://developer.box.com/tutorials/query-insights

Use .NET 8+ with ASP.NET Core Minimal APIs. Use NuGet and exactly one Box package: Box.V2.Core version 10 or later (do NOT also install Box.Sdk.Gen — v10 of Box.V2.Core already ships the generated client). Use BoxClient, BoxCcgAuth, CcgConfig, and BoxApiException from Box.Sdk.Gen. Also add xUnit and DotNetEnv — ASP.NET Core does not load a tutorial .env file by itself.

Prerequisites already done in Box (see 'Before you start' in the tutorial).
Do NOT try to create these; read their values from environment variables:
- Enterprise metadata template key: <TEMPLATE_KEY>
  (fields contractType as enum with Sales and Renewal, contractValue as float).
  If a template with that key already exists and contractType is a string, group_by
  returns 400 — use a different TEMPLATE_KEY whose contractType is an enum.
- Contracts folder ID: <FOLDER_ID>. CCG stores this folder in the app service
  account root, not the developer's personal All Files. The service account must
  be able to read it (Viewer or higher). Do not fail the project if the developer
  cannot see the folder in their own Box web app.
- Enterprise ID: <ENTERPRISE_ID>
- CCG app scopes enabled: Read and write all files and folders stored in Box

The tutorial's samples are Python; use the .NET equivalent of the same call. Call await client.Query.CreateQueryInsightV2026R0Async(new QueryInsightsRequestBodyV2026R0(query, metrics)). Build the query with new QueryInsightsRequestBodyV2026R0QueryField(predicate) { Parameters = ..., Ancestors = ..., GroupBy = ... }. The predicate params map is Parameters because params is a C# keyword. Group-by is new QueryInsightsGroupByV2026R0(field) { BucketLimit = n }. Each metric is new QueryInsightsMetricDefinitionV2026R0(QueryInsightsMetricDefinitionV2026R0TypeField.Avg, field). Ancestors are new QueryAncestorReferenceV2026R0(folderId, "folder"). Types live in Box.Sdk.Gen and Box.Sdk.Gen.Schemas and ship inside Box.V2.Core — do not add a second Box.Sdk.Gen package. Entry types are StringEnum values: compare entry.Type.Value to QueryInsightEntryV2026R0TypeField.Overall, Group, or Other, and read metrics with entry.Metrics[alias].Values[metricType]. Build auth with new BoxCcgAuth(new CcgConfig(clientId, clientSecret) { EnterpriseId = enterpriseId }) and new BoxClient(auth). The CcgConfig third constructor argument is tokenStorage, not the enterprise ID. Errors are BoxApiException; read ResponseInfo.StatusCode and ResponseInfo.Code. In tests, deserialize with System.Text.Json.JsonSerializer.Deserialize<QueryInsightsV2026R0>(json) — SimpleJsonSerializer is internal. Qualify System.IO.File because Box.Sdk.Gen.Schemas.File conflicts with it. Confirm the property names against the installed SDK before writing code. Build the client with BoxCcgAuth and CcgConfig using the enterprise ID. All SDK calls are async — await them inside the endpoint delegates. Call DotNetEnv.Env.Load() at startup, bind with builder.WebHost.UseUrls("http://127.0.0.1:5000"), skip UseHttpsRedirection, and run with --no-launch-profile (or edit launchSettings.json). The webapi template otherwise listens on 5020/7060 and enables HTTPS, so curl to port 5000 fails.

The app must:
1. Authenticate to Box with Client Credentials Grant (enterprise / service account).
   Use BOX_ENTERPRISE_ID — do NOT authenticate as a managed user.
2. Call the Box Query Insights API through the SDK's query manager method named above.
   That method targets the v2026.0 API and sends the required box-version: 2026.0 header
   for you. Do not hand-roll the HTTP call, do not set the version header yourself, and do
   not manage access tokens — the SDK's CCG auth handles refresh and retries.
3. Build fully qualified field references from the template key and enterprise ID:
   template ref is enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY> and a field ref is
   enterprise_<ENTERPRISE_ID>:<TEMPLATE_KEY>:<fieldKey>. Read TEMPLATE_REF,
   FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE from the environment when they are set;
   otherwise derive them from the field keys contractType and contractValue.
4. Expose a count query: predicate EXISTS(:templateArg) with params templateArg set to the
   template ref, ancestors set to the folder ID with type folder, and an empty metrics map.
   An empty metrics map returns the default totalResultCount metric.
5. Expose a value statistics query: avg, min, and max metrics on the contract value field,
   filtered by EXISTS(:templateArg) AND box:item:created_at >= :startDate AND
   box:item:created_at < :endDate. Accept the date range as input; use RFC 3339 timestamps.
6. Expose a grouped query for charts: group by the contract type enum field with a bucket
   limit, plus a sum metric on contract value and a count metric on contract type. Do not
   group on a free-text string field — Query Insights returns 400 Invalid query request.
   Present the buckets in the order the API returns them; do not re-sort them client-side,
   because re-sorting would misrepresent which buckets the other rollup excluded.
7. Parse the response by entry type, not by array position. Each entry has a type of
   overall, group, or other:
   - overall: read metric values directly; the key is always empty
   - group: use the first key value as the bucket label
   - other: read the count from the totalCountBeyondTopGroups alias, not your own alias
   Each metric value is nested under values and keyed by the metric type, so an avg metric
   exposes values.avg and a sum metric exposes values.sum. Buckets are always ordered by
   document count descending; that order cannot be customized.
8. Check the documented API limits before sending a request and fail fast with a clear
   message, so a bad request never reaches Box:
   - only one group-by field per request
   - bucket limit defaults to 5, maximum 10
   - maximum 10 metrics per request
   - each metric alias must start with a letter and contain only letters, digits,
     underscores, hyphens, and periods (maximum 256 characters)
   Document, but do not try to validate, the guidance that grouped fields should stay below
   10,000 distinct values — cardinality is a server-side property you cannot check locally.
9. Catch the SDK's Box API error type, read the HTTP status off it, and report 400
   BAD_REQUEST (predicate or param type problem), 403 FORBIDDEN (missing scope or
   inaccessible ancestor), 404 INSTANCE_NOT_FOUND (wrong template ref), and 429
   RATE_LIMIT_EXCEEDED distinctly, so the message tells the developer what to fix. Do not
   add your own retry loop: the SDK already retries 429 and 5xx with backoff and refreshes
   expired tokens, and a second layer would multiply the wait.
10. Fetch each aggregate once per report run and reuse the result rather than calling the
    API again for every panel. Add a short-TTL cache only if your stack serves repeated
    requests (a web layer or a refresh loop); a script that exits after printing does not
    need one. Note in the README that Box Query Insights API reads a metadata index that
    updates shortly after content is tagged, so a metric can return 0 immediately after
    tagging.

Read configuration from the same environment variables the tutorial uses, so a .env file
written for one path works for the other:
BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_ENTERPRISE_ID, TEMPLATE_KEY, FOLDER_ID, and
optionally TEMPLATE_REF, FIELD_CONTRACT_TYPE, and FIELD_CONTRACT_VALUE.

Create a complete runnable project with:
- a Box client factory, a ReportService with the count, value statistics, and grouped queries, and Program.cs with MapGet("/dashboard", ...) plus MapGet("/healthz", ...)
- an idiomatic Minimal APIs layout (MapGet in Program.cs); keep the Box client and report logic in dedicated classes separate from endpoint mapping
- dependency and run commands
- .env.example and .gitignore files
- a sample console or JSON report that shows the total contract count, the average,
  minimum, and maximum contract value, and the top contract types with the total value and
  count of each. The other rollup carries only a count, so leave its value column empty
  rather than reporting a total for it.
- focused unit tests that assert response parsing for the overall, group, and other entry
  types against recorded sample payloads (do not call Box in tests)
- Reference the app project from the test project so ReportService tests build. Keep parsing tests as plain xUnit tests (do not start a WebApplicationFactory that constructs BoxClient) so they pass without BOX_* environment variables. Copy fixture files to the test output directory. Run the tests before you finish
- a README with setup, run, and test steps. State that CCG content lives in the service
  account root, that Query Insights can return 0 until the metadata index updates, and
  that group_by requires an enum contractType

After you write the files, run the test command and fix any import or parse failures
before you stop. Do not call Box from tests. Do not require a filled .env for tests
to pass.

Never hard-code credentials. Add concise comments only where the Box-specific behavior is not obvious. Before writing code, summarize the architecture and list the files you will create.
```

Review generated code before using it in production. Never paste Box credentials into your
coding agent.

When the agent finishes, copy `.env.example` to `.env` and fill in your client ID, client secret, enterprise ID, metadata template key, and folder ID. Use a template whose `contractType` is an enum. Grouping on a free-text string field returns `400 Invalid query request`. A CCG app stores the contracts folder in the service account root, so the folder does not display in your personal **All Files**. Then skip ahead to [Run and verify](#run-and-verify).

Prefer to write the code yourself? See [Build by hand](#build-by-hand) below.

## Build by hand

**1. Set up the development environment**

1. Create a project directory and virtual environment:

```bash theme={null}
mkdir contract-insights && cd contract-insights
python3 -m venv .venv
source .venv/bin/activate
```

2. Install the required packages:

```bash theme={null}
pip install boxsdk python-dotenv
```

3. Create a `.env` file with your application credentials. Fill in the first three Client Credentials Grant values now – you add the folder and template values in the next step:

```bash theme={null}
BOX_CLIENT_ID=your_client_id
BOX_CLIENT_SECRET=your_client_secret
BOX_ENTERPRISE_ID=your_enterprise_id
FOLDER_ID=your_folder_id
TEMPLATE_KEY=sales
TEMPLATE_REF=your_template_ref
FIELD_CONTRACT_TYPE=your_contract_type_field
FIELD_CONTRACT_VALUE=your_contract_value_field
```

> [!WARNING]
>
> Add `.env` to `.gitignore`. Never commit credentials to version control.

**2. Authenticate the Box client**

Create `box_client.py`. Every other module imports `get_box_client` from here, so the credentials live in one place:

```python theme={null}
import os

from box_sdk_gen import BoxCCGAuth, BoxClient, CCGConfig
from dotenv import load_dotenv

load_dotenv()


def get_box_client() -> BoxClient:
    auth = BoxCCGAuth(
        config=CCGConfig(
            client_id=os.environ["BOX_CLIENT_ID"],
            client_secret=os.environ["BOX_CLIENT_SECRET"],
            enterprise_id=os.environ["BOX_ENTERPRISE_ID"],
        )
    )
    return BoxClient(auth=auth)
```

`BoxCCGAuth` fetches and refreshes the access token for you, so you never manage the token endpoint directly.

> [!WARNING]
>
> `BOX_ENTERPRISE_ID` must be the numeric enterprise ID from **Developer Console > Configuration > App Details**. Pasting a client ID or client secret here fails with `invalid_grant: Grant credentials are invalid`.

The remaining steps call `client.query.create_query_insight_v2026_r0`, which sends `POST https://api.box.com/2.0/query_insights` with the `box-version: 2026.0` header. Because the method is generated from the API spec, you pass typed request objects and read a typed `QueryInsightsV2026R0` result instead of assembling JSON by hand.

**3. Create a metadata template and sample data**

Create `setup_test_data.py` to create the `sales` metadata template, a folder, and a few tagged sample contracts. It reuses `get_box_client` from the previous step:

```python theme={null}
import io
import os

from box_sdk_gen import (
    BoxAPIError,
    BoxClient,
    CreateFileMetadataByIdScope,
    CreateFolderParent,
    CreateMetadataTemplateFields,
    CreateMetadataTemplateFieldsOptionsField,
    CreateMetadataTemplateFieldsTypeField,
    GetMetadataTemplateScope,
    UpdateFileMetadataByIdRequestBody,
    UpdateFileMetadataByIdRequestBodyOpField,
    UpdateFileMetadataByIdScope,
    UpdateMetadataTemplateRequestBody,
    UpdateMetadataTemplateRequestBodyOpField,
    UpdateMetadataTemplateScope,
    UploadFileAttributes,
    UploadFileAttributesParentField,
)
from dotenv import load_dotenv

from box_client import get_box_client

load_dotenv()

TEMPLATE_KEY = os.environ.get("TEMPLATE_KEY") or "sales"
ENUM_FIELD_KEY = "contractType"
ENUM_FALLBACK_KEY = "contractTypeEnum"
FOLDER_NAME = "Sales Contracts"
SAMPLE_CONTRACTS = [
    {"name": "Contract 1", "contractType": "Sales", "contractValue": 100000},
    {"name": "Contract 2", "contractType": "Sales", "contractValue": 200000},
    {"name": "Contract 3", "contractType": "Renewal", "contractValue": 150000},
    {"name": "Contract 4", "contractType": "Renewal", "contractValue": 45000},
]


def _enum_field_key(template) -> str | None:
    fields = {field.key: field for field in (template.fields or [])}
    for key in (ENUM_FIELD_KEY, ENUM_FALLBACK_KEY):
        field = fields.get(key)
        if field and str(getattr(field.type, "value", field.type)) == "enum":
            return key
    return None


def _add_enum_field(client: BoxClient) -> None:
    client.metadata_templates.update_metadata_template(
        UpdateMetadataTemplateScope.ENTERPRISE,
        TEMPLATE_KEY,
        [
            UpdateMetadataTemplateRequestBody(
                op=UpdateMetadataTemplateRequestBodyOpField.ADDFIELD,
                data={
                    "type": "enum",
                    "key": ENUM_FALLBACK_KEY,
                    "displayName": "Contract type",
                    "options": [{"key": "Sales"}, {"key": "Renewal"}],
                },
            )
        ],
    )


def ensure_template(client: BoxClient) -> str:
    """Create the template if needed, then return the field to group on."""
    try:
        client.metadata_templates.create_metadata_template(
            scope="enterprise",
            display_name="Sales Contracts",
            template_key=TEMPLATE_KEY,
            fields=[
                CreateMetadataTemplateFields(
                    type=CreateMetadataTemplateFieldsTypeField.ENUM,
                    key=ENUM_FIELD_KEY,
                    display_name="Contract type",
                    options=[
                        CreateMetadataTemplateFieldsOptionsField(key="Sales"),
                        CreateMetadataTemplateFieldsOptionsField(key="Renewal"),
                    ],
                ),
                CreateMetadataTemplateFields(
                    type=CreateMetadataTemplateFieldsTypeField.FLOAT,
                    key="contractValue",
                    display_name="Contract value",
                ),
            ],
        )
        return ENUM_FIELD_KEY
    except BoxAPIError:
        pass

    # The template already exists. Box Query Insights API groups on enum
    # fields only, so reuse an existing enum or add one now. Otherwise the
    # grouped query fails later with 400 Invalid query request.
    template = client.metadata_templates.get_metadata_template(
        GetMetadataTemplateScope.ENTERPRISE, TEMPLATE_KEY
    )
    type_field = _enum_field_key(template)
    if type_field is None:
        _add_enum_field(client)
        type_field = ENUM_FALLBACK_KEY
        print(
            f"'{TEMPLATE_KEY}.{ENUM_FIELD_KEY}' is not an enum. "
            f"Added enum field '{ENUM_FALLBACK_KEY}' to group on."
        )
    elif type_field != ENUM_FIELD_KEY:
        print(
            f"'{TEMPLATE_KEY}.{ENUM_FIELD_KEY}' is not an enum. "
            f"Grouping on existing enum field '{type_field}'."
        )
    return type_field


def ensure_folder(client: BoxClient) -> str:
    for item in client.folders.get_folder_items("0").entries:
        if item.type == "folder" and item.name == FOLDER_NAME:
            return item.id
    folder = client.folders.create_folder(
        name=FOLDER_NAME, parent=CreateFolderParent(id="0")
    )
    return folder.id


def existing_files(client: BoxClient, folder_id: str) -> dict[str, str]:
    files: dict[str, str] = {}
    offset = 0
    limit = 1000
    while True:
        items = client.folders.get_folder_items(
            folder_id, limit=limit, offset=offset
        )
        entries = items.entries or []
        for item in entries:
            if item.type == "file":
                files[item.name] = item.id
        if len(entries) < limit:
            break
        offset += limit
    return files


def apply_metadata(client: BoxClient, file_id: str, payload: dict) -> None:
    try:
        client.file_metadata.create_file_metadata_by_id(
            file_id,
            CreateFileMetadataByIdScope.ENTERPRISE,
            TEMPLATE_KEY,
            payload,
        )
    except BoxAPIError as error:
        if error.response_info.status_code != 409:
            raise
        client.file_metadata.update_file_metadata_by_id(
            file_id,
            UpdateFileMetadataByIdScope.ENTERPRISE,
            TEMPLATE_KEY,
            [
                UpdateFileMetadataByIdRequestBody(
                    op=UpdateFileMetadataByIdRequestBodyOpField.ADD,
                    path=f"/{key}",
                    value=value,
                )
                for key, value in payload.items()
            ],
        )


def _metadata_payload(contract: dict, type_field: str) -> dict:
    payload = {
        type_field: contract["contractType"],
        "contractValue": contract["contractValue"],
    }
    if type_field != ENUM_FIELD_KEY:
        # Keep the original string field populated so both stay in sync.
        payload[ENUM_FIELD_KEY] = contract["contractType"]
    return payload


def upload_contracts(client: BoxClient, folder_id: str, type_field: str) -> None:
    existing = existing_files(client, folder_id)
    for contract in SAMPLE_CONTRACTS:
        filename = f"{contract['name']}.txt"
        payload = _metadata_payload(contract, type_field)
        file_id = existing.get(filename)
        if file_id is None:
            uploaded = client.uploads.upload_file(
                attributes=UploadFileAttributes(
                    name=filename,
                    parent=UploadFileAttributesParentField(id=folder_id),
                ),
                file=io.BytesIO(b"Sample contract for Box Query Insights API."),
            )
            file_id = uploaded.entries[0].id
        apply_metadata(client, file_id, payload)


if __name__ == "__main__":
    client = get_box_client()
    enterprise_id = os.environ["BOX_ENTERPRISE_ID"]

    type_field = ensure_template(client)
    folder_id = ensure_folder(client)
    upload_contracts(client, folder_id, type_field)

    template_ref = f"enterprise_{enterprise_id}:{TEMPLATE_KEY}"
    print("Add these values to your .env file:")
    print(f"FOLDER_ID={folder_id}")
    print(f"TEMPLATE_KEY={TEMPLATE_KEY}")
    print(f"TEMPLATE_REF={template_ref}")
    print(f"FIELD_CONTRACT_TYPE={template_ref}:{type_field}")
    print(f"FIELD_CONTRACT_VALUE={template_ref}:contractValue")
```

Run the script, then copy the printed values into your `.env` file:

```bash theme={null}
python setup_test_data.py
```

Alternatively, you can point the tutorial at an existing template.

The Box Query Insights API can group on enum fields, not free-text string fields:

* `contractType` (enum, for example, Sales and Renewal)
* `contractValue` (float)

The script is safe to run more than once. It reuses the template and folder, skips uploading a sample file whose name already exists, and falls back to a JSON-Patch update when a file already carries the template. Box Query Insights API reads a metadata index that updates shortly after you tag content. If a metric returns 0 right after setup, wait a minute and run the reporting steps again.

> [!WARNING]
>
> If a `sales` template already exists whose `contractType` is a free-text string, the script adds an enum field named `contractTypeEnum` and prints it as your `FIELD_CONTRACT_TYPE`. Copy whichever field key the script prints. Grouping on a string field returns `400 Invalid query request`.

> [!NOTE]
>
> A Client Credentials Grant app acts as a service account with its own folder tree, so `Sales Contracts` does not display in your personal **All Files**. To see it, collaborate yourself on the folder using the service account email from **Developer Console > App Details**, or open the content as an admin in Content Manager.

**4. Count matching contracts**

Create `reports.py` with the queries that power your dashboard. Pass an empty `metrics` map to get the default `totalResultCount` metric across all items that match your filter.

Every metric result nests its value under `values`, keyed by the metric type, so `metric.values[metric.type]` reads the value without hard-coding the type. The remaining steps use the same pattern.

```python theme={null}
from box_sdk_gen import (
    BoxClient,
    CreateQueryInsightV2026R0Query,
    QueryAncestorReferenceV2026R0,
    QueryInsightEntryV2026R0TypeField,
    QueryInsightsGroupByV2026R0,
    QueryInsightsMetricDefinitionV2026R0,
    QueryInsightsMetricDefinitionV2026R0TypeField,
    QueryInsightsV2026R0,
)


def overall_entry(result: QueryInsightsV2026R0):
    return next(
        entry
        for entry in result.insights
        if entry.type == QueryInsightEntryV2026R0TypeField.OVERALL
    )


def count_contracts(client: BoxClient, template_ref: str, folder_id: str) -> float:
    result = client.query.create_query_insight_v2026_r0(
        CreateQueryInsightV2026R0Query(
            predicate="EXISTS(:templateArg)",
            params={"templateArg": template_ref},
            ancestors=[QueryAncestorReferenceV2026R0(id=folder_id, type="folder")],
        ),
        {},
    )
    metric = overall_entry(result).metrics["totalResultCount"]
    return metric.values[metric.type]
```

Equivalent cURL request:

```bash theme={null}
curl -X POST "https://api.box.com/2.0/query_insights" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "box-version: 2026.0" \
  -H "Content-Type: application/json" \
  -d '{
    "query": {
      "predicate": "EXISTS(:templateArg)",
      "params": { "templateArg": "enterprise_12345678:sales" },
      "ancestors": [{ "id": "987654321", "type": "folder" }]
    },
    "metrics": {}
  }'
```

> [!NOTE]
>
> The `box-version: 2026.0` header is required. The SDK method sets it for you, so you only pass it when you call the endpoint directly.

**5. Compute overall contract value statistics**

Add this function to `reports.py`, below `count_contracts`. It requests multiple metrics in one call to populate summary cards – average, minimum, and maximum deal size for contracts created in a date range.

```python theme={null}
def contract_value_stats(
    client: BoxClient,
    template_ref: str,
    folder_id: str,
    value_field: str,
    start_date: str,
    end_date: str,
) -> dict:
    result = client.query.create_query_insight_v2026_r0(
        CreateQueryInsightV2026R0Query(
            predicate=(
                "EXISTS(:templateArg) AND "
                "box:item:created_at >= :startDate AND "
                "box:item:created_at < :endDate"
            ),
            params={
                "templateArg": template_ref,
                "startDate": start_date,
                "endDate": end_date,
            },
            ancestors=[QueryAncestorReferenceV2026R0(id=folder_id, type="folder")],
        ),
        {
            "avgContractValue": QueryInsightsMetricDefinitionV2026R0(
                type=QueryInsightsMetricDefinitionV2026R0TypeField.AVG,
                field=value_field,
            ),
            "minContractValue": QueryInsightsMetricDefinitionV2026R0(
                type=QueryInsightsMetricDefinitionV2026R0TypeField.MIN,
                field=value_field,
            ),
            "maxContractValue": QueryInsightsMetricDefinitionV2026R0(
                type=QueryInsightsMetricDefinitionV2026R0TypeField.MAX,
                field=value_field,
            ),
        },
    )
    return {
        alias: metric.values[metric.type]
        for alias, metric in overall_entry(result).metrics.items()
    }
```

Because each metric result is keyed by its own type, the same `metric.values[metric.type]` lookup reads `avg`, `min`, and `max` without a branch per metric. The underlying response looks like this:

```json theme={null}
{
  "insights": [
    {
      "key": [],
      "type": "overall",
      "metrics": {
        "avgContractValue": { "type": "avg", "values": { "avg": 45055.5 } },
        "minContractValue": { "type": "min", "values": { "min": 22000 } },
        "maxContractValue": { "type": "max", "values": { "max": 75000 } }
      }
    }
  ]
}
```

See [Overall average, minimum, maximum contract value](https://developer.box.com/guides/query/insights.md#overall-average-minimum-maximum-contract-value) for the full request and response.

**6. Group results for dashboard charts**

Add this last function to `reports.py`. Grouping by a metadata field powers bar or pie charts. The API returns the top buckets ordered by document count (descending). Remaining groups are rolled into an `other` entry.

```python theme={null}
def top_contract_types(
    client: BoxClient,
    template_ref: str,
    folder_id: str,
    type_field: str,
    value_field: str,
    bucket_limit: int = 5,
) -> list[dict]:
    result = client.query.create_query_insight_v2026_r0(
        CreateQueryInsightV2026R0Query(
            predicate="EXISTS(:templateArg)",
            params={"templateArg": template_ref},
            ancestors=[QueryAncestorReferenceV2026R0(id=folder_id, type="folder")],
            group_by=[
                QueryInsightsGroupByV2026R0(
                    field=type_field, bucket_limit=bucket_limit
                )
            ],
        ),
        {
            "totalContractValue": QueryInsightsMetricDefinitionV2026R0(
                type=QueryInsightsMetricDefinitionV2026R0TypeField.SUM,
                field=value_field,
            ),
            "countByType": QueryInsightsMetricDefinitionV2026R0(
                type=QueryInsightsMetricDefinitionV2026R0TypeField.COUNT,
                field=type_field,
            ),
        },
    )

    groups = []
    for entry in result.insights:
        if entry.type == QueryInsightEntryV2026R0TypeField.GROUP:
            groups.append(
                {
                    "contract_type": entry.key[0],
                    "total_value": entry.metrics["totalContractValue"].values["sum"],
                    "count": entry.metrics["countByType"].values["count"],
                }
            )
        elif entry.type == QueryInsightEntryV2026R0TypeField.OTHER:
            groups.append(
                {
                    "contract_type": "Other",
                    "count": entry.metrics["totalCountBeyondTopGroups"].values["count"],
                }
            )
    return groups
```

> [!TIP]
>
> Only one `group_by` field is supported per request, and `bucket_limit` defaults to 5 with a maximum of 10. Group on an enum field such as `contractType`. A free-text string field returns `400 Invalid query request`. Avoid grouping on high-cardinality fields – cardinality of 10,000 or above can cause errors.

**7. Combine metrics in a dashboard function**

Create `dashboard.py` to wire the helpers together and print a console report you can adapt for a web dashboard or business intelligence export.

```python theme={null}
import os

from box_client import get_box_client
from reports import contract_value_stats, count_contracts, top_contract_types


def _format_cell(value) -> str:
    if value is None:
        return "—"
    if isinstance(value, float) and value.is_integer():
        value = int(value)
    if isinstance(value, int):
        return f"{value:,}"
    if isinstance(value, float):
        return f"{value:,.2f}"
    return str(value)


def _print_table(headers: list[str], rows: list[list]) -> None:
    str_rows = [[_format_cell(cell) for cell in row] for row in rows]
    widths = [len(header) for header in headers]
    for row in str_rows:
        for index, cell in enumerate(row):
            widths[index] = max(widths[index], len(cell))

    def format_row(cells: list[str]) -> str:
        parts = []
        for index, cell in enumerate(cells):
            # Labels stay left-aligned; numeric columns stay right-aligned.
            aligned = (
                cell.ljust(widths[index])
                if index == 0
                else cell.rjust(widths[index])
            )
            parts.append(aligned)
        return "  ".join(parts)

    print(format_row(headers))
    print("  ".join("-" * width for width in widths))
    for row in str_rows:
        print(format_row(row))


def print_contract_dashboard(
    template_ref: str,
    type_field: str,
    value_field: str,
) -> None:
    client = get_box_client()
    folder_id = os.environ["FOLDER_ID"]

    total = count_contracts(client, template_ref, folder_id)
    stats = contract_value_stats(
        client,
        template_ref,
        folder_id,
        value_field,
        "2020-01-01T00:00:00Z",
        "2030-01-01T00:00:00Z",
    )
    groups = top_contract_types(
        client, template_ref, folder_id, type_field, value_field
    )

    print("Contract analytics dashboard")
    print()
    _print_table(
        ["Metric", "Value"],
        [
            ["Total contracts", total],
            ["Average value", stats["avgContractValue"]],
            ["Minimum value", stats["minContractValue"]],
            ["Maximum value", stats["maxContractValue"]],
        ],
    )
    print()
    # Buckets arrive ordered by document count, so print them as returned.
    print("Top contract types")
    _print_table(
        ["Type", "Total value", "Count"],
        [
            [row.get("contract_type"), row.get("total_value"), row.get("count")]
            for row in groups
        ],
    )


if __name__ == "__main__":
    print_contract_dashboard(
        template_ref=os.environ["TEMPLATE_REF"],
        type_field=os.environ["FIELD_CONTRACT_TYPE"],
        value_field=os.environ["FIELD_CONTRACT_VALUE"],
    )
```

## Run and verify

Run the report against your own content to confirm that authentication, the template reference, and response parsing all work.

1. **Confirm your environment values.** Your `.env` file needs the three Client Credentials Grant values plus `FOLDER_ID`, `TEMPLATE_REF`, `FIELD_CONTRACT_TYPE`, and `FIELD_CONTRACT_VALUE`. The setup script in [Create a metadata template and sample data](#build-by-hand) prints the last four values for you.

2. **Run the report.** Activate your virtual environment first, then use the command for your path:

   ```bash Build by hand theme={null}
   cd contract-insights
   source .venv/bin/activate
   python dashboard.py
   ```

   ```bash Agent theme={null}
   # Console script: run the entry point the agent created
   cd contract-insights
   source .venv/bin/activate
   python dashboard.py

   # Flask, FastAPI, Express, Fastify, Spring Boot, or ASP.NET Core: start the server, then query it from a second terminal
   curl http://127.0.0.1:5000/dashboard
   ```

   If you built with an agent, check the README it generated for the exact module and run command.

3. **Check the output.** With the four sample contracts from the setup step, the console report looks like this:

   ```text theme={null}
   Contract analytics dashboard

   Metric             Value
   ---------------  -------
   Total contracts        4
   Average value    123,750
   Minimum value     45,000
   Maximum value    200,000

   Top contract types
   Type     Total value  Count
   -------  -----------  -----
   Sales        300,000      2
   Renewal      195,000      2
   ```

   Buckets are ordered by document count in descending order, so your group order varies with your own data.

4. **Cross-check one number in Box.** Open the contracts folder in Box and count the files that carry your metadata template. That count matches `Total contracts` from step 3, which confirms your `ancestors` filter and template reference point at the content you expect.

> [!NOTE]
>
> A metric of 0 right after you tag content usually means the metadata index has not caught up yet. Wait a minute and run the report again. For other symptoms, see [Troubleshooting](#troubleshooting).

## Response entry types

Every insight in the response has a `type` that tells you how to interpret it.

| Type | When it displays | How to use it |
| - | - | - |
| `overall` | No `group_by`, or global metric row | Read metric values directly from `metrics` |
| `group` | Top buckets from `group_by` | Use `key[0]` as the group label (for example, contract type) |
| `other` | Groups outside the top `bucket_limit` | Read `totalCountBeyondTopGroups` for the remaining document count |

For grouped requests, buckets are always sorted by document count in descending order. You cannot customize sort order.

## Troubleshooting

| Symptom | Likely cause | What to do |
| - | - | - |
| `KeyError` on an environment variable | `.env` not created, or placeholders not replaced | Fill in the Client Credentials Grant values, then paste the four values printed by `setup_test_data.py` |
| `invalid_grant: Grant credentials are invalid` | `BOX_ENTERPRISE_ID` holds a client ID or secret | Use the numeric enterprise ID from **Developer Console > Configuration > App Details** |
| `unauthorized_client` | App not authorized, or scopes changed since authorization | Authorize or re-authorize the app in the Developer Console and Admin Console |
| `404 INSTANCE_NOT_FOUND` | Wrong template reference in `EXISTS(:templateArg)` | Confirm `enterprise_<id>:<templateKey>` matches your template |
| `403 FORBIDDEN` | Missing scope or inaccessible ancestors | Verify app scopes and that the service account can read the folder |
| `400 BAD_REQUEST` | Invalid predicate or parameter types | Check placeholder names match `params` keys and field types |
| `400 Invalid query request` | `group_by` points at a free-text string field | Re-run `setup_test_data.py` and copy the `FIELD_CONTRACT_TYPE` it prints. On a template with a string `contractType`, that is `contractTypeEnum` |
| `429 RATE_LIMIT_EXCEEDED` | Too many requests for the enterprise | Cache dashboard results; the SDK already retries with backoff |
| Every metric returns 0 | Index lag, wrong folder, or untagged files | Wait a minute after tagging, then confirm `FOLDER_ID` and the field references |
| `Sales Contracts` is missing from Box | Files belong to the service account, not your user | Collaborate yourself on the folder, or open it as an admin in Content Manager |

See [Box Query Insights API guide](https://developer.box.com/guides/query/insights.md) for the full parameter reference, response fields, and additional examples.

## Scaling to production

> **Cache dashboard results**
>
> Dashboard widgets rarely need real-time precision. Refresh aggregates on a schedule, for example, every 15 minutes or hourly, instead of calling the Box Query Insights API on every page load. Store results in your app cache or a data warehouse. The SDK already retries `429 RATE_LIMIT_EXCEEDED` and `5xx` responses with backoff, so cache to reduce the number of calls rather than adding a retry loop of your own.

> **Scope queries with ancestors**
>
> Pass department or program folder IDs in `ancestors` so each team sees only its content. This reduces result set size, keeps dashboards tenant-aware, and aligns with how Box enforces folder-level access for the service account.

> **Pair insights with Box Query API for drill-down**
>
> Use the Box Query Insights API for summary tiles and charts, then call [Box Query API](https://developer.box.com/guides/query/box-query) when you select a bucket to load the underlying files. The Box Query Insights API returns aggregates only. Box Query API returns individual items with the same predicate syntax.

> **Validate group\_by field cardinality**
>
> Make sure the `bucket_limit` is set to 10 or less. Prefer low-cardinality dimensions for chart axes. Cardinality of 10,000 or above can cause errors, though that threshold is rarely reached in practice.

**RELATED GUIDES**
