> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Workspaces

> Persistent file collections — 6 endpoints

A workspace is a named collection of source files and derived outputs, owned by
your organization, pinned to a domain vocabulary and an access-control configuration.

All list routes use **keyset cursor pagination** (`cursor` / `next_cursor`).

***

## Create workspace

```
POST /v1/workspaces
```

Provision a new workspace. Every field has a default, so a name alone produces a
working workspace. The workspace is immediately usable for file uploads and ingestion.

**Request:**

```json theme={"dark"}
{
  "name": "q4-2025-audit",
  "domain_slug": "audit",
  "domain_version": null,
  "access": {
    "labels": [
      {"name": "financial", "description": "Financial documents."},
      {"name": "confidential", "description": "Restricted documents."}
    ],
    "default_label": "financial"
  },
  "retention_policy": {
    "derived_ttl_days": null,
    "source_ttl_days": null,
    "job_ttl_days": 30
  }
}
```

**Body fields:**

* **`name`** (required, 1–200 chars) — Human-readable workspace name.
* **`domain_slug`** (optional, default `"generic"`) — Domain vocabulary slug. Resolved to the latest published version unless `domain_version` is also set.
* **`domain_version`** (optional) — Pin a specific published domain version. Omit to take the latest.
* **`access`** (optional) — Access-control configuration:
  * **`labels`** — List of `{name, description}` labels. Empty list (the default) disables filtering entirely.
  * **`default_label`** — Required when `labels` is non-empty. Applied when the classifier cannot decide. Must name one of the declared labels.
* **`retention_policy`** (optional):
  * **`derived_ttl_days`** — Days before derived representations expire (anchored on `ingested_at`). Null = keep forever.
  * **`source_ttl_days`** — Days before source bytes are deleted (anchored on `created_at`). Null = keep forever.
  * **`job_ttl_days`** (default 30) — Days to retain job input and result payloads. Null = keep forever.
* **`knowledge_graph`** (optional) — Graph build policy. Editable later via [Update workspace](#update-workspace).
  * **`auto_build`** (default `false`) — Build the graph automatically after each ingestion completes. Off by default because a build is billable and long-running; set `true` for a workspace that should always hold a graph over what it ingests.
  * **`exclude_path_prefixes`** / **`exclude_categories`** — Files matching either are kept out of the graph corpus, unless the file's own `graph_inclusion` is `include`.

**Response (201 Created):**

```json theme={"dark"}
{
  "workspace_id": "550e8400-e29b-41d4-a716-446655440003",
  "name": "q4-2025-audit",
  "domain_id": "550e8400-e29b-41d4-a716-446655440000",
  "domain_slug": "audit",
  "domain_version": 2,
  "access": {"labels": [...], "default_label": "financial"},
  "retention_policy": {"derived_ttl_days": null, "source_ttl_days": null, "job_ttl_days": 30},
  "knowledge_graph": {"auto_build": false, "exclude_path_prefixes": [], "exclude_categories": []},
  "config_version": 1,
  "status": "active",
  "created_at": "2026-08-09T12:00:00Z",
  "updated_at": "2026-08-09T12:00:00Z"
}
```

**Response fields:**

* **`workspace_id`** — UUID for the workspace.
* **`domain_slug`** / **`domain_version`** — The resolved vocabulary this workspace uses.
* **`knowledge_graph.auto_build`** — Whether ingestion automatically triggers a KG build (default `false`).
* **`status`** — `active` or `deleting`.

**Errors:**

* **`domain_not_found`** (404) — Slug does not match any visible domain.
* **`domain_not_published`** (409) — The resolved domain version is a draft, not published.
* **`default_label_required`** (422) — `access.labels` is non-empty but `default_label` is missing.

***

## List workspaces

```
GET /v1/workspaces
```

Page through the caller's workspaces, oldest first.

**Query parameters:**

* **`name_contains`** (optional) — Case-insensitive substring filter on workspace name.
* **`cursor`** (optional) — Opaque cursor from `next_cursor`.
* **`limit`** (optional, default 50, max 200)

**Response:**

```json theme={"dark"}
{
  "items": [
    {
      "workspace_id": "550e8400-e29b-41d4-a716-446655440003",
      "name": "q4-2025-audit",
      "domain_slug": "audit",
      "domain_version": 2,
      "status": "active",
      "created_at": "2026-08-09T12:00:00Z",
      "updated_at": "2026-08-09T12:00:00Z"
    }
  ],
  "next_cursor": null,
  "total_count": 1
}
```

***

## Get workspace

```
GET /v1/workspaces/{workspace_id}
```

Retrieve one workspace.

**Response:** Full workspace object (same shape as Create response).

**Errors:**

* **`workspace_not_found`** (404) — No such workspace for this key. Cross-tenant reads are indistinguishable from missing rows.

***

## Update workspace

```
PATCH /v1/workspaces/{workspace_id}
```

Change a workspace's mutable configuration. Only the knowledge-graph policy is
mutable — the name, domain, access labels, and retention policy are fixed at
creation, because every derived artifact is stamped with the configuration it was
produced under.

Omit a block to leave it unchanged. A block you *do* send replaces its previous
value entirely, so read the workspace, edit the block, and send it back — a
partial block falls back to the schema defaults and would clear rules you meant
to keep.

**Request:**

```json theme={"dark"}
{
  "knowledge_graph": {
    "auto_build": true,
    "exclude_path_prefixes": ["drafts/"],
    "exclude_categories": []
  }
}
```

**Response (200 OK):** Full workspace object (same shape as Create response).

Builds are explicit by default: a graph is built when
[Build knowledge graph](/api-reference/knowledge-graph) asks for one. Setting
`auto_build` to `true` hands that trigger to ingestion instead, for a workspace that
wants a graph over everything it ingests and accepts one build per ingestion. Either
way a build already in flight keeps running — cancel its job if that is what you
meant.

**Errors:**

* **`workspace_not_found`** (404) — No such workspace for this key.
* **`workspace_deleting`** (409) — A teardown is in flight; the workspace accepts no further writes.
* **`invalid_request`** (422) — An unknown field, such as an attempt to rename the workspace.

***

## Get workspace stats

```
GET /v1/workspaces/{workspace_id}/stats
```

Compute workspace content statistics without paging through files.

**Response:**

```json theme={"dark"}
{
  "files_total": 42,
  "files_by_ingestion_status": {
    "discovered": 2,
    "queued": 0,
    "ingesting": 0,
    "ingested": 38,
    "stale": 1,
    "expired": 1,
    "failed": 0,
    "not_required": 0
  },
  "bytes_total": 524288000,
  "pages_ingested": 4200,
  "index_documents": 4200,
  "kg_nodes": 1247,
  "kg_edges": 3156,
  "last_ingestion_at": "2026-08-09T11:30:00Z",
  "last_reconciliation_at": null,
  "next_expiry_at": null,
  "files_expiring_within_7d": 0
}
```

`stale > 0` means observed and ingested hashes diverged — run an ingestion with
`selection.stale_only=true` to catch up. `expired > 0` means derived representations
were dropped by retention; re-ingest to make those files searchable again.

***

## Delete workspace

```
DELETE /v1/workspaces/{workspace_id}
```

Permanently delete a workspace and all of its content. **Irreversible.**

`confirm_name` must equal the workspace's name exactly; this is deliberate friction.
The teardown (storage prefix, search index, graph nodes) runs asynchronously — poll
the returned job.

**Request:**

```json theme={"dark"}
{
  "confirm_name": "q4-2025-audit"
}
```

**Response (202 Accepted):** A `Job` with `kind: "workspace_delete"`.

**Errors:**

* **`workspace_deleting`** (409) — A teardown is already in flight.
* **`invalid_request`** (422) — `confirm_name` does not match the workspace name.
