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

# Query build results with the REST API

> Read the build data behind the Build Results UI from your own tooling or AI agents: authentication with the Aspect CLI, addressing builds, logs, targets, and cross-invocation statistics.

export const gatedAccess = (user, group) => {
  const loggedIn = !!(user && user.loggedIn);
  const groups = user && user.tenantMetadata && user.tenantMetadata.docsGroups || [];
  if (loggedIn && (!group || groups.indexOf(group) >= 0)) {
    return "entitled";
  }
  return loggedIn ? "signed-in" : "anonymous";
};

export const GatedLink = ({access, href, group, children}) => {
  const note = group ? "Aspect Enterprise customers" : "free Aspect account";
  const muted = {
    fontSize: "0.85em",
    opacity: 0.7,
    whiteSpace: "nowrap"
  };
  if (access === "entitled") {
    return <a href={href}>{children}</a>;
  }
  if (access !== "signed-in") {
    return <span>
        <a href={"/login?redirect=" + encodeURIComponent(href)}>{children}</a>
        <span style={muted}> (sign in: {note})</span>
      </span>;
  }
  return <span>
      {children}
      <span style={muted}> ({note})</span>
    </span>;
};

The build results REST API is an HTTP interface at `/api/v1` on the Build Results UI's host. It serves
the data the UI displays: invocations and their configurations, metadata, metrics and logs, per-build
targets and target summaries, cross-invocation target statistics, and action history. Use it to feed
your own tooling, such as a release dashboard, a CI gate, or an internal report, or hand it to AI agents
through the [built-in MCP server](/docs/aspect-workflows/platform/guides/build-results-mcp).

## Where it's available

The API is available on [Aspect Cloud](/docs/aspect-workflows/cloud/overview), at `https://app.aspect.build/api/v1`, and on
[Aspect Enterprise](/docs/aspect-workflows/enterprise/overview) deployments running Aspect Workflows 6.0.30 or
later. On Aspect Enterprise it's off by default. On a deployment
[hosted by Aspect](/docs/aspect-workflows/enterprise/hosted/overview),
[ask for it](/docs/aspect-workflows/enterprise/hosted/requesting-changes). On a self-hosted deployment,
your operator enables it (see the
<GatedLink access={gatedAccess(user, "workflows-subscriber")} href="/docs/aspect-workflows/enterprise/self-hosted/reference/terraform-config-aws#webapp-web-ui" group="workflows-subscriber">Terraform reference</GatedLink>).
Until it's on, API requests are redirected to the browser login instead of answered.

<Note>
  The API is experimental. Routes, parameters and response shapes can change between Aspect Workflows
  releases without a new version prefix; each such change is called out in that release's notes. The
  <code>v1</code> in the path names the current contract, not a compatibility promise. Expect to revisit a client
  when the deployment it talks to upgrades, and keep anything that can't tolerate that off the API for
  now.
</Note>

## Prerequisites

An Aspect Cloud account, or an Aspect Enterprise deployment running Aspect Workflows 6.0.30 or later with the API enabled (see [Where it's available](#where-its-available)).

### If the deployment uses your own identity provider

* **Register one identity provider application covering both the remote cluster and the Build Results UI.** That's
  what lets a token minted during CLI login be accepted by the API with no extra configuration.
* **Register the Aspect CLI redirect URI on it.** The
  [redirect URI table](/docs/aspect-workflows/enterprise/connect/local-setup#if-the-deployment-uses-your-own-identity-provider)
  lists it. If CLI logins to the remote cache or BES already work, it's registered.

If your deployment deliberately uses **two** identity provider applications (one per endpoint), the API host must be
configured to accept the second application's audience. Contact Aspect support.

## Set up authentication

Authentication uses the same CLI login as the remote cache. Nothing new is provisioned for the API.

```bash theme={null}
# 1. Log in. On Aspect Cloud:
aspect auth login
#    On Aspect Enterprise, record the deployment, make it the default, and log in
#    (this reads the deployment's discovery document and opens a browser):
aspect auth configure --default remote.<your-domain>

# 2. Call the API on the Build Results UI's host.
curl -H "Authorization: Bearer $TOKEN" \
  "https://app.<your-domain>/api/v1/invocations?limit=5"
```

Note the **two hostnames**: you authenticate against the remote cluster's host and call the API on the
Build Results UI's host. One identity provider application covers both, so the token from step 1 is accepted in step 2.

To extract the current token from the CLI's credential store (for example to place it in `$TOKEN`
above), use the CLI's credential helper:

```bash theme={null}
TOKEN=$(echo '{"uri":"https://remote.<your-domain>/"}' | aspect get \
  | jq -r '.headers.Authorization[0]' | cut -d' ' -f2)
```

In CI, record the deployment without the browser login, then log in with the deployment's API token
from `ASPECT_API_TOKEN_<NAME>` (the deployment name uppercased, with every non-alphanumeric character
replaced by `_`). The session goes in the OS keyring where one is reachable (the kernel keyring on
Linux), and otherwise in a `0600` file at `~/.aspect/credentials.json`:

```bash theme={null}
aspect auth configure remote.<your-domain> --name <name> --default --login=false
echo "$ASPECT_API_TOKEN_<NAME>" | aspect auth login --with-api-token --deployment <name>
```

## How authentication works

Every `/api/v1` request needs `Authorization: Bearer <token>` from the deployment's configured
identity provider. The gateway verifies the token's signature against the issuer's published keys,
checks the audience, and resolves the token's tenant to a provisioned organization. Any failure is a
`401` that doesn't say which check failed.

Clients that need to discover the authorization server programmatically read the RFC 9728
protected-resource metadata at `GET /.well-known/oauth-protected-resource/api/v1` on the API host
(unauthenticated), then follow its `authorization_servers`.

<Note>
  Don't build a 401-then-discover flow. With the authenticating proxy in front of the Build Results UI, an
  unauthenticated <code>/api/v1</code> request is redirected to the identity provider rather than
  answered with a challenge. Read the well-known document directly instead.
</Note>

## Addressing a build

A build is addressed by its Aspect invocation ID, the `id` field in API responses, not by the
invocation ID Bazel printed, which is client-chosen and unique only per user. To go from a Bazel
invocation ID to the build's resources, resolve it first:

```bash theme={null}
# Resolve the Bazel-printed UUID to the API's build id ...
curl … "https://app.<your-domain>/api/v1/invocations?invocation_id=<bazel-uuid>"

# ... then address the build's resources with the returned `id`.
curl … "https://app.<your-domain>/api/v1/invocations/<id>/metrics"
```

Every response carries a `links` object with named relations, so after the first request a client
navigates by following links rather than assembling URLs.

## Reading a build's log

`GET /invocations/<id>/log?page=N` returns the build log in pages, zero-indexed. Every page reports
`page` and `page_count`, so start at `?page=0` and page forward. A page past the end is empty, since
the log grows while the build runs.
`GET /invocations/<id>/log/tail` is the one-request shortcut to the *last* page, useful for "why did
this build fail" tooling.

## Cross-invocation target statistics

`GET /target-stats?range=d7` lists the organization's targets with their statistics over the lookback
window (`d1`, `d3`, `d7`, `m1`, `m3` or `y1`). Adding `label=<percent-encoded-label>` narrows the same
response shape to one target. Supply `repo=<repository>` with the label to select that repository;
omitting it selects builds with no recorded repository. Other filters, ordering, or paging parameters
are rejected in this form (they return a `400`), and the collection-wide `profiled_invocations_daily` series is empty. A label
with no builds in the window returns an empty page, not a `404`.

`GET /target-invocations?label=…&range=…` lists the builds that built one target. Bazel labels are
always query parameters, never path segments, because a label contains `/` and `:`.

## Action performance history

`GET /api/v1/action-history` reads execution-log records for an exact action-owner label, including
private or transitive labels without a target completion record. Use this endpoint to compare a
build action before and after a performance change.

Action history requires Aspect Workflows 6.0.33 or later. An older deployment returns `404`.

```bash theme={null}
curl --get -H "Authorization: Bearer $TOKEN" \
  "https://app.<your-domain>/api/v1/action-history" \
  --data-urlencode 'label=//web:bundle' \
  --data-urlencode 'start=2026-09-01T00:00:00Z' \
  --data-urlencode 'end=2026-09-15T00:00:00Z' \
  --data-urlencode 'repo=my-repository' \
  --data-urlencode 'branch=main' \
  --data-urlencode 'cache=miss' \
  --data-urlencode 'daily=true' \
  --data-urlencode 'limit=20'
```

`label`, `start`, and `end` are required. Times are RFC 3339 timestamps and filter the invocation's
received time, with an inclusive start and exclusive end. A request may span at most 31 days; query
separate windows for longer comparisons. Repository and branch filters are exact matches. Omitting
them includes all repositories and branches within your authenticated organization.

Cache filtering selects whole invocation/label records:

* `cache=hit`: all recorded spawns were cached, or the label was locally cached.
* `cache=miss`: at least one spawn was not cached. Mixed records retain their cached-spawn counts.
* Omit `cache` to include both outcomes.

The response contains:

* `entries`: one record per invocation and label, newest first, with its Aspect invocation `id`,
  repository, branch, commit, spawn/cache counts, execution-wall-time sum, and longest-path timings.
* `total`: matching records before pagination. Follow `links.next` with the same time window; `limit`
  defaults to 20 and accepts 1–100, and `offset` defaults to zero.
* `summary`: counts and timing aggregates for the entire filtered window, independent of pagination.
  `execution_count` is recorded spawns minus cache hits. `exec_wall_ms_total` sums recorded execution
  wall times; it isn't elapsed build time because actions may run concurrently.
* `daily`: the same aggregates per UTC date when `daily=true`. Days without records are omitted.

**Percentiles describe per-invocation totals for the label, not individual action durations.** Each
record may combine several spawns or configurations. `exec_wall_ms_p50`, `exec_wall_ms_p90`, and
`exec_wall_ms_p99` use exact interpolated percentiles of those stored totals. Cached records are
included unless filtered out, so use `cache=miss` when comparing execution performance. A mixed
record's timing is still its full stored sum; this endpoint can't split it into individual spawns.

Empty selections return zero counts, null percentiles, and empty arrays. History depends on retained,
ingested execution logs: an empty result doesn't prove the action never ran. Ingestion can add or
update records while you paginate. Results are cached; repeating the same query within 30 seconds
may return the previous result. After ingestion completes, allow the cache to refresh before
re-fetching for a stable comparison.

When the service is busy, a query may wait up to five seconds before returning
`503 service_unavailable`. Wait for the `Retry-After` interval before retrying.

## Conventions

* Every route is a `GET`.
* Pagination is `limit` (1–100) and `offset`. Every list response carries the unpaginated `total`.
* An unknown query parameter is a `400` naming the valid fields, never silently ignored.
* Errors are `{"code": …, "message": …}`. On 6.0.33, `code` is one of `bad_request` (400),
  `unauthorized` (401), `not_found` (404), `internal` (500), or `service_unavailable` (503). A build
  belonging to another organization is a `404`, never a `403`.
* Byte counts and recorded durations are integers; interpolated percentiles can be fractional; `links` relation names are snake\_case.
* Metadata filters repeat as `metadata=key:value`. Values for the same key are ORed, distinct keys ANDed.

## API reference

The deployment serves its own reference documentation:

* **Browsable reference** at `https://app.<your-domain>/api/v1/docs`. Open it in a signed-in browser
  session.
* **OpenAPI document** at `https://app.<your-domain>/api/v1/openapi.json`. Fetch it with a bearer
  token, for client generation or import into API tooling. Its `info.version` tracks the API contract
  (`1.0.0` under `/api/v1`), not the deployment's Workflows version.

## See also

* [Connect an AI assistant to build results with MCP](/docs/aspect-workflows/platform/guides/build-results-mcp): the Aspect CLI's MCP server over this API, with per-client setup, the published tools, and troubleshooting.
* [Build Results UI](/docs/aspect-workflows/platform/features/webui): the web interface serving the same data.
