**Attributes** are the custom fields you store on a contact — things like `plan`, `region`, or `signup_date`. They serve two purposes: they describe your contacts, and they're the vocabulary your [Segment](/audience/segments) rules are built from. A rule like `attr.plan = "pro"` works because `plan` is a defined attribute.

There are two sides to attributes: the **registry** (the definitions — which fields exist and what type each is) and the **per-contact values** (what each contact's `plan` actually is).

## The attribute registry

The registry is the schema: each attribute has a **name** (lowercase, referenced in rules as `attr.<name>`), a **type**, and an optional description.

| Type | Use for |
|---|---|
| **string** | Free-form text. |
| **number** | Numeric values you may want to compare (`>`, `<`). |
| **bool** | True/false flags. |
| **datetime** | Timestamps. |
| **enum** | A fixed set of allowed values (the definition lists them). |

Defining an attribute's type lets SendOps validate Segment rules — a rule that compares a `number` attribute to text, or references an attribute that doesn't exist, is caught as invalid.

You can author attribute definitions three ways: **in the dashboard editor**, **as code in a connected repository**, or **through the SendOps API**. Each definition has an **origin** — *managed* (app/API) or *git* (a repository file) — and the origin decides where you edit it. See [Managed vs git-backed authoring](/audience/managed-vs-git-authoring) for how the two relate and how to move a definition between them.

## Editing attributes in the app

On the **Attributes** page, managed attributes show an **Edit** button (and there's a **New attribute** action); git-backed ones show a **Git** badge and are read-only here — edit those in your repository. The editor is a simple form: the **name**, a **type** picker, an **enum values** editor (when the type is `enum`), and a description.

Because changing an attribute can affect the Segments that reference it, the editor shows the **impact before you save**:

- **No impact** — adding an enum value, editing the description, or switching between `string` and `enum` changes nothing about how rules run.
- **Safe** — removing an enum value: rules keep working; some already-stored values just won't match new writes.
- **Disruptive** — renaming, changing the type across families (e.g. `number` → `string`), or deleting. The editor lists the affected Segments and how many stored values stop matching, and asks you to **confirm** before saving.


  The registry is authoritative, so a disruptive change goes through when you confirm it. Affected Segments keep evaluating best-effort and carry an *evaluation-disrupted* warning until their rule is updated to match the new schema. Every save is **versioned**, so you can restore a previous definition (a restore is itself impact-checked). This is true however the edit arrives — a change merged through git raises the same warnings as one made here.


When a definition comes from a repository, SendOps records the source file and keeps it in sync on every push. You can **Promote** a managed attribute into git or **Adopt** a git-backed one back into the app — see [Managed vs git-backed authoring](/audience/managed-vs-git-authoring). The same operations are available over the API (scope `api.attributes.manage`), including a dry-run **preview** of an edit's impact — see the [attributes API reference](https://developers.sendops.dev/api-reference/endpoints/attributes/preview-attribute).

## Derived attributes

An attribute doesn't have to be set manually or through the API — it can be **derived**: computed and kept current by SendOps from the contact's activity and event stream. A derived attribute has a **formula** instead of values you write yourself, built from one of four aggregate functions over an activity or event name:

| Function | Returns | Example |
|---|---|---|
| `exists(<name>)` | boolean | `exists(send)` — has this contact ever received a send |
| `count(<name>)` | number | `count(activity.order)` — how many `order` activities they've logged |
| `first(<name>)` | datetime | `first(send)` — when they first received one |
| `last(<name>)` | datetime | `last(open)` — their most recent open |

Use the bare name for a well-known SES event (`send`, `open`, `click`, …); use the `activity.<name>` form for one of your own custom [activities](/audience/activities) (`activity.order`). These are the same four functions [Segment rules](/audience/segment-syntax#events-and-activities) use to query the activity stream directly — a derived attribute just gives the result a permanent name you can reuse anywhere an attribute is expected.


  You can't set a derived attribute's value yourself — SendOps computes it automatically on a schedule (every few minutes) and recomputes it across all past activity the moment you create one, so historical activity is reflected immediately. Formula-based attributes are **managed only**: they can't be promoted to a git-defined attribute, since there's no per-contact value for a repository file to declare.


Derived attributes exist so you don't need a separate manual sync step to answer questions like "has this contact ever done X" — define the formula once and reference `attr.<name>` in Segments and Drip Workflows exactly like any other attribute.


  A formula aggregates over the contact's **own** event and activity stream **in this org** — so `exists(send)` means "have *we* ever sent to them," not "have they ever sent from their own account." It can't reflect state that happened in another system or org. When your source of truth lives outside SendOps, don't reach for a derived attribute — carry the state in on a schedule instead. See [Syncing external state](/audience/syncing-external-state).


## Per-contact values

A contact's actual attribute values appear on its **detail page**, under **Attributes** — a simple list of keys and values. If you have permission to manage contacts, you can add, edit, and remove a contact's attributes there. You can also set them programmatically when you create or update the contact through the [Contacts API](https://developers.sendops.dev/api-reference/contacts) (scope `api.contacts.manage`); values are validated against the registry, so an unknown attribute or a type mismatch is rejected.



These per-contact values are what Segment rules evaluate against: when a contact's `plan` changes from `free` to `pro`, any Segment whose rule depends on `plan` is re-evaluated and the contact moves in or out accordingly.

## What's next?

- [Segments](/audience/segments) — build dynamic audiences from attribute rules.
- [Lists](/audience/lists) — static audiences with explicit membership.
- [Lists & Segments API](https://developers.sendops.dev/api-reference/lists-segments) — read the attribute registry programmatically.