# Custom Attribute registry mode
Custom Attributes are the one v1.0 resource where `braze-sync` cannot
act as a full reconciler. This page explains why, and what the
**registry** does instead.
## Why Custom Attributes are different
Braze does not expose a create-attribute API. Instead, a Custom
Attribute comes into existence **implicitly** the first time
`/users/track` receives a payload containing it. From Braze's
perspective, attributes are a side effect of SDK traffic, not
declaratively managed objects.
This breaks the usual GitOps loop:
- `braze-sync` cannot **create** an attribute — Braze creates them.
- `braze-sync` cannot **delete** an attribute — Braze has no delete
endpoint (only deprecation).
- `braze-sync` *can* **toggle the `deprecated` flag** via the attribute
metadata endpoint.
Claiming to "manage" Custom Attributes the same way as catalogs would
be dishonest. `braze-sync` runs the one mode that is actually honest:
**registry mode**.
## What the registry is
A single file — `custom_attributes/registry.yaml` — enumerates every
Custom Attribute that exists in your Braze workspace, along with
lightweight metadata:
```yaml
# Generated by braze-sync.
#
# This is a REGISTRY of Custom Attributes that exist in Braze.
# braze-sync cannot create new Custom Attributes — Braze creates them
# implicitly when /users/track receives data containing them.
# braze-sync can only mark them as deprecated.
attributes:
- name: last_visit_date
type: time
description: Most recent visit timestamp
- name: preferred_clinic_id
type: string
description: User's preferred clinic
- name: legacy_segment
type: string
description: Pre-2024 segment marker
deprecated: true
```
Fields:
| Field | Type | Role |
|:---|:---|:---|
| `name` | string | Attribute name as seen by Braze and the SDK. |
| `type` | enum | One of `string`, `number`, `boolean`, `time`, `array`. Reported by Braze; not something `apply` can change. |
| `description` | string | Free-text documentation. Braze returns it on read but has no write endpoint, so this is effectively a local-only annotation and `apply` ignores changes. |
| `deprecated` | bool | The only field `apply` can actually write back to Braze. |
## What `diff` reports
`diff` classifies each attribute into one of five states:
| State | Meaning | `apply` action |
|:---|:---|:---|
| `Unchanged` | Identical in Git and Braze | None |
| `UnregisteredInGit` | Exists in Braze, missing from the registry | Report; prompts a follow-up `export` |
| `PresentInGitOnly` | In the registry, not in Braze | Warning — usually a typo in the registry, occasionally an attribute that hasn't seen traffic yet |
| `DeprecationToggled` | `deprecated` differs between Git and Braze | **Writes** — this is the only mutation `braze-sync` performs for Custom Attributes |
| `MetadataOnly` | Only `description` differs | Report; no API call (Braze has no description endpoint) |
## What `apply` does
Exactly one thing: toggle the `deprecated` flag via the attribute
metadata endpoint. Every other diff state is report-only.
That narrow surface is the whole point — the registry accurately
describes the slice of management Braze actually exposes. Nothing
pretends to be more powerful than it is.
## The recommended workflow
1. **Seed the registry from Braze**:
```bash
braze-sync export
```
This pulls every attribute Braze currently knows about into
`custom_attributes/registry.yaml`, writing `description` fields as
empty strings you can then fill in by hand.
2. **Treat the registry as documentation**. The `description` field is
the main reason the registry exists: it gives engineers a
checked-in answer to "what does `preferred_clinic_id` mean?" without
digging through dashboard notes.
3. **Refresh on a schedule**. Because Braze creates attributes
implicitly from SDK traffic, new attributes appear without any PR.
A nightly `diff --fail-on-drift` (see
[integration.md](integration.md)) will flag new attributes as
`UnregisteredInGit` so someone opens a PR with a description.
4. **Deprecate deliberately**. When an attribute is truly retired, mark
it `deprecated: true` in the registry and run `apply --confirm`.
Braze will stop surfacing it in the segment-builder UI.
## What registry mode is *not*
- It is **not** a schema enforcement tool. There's no way to stop a
mis-typed attribute from being created by SDK traffic; the registry
will just flag it as `UnregisteredInGit` on the next diff.
- It is **not** a delete mechanism. Deprecation hides the attribute in
the dashboard; it does not remove data.
- It is **not** a type registry. Types come from Braze and cannot be
changed via API. If a type is wrong, the fix is on the sender side,
not in this file.
Registry mode is narrow on purpose. It does the one thing the Braze
API actually supports, and refuses to fake the rest.