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

# Building a classification tree

> Create content types and custom attributes: the schema that powers all facet queries.

<Tip>
  This tutorial uses [`POST /api/v3/content-types`](/api-reference/facets/manage-content-types-and-attributes) and [`GET /api/v3/content-types`](/api-reference/facets/list-content-types). The full schema lives in the [API reference](/api-reference/introduction).
</Tip>

After this tutorial you'll have a content type tree with custom attributes, the schema that powers all facet queries. You define this once at the company level, then apply it to as many files as you need.

There are two ways to build your schema:

1. **Adopt starter templates:** import ready-made trees for common verticals and customize them
2. **Build from scratch:** create your own trees, nodes, and attributes entirely

## Option A: Adopt starter templates

LightOn ships with starter templates for legal, finance, healthcare, tech, and manufacturing. Browse what's available, adopt the ones you need, then customize.

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

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

# Browse available starter templates (roots only)
response = requests.get(
    "https://api.lighton.ai/api/v3/content-types/templates",
    headers=headers,
    params={"depth": 0},
)

for tree in response.json().get("content_types", []):
    print(f"{tree['code']:20s} {tree['label']}")
```

This lists all available template roots. To adopt one or more:

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

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

# Adopt the legal and finance starter trees
response = requests.post(
    "https://api.lighton.ai/api/v3/content-types",
    headers=headers,
    json={"action": "adopt", "content_types": ["legal", "finance"]},
)
print(response.json())
```

Once adopted, the trees become **your company's own** content types, fully editable. You can rename nodes, add children, define new attributes, or delete what you don't need using the same actions described below.

Here's what you get with **legal** and **finance**:

```mermaid theme={null}
graph TD
    legal["legal"]
    contract["legal:contract"]
    nda["legal:contract:nda"]
    sla["legal:contract:sla"]
    msa["legal:contract:msa"]
    litigation["legal:litigation"]
    compliance["legal:compliance"]
    legal --> contract
    legal --> litigation
    legal --> compliance
    contract --> nda
    contract --> sla
    contract --> msa
```

Each node carries its own attributes and inherits from its parent. For example, `legal` defines `confidentiality_level` and `jurisdiction`. `legal:contract` adds `parties`, `contract_value`, and `effective_date`. `legal:contract:nda` adds `counterparty`, `is_mutual`, and `duration_years`, giving it 8 attributes total.

Here's a second schema with its attributes. A file can be classified under multiple trees at once: for example, an NDA could be both a `legal:contract:nda` and a `finance:investment:due-diligence` if it's part of an investment deal.

```mermaid theme={null}
graph TD
    finance["finance<br/><i>currency · fiscal_year</i>"]:::fin
    reporting["finance:reporting<br/><i>+ reporting_date · fiscal_period · is_audited</i>"]:::fin
    investment["finance:investment<br/><i>+ asset_class · investment_value</i>"]:::fin
    dd["finance:investment:due-diligence<br/><i>+ target_company</i>"]:::fin
    memo["finance:investment:investment-memo<br/><i>+ recommendation</i>"]:::fin
    risk["finance:risk<br/><i>+ regulatory_framework · risk_level</i>"]:::fin
    treasury["finance:treasury<br/><i>+ instrument_type</i>"]:::fin
    finance --> reporting
    finance --> investment
    finance --> risk
    finance --> treasury
    investment --> dd
    investment --> memo
    classDef fin fill:#e8f4f8,stroke:#1f99bb
```

<Note>
  **Adoption behavior:**

  * Trees are adopted **wholesale**: you cannot pick individual branches from a template.
  * Adoption is **idempotent**: re-adopting the same tree updates existing nodes based on their path. It does not auto-delete nodes you've removed or added since the last adoption.
  * After adoption, all content types are fully yours. The link to the original template is gone.
</Note>

## Option B: Build from scratch

Here's what we're going to build:

```mermaid theme={null}
graph TD
    contract["<b>contract</b><br/>counterparty · jurisdiction · effective_date · signed"]
    contract --> nda["<b>contract:nda</b><br/>is_mutual · duration_years"]
    contract --> sa["<b>contract:service-agreement</b><br/>contract_value · payment_terms"]
    sa --> fp["<b>contract:service-agreement:fixed-price</b>"]
```

All writes go to a single endpoint with an `action` field. Every action is **idempotent** and safe to replay. You can send a single action object or a **JSON array** to batch multiple actions in one call.

## Step 1: Create your first Content Type

Start from zero and create a root content type called `contract`.

```python title="define_root_content_type.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/content-types",
    headers=headers,
    json={
        "action": "define_content_type",
        "code": "contract",
        "label": "Contract",
        "description": "Any legally binding agreement between two or more parties",
    },
)
print(response.json())
```

Returns `201 Created` on first call, `200 OK` if the node already exists (idempotent update). This makes scripts safe to run in CI pipelines or onboarding automations.

`path` is the unique identifier you'll use everywhere: to classify files, define attributes, and filter searches. For a root node, `path` equals `code`.

`inherit_attributes` defaults to `true`, which means any attributes you define on `contract` are automatically available to all child types (`contract:nda`, `contract:service-agreement`, etc.). You define shared fields once on the parent.

## Step 2: Build a tree of Content Types

Add child nodes. The path separator is `:`. LightOn builds the full path for you from `parent_path` + `code`.

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

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}
url = "https://api.lighton.ai/api/v3/content-types"

child_types = [
    {
        "action": "define_content_type",
        "parent_path": "contract",
        "code": "nda",
        "label": "Non-Disclosure Agreement",
        "description": "Confidentiality agreement preventing disclosure of proprietary information",
    },
    {
        "action": "define_content_type",
        "parent_path": "contract",
        "code": "service-agreement",
        "label": "Service Agreement",
        "description": "Agreement defining the delivery of services between a provider and a client",
    },
    {
        "action": "define_content_type",
        "parent_path": "contract:service-agreement",
        "code": "fixed-price",
        "label": "Fixed Price",
        "description": "Service agreement with a fixed total price, regardless of time spent",
    },
]

for payload in child_types:
    response = requests.post(url, headers=headers, json=payload)
    print(response.json())
```

Your tree is now:

```
contract
  contract:nda
  contract:service-agreement
    contract:service-agreement:fixed-price
```

You can go up to 4 levels deep. A company can have multiple independent trees (different roots). Each tree is completely isolated.

### Read your classification tree at any time

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

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

response = requests.get(
    "https://api.lighton.ai/api/v3/content-types",
    headers=headers,
    params={"path": "contract", "depth": 2, "include_attributes": False},
)
print(response.json())
```

## Step 3: Define attributes

Attributes are the typed fields you want to store per document. Define them on a node, and if `inherit_attributes: true`, all children get them too.

See [Rules & constraints](/tutorials/facets/rules#my-attribute-wont-save) for naming restrictions, type immutability, and other validation rules.

### Shared attributes on `contract` (inherited by all subtypes)

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

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}
url = "https://api.lighton.ai/api/v3/content-types"

attributes = [
    {
        "action": "define_attribute",
        "content_type_path": "contract",
        "name": "counterparty",
        "label": "Counterparty",
        "attribute_type": "text",
        "description": "Full legal name of the other party in the agreement",
        "required": True,
    },
    {
        "action": "define_attribute",
        "content_type_path": "contract",
        "name": "jurisdiction",
        "label": "Jurisdiction",
        "attribute_type": "multi-select",
        "description": "Countries whose law governs this agreement",
        "choices": ["FR", "US", "UK", "DE", "CH", "BE", "NL"],
    },
    {
        "action": "define_attribute",
        "content_type_path": "contract",
        "name": "effective_date",
        "label": "Effective Date",
        "attribute_type": "date",
        "description": "Date from which the contract is legally binding",
    },
    {
        "action": "define_attribute",
        "content_type_path": "contract",
        "name": "signed",
        "label": "Signed",
        "attribute_type": "boolean",
        "description": "Whether the contract has been signed by all parties",
    },
]

for payload in attributes:
    response = requests.post(url, headers=headers, json=payload)
    print(response.json())
```

Because `contract` has `inherit_attributes: true`, every child type (`contract:nda`, `contract:service-agreement`, etc.) automatically exposes these four attributes. You don't need to redefine them.

### NDA-specific attributes (only on `contract:nda`)

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

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}
url = "https://api.lighton.ai/api/v3/content-types"

attributes = [
    {
        "action": "define_attribute",
        "content_type_path": "contract:nda",
        "name": "is_mutual",
        "label": "Mutual",
        "attribute_type": "boolean",
        "description": "True if confidentiality obligations apply to both parties",
    },
    {
        "action": "define_attribute",
        "content_type_path": "contract:nda",
        "name": "duration_years",
        "label": "Duration (years)",
        "attribute_type": "number",
        "description": "Number of years the confidentiality obligation lasts",
    },
]

for payload in attributes:
    response = requests.post(url, headers=headers, json=payload)
    print(response.json())
```

### Service agreement-specific attributes

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

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}
url = "https://api.lighton.ai/api/v3/content-types"

attributes = [
    {
        "action": "define_attribute",
        "content_type_path": "contract:service-agreement",
        "name": "contract_value",
        "label": "Contract Value (EUR)",
        "attribute_type": "number",
        "description": "Total value of the service agreement in euros",
    },
    {
        "action": "define_attribute",
        "content_type_path": "contract:service-agreement",
        "name": "payment_terms",
        "label": "Payment Terms",
        "attribute_type": "select",
        "description": "How payment is structured over the duration of the agreement",
        "choices": ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"],
    },
]

for payload in attributes:
    response = requests.post(url, headers=headers, json=payload)
    print(response.json())
```

## How inheritance works

Attribute inheritance is resolved **at query time**: when you read a file's facets, LightOn walks up the tree and collects attributes from each ancestor with `inherit_attributes: true`. Adding an attribute to a parent immediately makes it available on all descendants, no migration needed.

For details on breaking the chain or shadowing inherited attributes, see [Rules & constraints](/tutorials/facets/rules#how-does-inheritance-work).

## Full script: build the contract classification tree in one batch call

All schema operations can be batched in a single request by sending an `{"actions": [...]}` object to the `/batch` endpoint. LightOn processes them in order and fails fast on the first error. This is the recommended approach for onboarding automations.

```python title="batch_tree.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/content-types/batch",
    headers=headers,
    json={
        "actions": [
            {"action": "define_content_type", "code": "contract", "label": "Contract", "description": "Any legally binding agreement between two or more parties"},
            {"action": "define_content_type", "parent_path": "contract", "code": "nda", "label": "Non-Disclosure Agreement", "description": "Confidentiality agreement preventing disclosure of proprietary information"},
            {"action": "define_content_type", "parent_path": "contract", "code": "service-agreement", "label": "Service Agreement", "description": "Agreement defining the delivery of services between a provider and a client"},
            {"action": "define_content_type", "parent_path": "contract:service-agreement", "code": "fixed-price", "label": "Fixed Price", "description": "Service agreement with a fixed total price regardless of time spent"},
            {"action": "define_attribute", "content_type_path": "contract", "name": "counterparty", "label": "Counterparty", "attribute_type": "text", "description": "Full legal name of the other party in the agreement", "required": True},
            {"action": "define_attribute", "content_type_path": "contract", "name": "jurisdiction", "label": "Jurisdiction", "attribute_type": "multi-select", "description": "Countries whose law governs this agreement", "choices": ["FR", "US", "UK", "DE", "CH", "BE", "NL"]},
            {"action": "define_attribute", "content_type_path": "contract", "name": "effective_date", "label": "Effective Date", "attribute_type": "date", "description": "Date from which the contract is legally binding"},
            {"action": "define_attribute", "content_type_path": "contract", "name": "signed", "label": "Signed", "attribute_type": "boolean", "description": "Whether the contract has been signed by all parties"},
            {"action": "define_attribute", "content_type_path": "contract:nda", "name": "is_mutual", "label": "Mutual", "attribute_type": "boolean", "description": "True if confidentiality obligations apply to both parties"},
            {"action": "define_attribute", "content_type_path": "contract:nda", "name": "duration_years", "label": "Duration (years)", "attribute_type": "number", "description": "Number of years the confidentiality obligation lasts"},
            {"action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "contract_value", "label": "Contract Value (EUR)", "attribute_type": "number", "description": "Total value of the service agreement in euros"},
            {"action": "define_attribute", "content_type_path": "contract:service-agreement", "name": "payment_terms", "label": "Payment Terms", "attribute_type": "select", "description": "How payment is structured over the duration of the agreement", "choices": ["monthly", "quarterly", "milestone", "upfront", "upon-delivery"]},
        ],
    },
)
print(response.json())
```

The response is a `{"results": [...]}` object with one entry per action, in order. Each entry has `status` (`201` created, `200` updated) and a `data` object.

If any action fails, the batch stops at that point. Actions before the failure are committed; actions after are not.

## Verify: read your complete classification tree

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

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

response = requests.get(
    "https://api.lighton.ai/api/v3/content-types",
    headers=headers,
    params={"path": "contract", "include_attributes": True},
)
print(response.json())
```

The response shows own attributes per node. Inherited attributes are resolved when you read a file's facets, not pre-expanded here.

## Action reference

| Action                  | What it does                                                               |
| ----------------------- | -------------------------------------------------------------------------- |
| `define_content_type`   | Create or update a content type node (idempotent)                          |
| `undefine_content_type` | Delete a node, its subtree, and all file classifications + values under it |
| `define_attribute`      | Create or update an attribute on a content type (idempotent)               |
| `undefine_attribute`    | Delete an attribute and all its stored values across all files             |
