> ## Documentation Index
> Fetch the complete documentation index at: https://80x.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Create or update record

> The Create or update record block (Records) matches on a unique attribute, updates the record if found, or creates one if not.

The **Create or update record** block (Records) matches on a unique attribute, updates the record if found, or creates one if not. This is the upsert step for records. Upsert means update if a record already exists, or insert a new one if it does not. The block reuses the existing record instead of adding another. So a webhook or trigger that fires twice for the same person or company produces one record, not two.

The match works only if the attribute you match on is truly unique to one record. Good choices are `email_address` for a person or `domains` for a company. If you match on something that several records share, the block can overwrite the wrong one.

| | |
| - | - |
| **Category** | Records |
| **Credit cost** | 1 credit |
| **Engine** | New Workflows engine (June 2026+) |
| **Plan availability** | All plans that include the new Workflows engine |
| **Limits** | None documented. The workflow needs write access to the target object before it can run. |

## Inputs

| Input | Type | Required | Default | Notes |
| - | - | - | - | - |
| Object | Object | Yes | None | The object to create or update a record in, such as People or Companies. |
| Matching attribute | Attribute | Yes | None | The unique attribute used to find an existing record. Choose an attribute that identifies one record only, such as email or domain. |
| Attributes to set | Attribute values | No | None | The values to write. On a match these update the existing record. With no match they populate the new record. |
| Replace existing values | Boolean | No | Off | Controls multi-select behavior. When on, the block replaces the existing values. When off, it appends to them. |

## Outputs

| Output | Type | Example | Notes |
| - | - | - | - |
| Record | Record | The Priya Sharma person record | The created or updated record, with its attribute values after the write. |
| Record ID | Text | `a1b2c3d4-...` | The stable identifier of the matched or newly created record. |

Reference the resulting record's ID in a later block as `{{ steps.create_or_update_record.record_id }}`.

## Behavior

The block reads the matching attribute first, then decides whether to update or create. If exactly one record has the same value on the matching attribute, the block updates that record. If no record matches, the block creates a new one and fills it from the **Attributes to set** input. The output looks the same in both cases. A downstream block does not need to know which branch ran.

Single-value attributes always overwrite. Writing a new email or stage replaces the previous value no matter how **Replace existing values** is set. That toggle only affects multi-value attributes.

Multi-value attributes follow the **Replace existing values** toggle. With the toggle off, the block adds the new values to whatever is already on the record. With the toggle on, the block throws away the existing values and writes only the new set. This toggle is the single most common cause of unexpected data loss with this block. Turning it on for a `tags` or `categories` attribute deletes every value the block did not explicitly include.

The block writes on every run. It does not deduplicate beyond the single matching attribute. A record that matches on email still updates even if its other attributes differ.

## Gotchas

* **The wrong record gets overwritten.** A non-unique matching attribute, such as a first name or a company stage, can match several records. The block then updates one of them instead of the one you meant. Match on an attribute that is unique per record, such as `email_address` or `domains`.
* **Multi-select values disappear.** Turning on **Replace existing values** for a multi-value attribute deletes every existing value the run does not re-supply. Leave the toggle off to append. Turn it on only when you mean to replace the full set.
* **The block updates with stale data.** Reading from the trigger payload after an earlier block has already changed the record writes outdated values. Reference the output of the block that made the change, not the original trigger data.
* **The run fails on a write without access.** A workflow has no write access to objects by default, so the block errors until you grant the workflow write access to the target object. Grant access before you publish.

## Examples

### Upserting an inbound person by email

A webhook lead-intake workflow receives a form submission. It needs to create the person once, then update them on every later submission.

| Field | Value |
| - | - |
| Object | People |
| Matching attribute | `email_address` |
| Attributes to set | `name` = `Priya Sharma`, `job_title` = `VP of Revenue`, `company` = `acme.com` |
| Replace existing values | Off |

The first submission from `priya@acme.com` creates the person record. A second submission with an updated job title finds the same record by email and updates it in place. The workspace keeps one Priya Sharma record.

### Appending a source tag without losing existing tags

A routing workflow tags inbound people with the channel they arrived on. It keeps the tags they already carry.

```json theme={null}
{
  "object": "People",
  "matching_attribute": "email_address",
  "attributes_to_set": {
    "lead_source": ["Webinar"]
  },
  "replace_existing_values": false
}
```

A person who already has `["Newsletter"]` on `lead_source` ends up with `["Newsletter", "Webinar"]`. The toggle is off, so the block appends rather than replaces.

### Resetting a multi-select to a single value

A cleanup workflow needs the `lifecycle_stage` tags on a company to reflect exactly one current state and to drop the rest.

| Field | Value |
| - | - |
| Object | Companies |
| Matching attribute | `domains` |
| Attributes to set | `lifecycle_stage` = `Customer` |
| Replace existing values | On |

The company matched on `acme.com` keeps only `Customer` on `lifecycle_stage`. Any earlier values, such as `Prospect` or `Trial`, are removed.

## Credit cost worked example

The block costs 1 credit per run. You pay it whether the block creates a record or updates one. A webhook lead-intake workflow that upserts 800 inbound people a month spends 800 credits on this block. Triggers, conditions, and lookups in the same workflow are free, so the upsert is the only paid step in a typical intake flow. Compare that monthly total against your plan's allowance on [Credits & pricing explained](/reference/credits-and-pricing).

## Source

[Attio Workflows block library](https://attio.com/help/reference/automations/workflows/workflows-block-library).

Last reviewed against the Attio Help Center, June 2026.

## See also

* [Create record](/reference/records/create-record)
* [Update record](/reference/records/update-record)
* [Find records](/reference/records/find-records)
* [Dedupe on create](/guides/pipeline-hygiene/dedupe-on-create)
* [Webhook lead intake with safe upsert](/guides/capture-and-qualify/webhook-lead-intake)


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