Graph API¶
Base path: /api/v1/graph
The Graph module exposes the knowledge graph for the requesting user's organisation. It provides the full paginated graph, per-entry subgraphs, topic clusters derived from tag co-occurrence, aggregate statistics, and an on-demand auto-link trigger that wires together semantically similar entries via embedding cosine similarity.
See API Reference for auth, errors, and pagination.
Plan gating¶
Every route in this module requires Growth plan or above (min_plan="growth", feature "knowledge_graph"). Requests from trial or starter organisations are rejected with 403 PLAN_UPGRADE_REQUIRED before reaching any route handler.
Plan hierarchy: trial < starter < growth < enterprise.
Data types¶
GraphNode¶
| Field | Type | Description |
|---|---|---|
id |
string |
Node UUID. |
label |
string |
Display name (entry title, username, dept name). |
type |
"entry" \| "person" \| "department" |
Discriminates what the node represents. |
metadata |
object |
Flexible per-type property bag. |
GraphEdge¶
| Field | Type | Description |
|---|---|---|
source |
string |
Source node UUID. |
target |
string |
Target node UUID. |
type |
"explicit" \| "similarity" \| "same_author" \| "same_dept" |
Relationship kind. |
weight |
float |
1.0 for structural edges; cosine similarity score for similarity edges. |
Edge types:
| Type | Origin |
|---|---|
explicit |
Manually declared via the related_entries many-to-many table. |
similarity |
Auto-detected: embedding cosine similarity >= 0.85. |
same_author |
Two entries share the same created_by user. |
same_dept |
Two entries share the same department. |
GraphCluster¶
| Field | Type | Description |
|---|---|---|
tag |
string |
The tag that defines the cluster. |
entry_ids |
string[] |
UUIDs of entries in the cluster. |
size |
integer |
Count of entries in the cluster. |
GraphResponse¶
Returned by GET /graph and GET /graph/entry/:id.
| Field | Type | Description |
|---|---|---|
nodes |
GraphNode[] |
All nodes in the graph slice. |
edges |
GraphEdge[] |
All edges between the returned nodes. |
clusters |
GraphCluster[] |
Topic clusters visible within this slice. |
total_entries |
integer |
Total entries matching the applied filters. |
page |
integer |
Current page number. |
per_page |
integer |
Page size used. |
has_more |
boolean |
true if additional pages exist. |
GraphStatsResponse¶
| Field | Type | Description |
|---|---|---|
total_nodes |
integer |
Count of active entries. |
total_edges |
integer |
Count of explicit links. |
total_clusters |
integer |
Tag clusters containing >= 2 entries. |
orphan_entries |
integer |
Entries with no edges of any kind. |
avg_connections_per_entry |
float |
Mean edge count per node. |
most_connected_entry_id |
string \| null |
UUID of the most-linked entry. |
most_connected_entry_title |
string \| null |
Title of the most-linked entry. |
most_connected_count |
integer |
Edge count for the most-linked entry. |
Endpoints¶
GET /graph¶
Auth: JWT · Any role · Growth+
Returns the full knowledge graph for the organisation, paginated. Each response slice contains matching entry nodes, edges between all returned nodes, and any topic clusters visible within the slice.
Query parameters
| Parameter | Type | Default | Constraints | Description |
|---|---|---|---|---|
page |
integer |
1 |
>= 1 | Page number. |
per_page |
integer |
50 |
1–200 | Entry nodes per page. |
department_id |
UUID |
— | Valid UUID v4 | Filter nodes to entries in this department. |
tag_ids |
UUID[] |
— | Valid UUID v4s | Entries matching any of these tags. Supports repeated params and comma-separated values. |
status |
string |
— | — | Filter by entry status. |
confidence |
string |
— | — | Filter by entry confidence. |
include_persons |
boolean |
false |
true/false |
Add person nodes for entry authors. |
include_depts |
boolean |
false |
true/false |
Add department cluster nodes. |
Both formats are accepted for tag_ids:
Response — 200 OK
{
"nodes": [
{
"id": "a1b2c3d4-0000-0000-0000-000000000001",
"label": "Onboarding Best Practices",
"type": "entry",
"metadata": {
"status": "published",
"confidence": "high",
"department_id": "dept-uuid-here",
"tag_ids": ["tag-uuid-1", "tag-uuid-2"]
}
},
{
"id": "user-uuid-here",
"label": "Alice Smith",
"type": "person",
"metadata": { "email": "alice@example.com" }
}
],
"edges": [
{
"source": "a1b2c3d4-0000-0000-0000-000000000001",
"target": "a1b2c3d4-0000-0000-0000-000000000002",
"type": "similarity",
"weight": 0.91
}
],
"clusters": [
{
"tag": "onboarding",
"entry_ids": [
"a1b2c3d4-0000-0000-0000-000000000001",
"a1b2c3d4-0000-0000-0000-000000000003"
],
"size": 2
}
],
"total_entries": 143,
"page": 1,
"per_page": 50,
"has_more": true
}
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | AUTHENTICATION_FAILED |
Missing or invalid JWT. |
| 402 | SUBSCRIPTION_REQUIRED |
Org subscription inactive or trial expired. |
| 403 | ORG_SCOPE_REQUIRED |
JWT does not contain an org_id claim. |
| 403 | PLAN_UPGRADE_REQUIRED |
Organisation is on trial or starter plan. |
| 422 | VALIDATION_ERROR |
A UUID query parameter contains a non-UUID value. |
Notes
- Pagination applies to entry nodes only. Edges and clusters are computed for the returned node set and are not independently paginated.
include_personsandinclude_deptsexpand the node count beyondper_page;total_entriesand pagination fields still reflect only entry nodes.
curl -G "https://api.knora.io/api/v1/graph" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "page=1" \
--data-urlencode "per_page=50" \
--data-urlencode "include_persons=true" \
--data-urlencode "status=published"
GET /graph/entry/:id¶
Auth: JWT · Any role · Growth+
Returns the subgraph centred on a single knowledge entry — the entry itself plus its direct (depth 1) or indirect (depth 2) neighbours, along with all edges between those nodes.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id |
UUID |
UUID of the knowledge entry to focus on. |
Query parameters
| Parameter | Type | Default | Constraints | Description |
|---|---|---|---|---|
depth |
integer |
1 |
1 or 2 | Traversal depth from the focal entry. |
depth is clamped server-side to [1, 2]; values outside this range are silently adjusted.
Response — 200 OK
{
"nodes": [
{
"id": "a1b2c3d4-0000-0000-0000-000000000001",
"label": "Onboarding Best Practices",
"type": "entry",
"metadata": {}
},
{
"id": "a1b2c3d4-0000-0000-0000-000000000002",
"label": "New Hire Checklist",
"type": "entry",
"metadata": {}
}
],
"edges": [
{
"source": "a1b2c3d4-0000-0000-0000-000000000001",
"target": "a1b2c3d4-0000-0000-0000-000000000002",
"type": "explicit",
"weight": 1.0
}
],
"clusters": [],
"total_entries": 2,
"page": 1,
"per_page": 2,
"has_more": false
}
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | AUTHENTICATION_FAILED |
Missing or invalid JWT. |
| 402 | SUBSCRIPTION_REQUIRED |
Org subscription inactive or trial expired. |
| 403 | ORG_SCOPE_REQUIRED |
JWT does not contain an org_id claim. |
| 403 | PLAN_UPGRADE_REQUIRED |
Organisation is on trial or starter plan. |
| 404 | NOT_FOUND |
Entry does not exist or belongs to another org. |
curl "https://api.knora.io/api/v1/graph/entry/a1b2c3d4-0000-0000-0000-000000000001?depth=2" \
-H "Authorization: Bearer $TOKEN"
GET /graph/clusters¶
Auth: JWT · Any role · Growth+
Returns all topic clusters for the organisation. A cluster groups entries that share a common tag and contains at least two members. Clusters are derived from tag co-occurrence across all active entries. This endpoint is not paginated — all clusters are returned in a single response.
Response — 200 OK
Returns a JSON array of GraphCluster objects.
[
{
"tag": "onboarding",
"entry_ids": [
"a1b2c3d4-0000-0000-0000-000000000001",
"a1b2c3d4-0000-0000-0000-000000000003",
"a1b2c3d4-0000-0000-0000-000000000007"
],
"size": 3
},
{
"tag": "security",
"entry_ids": [
"a1b2c3d4-0000-0000-0000-000000000004",
"a1b2c3d4-0000-0000-0000-000000000009"
],
"size": 2
}
]
An empty array is returned when no qualifying clusters exist.
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | AUTHENTICATION_FAILED |
Missing or invalid JWT. |
| 402 | SUBSCRIPTION_REQUIRED |
Org subscription inactive or trial expired. |
| 403 | ORG_SCOPE_REQUIRED |
JWT does not contain an org_id claim. |
| 403 | PLAN_UPGRADE_REQUIRED |
Organisation is on trial or starter plan. |
POST /graph/auto-link¶
Auth: JWT · admin or manager role · Growth+
Enqueues a background Celery task (graph.auto_link_entries) that scans all active entries in the organisation and creates related_entries links for pairs whose embedding cosine similarity score is >= 0.85 and that are not already explicitly linked. Returns a task ID to poll for completion.
No request body.
Response — 202 Accepted
{
"message": "Auto-link task queued.",
"task_id": "b3d2e1f0-aaaa-bbbb-cccc-000000000001",
"org_id": "org-uuid-here"
}
| Field | Type | Description |
|---|---|---|
message |
string |
Human-readable confirmation. |
task_id |
string |
Celery task ID. Use with GET /graph/auto-link/:task_id to poll status. |
org_id |
string |
UUID of the organisation being processed. |
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | AUTHENTICATION_FAILED |
Missing or invalid JWT. |
| 402 | SUBSCRIPTION_REQUIRED |
Org subscription inactive or trial expired. |
| 403 | ORG_SCOPE_REQUIRED |
JWT does not contain an org_id claim. |
| 403 | INSUFFICIENT_ROLE |
Caller's role is member — admin or manager is required. |
| 403 | PLAN_UPGRADE_REQUIRED |
Organisation is on trial or starter plan. |
Notes
- The task runs asynchronously. A 202 response means the task was accepted by the broker, not that linking is complete.
- Soft time limit: 10 minutes; hard limit: 11 minutes. Large orgs may use the full limit.
- Up to 3 automatic retries on failure, with a 60-second delay between attempts.
- Similarity threshold: 0.85 cosine similarity. Pairs below this threshold are not linked.
- Already-linked pairs are skipped — no duplicate links are created.
- There is no built-in guard against concurrent tasks for the same org.
curl -X POST "https://api.knora.io/api/v1/graph/auto-link" \
-H "Authorization: Bearer $ADMIN_TOKEN"
GET /graph/auto-link/:task_id¶
Auth: JWT · Any role · Growth+
Polls the status of an auto-link Celery task previously enqueued by POST /graph/auto-link.
Path parameters
| Parameter | Type | Description |
|---|---|---|
task_id |
string |
Celery task ID returned by auto-link. |
Response — 200 OK
{
"task_id": "b3d2e1f0-aaaa-bbbb-cccc-000000000001",
"state": "SUCCESS",
"result": { "links_created": 42 }
}
| Field | Type | Description |
|---|---|---|
task_id |
string |
The polled task ID. |
state |
string |
Celery task state: PENDING, STARTED, SUCCESS, FAILURE, RETRY. |
result |
object \| null |
Task result payload when state is SUCCESS; null otherwise. |
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | AUTHENTICATION_FAILED |
Missing or invalid JWT. |
| 402 | SUBSCRIPTION_REQUIRED |
Org subscription inactive or trial expired. |
| 403 | ORG_SCOPE_REQUIRED |
JWT does not contain an org_id claim. |
| 403 | PLAN_UPGRADE_REQUIRED |
Organisation is on trial or starter plan. |
curl "https://api.knora.io/api/v1/graph/auto-link/b3d2e1f0-aaaa-bbbb-cccc-000000000001" \
-H "Authorization: Bearer $TOKEN"
GET /graph/stats¶
Auth: JWT · Any role · Growth+
Returns aggregate statistics for the organisation's knowledge graph. Useful for dashboard summary widgets, health indicators, and identifying highly connected or isolated entries.
No query parameters.
Response — 200 OK
{
"total_nodes": 143,
"total_edges": 312,
"total_clusters": 18,
"orphan_entries": 7,
"avg_connections_per_entry": 4.36,
"most_connected_entry_id": "a1b2c3d4-0000-0000-0000-000000000001",
"most_connected_entry_title": "Engineering Runbook",
"most_connected_count": 24
}
Errors
| Status | Code | Cause |
|---|---|---|
| 401 | AUTHENTICATION_FAILED |
Missing or invalid JWT. |
| 402 | SUBSCRIPTION_REQUIRED |
Org subscription inactive or trial expired. |
| 403 | ORG_SCOPE_REQUIRED |
JWT does not contain an org_id claim. |
| 403 | PLAN_UPGRADE_REQUIRED |
Organisation is on trial or starter plan. |
Notes
most_connected_entry_idandmost_connected_entry_titlearenullwhen the org has no entries.total_edgescounts only explicit links; similarity and structural edges are not included in this count.