Skip to main content
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.

Where it’s available

The API is available on Aspect Cloud, at https://app.aspect.build/api/v1, and on Aspect Enterprise deployments running Aspect Workflows 6.0.30 or later. On Aspect Enterprise it’s off by default. On a deployment hosted by Aspect, ask for it. On a self-hosted deployment, your operator enables it (see the ). Until it’s on, API requests are redirected to the browser login instead of answered.
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 v1 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.

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).

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 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.
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:
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:

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

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:
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.
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