---
title: "Edit an attribute's metadata, or migrate its scope"
method: PATCH
path: "/api/v1/ackdb/attributes/{key}"
tags: ["ackDB"]
---

# Edit an attribute's metadata, or migrate its scope

`PATCH /api/v1/ackdb/attributes/{key}`

## Request

- Base URL: `http://localhost:3009`
- URL: `http://localhost:3009/api/v1/ackdb/attributes/{key}`
- Auth: HTTP bearer

Custom attributes: `key`/`type` are immutable — label/description/allowedValues change freely.

**Standard attributes (#487):** `allowedValues` — and nothing else — may be edited, and only on an attribute that already ships an enum. A lifecycle funnel differs per deployment (an extra `"Implementation"` stage between `Opportunity` and `Customer`, say), so the ENUM belongs to the tenant while ackDB keeps the wording, grain and type. `null` is refused (the field must keep a fixed set of values), a freeform standard field stays freeform, and DELETE is still refused. The edit STICKS: it sets `userModified`, so the boot loader stops re-asserting the shipped list on restart. Every attribute view carries `editableFields` — the authoritative list of what PATCH will accept.

**Scope migration (#474):** `{ "scope": "person" }` re-files a CONTACT attribute onto the HUMAN, in one transaction: values fold onto each edge's person (winner = most recently updated among ACTIVE edges; existing person values are never overwritten; disagreeing edges are reported as `conflicts[]`), the definition + label override flip scope, the migrated contact rows are deleted, saved segments referencing the key are SCRUBBED (it leaves the contact catalog — a referencing segment would 400 its view; affiliation-`where` references on company/person segments too), and score definitions referencing it are pruned + flagged `degraded`. Must be the ONLY field in the request. `{ "dryRun": true }` runs the full cascade and rolls back — the report is real. contact → person is the only direction. The CLI twin is `npm run migrate-contact-attributes`.

## Path parameters

- `key` string, required

## Request body

- AckdbAttributePatchRequest: Edit an attribute's metadata (any subset of the fields its `editableFields` lists). `key` and `type` are always immutable. A custom attribute takes label/description/allowedValues; a STANDARD one takes `allowedValues` alone (#487). `scope` is the exclusive contact → person migration (#474) and must be the only field in the request.
  - `label` string
  - `description` string, nullable
  - `allowedValues` string[], nullable
  - `scope` 'person': Scope migration (#474) — only "person".
  - `dryRun` boolean: Scope migration only: run the full cascade and roll back.

## Response `200`

The updated attribute definition — or, for a scope migration, `{ data: { contactValues, personValues, conflicts[], scrubbedSegments, degradedScores[], dryRun } }`.

- object
  - `data` unknown, required

## Other responses

- `400`: VALIDATION_ERROR — no fields provided, allowedValues invalid (including `null` on a standard attribute), a scope migration mixed with other fields, an unsupported scope direction, or the attribute is not contact-scope.
- `403`: ATTRIBUTE_PROTECTED — a field this attribute does not expose: label/description/scope on a standard attribute, or allowedValues on a standard attribute that is freeform by design. The message names what IS editable (see `editableFields`).
- `404`: ATTRIBUTE_NOT_FOUND.
- `409`: ATTRIBUTE_VALUES_IN_USE — shrinking allowedValues would drop a value records still hold; details: [{ value, count }]. Clear or re-map those values first.

---

[API](https://skmtc.dev/floqer/apis/floqer-public-api.md) - [All operations](https://skmtc.dev/floqer/apis/floqer-public-api/llms.txt) - [OpenAPI document](https://skmtc.dev/floqer/apis/floqer-public-api/revisions/fce50dc07b20?raw)
