Knowledge Graph API
Last updated: August 19, 2026
How to use the Knowledge Graph API to query engineering data in Span
This guide explains how to read engineering data out of Span for a script, dashboard, or agent. Typical examples include pull request cycle time, merged pull request volume, and deployment counts.
The Knowledge Graph API (/next) is built around two calls:
Metadata. Discover which assets, fields, and metric IDs your token can access.
Query. Request those fields and metrics with filters and a time window.
This API is dynamic. Some fields and response shapes are subject to change. Re-fetch metadata for your own token before building or updating an integration, and do not hard-code identifiers from another organization.
We strongly recommend the Span Claude/Cursor Skill to explore the Knowledge Graph API. It calls this same API and handles metadata discovery, metric selection, and query construction for you, so you can ask questions in natural language instead of writing requests by hand. The rest of this guide is for cases where you need to call the API directly.
The machine-readable OpenAPI specification is available at https://api.span.app/docs-next.
Base URL and endpoints
All requests use the base URL https://api.span.app.
Method | Endpoint | Purpose |
|---|---|---|
|
| Discover assets, fields, relations, dimensions, and metrics |
|
| Query one asset for records or aggregated metrics |
The older catalog API at https://api.span.app/docs is separate. Use /next for Knowledge Graph queries.
Getting started
1. Get a token
Authenticate with a bearer token:
Authorization: Bearer <access token>
Personal Access Token. Settings > Personal > Access Token. See How to Create a Span API Token.
Service Account (recommended for shared tools). Settings > Service Accounts, then add the account to a permission group. See Service Account API permissions and authorization.
2. Fetch metadata for the asset you need
curl -s -X GET "https://api.span.app/next/metadata/assets?facade=PullRequest" \
-H "Authorization: Bearer <access token>"
The response describes the asset. The example below is trimmed for readability; the full response contains every field, relation, dimension, and metric your token can access. The exact structure is subject to change, so read it at runtime rather than hard-coding it.
A 401 response indicates a missing or invalid token. A 403 indicates the token lacks permission for the requested data.
{
"data": [
{
"name": "PullRequest",
"fields": [
{ "name": "title", "type": ["string"], "required": true },
{
"name": "extMergedAt",
"description": "The date-time the pull request was merged, in ISO 8601 format.",
"type": ["date-time"],
"required": false
}
],
"relations": [
{ "name": "Author", "type": "Person" },
{ "name": "Repository", "type": "Repository" }
],
"dimensions": [
{ "name": "ts", "label": "Timestamp", "description": "Primary timestamp of the record" }
],
"metrics": [
{
"id": "988a86d4-1da7-40ed-b429-1b65e7c63865",
"label": "Time from first commit to merge",
"description": "Time from pull request first commit to merge.",
"unit": "seconds",
"category": "cycle-time",
"aggregation": {
"default": "avg",
"available": ["p50", "p75", "p90", "max", "avg"]
}
}
]
}
],
"meta": {
"page": {
"endCursor": null,
"startCursor": null,
"hasNextPage": false,
"hasPreviousPage": false,
"pageSize": 1
},
"total": 1
}
}
From this response, copy:
Field and relation names for
selectandfilters, using the exact spelling (for exampletitle, orRepositoryto reachrepositoryName).A metric's
id(UUID). Do not rely on thelabelalone, and note theunitand availableaggregationvalues.
3. Run a query
Send a POST /next/assets/query with a body built from that metadata (see the use cases below). Read annotations.unit in the response (seconds, count, percentage_as_ratio, and similar) before displaying values.
For list-style queries, paginate with ?limit=25&after=<endCursor> (limit defaults to 10, maximum 100). Groups-mode results are not paginated.
Common use cases
The metric IDs and field names in these examples come from real metadata (the PullRequest sample above, plus the Person and Team excerpts at the end of this article). They are illustrative and subject to change. Resolve the IDs from your own /next/metadata/assets response before production use.
Use case A: recent pull requests in a repository, with cycle time
Goal: audit or export pull requests for one repository, sorted by time to merge.
Approach: query PullRequest. Select title, author, and repository name. Filter on Repository.repositoryName. Attach the metric labeled "Time from first commit to merge".
curl -s -X POST "https://api.span.app/next/assets/query?limit=25" \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{
"select": [
"PullRequest.title",
"PullRequest.Author.email",
"PullRequest.Repository.repositoryName"
],
"filters": [
{
"field": "PullRequest.Repository.repositoryName",
"operator": "=",
"value": "myrepo"
}
],
"metrics": [
{
"metricId": "988a86d4-1da7-40ed-b429-1b65e7c63865",
"responseKey": "PullRequest.cycleTime"
}
],
"timeDimension": {
"timeRange": { "startTime": "2026-07-01", "endTime": "2026-07-31" }
},
"order": { "field": "PullRequest.cycleTime", "direction": "desc" }
}'
This returns one row per pull request. PullRequest.cycleTime is reported in seconds (confirm via annotations). To fetch the next page, resend the same body with &after=<endCursor>.
Use case B: merged pull requests broken down by engineer tenure
Goal: compare throughput across tenure bands, or any other person dimension.
Approach: use mode: "groups" on Person. Select only the dimension (personTenure). Attach the "Total PRs merged" metric.
curl -s -X POST "https://api.span.app/next/assets/query" \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{
"mode": "groups",
"select": ["Person.personTenure"],
"filters": [],
"metrics": [
{
"metricId": "01fcd390-ae25-4728-b55c-686e103e3b7d",
"responseKey": "totalMergedPRs"
}
],
"timeDimension": {
"timeRange": { "startTime": "2026-01-01", "endTime": "2026-03-31" }
}
}'
This returns one row per tenure bucket. Add "granularity": "week" under timeDimension to receive a weekly series instead of a single value per bucket.
Groups mode always requires at least one dimension in select (the field you are grouping by), plus at least one metric. If you want a single organization-wide series with no breakdown, there is no dimension to group by, so do not use mode: "groups" — see Use case D instead.
Related variations: for organization-wide cycle time, query Team with Team.name = "Organization" (confirm the root team name from metadata) and the same cycle-time metric. For a weekly trend for one person, query Person with an email filter and granularity: "week".
Use case C: production deploys for a team and its sub-teams
Goal: count deployments across a team tree. Note that Team.name = "X" returns only that team's own deploys and does not roll up sub-teams.
Approach: resolve Team.path, then use groups mode with Team.groupPath and the DESCENDANT_OF operator, together with the "Deployments" metric.
curl -s -X POST "https://api.span.app/next/assets/query" \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{
"select": ["Team.name", "Team.path"],
"filters": [
{ "field": "Team.name", "operator": "=", "value": "Platform" }
],
"metrics": []
}'
curl -s -X POST "https://api.span.app/next/assets/query" \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{
"mode": "groups",
"select": ["Team.groupPath"],
"filters": [
{
"field": "Team.groupPath",
"operator": "DESCENDANT_OF",
"value": "<Team.path from the previous response>"
}
],
"metrics": [
{
"metricId": "c3377814-3805-4803-89ba-6f77bb3a8907",
"responseKey": "deploys"
}
],
"timeDimension": {
"timeRange": { "startTime": "2026-04-01", "endTime": "2026-04-30" }
}
}'
For organization-wide deploys, use the root team's Team.path (the path with no . separator) rather than Team.name = "Organization" alone.
To send deployment events into Span (the write path), use the DORA deployments API.
Use case D: an organization-wide metric over time (no breakdown)
Goal: track a single metric for the whole organization as a time series — for example, weekly PR throughput sampled month by month over the last six months — with no per-team or per-person breakdown.
Approach: this is not a groups query. Groups mode aggregates by a dimension and requires one; a whole-org series has no dimension to group by. Instead, query the Team facade filtered to the root (organization) team, attach the metric, and set granularity. Resolve the root team from metadata (its Team.path has no . separator; Team.name is usually your organization's name).
curl -s -X POST "https://api.span.app/next/assets/query" \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{
"select": ["Team.name"],
"filters": [
{ "field": "Team.name", "operator": "=", "value": "Organization" }
],
"metrics": [
{
"metricId": "<your metric id from metadata>",
"responseKey": "prRate"
}
],
"timeDimension": {
"timeRange": { "startTime": "2026-02-01", "endTime": "2026-08-01" },
"granularity": "month"
}
}'
This returns a single row for the organization with prRate as a monthly { time, value } series. Confirm the metric's unit via annotations. If the metric is an additive count and you need sub-teams rolled up under the root, use Team.groupPath with DESCENDANT_OF from the root path instead (see Use case C). To break the same metric down later (by team, repo, or person), switch to that asset with mode: "groups" and a dimension (see Use cases B and C).
Building other payloads from metadata
When your use case is not covered above, build the request from metadata as follows.
You need | Look in metadata for | Put it in the query as |
|---|---|---|
Columns to return |
|
|
Filters | The same names |
|
A metric |
|
|
Time window or series |
|
|
Breakdown by dimension |
|
|
The most common operators are =, IN, CONTAINS (catalog fields only), and DESCENDANT_OF (team trees).
Match the grain of the question to the asset. A breakdown "by repository, team, or person" should query that asset. Querying PullRequest for a count by repository returns one row per pull request.
Optional helpers: POST /next/assets/query/schema accepts the same body and returns the column schema without running the full query. Send Accept: text/csv on the query endpoint to receive CSV.
Additional metadata used in the examples
These are illustrative excerpts for the other assets referenced above (the PullRequest sample appears in step 2). They are from one organization and token and are subject to change; re-fetch metadata before relying on these IDs. The Repository name field used in paths is repositoryName, not name.
Person
{
"name": "Person",
"fields": [{ "name": "email", "type": ["string"] }],
"dimensions": [{ "name": "personTenure", "label": "Tenure" }],
"metrics": [
{
"id": "01fcd390-ae25-4728-b55c-686e103e3b7d",
"label": "Total PRs merged",
"unit": "count"
}
]
}
Team
{
"name": "Team",
"fields": [
{ "name": "name", "type": ["string"] },
{ "name": "path", "type": ["string"] }
],
"dimensions": [
{
"name": "groupPath",
"label": "Team path",
"description": "Filter with DESCENDANT_OF to scope a metric to a team and all of its subteams."
}
],
"metrics": [
{
"id": "c3377814-3805-4803-89ba-6f77bb3a8907",
"label": "Deployments",
"unit": "count"
}
]
}
Troubleshooting
Symptom | Likely fix |
|---|---|
| Re-check spelling against metadata ( |
| Add a dimension to |
Empty | Widen |
Incorrect deploy totals | Use |
Unexpected values | Read |
| Issue a new token, or correct the service account's permission group |
Related
OpenAPI specification: https://api.span.app/docs-next
Tokens: How to Create a Span API Token
Send deployments: DORA API