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

# Rules & constraints

> Troubleshoot errors and understand how Facets behaves when you hit a limit.

This page covers every constraint enforced by the Facets API, organized by the problem you're trying to solve. If your request failed or something behaved unexpectedly, find your scenario below.

## My content type won't create

You're calling `define_content_type` and getting an error.

| What you tried                                            | Why it failed                                                                                                               | Error |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----- |
| Used uppercase, spaces, or underscores in `code`          | Codes must be lowercase alphanumeric with hyphens only: `^[a-z0-9]+(?:-[a-z0-9]+)*$`. Examples: `nda`, `service-agreement`. | `422` |
| Created a child with the same code as an existing sibling | Codes must be unique among siblings (same parent).                                                                          | `409` |
| Created a root with a code that already exists            | Root codes must be unique per company.                                                                                      | `409` |
| Created a path that already exists (e.g. `contract:nda`)  | Full paths must be unique per company.                                                                                      | `409` |
| Added a 5th level to the tree                             | Trees can be at most **4 levels deep**.                                                                                     | `400` |
| Path exceeds 768 characters                               | Maximum path length is 768 characters.                                                                                      | `400` |

**Good to know:**

* A company can have multiple independent trees (different roots). They don't interact.
* Content types are company-scoped, not workspace-scoped.
* You **cannot reparent** a node. To move it, delete it with `undefine_content_type` and recreate it under the new parent. Deleting cascades: see [What happens when I delete something?](#what-happens-when-i-delete-something) below.

## My attribute won't save

You're calling `define_attribute` and getting an error.

| What you tried                                                | Why it failed                                                                                | Error |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----- |
| Changed an attribute's type (e.g. `text` → `number`)          | `attribute_type` is **immutable** once created. Delete the attribute and recreate it.        | `400` |
| Used a name that already exists on this content type          | Attribute names must be unique within a content type.                                        | `409` |
| Used a name that exists elsewhere in the same tree            | Names can't conflict across the tree. This prevents ambiguity when attributes are inherited. | `400` |
| Used a reserved name like `content_type`, `status`, or `page` | Some names are reserved by the system.                                                       | `400` |
| Created a `select` or `multi-select` without `choices`        | These types require a non-empty `choices` array.                                             | `400` |

**Good to know:**

* If you omit `label`, it's auto-generated from `name`: `fiscal_year` becomes `"Fiscal Year"`.
* 7 attribute types are available: `text`, `number`, `date`, `boolean`, `select`, `multi-select`, `rich-text`.

## My classification was rejected

You're calling `classify` on a file and getting an error.

| What you tried                                                                                                                             | Why it failed                                                                                                                        | Error |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ----- |
| Classified a file that's already classified in the **same tree** under a sibling path (e.g. `contract:nda` → `contract:service-agreement`) | A file can only have **one classification per tree**. You can't jump between siblings. Unclassify first, then classify the new path. | `400` |
| Called `set_value` on a file that isn't classified yet                                                                                     | You must classify a file before setting any attribute values.                                                                        | `400` |

### Reclassification: what's allowed

When a file is already classified and you call `classify` with a different path in the **same tree**, the result depends on the relationship:

| You're moving to...                                                | What happens                                                                                                      |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| The **same path**                                                  | Nothing changes. Idempotent, returns `200`.                                                                       |
| A **descendant** (e.g. `contract` → `contract:nda`)                | Auto-migrates. All attribute values are preserved.                                                                |
| An **ancestor** (e.g. `contract:nda` → `contract`)                 | Auto-migrates. Child-specific values (like `is_mutual`) are **deleted** because they don't exist on the ancestor. |
| A **sibling** (e.g. `contract:nda` → `contract:service-agreement`) | **Rejected.** Unclassify first, then classify the new path.                                                       |

A file **can** be classified under multiple trees at the same time (e.g. one `contract:nda` classification and one `regulation:gdpr` classification).

## My value was rejected

You're calling `set_value` and getting a `422`.

| Attribute type      | What's accepted                                      | What's rejected                      |
| ------------------- | ---------------------------------------------------- | ------------------------------------ |
| `text`, `rich-text` | String                                               | Anything else                        |
| `number`            | Integer, float, or numeric string (`50000`, `"1.5"`) | Booleans, non-numeric strings        |
| `date`              | ISO 8601 string, normalized to `YYYY-MM-DD`          | Non-date strings                     |
| `boolean`           | `true` or `false` (real booleans)                    | `"true"`, `1`, `"yes"`               |
| `select`            | Single string matching a defined choice              | String not in choices                |
| `multi-select`      | List of strings, each matching a defined choice      | Single string, values not in choices |

To **remove** a value, use `clear_value`. Passing `null` to `set_value` is rejected with `422`.

## What happens when I delete something?

Deleting things in Facets cascades. Here's exactly what gets removed:

| You delete...                                | What's also removed                                                                                   |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| A **content type** (`undefine_content_type`) | Its entire subtree, all file classifications referencing those paths, and all their attribute values. |
| A **classification** (`unclassify`)          | All attribute values stored under that classification on that file.                                   |
| An **attribute** (`undefine_attribute`)      | All stored values for that attribute across every file.                                               |
| **Choices** from a `select`/`multi-select`   | Any stored values that referenced the removed choices are deleted across all files.                   |

All deletions are **irreversible**.

## What can I filter on?

Not all attribute types support filtering via `GET /api/v3/files?attribute=`.

| Type           | Filterable? | Why                                             |
| -------------- | ----------- | ----------------------------------------------- |
| `text`         | Yes         | Indexed in `value_text`                         |
| `number`       | Yes         | Indexed in `value_number`                       |
| `date`         | Yes         | Indexed in `value_date`                         |
| `boolean`      | Yes         | Indexed in `value_boolean`                      |
| `select`       | Yes         | Indexed in `value_text`                         |
| `multi-select` | No          | Not indexed. Use `select` if you need filtering |
| `rich-text`    | No          | Not indexed. Use `text` if you need filtering   |

### Multi-select containment

When filtering on a `multi-select` attribute, the filter uses **containment**: `jurisdiction:FR` matches files with `["FR", "DE"]`, `["FR"]`, or any list containing `"FR"`.

### Date shortcuts and ambiguity

LightOn expands partial dates into ranges:

* `effective_date:2025` → any date in 2025 (Jan 1 – Dec 31)
* `effective_date:2025-03` → any date in March 2025
* `effective_date:2025-03-01` → exact date

A 4-digit value like `2025` is **ambiguous**: it could be a year, a number, or text. LightOn searches across all typed columns with an OR. Use full `YYYY-MM-DD` format for exact date matches.

## How does inheritance work?

Attribute inheritance is resolved **at query time**, not stored. When you read a file's facets, LightOn walks up the tree and collects attributes from each ancestor with `inherit_attributes: true`.

| Scenario                                                                | What happens                                                                                                   |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| You add an attribute to a parent node                                   | Immediately available on all descendants. No migration needed.                                                 |
| You set `inherit_attributes: false` on a node                           | Breaks the chain. That node and its descendants won't inherit attributes from above.                           |
| A child defines an attribute with the **same name** as an inherited one | The child's definition **shadows** the parent's. The child's type, choices, and required flag take precedence. |

## Batch behavior

When you send a JSON array of actions, LightOn processes them **in order** and **fails fast** on the first error:

* Actions before the failure are **committed**.
* Actions after the failure are **skipped**.
* BM25 reindex is scheduled **once per batch**, not per action.

All schema actions (`adopt`, `define_content_type`, `define_attribute`, `undefine_*`) are **idempotent** and safe to replay.
