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

# Organizing documents with workspaces

> Partition your corpus into isolated containers per team, customer, or tenant, then scope search and ask to a workspace.

A workspace is a container for documents. Every file lives in exactly one workspace, set at upload time. Workspaces are the top-level partition of your corpus: one per team, customer, or tenant. They're the natural fit for multi-tenant products, where each customer's documents must stay isolated from everyone else's.

> *"Search only this customer's documents."*
>
> *"Answer using just the engineering team's files."*

Where [tags](/tutorials/tags) and [facets](/tutorials/facets/overview) layer labels and metadata *across* your corpus, workspaces draw hard boundaries *through* it.

<Note>
  The workspace endpoints are in **alpha** and may change. Creating and managing workspaces from the API requires the appropriate role; some companies provision workspaces through the console instead. You can always upload into and search existing workspaces by `workspace_id`.
</Note>

<Tip>
  This tutorial covers [`POST /api/v3/workspaces`](/api-reference/workspaces/create-a-workspace), [`GET /api/v3/workspaces`](/api-reference/workspaces/list-workspaces), [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace), and [`DELETE /api/v3/workspaces/{id}`](/api-reference/workspaces/delete-a-workspace). Uploading files into a workspace is covered in [Uploading & managing files](/tutorials/files).
</Tip>

## Workspaces, tags, or facets?

All three organise documents, but at different layers. They compose: a file lives in one workspace, can carry several tags, and can be classified with facets.

|                        | Workspaces                                                     | Tags                                       | Facets                                     |
| ---------------------- | -------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------ |
| **What it is**         | A container; every file lives in exactly one                   | Flat, reusable labels                      | Typed, hierarchical metadata with a schema |
| **A file belongs to**  | exactly one workspace                                          | many tags, **across workspaces**           | many content types, **across workspaces**  |
| **Best for**           | Isolating teams, customers, tenants                            | Cross-cutting collections (project, topic) | Precise structured queries                 |
| **Setup cost**         | Create a workspace                                             | Create a tag                               | Design a content-type tree                 |
| **Scope a query with** | `workspace_id`                                                 | `tag_id`                                   | `content_type` / `attribute`               |
| **Access control**     | Yes: API keys can be scoped to a workspace with a per-key role | No: not a permission boundary              | No: not a permission boundary              |

Only workspaces are a permission boundary: you can issue [API keys scoped to specific workspaces](#segmenting-access-with-scoped-api-keys), which makes them the right tool for segmenting data that needs different permission levels. `workspace_id` and `tag_id` can be combined in a search or ask to narrow within a workspace; `file_id` is mutually exclusive with both. The rest of this tutorial covers workspaces.

## Step 1: Create a workspace

A workspace needs a `name`. The caller is automatically added as its owner.

```python title="create_workspace.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

response = requests.post(
    "https://api.lighton.ai/api/v3/workspaces",
    headers=headers,
    json={
        "name": "Engineering Team",
        "description": "Documents owned by the engineering org",
    },
)
print(response.json())
```

A `201` returns the new workspace with its `id`. You'll pass that ID when uploading files and when scoping queries.

```json theme={null}
{
  "id": 42,
  "name": "Engineering Team",
  "workspace_type": "shared",
  "document_upload_method": "manual",
  "description": "Documents owned by the engineering org",
  "files_count": 0,
  "user_role": "owner"
}
```

A `403` means workspace creation is disabled for your company and you're not an admin: create it from the console instead, or ask an admin.

## Step 2: Find a workspace's ID

List the workspaces you're a member of to grab an ID. Filter by `name`, or by `workspace_type`, `user_role`, and `document_upload_method`.

```python title="list_workspaces.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

# Workspaces you're a member of, optionally filtered by name
response = requests.get(
    "https://api.lighton.ai/api/v3/workspaces",
    headers=headers,
    params={"name": "engineering"},
)
print(response.json())
```

The response is paginated. Each workspace carries a `files_count` and your `user_role` (`owner`, `editor`, or `viewer`).

```json theme={null}
{
  "count": 1,
  "results": [
    {
      "id": 42,
      "name": "Engineering Team",
      "workspace_type": "shared",
      "files_count": 128,
      "user_role": "owner"
    }
  ]
}
```

## Step 3: Put files in a workspace

A file is assigned to its workspace at upload time: pass `workspace_id` in the upload payload. A file's workspace is fixed; there's no "move file" operation. See [Uploading & managing files](/tutorials/files#upload-a-file) for the full upload flow.

To pull back every file in a workspace, filter the file listing:

```python title="list_files_in_workspace.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

# Every file in a workspace
response = requests.get(
    "https://api.lighton.ai/api/v3/files",
    headers=headers,
    params={"workspace_id": "42"},
)
print(response.json())
```

<Tip>
  To populate a workspace automatically from Google Drive, SharePoint, ServiceNow, or a web scraper, convert it to a **synced** workspace with a `datasource` payload on [`PATCH /api/v3/workspaces/{id}`](/api-reference/workspaces/update-a-workspace). The workspace must be empty first.
</Tip>

## Step 4: Scope search and ask to a workspace

Both [`POST /api/v3/search`](/tutorials/search) and [`POST /api/v3/ask`](/tutorials/ask) accept a `workspace_id` array that restricts the query to those workspaces. Pass several IDs to query across a set of them.

Search within a workspace:

```python title="search_by_workspace.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

# Search only within these workspaces (pass several IDs to widen the scope)
response = requests.post(
    "https://api.lighton.ai/api/v3/search",
    headers=headers,
    json={
        "query": "deployment runbook",
        "workspace_id": [42],
    },
)
print(response.json())
```

Ask a grounded question within a workspace:

```python title="ask_by_workspace.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

# Grounded answer over only the documents in these workspaces
response = requests.post(
    "https://api.lighton.ai/api/v3/ask",
    headers=headers,
    json={
        "query": "What is our incident escalation policy?",
        "workspace_id": [42],
    },
)
print(response.json())
```

Combine `workspace_id` with `tag_id` to scope to a tagged collection *within* a workspace.

## Step 5: Rename or delete a workspace

Only **custom** workspaces can be updated, and only by their owner. Personal and company workspaces are managed by the platform.

```python title="rename_workspace.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

workspace_id = 42  # replace with your workspace ID

# Only CUSTOM workspaces can be updated, and only by their owner
response = requests.patch(
    f"https://api.lighton.ai/api/v3/workspaces/{workspace_id}",
    headers=headers,
    json={"name": "Engineering (EMEA)"},
)
print(response.json())
```

Deleting a workspace removes it and every document inside it, so use it deliberately.

```python title="delete_workspace.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

workspace_id = 42  # replace with your workspace ID

# Deletes the workspace and every document it contains
response = requests.delete(
    f"https://api.lighton.ai/api/v3/workspaces/{workspace_id}",
    headers=headers,
)
print(response.status_code)
```

A `204` confirms the workspace and its contents are gone.

## Segmenting access with scoped API keys

Workspaces aren't only an organisational boundary, they're a **permission boundary**. You can mint an API key that is scoped to specific workspaces, so the key can only read or write the documents in those workspaces and nothing else. This is what makes workspaces the right tool when different data needs different permission levels.

Pass a `scopes` list to [`POST /api/v3/keys`](/api-reference/api-keys/create-api-key). Each entry pairs a `workspace_id` with a `role`, one of `viewer`, `editor`, or `owner`. The requested role on a workspace is capped at the role you currently hold there, so you can't hand out more access than you have.

```python title="create_scoped_api_key.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

# Scope the key to a workspace with a per-workspace role (viewer | editor | owner).
# The requested role is capped at the role you hold on that workspace.
response = requests.post(
    "https://api.lighton.ai/api/v3/keys",
    headers=headers,
    json={
        "name": "Acme tenant key (read-only)",
        "expires_at": None,  # or an ISO 8601 datetime to auto-expire
        "scopes": [{"workspace_id": 42, "role": "viewer"}],
    },
)
print(response.json())  # the full `key` value is returned only once
```

The `key` value is returned **only once**, on creation, so store it immediately.

A few patterns this unlocks:

* **Multi-tenant isolation:** give each customer a key scoped to their own workspace. A request made with that key can never reach another tenant's documents, even if your application code has a bug.
* **Read-only integrations:** issue a `viewer`-scoped key to a dashboard or analytics job that should search and ask but never upload or delete.
* **Per-environment separation:** scope staging and production keys to different workspaces so a misconfigured job can't touch the wrong corpus.

Because a single key can list several scopes with **different roles per workspace**, you can grant, say, `editor` on one workspace and `viewer` on another with one key. An unscoped key, by contrast, inherits the full permissions of its owner across every workspace, so prefer scoped keys whenever a credential is handed to a narrower-trust consumer.

<Note>
  Tags and facets don't carry their own permissions: a key that can see a workspace can see every tag and facet on the files inside it. When access control is the goal, segment with workspaces.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Uploading & managing files" href="/tutorials/files">
    Upload documents into a workspace and manage file metadata
  </Card>

  <Card title="Organizing documents with tags" href="/tutorials/tags">
    Group files into collections that cut across workspaces
  </Card>

  <Card title="Searching documents" href="/tutorials/search">
    Rank passages, then scope the search by workspace
  </Card>

  <Card title="Asking questions" href="/tutorials/ask">
    Get grounded answers over a workspace
  </Card>
</CardGroup>
