> ## Documentation Index
> Fetch the complete documentation index at: https://docs.os.default.com/llms.txt
> Use this file to discover all available pages before exploring further.

# HubSpot contact matching in Default

> How Default finds an existing HubSpot contact by email before it creates one: Match Record runs first because HubSpot's Create Record has no Update on conflict, what to do on Match and No Match, and what happens when 2 runs for the same email overlap.

Default matches HubSpot contacts inside a workflow. Before a form fill, API call, or webhook creates a
contact, a **Match Record** node looks for a Contact with the same email. When one exists, the
workflow works with that contact and leaves its owner alone. When none exists, the workflow picks an
owner and creates the contact with **Create Record**.

Matching is built from workflow nodes, so each rule can be tested and each decision shows in the run
logs. The nodes are documented in [CRM nodes](/workflows/steps-crm). For Salesforce, see
[Salesforce contact matching](/lead-routing/matching/salesforce-contacts).

## How HubSpot contact matching works in Default

A contact intake workflow in Default runs these steps:

1. **A trigger starts the run with the person's email.** Use **Form Submission**, **API**, or
   **Incoming Webhook**. See [Triggers](/workflows/triggers).
2. **Match Record looks for the Contact.** Set **CRM** to **HubSpot** and **Record** to **Contact**,
   and compare **Email** with the email from the trigger.
3. **Match: the person is already a contact.** Later nodes use the contact's ID and properties, such
   as **Contact owner**. The contact keeps its owner.
4. **No Match: create the contact.** A **Round-Robin** node picks an owner, and **Create Record**
   creates the contact with that owner.
5. **Link the company.** Find the contact's Company and associate them. See
   [HubSpot contact-to-company matching](/lead-routing/matching/hubspot-contacts-to-companies).

## Why Match Record runs before Create Record in HubSpot

HubSpot's **Create Record** has no **Update on conflict** option. It always sends a create request, so
the workflow needs **Match Record** first to find a contact that already exists.

HubSpot also refuses a second record with a value that must be unique, such as a contact's email.
When HubSpot's rejection says that another record already has that value, **Create Record** does
this:

* It continues with the existing contact's ID when HubSpot's message names that ID, so later nodes
  can still use it. When the message names no ID, the step's ID is empty and the warning says that
  creation was skipped. If later nodes need the ID, add a **Multi-Branch** after **Create Record**
  that checks whether its ID is empty, and on that branch find the contact with a **Match Record**
  on **Email**.
* It logs a warning that the contact already exists, and the run counts the step as an error.
* It writes none of the mapped properties to the existing contact, so that contact keeps its owner.
* An owner picked by **Round-Robin** for the rejected create does not count as a queue assignment.

If HubSpot rejects the create with any other message, the step fails. This is a safety net.
**Match Record** is still the step that routes known contacts down their own branch.

## Match options for HubSpot contacts

| Match on | Condition in **Match Details** | When to use it |
| - | - | - |
| Email | **Email** **equals** the email from the trigger | The standard choice. An email identifies 1 person. |
| Name at a known company | **First Name** **equals**, **Last Name** **equals**, and the primary company ID (`associatedcompanyid`) **equals** a Company ID from an earlier step | The request carries no email, and an earlier **Match Record** already found the Company. |
| Contacts at a company | The primary company ID (`associatedcompanyid`) **equals** a Company ID from an earlier step. Or turn on **Match by association** with **Source record type** set to **Company**. | You start from the company and need a contact at it. |
| A known contact ID | **Record ID** (`hs_object_id`) **equals** an ID from the trigger or an earlier step | The request already passes a HubSpot contact ID. |

**Match Details** lists the contact properties you can filter on, including custom properties, and
needs at least 1 condition before the workflow can be published. With **Match by association** on,
the node ignores **Match Details** and considers only contacts associated with the **Source record**.

## When several HubSpot contacts match

An email search usually returns 1 contact. Name and company searches can return several.
**Prioritize matched records by** picks 1. Select **Add tiebreaker**:

| Tiebreaker | What it does |
| - | - |
| **Most recently modified** | Prefers the contact modified most recently. |
| **Newest created** | Prefers the most recently created contact. |
| **Oldest created** | Prefers the first contact created, often the original record. |
| **Sort by a field…** | Sorts by any date or number property, **Newest** or **Oldest** for dates and **Highest** or **Lowest** for numbers. |
| **Prefer records where…** | Prefers the matched contacts that meet your conditions, for example a contact that has an owner. |

Rules run from top to bottom, and only when 2 or more contacts match. A single matching contact is
used as it is, so put a hard requirement in **Match Details** instead. **Prefer records where…**
keeps only the contacts that meet it. When none of the remaining contacts meets it, Default skips the
rule, keeps those contacts for the next rules, and adds a warning to the run log. Without any rule,
Default takes the first contact HubSpot returns, and that order is not defined.
Default does not merge duplicate contacts. The rules only decide which one the workflow uses.

## When no HubSpot contact matches

The node follows **No Match** when no contact has the email. The run log then shows
**No matching records found**. It also follows **No Match**, without searching, when the email is
empty for that run. The run log then shows a warning that the match conditions could not be built
because upstream data was missing.

Create the contact on **No Match**. If the email can be empty, add a **Multi-Branch** node before
**Match Record** so a submission without an email takes its own path instead of creating a contact
with no email. See [Logic and timing nodes](/workflows/steps-logic-timing#multi-branch).

## Build a workflow that routes only new HubSpot contacts

This example handles a demo request form. Known contacts keep their owner, and the owner gets a Slack
message. New people become contacts owned by the next member of a queue named `Inbound Leads`.

<Steps>
  <Step title="Add the trigger">
    In **Workflows**, create a workflow and add the **Form Submission** trigger. Set
    **Connected Form** to your demo request form.
  </Step>

  <Step title="Match the contact by email">
    Add **Match Record** from the **Records** section. Set **CRM** to **HubSpot** and **Record** to
    **Contact**. Under **Match Details**, select **Edit**, choose **Email** and **equals**, and pick the
    email from the form. Select **Save changes**.
  </Step>

  <Step title="Tell the owner about a known contact">
    On the **Match** branch, add **Send Slack Message**. Under **Recipients**, choose the contact's
    **Contact owner** from the **Match Record** step. Write the **Message**, and insert form answers
    with `{{`. Leave the contact's owner unchanged.
  </Step>

  <Step title="Pick an owner for a new contact">
    On the **No Match** branch, add **Round-Robin** from the **Actions** section and set **Queue** to
    `Inbound Leads`.
  </Step>

  <Step title="Create the contact">
    Add **Create Record**. Set **Platform** to **HubSpot** and **Record Type** to **Contact**. In
    **Mapped Fields**, map **Email**, **First Name**, and **Last Name** from the form, and set
    **Contact owner** to **Latest assigned user** under **Round robin**.
  </Step>

  <Step title="Test and publish">
    Select **Test** and run the form trigger once with the email of an existing contact and once with
    a new email. Check HubSpot after each run. Then select **Publish** and check that the status
    next to the [deployment switch](/workflows#the-deployment-switch) reads **Live**. A new workflow
    goes live when you first publish it. If the status reads **Paused**, turn the switch on.
  </Step>
</Steps>

<Warning>
  A workflow test performs real actions in HubSpot. Use test contacts.
</Warning>

The owner in the Slack step needs a HubSpot Owner ID and a Slack User ID in
[**Settings → Users**](/settings/users), so Default can turn the contact owner into a Slack user.

Teams that do want to reroute existing contacts can add a **Round-Robin** node and an
**Update Record** node that sets **Contact owner** on the **Match** branch.

To link a new contact to its company, continue from **Create Record** with the steps in
[HubSpot contact-to-company matching](/lead-routing/matching/hubspot-contacts-to-companies#build-a-workflow-that-links-new-contacts-to-their-company).

## When 2 runs for the same email overlap

**Match Record** and **Create Record** are separate steps with no lock between them. If 2 runs for the
same email overlap, for example 2 form fills a second apart, both can find no contact and both try to
create one.

HubSpot's rule that a contact's email is unique is the guard here. The second create is rejected, so
HubSpot keeps 1 contact. When the rejection says another record already has that email,
**Create Record** continues with the contact the first run created, or with an empty ID when the
rejection names no contact ID, as described in
[Why Match Record runs before Create Record in HubSpot](#why-match-record-runs-before-create-record-in-hubspot).
The second run still took its own **Round-Robin** pick and runs the steps after **Create Record**, such
as a Slack message. Its pick is not written to the contact and does not count as an assignment. If
HubSpot words the rejection differently, the second run's **Create Record** step fails instead.

Records matched on other properties, or created without an email, have no such guard. An overlap can
then create 2 contacts.

## Check and troubleshoot HubSpot contact matching

Open the workflow, select **Runs** at the top, and select a run. Each **Match Record** step shows its
conditions and the record it chose, and each **Create Record** step shows the properties it wrote. See
[Run logs](/workflows/run-logs).

| What you see | What to check |
| - | - |
| Known people get a second contact | Check that **Match Record** uses **Record** **Contact** and compares **Email** with the email from the trigger. |
| **Create Record** shows a warning that the contact already exists | Another contact already had that email, usually from an overlapping run. The existing contact was not changed. If the warning says creation was skipped, HubSpot named no contact ID, so the step's ID is empty. Find the contact with a **Match Record** on **Email**. |
| **Create Record** fails for an email that already has a contact | HubSpot rejected the create with a message Default does not treat as a duplicate. Check that **Match Record** runs first and compares **Email**. |
| An existing contact's owner changed | Check the **Match** branch for an **Update Record** that sets **Contact owner**. |
| The wrong contact is used | Add or reorder tiebreakers under **Prioritize matched records by**. |
| The run log says the match conditions could not be built because upstream data was missing | The email was empty for this run. |
| The Slack step does not reach the owner | Map the owner's HubSpot Owner ID and Slack User ID in **Settings → Users**. |

Related: [Lead routing software from Default](https://www.default.com/product/lead-routing-software)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.