> ## 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.

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import LightOn, Workspace

  with LightOn() as client:  # reads LIGHTON_API_KEY from the environment
      workspace = Workspace(
          name="Engineering Team",
          description="Documents owned by the engineering org",
      ).create(client)
      print(workspace.id, workspace.name)
  ```

  ```python Plain Python 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())
  ```
</CodeGroup>

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`.

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import LightOn, Workspace

  with LightOn() as client:
      # Workspaces you're a member of, optionally filtered by name.
      # Workspace.list follows pagination to the end.
      for workspace in Workspace.list(client, name="engineering"):
          print(workspace.id, workspace.name)
  ```

  ```python Plain Python 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())
  ```
</CodeGroup>

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:

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import File, LightOn

  workspace_id = 42  # replace with your workspace ID

  with LightOn() as client:
      # Every file in a workspace
      for doc in File.list(client, workspace_id=workspace_id):
          print(doc.id, doc.title, doc.status)
  ```

  ```python Plain Python 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())
  ```
</CodeGroup>

<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:

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import LightOn

  workspace_id = 42  # replace with your workspace ID

  with LightOn() as client:
      # Search only within these workspaces (pass several IDs to widen the scope)
      response = client.search("deployment runbook", workspaces=[workspace_id])
      for result in response.results:
          print(result.score, result.source.filename, result.content)
  ```

  ```python Plain Python 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())
  ```
</CodeGroup>

Ask a grounded question within a workspace:

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import LightOn

  workspace_id = 42  # replace with your workspace ID

  with LightOn() as client:
      # Grounded answer over only the documents in these workspaces
      response = client.ask(
          "What is our incident escalation policy?", workspaces=[workspace_id]
      )
      print(response.answer)
  ```

  ```python Plain Python 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())
  ```
</CodeGroup>

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.

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import LightOn, Workspace

  workspace_id = 42  # replace with your workspace ID

  with LightOn() as client:
      # Only CUSTOM workspaces can be updated, and only by their owner
      workspace = Workspace.get(client, workspace_id)
      workspace.name = "Engineering (EMEA)"
      workspace.save()
      print(workspace.id, workspace.name)
  ```

  ```python Plain Python 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())
  ```
</CodeGroup>

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

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import LightOn, Workspace

  workspace_id = 42  # replace with your workspace ID

  with LightOn() as client:
      # Deletes the workspace and every document it contains
      Workspace.get(client, workspace_id).delete()
      print(f"deleted workspace {workspace_id}")
  ```

  ```python Plain Python 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)
  ```
</CodeGroup>

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.

<CodeGroup>
  ```python Python SDK theme={null}
  from lighton import ApiKey, ApiKeyScope, LightOn, Role

  workspace_id = 42  # replace with your workspace ID

  with LightOn() as client:
      # 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.
      # Omit `scopes` for an unscoped key, or set `expires_at` to auto-expire it.
      key = ApiKey(
          name="Acme tenant key (read-only)",
          scopes=[ApiKeyScope(workspace_id=workspace_id, role=Role.viewer)],
      ).create(client)
      print(key.key.get_secret_value())  # the secret is returned only once
  ```

  ```python Plain Python 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
  ```
</CodeGroup>

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>
