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

# Classify files and set attribute values

> Turn documents into structured, queryable data.

<Tip>
  This tutorial uses [`POST /api/v3/files/{file_id}/facets`](/api-reference/facets/classify-file-and-set-attribute-values) and [`GET /api/v3/files/{file_id}/facets`](/api-reference/facets/list-file-classifications-and-attribute-values). The full schema lives in the [API reference](/api-reference/introduction).
</Tip>

Your classification tree exists. Now you'll classify actual documents and fill in their metadata, turning unstructured files into queryable, structured data.

Applying facets to a document is a two-step act:

1. **Classify:** tell LightOn *what type* this document is (`contract:nda`)
2. **Set values:** fill in the attribute values for that classification (`counterparty`, `jurisdiction`, ...)

All actions are **idempotent**. Send a **JSON array** to batch multiple actions. LightOn queues exactly one BM25 reindex per batch, regardless of how many actions it contains.

## Step 1: Upload a document

Before classifying, you need a file. If you already have one, skip this step and use your existing `file_id`.

<Note>
  Replace `workspace_id` with your own. You can list your workspaces via [`GET /api/v3/workspaces`](/api-reference/workspaces/list-workspaces).
</Note>

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

  workspace_id = 42  # replace with your workspace ID

  with LightOn() as client:  # reads LIGHTON_API_KEY from the environment
      workspace = Workspace.get(client, workspace_id)
      # Uploading is the ingestion. Non-blocking: the returned File is "pending".
      # Pass wait=True to block until it's embedded and searchable.
      doc = workspace.ingest(
          File(path="acme_nda_2025.pdf", title="NDA with Acme Corp")
      )
      print(doc.id, doc.status)
  ```

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

  with open("acme_nda_2025.pdf", "rb") as f:
      response = requests.post(
          "https://api.lighton.ai/api/v3/files",
          headers=headers,
          files={"file": f},
          data={"workspace_id": workspace_id, "title": "NDA with Acme Corp"},
      )
  print(response.json())
  ```
</CodeGroup>

The response has an `id` field: that's your `file_id`. Ingestion is asynchronous (`status: pending`), but you don't need to wait before classifying.

## Step 2: Classify the document

Tell LightOn this file is a `contract:nda`:

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

  file_id = 1234  # replace with your file ID

  with LightOn() as client:
      doc = File.get(client, file_id)
      doc.classify("contract:nda")
      print(f"classified file {doc.id} as contract:nda")
  ```

  ```python Plain Python theme={null}
  import os
  import requests

  file_id = 1234  # replace with your file ID
  headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

  response = requests.post(
      f"https://api.lighton.ai/api/v3/files/{file_id}/facets",
      headers=headers,
      json={"action": "classify", "content_type_path": "contract:nda"},
  )
  print(response.json())
  ```
</CodeGroup>

Returns `201 Created` (or `200` if already classified, since classify is idempotent). This is lightweight: it only creates the link between the file and the content type path. No attribute values are set yet.

A file can hold classifications from multiple trees, but only **one per tree**. If you need to reclassify within the same tree, see [Rules & constraints](/tutorials/facets/rules#my-classification-was-rejected) for what's allowed.

## Step 3: Set attribute values

Fill in the structured metadata. Notice that you can set attributes like `counterparty` and `jurisdiction` on `contract:nda` even though they were defined on the parent `contract` node. This works because `inherit_attributes` is `true` in the classification tree you built earlier.

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

  file_id = 1234  # replace with your file ID

  with LightOn() as client:
      doc = File.get(client, file_id)
      # One request per attribute; the value type follows the attribute definition
      doc.set_attribute("contract:nda", "counterparty", "Acme Corp")
      doc.set_attribute("contract:nda", "jurisdiction", ["FR", "DE"])
      doc.set_attribute("contract:nda", "effective_date", "2025-03-01")
      doc.set_attribute("contract:nda", "signed", True)
      doc.set_attribute("contract:nda", "is_mutual", True)
      doc.set_attribute("contract:nda", "duration_years", 3)
      print(f"set 6 attributes on file {doc.id}")
  ```

  ```python Plain Python theme={null}
  import os
  import requests

  file_id = 1234  # replace with your file ID
  headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}
  url = f"https://api.lighton.ai/api/v3/files/{file_id}/facets"

  actions = [
      {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "counterparty", "value": "Acme Corp"},
      {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "jurisdiction", "value": ["FR", "DE"]},
      {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "effective_date", "value": "2025-03-01"},
      {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "signed", "value": True},
      {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "is_mutual", "value": True},
      {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "duration_years", "value": 3},
  ]

  for payload in actions:
      response = requests.post(url, headers=headers, json=payload)
      print(response.json())
  ```
</CodeGroup>

The file must be classified before you can set values. Each attribute type enforces strict validation. See [Rules & constraints](/tutorials/facets/rules#my-value-was-rejected) for the full type-by-type reference.

## The efficient way: batch everything in one call

In production, always batch the classify + all `set_value` actions together. This triggers exactly one BM25 reindex, not seven.

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

  file_id = 1234  # replace with your file ID

  with LightOn() as client:
      doc = File.get(client, file_id)
      # The SDK sends one request per action. To classify and set every value in a
      # single round trip, call /facets/batch directly (see the Plain Python tab).
      doc.classify("contract:nda")
      doc.set_attribute("contract:nda", "counterparty", "Acme Corp")
      doc.set_attribute("contract:nda", "jurisdiction", ["FR", "DE"])
      doc.set_attribute("contract:nda", "effective_date", "2025-03-01")
      doc.set_attribute("contract:nda", "signed", True)
      doc.set_attribute("contract:nda", "is_mutual", True)
      doc.set_attribute("contract:nda", "duration_years", 3)
      print(f"classified and populated file {doc.id}")
  ```

  ```python Plain Python theme={null}
  import os
  import requests

  file_id = 1234  # replace with your file ID
  headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

  response = requests.post(
      f"https://api.lighton.ai/api/v3/files/{file_id}/facets/batch",
      headers=headers,
      json={
          "actions": [
              {"action": "classify", "content_type_path": "contract:nda"},
              {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "counterparty", "value": "Acme Corp"},
              {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "jurisdiction", "value": ["FR", "DE"]},
              {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "effective_date", "value": "2025-03-01"},
              {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "signed", "value": True},
              {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "is_mutual", "value": True},
              {"action": "set_value", "content_type_path": "contract:nda", "attribute_name": "duration_years", "value": 3},
          ],
      },
  )
  print(response.json())
  ```
</CodeGroup>

## Step 4: Read back the full facets on a file

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

  file_id = 1234  # replace with your file ID

  with LightOn() as client:
      doc = File.get(client, file_id)
      # One Facet per assigned content type, each with its attribute values
      for facet in doc.facets():
          print(facet.path, {a.name: a.value for a in facet.attributes})
  ```

  ```python Plain Python theme={null}
  import os
  import requests

  file_id = 1234  # replace with your file ID
  headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

  response = requests.get(
      f"https://api.lighton.ai/api/v3/files/{file_id}/facets",
      headers=headers,
  )
  print(response.json())
  ```
</CodeGroup>

The response includes:

* `labels`: breadcrumb from root to leaf (e.g. `["Contract", "Non-Disclosure Agreement"]`), useful for display in a UI
* Attributes from parent nodes (`contract`), like `counterparty` and `jurisdiction`, are included alongside attributes defined directly on `contract:nda`, like `is_mutual` and `duration_years`
* `can_edit: true`: whether the current user can modify this file's facets

## Multi-classification: a document with two types

A file can be classified under multiple content types, but only from **different trees**. You cannot have two classifications from the same tree (see [Rules & constraints](/tutorials/facets/rules#my-classification-was-rejected)).

For example, if you have a second tree called `regulation`, you can classify a file as both `contract:nda` and `regulation:gdpr`:

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

  file_id = 1234  # replace with your file ID

  with LightOn() as client:
      doc = File.get(client, file_id)

      # The file is already classified as contract:nda (what it IS).
      # Add a second classification from a different tree: the investment team
      # also tracks this NDA as part of due diligence on Acme Corp (what it's FOR).
      doc.classify("finance:investment:due-diligence")
      doc.set_attribute(
          "finance:investment:due-diligence", "target_company", "Acme Corp"
      )
      print([facet.path for facet in doc.facets()])
  ```

  ```python Plain Python theme={null}
  import os
  import requests

  file_id = 1234  # replace with your file ID
  headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

  # The file is already classified as contract:nda (what it IS).
  # Add a second classification from a different tree: the investment team
  # also tracks this NDA as part of due diligence on Acme Corp (what it's FOR).

  response = requests.post(
      f"https://api.lighton.ai/api/v3/files/{file_id}/facets/batch",
      headers=headers,
      json={
          "actions": [
              {"action": "classify", "content_type_path": "finance:investment:due-diligence"},
              {"action": "set_value", "content_type_path": "finance:investment:due-diligence", "attribute_name": "target_company", "value": "Acme Corp"},
          ],
      },
  )
  print(response.json())
  ```
</CodeGroup>

The file now has two classifications from two independent trees. Each classification has its own set of attribute values. They don't interact.

## Remove a classification or a value

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

  file_id = 1234  # replace with your file ID

  with LightOn() as client:
      doc = File.get(client, file_id)
      # Clear one value but keep the classification
      doc.clear_attribute("contract:nda", "duration_years")
      # Or drop the classification entirely, along with all its values
      doc.unclassify("contract:nda")
      print([facet.path for facet in doc.facets()])
  ```

  ```python Plain Python theme={null}
  import os
  import requests

  file_id = 1234  # replace with your file ID
  headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}
  url = f"https://api.lighton.ai/api/v3/files/{file_id}/facets"

  response_clear = requests.post(
      url,
      headers=headers,
      json={"action": "clear_value", "content_type_path": "contract:nda", "attribute_name": "duration_years"},
  )

  response_unclassify = requests.post(
      url,
      headers=headers,
      json={"action": "unclassify", "content_type_path": "contract:nda"},
  )

  print(response_clear.status_code, response_unclassify.status_code)
  ```
</CodeGroup>

Both actions return `204 No Content` on success. `unclassify` cascades: it removes the classification and all its attribute values. See [Rules & constraints](/tutorials/facets/rules#what-happens-when-i-delete-something) for all cascade behaviors.

## Action reference

| Action        | What it does                                                        |
| ------------- | ------------------------------------------------------------------- |
| `classify`    | Assign a content type path to a file (idempotent)                   |
| `unclassify`  | Remove a classification and cascade-delete all its attribute values |
| `set_value`   | Set an attribute value on a classified file (idempotent)            |
| `clear_value` | Remove a single attribute value                                     |
