> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ravenna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Attributes

> Promote a custom field to an attribute so it appears on every ticket in the workspace, regardless of which form created the ticket.

Attributes are custom fields promoted to workspace-wide status. Once a field is an attribute, it appears on every ticket in the workspace, not just tickets created through the form that owns the field.

## Mental model

An attribute is a custom field that has been registered on a workspace. The registration lifts the field out of its form-only scope, so the field renders on every ticket in the workspace regardless of how the ticket was created.

Key points:

* Attributes are a per-workspace overlay on the custom field library. The custom field itself is unchanged. Adding it as an attribute is a separate join record on the workspace.
* Because attributes reuse the underlying custom field, editing an attribute edits the field for every form it also appears on.
* Removing an attribute detaches the field from the workspace overlay only. The field survives on any forms that still reference it.
* On tickets, attribute values live in `Ticket.attributeFields`, a keyed blob separate from `Ticket.customFields`. When a form submission includes an attribute, the form submission service splits the state with `splitStateForAttributeFields` and writes the value into `attributeFields`, not into the form response.

***

## Eligible types

Only these `CustomFieldType` values can be promoted to attributes: `TEXT`, `TEXT_AREA`, `DATE`, `DATETIME`, `BOOLEAN`, `SELECT`, `MULTI_SELECT`, `USER_SELECT`, `USER_MULTI_SELECT`. Other types (`NUMBER`, `DURATION`, `TIME`, `FILE_PICKER`, `APPLICATION_SELECT`, and the other resource pickers) render empty off a form context and are rejected by the API with `CUSTOM_FIELD_TYPE_NOT_ATTRIBUTE_ELIGIBLE`. `TIME` is ineligible even though `DATE` and `DATETIME` are eligible.

`USER_SELECT` and `USER_MULTI_SELECT` attributes render the same people picker used for assignees. The attribute's `config.allowList` (a list of user ids) restricts the picker to that subset; without it, the picker offers every workspace user.

The UI enforces this in three places:

* The **Add as attribute** menu item is disabled for ineligible fields.
* The **Create Attribute** dialog's type picker is filtered to eligible types only.
* The plus button on a ticket's **Attributes** section only lists eligible, non-system fields that aren't attributes yet.

***

## Attributes on forms

* The form builder tab formerly labeled **System** is now **Ticket**, and non-hidden attributes are listed there alongside built-in ticket fields. Attributes no longer appear under the **Shared** or **Custom** tabs.
* Hidden attributes are not form-eligible. The builder hides them from every tab, and `RequestTypeCustomFieldService` rejects the attach with `HIDDEN_ATTRIBUTE_NOT_FORM_ELIGIBLE`.
* When a submitter fills in an attribute on a form, the answer is stored on the ticket's `attributeFields`, not on the form response. The ticket's form response section does not list it, and the attribute card shows the value.
* On the **Create ticket** dialog, an attribute that is also a field on the selected form renders once, in the form's field list. The compact attribute bar receives an `excludeAttributeCustomFieldIds` set for exactly this.
* A form field that later becomes an attribute keeps its historical form-response values in place. Tickets created before the change still show blank on the attribute card until backfilled.

***

## Agent tools

Copilot and MCP tools for attributes:

* `search_workspace_attributes`: the discovery tool. Returns each attribute's `custom_field_id`, `label`, `type`, and (for `SELECT` / `MULTI_SELECT`) the option `id` + `value` pairs. Removed (archived) options are omitted, since they can no longer be set. Filter by `search` (case-insensitive label substring) or fetch specific ones with `attribute_ids`. IDs that don't resolve come back in `missing_ids` rather than as an error.
* `list_workspace_custom_fields`: lists every custom field in the workspace and marks which ones are attributes.
* `create_workspace_custom_field`: creates a field in the workspace field library, attached to no form. Pass `is_attribute: true` to mark it as an attribute in the same call, the equivalent of the Attributes tab's **New** dialog. Rejected for an ineligible type.
* `create_ticket` and `update_ticket`: accept an `attributeFields` object keyed by `custom_field_id`. Value shapes match `customFields`. `SELECT` and `MULTI_SELECT` values are validated against the attribute's real options, so a hallucinated option id fails loudly instead of being stored. A removed option is only accepted when the ticket already holds it; setting it on a ticket that doesn't returns `ATTRIBUTE_FIELD_VALUE_INVALID` with a note that the option was removed. `USER_SELECT` takes a user id and `USER_MULTI_SELECT` an array of user ids; each id is checked against the organization's users and rejected with `ATTRIBUTE_FIELD_VALUE_INVALID` if it doesn't resolve. Resolve user ids with `list_users` first.
* `get_ticket`: hydrates both `customFields` and `attributeFields` against the field definitions before returning, so the agent sees the current attribute values without a follow-up lookup. If a hydrated `SELECT` / `MULTI_SELECT` value points at a removed option, that option is included in `options` with `removed: true` so the agent can still read its label; other removed options are omitted.

The intended flow is: call `search_workspace_attributes` to resolve label → id (and option label → option id for selects), then pass that id into `create_ticket` / `update_ticket` under `attributeFields`.

***

## API

The public REST endpoints are under `/workspace-custom-fields`:

| Endpoint | Method | Purpose |
| - | - | - |
| `/workspace-custom-fields` | GET | List attributes in the current workspace |
| `/workspace-custom-fields` | POST | Promote an existing custom field to attribute |
| `/workspace-custom-fields/{id}` | PUT | Reorder an attribute |
| `/workspace-custom-fields/{id}` | DELETE | Remove an attribute from the workspace |

POST accepts `customFieldId` and an optional `order`. The custom field must already exist in the workspace and be an eligible type. `order` defaults to one past the current highest order in the workspace.

PUT accepts only `id` and `order`, so it is a reorder, not an edit. Change an attribute's label, description, or options through the custom field endpoints instead.

***

## Constraints and gotchas

* Custom fields are workspace-specific, so an attribute belongs to exactly one workspace.
* Removing an attribute never deletes the underlying custom field. Delete the field from **Settings > Workspace > Fields > Fields** if you want it gone entirely.
* Deleting the underlying custom field cascades: the attribute registration goes with it.
* Editing an attribute's label, description, or options edits the underlying custom field, so the change propagates to every form referencing it. Edits from the **Attributes** tab apply immediately with no confirmation step.
* Attributes render in the order set on the **Attributes** tab. A single PUT with a new `order` is enough, since the sibling attributes are reshuffled around it server-side.
* `system` custom fields are excluded from attribute filters, table columns, and exports even when registered as attributes.
* Attribute values populate on any ticket the field is set on, regardless of source. Tickets created before an attribute was added simply start empty and can be filled in afterwards.
* Attribute values are merged into the ticket's `attributeFields` blob by key, not replaced wholesale. Clearing a value requires sending an explicit `null` for that key.
* Removing an option from a `SELECT` or `MULTI_SELECT` attribute archives it (sets `archivedAt`) rather than deleting the `CustomFieldOption` row. Tickets holding the option keep resolving its label. Pickers, `search_workspace_attributes`, and the update-attribute form all filter archived options out, but filters, analytics group-by, and `get_ticket` hydration include them (labeled `(removed)` or flagged `removed: true`) so held values remain findable. A daily Temporal sweep hard-deletes archived options once no ticket references them.
* CSV exports resolve `USER_SELECT` and `USER_MULTI_SELECT` values (and user form fields) to the user's display name via `formatUserName`. The raw user id is not written to the export.
* Analytics group-by on `USER_MULTI_SELECT` joins the ticket's stored `{ id }` entries against the `User` table and buckets by `firstName lastName`, so groups are labeled with the person's name rather than the raw stored value.


## Related topics

- [Remove a ticket attribute](/api/workspace-custom-fields/remove-a-ticket-attribute.md)
- [List ticket attributes for the workspace](/api/workspace-custom-fields/list-ticket-attributes-for-the-workspace.md)
- [Mark a custom field as a ticket attribute](/api/workspace-custom-fields/mark-a-custom-field-as-a-ticket-attribute.md)
- [Update a ticket attribute (e.g. reorder)](/api/workspace-custom-fields/update-a-ticket-attribute-eg-reorder.md)
- [Setup](/integrations/vanta/setup.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.