Skip to content
Authors
  • Sem Tadema
    Sem TademaCTO

Morphing lets one record exist in multiple object contexts without duplicating data. This post explains how primary object and morph objects work, how object-level extendsTo configuration drives automatic morphing, and how to model a shared person record that is visible as both contact and candidate.

In many Caraer setups, the same real-world entity appears in more than one object:

  • A person can be a candidate and later also a contact
  • A lead can become a candidate without copying firstname, email, and other shared fields

Without morphing, you would duplicate records or maintain fragile cross-links. Morphing solves this by keeping one record (one UUID, one set of property values) while making it visible in multiple object contexts.


The mental model

Every record has:

ConceptPropertyMeaning
Primary objectprimary_objectThe object the record was created as. This is the record's home type.
Morph objectsmorph_objectsAdditional object contexts where the record should also appear. Semicolon-separated object names, e.g. contact;candidate.
All objectsDerivedPrimary object + morph objects. Used for validation and unique checks.

Important rules:

  • The primary object is never a morph object.
  • Morph objects are additional visibility contexts, not separate records.
  • A morphed record still has one UUID and one value set.

When a record is morphed, Caraer makes it available in every object listed in all objects. That is why the same record appears when you list contacts, candidates, or persons — you are querying different object views of the same underlying data.

Example record after morphing:

{
  "uuid": "RECORD_UUID",
  "primary_object": "person",
  "morph_objects": "contact;candidate",
  "properties": {
    "firstname": "Alex",
    "email": "alex@example.com"
  }
}

How morphing works

When a record is saved or morphed, Caraer:

  1. Resolves the record's primary object and morph objects.
  2. Applies default morph objects from the primary object's extendsTo configuration (see below).
  3. Validates morph rules (no duplicate primary, trait limits).
  4. Persists primary_object and morph_objects on the record.

On create and update, morphing runs automatically as part of the save flow. You can also morph an existing record explicitly through the API.


Object-level configuration: extendsTo

Objects can define default morph targets through extendsTo. When person has extendsTo: [contact, candidate]:

  • A record created with primary object person automatically gets contact and candidate as morph objects.
  • You do not need to call the morph endpoint for every new person record.

extendsTo is schema configuration on the object, not per-record data. Use it when a primary type should always be visible in other object contexts.

Syncing existing records after schema changes

If you add or change extendsTo on an object, existing records are not updated automatically. Call:

  • POST /api/v2/objects/{uuid}/syncMorphObjects

This re-applies morph configuration for existing records that belong to that object.


Example: person → contact and candidate

Scenario

You want:

  • person to hold shared identity fields (firstname, lastname, email)
  • candidate to expose recruitment-specific fields and views
  • contact to expose CRM-specific fields and views
  • One underlying record to appear in all relevant lists
  1. Create three objects: person, candidate, contact.
  2. Attach shared properties (firstname, lastname, email) to all three objects, or at least to person and the specialized types.
  3. Configure person.extendsTo = [contact, candidate].
  4. Create records with primary object person.

Result for a new person record:

{
  "primary_object": "person",
  "morph_objects": "contact;candidate"
}

The record is visible in person, contact, and candidate list views.

Querying the same record in different contexts

Use mainObject to choose which object view you are listing:

# Same record appears here because it is morphed into candidate
curl -X POST "https://your-company.caraer.com/api/v2/records/index" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "page": 1,
    "limit": 25,
    "mainObject": "candidate"
  }'
# Same UUID, different object context
curl -X POST "https://your-company.caraer.com/api/v2/records/index" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "page": 1,
    "limit": 25,
    "mainObject": "contact"
  }'
# List all persons, including those morphed into other types
curl -X POST "https://your-company.caraer.com/api/v2/records/index" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "page": 1,
    "limit": 25,
    "mainObject": "person"
  }'

The property values are identical across contexts. What changes is which object schema, views, and permissions apply for that request.

Alternative: morph per record

If only some people should also be contacts or candidates, skip extendsTo and morph records individually:

curl -X POST "https://your-company.caraer.com/api/v2/records/RECORD_UUID/morph" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "objects": [
      { "name": "contact" },
      { "name": "candidate" }
    ]
  }'

The morph endpoint sets morph objects for that record. It does not change the record's primary object.


Morph API reference

Morph a record

POST /api/v2/records/{uuid}/morph

Request body:

{
  "objects": [
    { "name": "contact" },
    { "name": "candidate" }
  ]
}

Use this to:

  • Add visibility in additional object contexts
  • Remove visibility by sending a smaller object list (objects not in the list are removed from the morph set)

Query params:

  • recordReturnFormatLEGACY, USER_FRIENDLY, or EXPANDED
  • parse — parse formatted values before returning the record

Sync morph objects for an object

POST /api/v2/objects/{uuid}/syncMorphObjects

Use after changing an object's extendsTo configuration so existing records pick up the new default morph targets.


Validation and constraints

Caraer enforces these morph rules:

  • Primary object cannot appear in morph objects. If person is primary, do not include person in the morph list.
  • At most one Page trait across primary + morph objects.
  • At most one User trait across primary + morph objects.

These limits prevent conflicting trait behavior when one record spans multiple object types.

Unique-property checks consider all objects on the record. If email is unique on both person and contact, duplicate detection spans the combined object context.


Design guidelines

  • Use a base primary object (person) when multiple specialized types share the same identity data.
  • Use extendsTo for defaults that should apply to every new record of that type.
  • Use the morph endpoint for exceptions and lifecycle changes (for example, promoting one person to candidate only).
  • Keep shared property names aligned across morphed objects so values stay consistent in every view.
  • After schema changes to extendsTo, run syncMorphObjects.
  • Query with the object context you need via mainObject; do not assume records only exist under their primary object.

Common pitfalls

PitfallWhat happensFix
Expecting morph to change primary objectPrimary object stays the sameCreate the record under the intended primary type, or accept that primary_object reflects creation context
Forgetting extendsTo only affects new saves + syncExisting records keep old morph setCall syncMorphObjects after schema changes
Including primary object in morph requestValidation errorRemove the primary object from the morph list
Different property sets per objectRecord may lack expected fields in one viewAttach required properties to every object context users will open