# Choice Sets Reference

Reusable picklists that back `CHOICE_SET_SINGLE` and `CHOICE_SET_MULTIPLE` entity fields. Full CRUD via CLI — sets and their values.

> **Preview-then-confirm gate (data-fabric.md Rule 14).** Before invoking `choice-sets create` or `choice-set-values create`, show the full proposed set — name, displayName, description, and every value (`Name` + `DisplayName`) in creation order — and wait for explicit user approval. Value order matters: `NumberId` is assigned 0-based by creation order and is immutable.

## Commands

Every command below accepts `--folder-key <GUID>` for folder-scoped choice sets — required on writes against a folder-scoped set, recommended on reads. `list` additionally accepts `--include-folders` (mutually exclusive with `--folder-key`). See [Folder scope](#folder-scope) below for the picker rules.

| Command | Use |
|---------|-----|
| `uip df choice-sets list [--folder-key <…> \| --include-folders] --output json` | Find an existing choice set's `Id`. No flags → tenant only; `--folder-key` → that folder only; `--include-folders` → tenant + every visible folder. Each row carries `FolderId`. |
| `uip df choice-sets list-values <choice-set-id> [--folder-key <…>] --output json` | Page through values; pagination `{ Items, TotalCount, HasNextPage, … }` (use `--limit` / `--cursor` / `--offset`) |
| `uip df choice-sets create <name> [--folder-key <…>] [--display-name <…>] [--description <…>] --output json` | Create a choice set; response `Code: ChoiceSetCreated`, `Data.Id`. **Scope-bound**: pass `--folder-key` to place inside a folder; omit for tenant level. |
| `uip df choice-sets update <choice-set-id> [--folder-key <…>] --display-name <…> --description <…> --output json` | Rename / re-describe the set. **Both `--display-name` and `--description` are required on every call** — sending only `--description` returns *"DisplayName is required."*; sending only `--display-name` returns `Internal Server Error`. To change one, re-send the other's current value (run `choice-sets list` first to read it). |
| `uip df choice-sets delete <choice-set-id> [--folder-key <…>] --yes --reason "<why>" --output json` | Irreversible — `--yes` and `--reason` are required (`--confirm` accepted as deprecated alias) |
| `uip df choice-set-values create <choice-set-id> <name> [--folder-key <…>] [--display-name <…>] --output json` | Add a value; server assigns `NumberId` (0-based, monotonic by creation order) |
| `uip df choice-set-values update <choice-set-id> <value-id> "<new display name>" [--folder-key <…>] --output json` | Display-name only — `Name` and `NumberId` are immutable |
| `uip df choice-set-values delete <choice-set-id> --ids <value-id>[,<value-id>…] [--folder-key <…>] --yes --reason "<why>" --output json` | Irreversible — same gating as `choice-sets delete` |

## Folder scope

Choice sets live at the tenant level or inside a folder. The flag matrix mirrors entities:

| Goal | Flags |
|---|---|
| List only tenant-level choice sets | (none) — default |
| List a single folder's choice sets | `--folder-key <folder-guid>` |
| List tenant + every folder you can see | `--include-folders` |
| Create/update/delete a folder-scoped set or value | `--folder-key <folder-guid>` (required) |
| Read or operate on a tenant-scoped set | `--folder-key` is harmless when passed; the server resolves by UUID |

Bind a folder-scoped choice set to an entity in a different folder by passing only `choiceSetId` — the server resolves the choice-set's folder from that UUID. Do NOT pass `referenceFolderKey` on `CHOICE_SET_*` fields. The tenant ↔ folder boundary still applies (folder parent cannot bind a tenant-level choice set, and vice versa). See [`entity-schema.md` → Cross-folder references](entity-schema.md#cross-folder-references).

## Use the IDs

- `Id` from `list` → `choiceSetId` on the field definition.
- `NumberId` from `list-values` → the record value (integer for `_SINGLE`, integer array for `_MULTIPLE`). **0-based, set by creation order.**
- `Name` / `DisplayName` are human display — never write these on a record.

## Value `Name` validation

A choice-set value's `Name` must start with a letter and then contain letters and digits only, up to 250 characters. Underscores are rejected. Its `DisplayName` is limited to 500 characters. The server rejects C# / VB reserved keywords with *"Choiceset member name must … not be C# keyword"* — but this is a **separate code path from the entity/field-name validator (data-fabric.md Rule 4)**, and the two behave differently:

| Aspect | Entity / field name (Rule 4) | Choice-set value `Name` (here) |
|---|---|---|
| Case match | **case-insensitive** (`Class`, `class`, `CLASS` all rejected) | **case-sensitive** (`class` rejected, `Class` may pass — empirically verified: `New` accepted while `new` would be rejected) |
| Keyword list | full C#/VB reserved list — incl. `Select`, `Return`, `New`, `Internal`, … | partial list — some keywords missing (empirically `select` is NOT rejected as a choice-set value, but `Select` IS rejected as a field name) |

Do not assume a name is legal in one place because it's legal in the other. Use a descriptive alphanumeric token that is not itself a language keyword, such as `internalAudit`, `newLead`, or `classOption`. Move the human label to `DisplayName`: `Name: "internalAudit"` with `DisplayName: "Internal"`. Lowercase tokens that the choice-value validator rejects include `internal`, `public`, `private`, `class`, `case`, `new`, `default`, `static`, `void`, `event`, `lock`, `object`, `string`, and `int`.

## Sourcing `NumberId` after batch value creates

`NumberId` is assigned 0-based by creation order and is immutable, but the server does not always reserve a slot for a rejected `choice-set-values create` — a subsequent successful create can take the `NumberId` the failed one was meant to occupy. Treat the announced creation order as a proposal, not the authoritative mapping.

Two rules for any script that batch-creates values:

1. Fail loud on each `choice-set-values create`. Never redirect stderr to `/dev/null` or strip non-zero exits inside the loop — a silenced rejection shifts every later `NumberId` without surfacing why.
2. After the batch, re-read with `choice-sets list-values <id>` and persist the actual `{Name → NumberId}` map to a side file. Read record-write payloads from that file — never from the announced order.

## Add a choice-set field to an entity

1. List choice sets and let the user reuse one or approve a new set.
2. For a new set, create it, then create each value separately in the approved order. `choice-set-values create` takes the set ID and value name as positional arguments; it does not accept a batch `--body`.
3. Re-list values to obtain their assigned `NumberId`s.
4. Bind the set with `{"name":"<field>","type":"CHOICE_SET_SINGLE","choiceSetId":"<id>"}` (or `CHOICE_SET_MULTIPLE`) in `entities create` or `entities update`.

## Write / read / filter record values

Record value = integer `NumberId` (single) or integer array (multi); reads echo the same shape. Filter operator semantics — especially `CHOICE_SET_MULTIPLE` (`contains` vs `=`) — are in [`filter-platform-contract.md`](filter-platform-contract.md#operator-support-by-field-type).

```bash
uip df records insert <entity-id> --body '{"amount":250,"category":1,"tags":[1,2]}' --output json
```

Passing a display label (`"category":"Travel"`) is rejected — resolve to `NumberId` first.

## Decision: is this field a choice set?

- Finite, reused list of named options → choice set. Single value → `_SINGLE`; multiple → `_MULTIPLE`.
- Link to a *row* in another entity → `RELATIONSHIP` (see [`entity-schema.md` → Relationship Fields](entity-schema.md#relationship-fields)).

## Pick-or-create flow

When the user's request needs a choice set but they didn't name one (or the name they gave doesn't exist):

1. Run `choice-sets list --output json`.
2. Surface every existing choice set to the user with its `Name` and `DisplayName` — don't pre-filter. The user is the judge of relevance.
3. For each plausibly-matching set, run `choice-sets list-values <id>` and show its values so the user can confirm fit.
4. Ask explicitly: *"Use one of these, or create a new choice set named `<X>`?"*
5. Only `choice-sets create` + `choice-set-values create` after explicit approval, using the user's chosen name and values.

Never fall back to `STRING`. Never auto-create without confirming the values.

## Deleting a choice set

```bash
uip df choice-sets delete <choice-set-id> [--folder-key <…>] --yes --reason "<why>" --output json
```

Irreversible. Before invoking, run `entities list --output json` and find every entity whose `Fields[].ChoiceSetId == <choice-set-id>`. Surface those entities to the user and ask: *"This choice set is used by `<entity>.<field>` — delete it anyway (those fields will break), pick a replacement choice set, or stop?"* Apply only what the user confirms.
