Harnesses API
A harness is a git-backed, Jinja2-templated bundle of Primer entities (agents, graphs, collections, toolsets) that can be installed from a remote repository (inbound) or assembled from live entities and pushed to one (outbound).
Agents and the entities a harness manages.
Create and manage harnesses in the console.
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/harnesses |
List harnesses |
| POST | /v1/harnesses |
Create a harness (DRAFT) |
| GET | /v1/harnesses/{harness_id} |
Get a harness by id |
| PUT | /v1/harnesses/{harness_id} |
Update a harness |
| DELETE | /v1/harnesses/{harness_id} |
Enqueue UNINSTALL |
| POST | /v1/harnesses/{harness_id}/fetch |
Fetch bundle from remote |
| POST | /v1/harnesses/{harness_id}/install |
Install (render templates into entities) |
| POST | /v1/harnesses/{harness_id}/sync |
Sync to latest ref |
| POST | /v1/harnesses/{harness_id}/build |
Build outbound bundle from tracked entities |
| POST | /v1/harnesses/{harness_id}/push |
Push outbound bundle to git |
| PUT | /v1/harnesses/{harness_id}/overrides |
Set override values |
| PUT | /v1/harnesses/{harness_id}/tracked_entities |
Replace tracked entity set |
Harness object
{
"id": "hrn-a1b2c3d4e5f6",
"slug": "my-harness",
"name": "My Harness",
"description": "Coding assistants bundle.",
"git_url": "https://github.com/example/harness-bundle",
"ref": "main",
"subpath": null,
"direction": "inbound",
"status": "installed",
"pending_operation": null,
"bundle_hash": "sha256:abc123",
"available_bundle_hash": "sha256:abc123",
"available_commit": "deadbeef",
"resolved_commit": "deadbeef",
"commits_ahead": false,
"overrides": {"provider_id": "anthropic-prod", "model_name": "claude-sonnet-4-6"},
"overrides_schema": {"type": "object", "properties": {}},
"overrides_hash": "sha256:def456",
"overrides_dirty": false,
"schema_hash": "sha256:ghi789",
"schema_missing_input": false,
"tracked_entities": [],
"dependencies_resolved": [],
"last_operation_at": "2026-06-07T19:00:55.000000Z",
"last_operation_error": null,
"last_pushed_at": null,
"last_pushed_commit": null,
"last_pushed_bundle_hash": null,
"created_at": "2026-06-07T18:00:00.000000Z"
}
| Field | Type | Description |
|---|---|---|
id |
string | System-assigned identifier |
slug |
string | URL-safe identifier (2-64 chars) |
name |
string | Human-readable name |
git_url |
string | Remote git URL for the bundle |
ref |
string | Git branch or tag (default: main) |
subpath |
string or null | Optional subdirectory within the repo |
direction |
inbound or outbound |
Whether this harness installs or publishes |
status |
string | One of draft, ready, installed, outdated, error |
pending_operation |
string or null | Active async operation: fetch, install, sync, uninstall, build, push |
commits_ahead |
boolean | True when the remote has commits not yet synced |
overrides |
object | Current override values |
overrides_schema |
object or null | JSON Schema describing available override fields |
overrides_dirty |
boolean | True when overrides changed since last install |
tracked_entities |
array | Outbound tracked entity list |
dependencies_resolved |
array | Transitive dependency tree resolved at last fetch |
Create a harness
POST /v1/harnesses - returns 201 Created with the harness object.
Required fields: name, slug, git_url. The harness starts in draft status; call /fetch to retrieve the bundle.
curl -X POST https://your-host/v1/harnesses \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Harness",
"slug": "my-harness",
"git_url": "https://github.com/example/harness-bundle",
"ref": "main",
"direction": "inbound"
}'import httpx
r = httpx.post(
"https://your-host/v1/harnesses",
headers={"Authorization": f"Bearer {token}"},
json={
"name": "My Harness",
"slug": "my-harness",
"git_url": "https://github.com/example/harness-bundle",
"ref": "main",
"direction": "inbound",
},
)
assert r.status_code == 201
harness_id = r.json()["id"]const r = await fetch("/v1/harnesses", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
name: "My Harness",
slug: "my-harness",
git_url: "https://github.com/example/harness-bundle",
ref: "main",
direction: "inbound"
})
})
const harness = await r.json()Errors: 409 if a harness with the same slug already exists.
Get a harness
GET /v1/harnesses/{harness_id} - returns 200 OK with the harness object.
curl https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6 \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6",
headers={"Authorization": f"Bearer {token}"},
)
h = r.json()const r = await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6", {
headers: {"Authorization": `Bearer ${token}`}
})
const h = await r.json()Errors: 404 if the id does not exist.
List harnesses
GET /v1/harnesses - returns an offset or cursor page of harness objects.
Query parameters: slug (filter), status (filter), direction (filter), limit (1-200, default 20), offset, cursor.
curl "https://your-host/v1/harnesses?status=installed&limit=20" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://your-host/v1/harnesses",
headers={"Authorization": f"Bearer {token}"},
params={"status": "installed", "limit": 20},
)
items = r.json()["items"]const r = await fetch("/v1/harnesses?status=installed&limit=20", {
headers: {"Authorization": `Bearer ${token}`}
})
const {items} = await r.json()Fetch bundle from remote
POST /v1/harnesses/{harness_id}/fetch - pulls the bundle from the remote git URL and caches the overrides schema. Returns 202 Accepted (async). Poll GET /v1/harnesses/{harness_id} until pending_operation is null.
curl -X POST https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/fetch \
-H "Authorization: Bearer $TOKEN"import httpx, time
r = httpx.post(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/fetch",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 202
# Poll until idle
while True:
h = httpx.get(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6",
headers={"Authorization": f"Bearer {token}"},
).json()
if not h.get("pending_operation"):
break
time.sleep(0.5)await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6/fetch", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`}
})After fetch completes, overrides_schema is populated on the harness object.
Set override values
PUT /v1/harnesses/{harness_id}/overrides - stores the override values used during install/sync. Returns 200 OK with the updated harness object.
curl -X PUT https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/overrides \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider_id": "anthropic-prod", "model_name": "claude-sonnet-4-6"}'import httpx
r = httpx.put(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/overrides",
headers={"Authorization": f"Bearer {token}"},
json={"provider_id": "anthropic-prod", "model_name": "claude-sonnet-4-6"},
)
assert r.status_code == 200await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6/overrides", {
method: "PUT",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({provider_id: "anthropic-prod", model_name: "claude-sonnet-4-6"})
})Install
POST /v1/harnesses/{harness_id}/install - renders bundle templates into live entities. Returns 202 Accepted. Poll until pending_operation is null.
Entities created by install carry a harness_id field and are write-protected; direct PUT/DELETE on them returns 409.
curl -X POST https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/install \
-H "Authorization: Bearer $TOKEN"import httpx, time
r = httpx.post(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/install",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 202
while True:
h = httpx.get(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6",
headers={"Authorization": f"Bearer {token}"},
).json()
if not h.get("pending_operation"):
break
time.sleep(0.5)
assert h["status"] == "installed"await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6/install", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`}
})Errors: 409 if the harness is already being operated on.
Sync to latest ref
POST /v1/harnesses/{harness_id}/sync - re-fetches the bundle and re-renders entities in place. Returns 202 Accepted. Safe to call when status is outdated.
curl -X POST https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/sync \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.post(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/sync",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 202await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6/sync", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`}
})Build outbound bundle
POST /v1/harnesses/{harness_id}/build - renders tracked live entities into bundle templates. Requires direction: outbound and a populated tracked_entities list. Returns 202 Accepted.
curl -X POST https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/build \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.post(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/build",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 202await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6/build", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`}
})Push outbound bundle to git
POST /v1/harnesses/{harness_id}/push - commits and pushes the built bundle to the configured remote. Requires a git_token with push access. Returns 202 Accepted.
curl -X POST https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/push \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.post(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/push",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 202await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6/push", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`}
})Update tracked entities
PUT /v1/harnesses/{harness_id}/tracked_entities - replaces the full tracked entity set for an outbound harness. Each entry maps a live entity to a template name.
curl -X PUT https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/tracked_entities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tracked_entities": [
{"kind": "agent", "source_id": "my-agent", "template_name": "agent-main", "overrides": []}
]
}'import httpx
r = httpx.put(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6/tracked_entities",
headers={"Authorization": f"Bearer {token}"},
json={
"tracked_entities": [
{"kind": "agent", "source_id": "my-agent",
"template_name": "agent-main", "overrides": []},
]
},
)await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6/tracked_entities", {
method: "PUT",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
tracked_entities: [
{kind: "agent", source_id: "my-agent", template_name: "agent-main", overrides: []}
]
})
})TrackedEntity fields:
| Field | Required | Description |
|---|---|---|
kind |
yes | One of agent, graph, collection, document, toolset |
source_id |
yes | Id of the live entity to track |
template_name |
yes | Template filename (without extension) in the bundle |
overrides |
no | Override mapping list |
Uninstall a harness
DELETE /v1/harnesses/{harness_id} - enqueues an UNINSTALL operation that removes all harness-managed entities and lifts write-protection. Returns 202 Accepted. Poll GET /v1/harnesses/{harness_id} until the harness is gone or status transitions away from installed.
curl -X DELETE https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6 \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.delete(
"https://your-host/v1/harnesses/hrn-a1b2c3d4e5f6",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 202await fetch("/v1/harnesses/hrn-a1b2c3d4e5f6", {
method: "DELETE",
headers: {"Authorization": `Bearer ${token}`}
})Errors: 404 if the harness does not exist, 409 if a conflicting operation is already pending.
Errors note
All error responses use the RFC 7807 ProblemDetails envelope with type, title, status, detail, instance, and extensions (which includes request_id and, for 422 errors, an errors array with field paths). See the REST API overview for details.