Record reference

Relationships and one-way links between records

Record reference attributes are one of the most useful types of attribute. They allow pointing to other records of the same object, or records of other objects. When combined with features such as drill-down filters, it becomes possible to deeply relate the various parts of your data model.

By default, all of the standard objects have at least one record reference:

  • Company has team (people), associated_deals (deals) and associated_workspaces (workspaces)
  • Person has company (company), associated_deals(deals) and associated_users (users)
  • Deal has associated_people (people) and associated_company (company)
  • User has person (person) and workspace (workspace)
  • Workspace has users (users) and company (company)

A note about relationship attributes

All of the example attributes above are also relationship attributes.

Relationship attributes describe a relationship between two objects, which appear as a pair of record reference attributes, one on each object. Updating an attribute value on either of the attributes automatically updates the other attribute, which means you don't need to explicitly update both. For example, adding a person to a company by updating the company team property, will also update the company property on that person.

Currently, you will be able to see relationship attributes alongside other non-relationship record reference attributes in the API. The attribute type is still marked as record-reference, but there is an additional property, relationship, that you can use to distinguish these.

If the attribute is also a relationship attribute, the relationship property will be an object containing an id property. In the example below, our company's team attribute has a relationship with the person's company attribute:

{
  "id": { ... },
  "title": "Team",
  "api_slug": "team",
  "type": "record-reference",
  "relationship": {
     "id": {
       "workspace_id": "14beef7a-99f7-4534-a87e-70b564330a4c",
       "object_id": "4e71c40b-7d35-463c-afcb-e339cfd6dbd1",   // Person object
       "attribute_id": "41252299-f8c7-4b5e-99c9-4ff8321d2f96" // "Company" attribute on Person object
     }
  },
  ...
}

If the record reference attribute is not a relationship attribute, the relationship property will be set to null.

In the web application, when setting up a relationship, the user specifies whether the relationship is many-to-many, many-to-one, one-to-many or one-to-one. It is possible to discern this relationship by looking at the is_multiselect property on each attribute: if true, this attribute is a "many", if false it is a "one".

Unfortunately, at present it is not possible to create relationship attributes using the API; they can only be created in the web application and then used in the API.

Configuration

Record references are usually constrained to referencing a specific object. For example, you can't specify a deal for the company team attribute. This is accomplished with the configuration property allowed_object_ids which is an array of object IDs (slugs are supported when writing this property):

POST /v2/objects/:object/attributes HTTP/1.1
Authorization: Bearer <<oauth2>>
Content-Type: application/json

{
  "title": "Owner",
  "api_slug": "owner",
  "type": "record-reference",
  "config": {
    "record_reference": {
      "allowed_objects": ["person"]
    }
  }
}

In responses from the API, this appears as allowed_object_ids, like so:

{
  "id": { ... },
  "title": "Team",
  "api_slug": "team",
  "type": "record-reference",
  "config": {
    "record_reference": {
      "allowed_object_ids": [
        "4e71c40b-7d35-463c-afcb-e339cfd6dbd1"
      ]
    }
  },
  ...
}

Reading values

Record reference values have two properties, target_object (the api_slug representing what kind of object it is) and target_record_id (the ID of the Record).

{
  "active_from": "2023-04-03T15:21:06.447000000Z",
  "active_until": null,
  "created_by_actor": {...},
  "attribute_type": "record-reference",
  "target_object": "people",
  "target_record_id": "891dcbfc-9141-415d-9b2a-2238a6cc012d"
}

Writing values

There are multiple ways to write a record reference. Since it's such a common operation, Attio provides special write functionality if the record reference only allows a single object and that object is one of our standard objects:

  • If the allowed object is a company, you can use the domains attribute.
  • If the allowed object is a person, you can use the email_addresses attribute.
  • If the allowed object is a user, you can use the user_id attribute.
  • If the allowed object is a workspace, you can use the workspace_id attribute.
{
  "associated_company": [
    {
      "domains": [{"domain": "company.com"}],
      "target_object": "companies"
    }
  ]
}
{
  "team": [
    {
      "email_addresses": [{"email_address": "[email protected]"}],
      "target_object": "people"
    }
  ]
}
{
  "user": [
    {
      "user_id": [{"value": "my-user-id"}],
      "target_object": "users"
    }
  ]
}
{
  "workspace": [
    {
      "workspace_id": [{"value": "my-workspace-id"}],
      "target_object": "workspaces"
    }
  ]
}

Furthermore, we allow writing to these attributes using string values.

  • If the allowed object is a company, string values will be interpreted as a domain.
  • If the allowed object is a person, string values will be interpreted as email addresses.
  • If the allowed object is a user, string values will be interpreted as user_id text values.
  • If the allowed object is a workspace, string values will be interpreted as workspace_id text values.
{
  "associated_company": "company.com"
}
{
  "associated_company": ["company.com", "company.net"]
}
{
  "team": ["[email protected]"]
}
{
  "team": ["[email protected]", "[email protected]"]
}
{
  "user": "my-user-id"
}
{
  "user": ["my-user-id", "another-user-id"]
}
{
  "workspace": "my-workspace-id"
}
{
  "workspace": ["my-workspace-id", "another-workspace-id"]
}

If the attribute is multiselect ("many"), you can also pass these as a series of values like so:

It's also possible to write record references using record IDs.

{
  "associated_company": [
    {
      "target_record_id": "99a03ff3-0435-47da-95cc-76b2caeb4dab",
      "target_object": "companies"
    }
  ]
}

Note that the write will fail if the target record does not exist, i.e. you can't create the target record automatically. For example, if you tried to assert a person and referenced a company that did not exist, the request would fail. This means that you need to do your writes in reverse-order, e.g. starting with the company that the person is linked to, then creating the person.

Filtering

Record reference values can be filtered by target_object and target_record_id using exact equality matches.

{
  "filter": {
    "company": {
      "target_object": "companies",
      "target_record_id": "99a03ff3-0435-47da-95cc-76b2caeb4dab"
    }
  }
}
{
  "filter": {
    "users": {
      "target_object": "97052eb9-e65e-443f-a297-f2d9a4a7f795",
      "target_record_id": "5e3fb280-007b-495a-a530-9354bde01de1"
    }
  }
}

As well as exact equality matches, record reference values also support the $in operator.

{
  "filter": {
    "company": {
      "target_object": "companies",
      "target_record_id": {
        "$in": [
          "3aeb39cd-fed8-524e-94b8-1300549354ac",
          "8c5cd602-1297-45d2-a99c-14ae091cbac3"
        ]
      }
    }
  }
}

Record references are special, because they are also filterable using "paths", also known as "drill-downs". You can filter records based on attributes of the target records. paths is an array containing tuples of (object type, attribute slug or ID), while constraints are applied to the attribute identified by the final path element.

For example, we could construct a filter like "find me Companies which have an employee named John":

{
  "filter": {
    "path": [
      ["companies", "team"],
      ["people", "name"]
    ],
    "constraints": {
      "first_name": { "$eq": "John" }
    }
  }
}

Here, we're using the team attribute on a Company, which is a multi-select record reference to the person record. We then look across at those related people, and their name attribute values. Finally, since name is a (personal) name attribute, we can query against the first_name property.

Paths can be more complex, and even somewhat recursive. For example, if you had a manager attribute on the Person object, and it pointed to another Person, you could find people by who their manager's manager's manager was:

{
  "filter": {
    "path": [
      ["people", "manager"],
      ["people", "manager"],
    ],
    "constraints": {
      "target_object": "people",
      "target_record_id": "managers-manager-id"
    }
  }
}