Custom Fields
Orbit’s contact record covers what every business needs (name, phone, email, channel preferences). The moment you need something specific to your business —plan_tier, lifetime_value, account_owner, renewal_date — you define a custom field instead of stuffing it into a note or an external spreadsheet. Once defined, the field behaves like a first-class contact attribute: it shows up on the contact record, it’s filterable in segments, it’s usable as a condition in flows, it drives campaign audience filters and variables, and it can be dropped into a message with {{contact.custom.plan_tier}}-style personalization.
This guide is the practical companion to the Custom Fields API reference. The reference lists every request/response shape; this page walks through per-type recipes, the validator matrix, the lifecycle of a definition, and the limits to plan around.
Why custom fields instead of notes or a spreadsheet
A freeform note and a synced spreadsheet both fail the same way: the data exists but nothing can act on it. Reach for a custom field when a piece of contact data needs to be:- Structured — typed and validated at write time, not a freeform note. A
plan_tierselect can never drift into “Enterprise” vs “enterprise” vs “ENT”. - Queryable — you want to build a segment like “plan_tier = enterprise AND lifetime_value > 5000” and have it evaluate correctly for every contact, every time.
- Reusable — the same value drives a flow condition, a campaign filter, and a personalization token, and you don’t want three separate places to keep it in sync.
- Actionable — segments, flows, and campaigns all read custom fields directly. A note is prose a human can read; a custom field is a fact the platform can act on.
The seven type families
Every field has atype fixed at creation. Pick by what you’ll do with the value, not by what the data looks like today — if you’ll ever filter on “greater than”, it needs to be a number, not a text.
Sections 2a–2g give a full recipe per type: definition, value write, and where it shows up.
2a. text — free-form strings
Usetext when values can’t be enumerated in advance: an external CRM id, an account manager’s display name, a support PIN.
validator_regex, or falls outside min_len/max_len, is rejected with 422 at write time. The regex is anchored to the whole value (HTML5 pattern semantics) — a pattern like [0-9]{6} rejects xx123456yy instead of matching the six digits inside it.
2b. number — range-filterable numerics
Usenumber whenever you’ll ever ask “greater than X” in a segment or compare values in a flow. Strings sort lexicographically; numbers sort numerically — this choice is unfixable later, so make it now.
min_len/max_len are the inclusive numeric range — min_len: 0 above rejects negative values. A numeric string like "482.50" is coerced to a number at write time; a non-numeric value is rejected with 422.
2c. boolean — two-valued flags
Useboolean for flags a workflow branches on: VIP status, beta enrollment, do-not-disturb. Booleans accept strict true/false, and coerce the common string shorthands ("true", "1", "yes" / "false", "0", "no").
"false" comes back through JSON as a truthy value and would flip a segment’s logic. The type gate keeps every stored value a real JSON boolean.
2d. date — calendar dates and renewal windows
Usedate for anything you’d phrase as “before” or “after”: renewal date, contract signed date, last demo date. Values are accepted as ISO 8601 dates or datetimes and normalized to an ISO string, so every contact stores the same shape.
renewal_date within the next 30 days, feed that segment to a flow, and render the actual date back in the message with {{contact.custom.renewal_date}}.
2e. select — one value from a fixed list
Useselect when a contact holds exactly one value from an enumerated set: plan tier, lifecycle stage, billing region. select requires an options array at creation; writing a value not in options is rejected.
validator_regex, min_len, and max_len are not applied to a select’s values (the option membership check handles everything). Keep option values short, lowercase, and stable — they appear verbatim in segment filters and personalization output.
A select shows up in all four applications at once: a segment filter (plan_tier = enterprise), a flow branch (route enterprise renewals to the account team), a campaign audience, and a message token ({{contact.custom.plan_tier}}).
2f. multiselect — several values from a fixed list
Usemultiselect when a contact holds a subset of a fixed set: interests, product lines, communication topics. The value is an array of strings, every entry drawn from options.
min_len/max_len bound the number of selections (array length), not the characters. Unlike select, per-entry validator_regex and enum_values do apply — every entry in the array must satisfy them. An entry outside options is rejected with 422.
Multiselect has one gap to plan around: it has no default_value (the default is stored as a scalar, which can’t represent an array), and the default-backfill skips multiselect definitions for the same reason.
2g. user_ref — a reference to a teammate
Useuser_ref to model ownership: account_owner, support_rep, success_manager. The value is a string — the id of the teammate — and it validates like a text field (string-shaped, subject to min_len/max_len, validator_regex, and enum_values).
usr_ id, and use it as a flow condition so an inbound conversation from an owned contact lands with its account owner first.
The validator matrix
Validators layer on top of the type system and apply whenever a value is written — API, dashboard edit, or CSV import. A value that fails is rejected with422 VALIDATION_ERROR; nothing partially written ever reaches a contact.
Field-by-field notes:
validator_regex— a JavaScript regex (no flags), anchored to the whole value. Max 200 characters. Patterns that match a known ReDoS shape (nested quantifiers, alternation under a quantifier) are rejected at save with400— fix alternation like(a|b|c)+into a character class like[abc]+. Each match attempt is time-boxed, so a pathological pattern can’t stall a write.min_len/max_len— inclusive bounds, interpreted per row of the matrix. Supply both andmax_lenmust be ≥min_len. Inclusive upper bounds sit under a global ceiling: no single value can exceed 8,192 characters regardless ofmax_len.enum_values— an exact allow-list of up to 500 entries (200 chars each). Onnumber/boolean/datedefinitions it’s rejected at save — usemin_len/max_lenfor range bounds there.selectandmultiselectnormally rely onoptions;enum_valuesis a second, independent gate you can stack ontextoruser_refwhere there’s no option list.- select is self-constraining — the
optionsmembership check is the only value constraint a select needs, sovalidator_regexand length bounds are skipped forselectvalues even if a stale one lingers on the definition.
default_value that violates its own type or validators is rejected at save, and an update that would leave the stored default violating the new validators is rejected too. This keeps the default (which backfills onto existing contacts) from seeding invalid data past the write gate.
Pass null for a validator on PATCH to clear it; omit the key to leave it untouched.
Operating definitions: the full lifecycle
Definitions are created, read, updated, and deleted through/api/v1/custom-fields; values ride on contacts through /api/v1/custom-fields/values and come back inline on the contact read.
Two write semantics to internalize:
- Writes are atomic per contact.
PUT /valuessets exactly one key; it never round-trips the contact’s whole attribute map, so two integrations can safely update different fields on the same contact concurrently. - Passing
null(or empty) as a value removes the key from the contact — unless the field isrequired, in which case the write is rejected. The bulk CSV-import path follows the same rules, and a value that fails validation against its definition rows as invalid row-by-row without aborting the rest of the batch.
Dependencies and the explorer: retire a field without breaking anything
A custom field accumulates dependents fast: segments filter on it, campaign templates personalize with it, flows branch on it, contacts carry values. Deleting it blindly turns every dependent filter into a “match nothing” — silently. Two tools keep you ahead of that.The dependency scan
contacts_with_data (contacts carrying a value), plus the specific segments, campaigns, and flows (both automation flows and IVR flows) that reference it, and the count of active or paused drip_enrollments tied to those campaigns. Run this first whenever you’re considering a deletion or a repurposing.
The explorer
total), and up to ten of the most recently written distinct values, each PII-redacted and truncated to 120 characters. Use it before you narrow an option list or tighten a validator: if the samples show values you’re about to make illegal, retire the field instead of breaking writes for existing data.
The forced deletion flow
DELETE is guarded by the dependency scan. If anything references the field, the request fails with 409 CUSTOM_FIELD_IN_USE instead of silently breaking a live segment or automation, and the error’s details.dependencies carry the same snapshot the scan endpoint returns:
- Scan the dependencies and read the specifics — which segments, which campaigns, which flows.
- Clean up or accept — either remove the references first (preferred for live automations) or decide which dependents you’ll rebuild after.
- Force delete when you’re ready:
force=true, the delete proceeds and each dependent is visibly flagged rather than silently broken:
- Segments are set to
status: "broken"so they stop matching until you edit the filter. - Flows (automation and IVR) are marked broken — the automation runner skips them, so an orphaned condition can’t fire.
- Campaigns are not interrupted — an in-flight send finishes — but they get a
broken_custom_fieldsmarker in their metadata so you can find and fix them. - Contacts lose the key from their attributes as part of the same operation, and the filter JSON in each dependent is left intact (only flagged) — rebuild it explicitly rather than inheriting a stale reference.
force on the first request.
Limits to plan around
Definitions create whenever the payload passes validation — per-tenant definition count is bounded by practical UI ordering rather than a hard cap; keep the set focused and use reorder (
POST /custom-fields/reorder) to control how fields appear in the dashboard pickers.
Lifecycle rules: what’s locked and how to migrate
Two properties are locked at creation, and this is a deliberate data-integrity rule, not a gap:keyis frozen. The key is the lookup inside every contact’s attribute map, every segment filter, and every personalization token. Changing keys would orphan all three at once.typeis frozen. Stored values were validated and coerced against the original type — anumberflipped totextwould silently reinterpret every existing value (and mis-sort every range filter).
PATCH: name, description, options (you can append a plan tier to a select), default_value, required, searchable, and all validators.
When you do need a new type or key, treat it as a migration, not an edit:
- Create the replacement field (
plan_tier_v2of the new type). The dependency scans on the old field show you exactly what points at it. - Backfill values by reading the old field from each contact (
GET /contacts?filter=...) and writing the new one withPUT /custom-fields/values. For large audiences, use the bulk contact update operation rather than per-contact calls, or re-run the CSV import against the new field. - Repoint dependents — edit each segment filter, campaign variable, and flow branch the dependency scan listed.
- Retire the old field — delete it (force only if you skipped step 3), then optionally reorder the remaining definitions.
default_value is backfilled onto every existing contact that doesn’t yet carry the key, both at creation and when you change the default later — contacts that never had a value get it, and values anyone already set are never overwritten. (multiselect is the exception noted in its recipe above.)
Ownership, access, and where this data is controlled
Custom-field definitions are tenant-owned data in the strictest sense: they live in your tenant’s schema, they’re writable only by your owner, admin, and developer roles, and every definition or value change is recorded in your own audit log. Reads against definitions and values ride on the same API-key or session authentication as every other contact endpoint. Compliance handling for the values is yours — put into a custom field only what your retention policy allows, and when a field must go, the delete path strips its values from every contact in the same operation.See also
- Custom Fields API reference — full endpoint list and request/response shapes
- Contacts API — where field values are read inline
- Segments — filtering contacts by a custom field
- Audit log — tracing definition and value changes