---
title: The Ultimate monday.com to Attio Migration Guide (2026)
slug: the-ultimate-mondaycom-to-attio-migration-guide-2026
date: 2026-04-21
author: Raaj
categories: [Migration Guide, Attio, Monday.com]
excerpt: "Technical guide to migrating from monday.com to Attio. Covers schema mapping, API limits, Import2 trade-offs, and step-by-step migration."
tldr: CSV exports flatten monday.com relationships and skip activity history. API-based migration with strict dependency ordering is the only path that preserves Account → Contact → Deal chains in Attio.
canonical: https://clonepartner.com/blog/the-ultimate-mondaycom-to-attio-migration-guide-2026/
---

# The Ultimate monday.com to Attio Migration Guide (2026)


Migrating from monday.com to Attio is a data-model translation problem, not a simple export/import chore. monday.com CRM organizes data as boards, items, columns, and Connect Boards links — a flat, spreadsheet-like structure that mirrors how a team *uses* data but doesn't enforce how it *relates*. Attio is an object-relational CRM where Companies, People, Deals, and custom objects have enforced schema, relationship attributes, and lists that separate workflow context from record-level truth.

If you attempt to lift and shift monday.com boards into Attio without restructuring the schema, you will flatten your data, sever relationship chains, and lose historical context. A CSV export moves rows. It won't preserve Account → Contact → Deal chains, the Emails & Activities timeline, or pipeline stage history. For any real CRM migration, those things matter.

This guide covers the structural mismatches between the two platforms, every viable migration method and its trade-offs, the API constraints on both sides, concrete object and field mapping, and the edge cases that silently destroy data integrity.

For context on board-based schemas, see [Zoho CRM to Monday.com Migration: The CTO's Technical Guide](https://clonepartner.com/blog/blog/zoho-crm-to-mondaycom-migration-the-ctos-technical-guide/). For Attio's data model and workspace configuration, see [The Ultimate Guide to Attio CRM (2026)](https://clonepartner.com/blog/blog/ultimate-guide-attio-crm-2025/).

## The Data Model Shift: Boards vs. Objects

Understanding the architectural gap is the prerequisite for every migration decision.

**monday.com CRM** uses a board-based model. Each CRM entity — Contacts, Leads, Accounts, Deals — lives on its own board as a row (item). Columns define the fields. Relationships between boards use **Connect Boards columns**, which are cross-references, not enforced foreign keys.

**Attio** models data as **objects** (Companies, People, Deals, Users, Workspaces) and **records**, organized into **lists** that represent workflow contexts like sales pipelines or investor tracking. Relationships between objects are first-class: a Person record has a relationship attribute linking it to a Company, and a Deal can reference both. Lists add workflow-specific attributes (like deal stage or pipeline value) on top of the underlying record without duplicating the core data.

| Concept | monday.com CRM | Attio |
|---|---|---|
| Data unit | Item (row on a board) | Record (instance of an object) |
| Schema | Columns per board, no global schema | Attributes per object, workspace-wide |
| Relationships | Connect Boards column (link, not FK) | Relationship attributes (typed, enforced) |
| Pipeline | Board groups + Status column | Lists with stage attributes |
| Activity history | Emails & Activities timeline (app) | Notes, email sync, communication intelligence |
| Custom entities | Additional boards | Custom objects (plan-limited) |

In monday.com, a Contact *item* on the Contacts board is linked to an Account *item* on the Accounts board via a Connect Boards column — but that link is a UI reference, not a database constraint. In Attio, a Person *record* has a typed relationship attribute pointing to a Company *record*, and both can appear on multiple lists without duplicating the underlying data. This distinction drives every mapping decision in the migration.

## Pre-Migration Planning & Schema Mapping

Before writing any migration code, you need a clear scope document and a field-level mapping. Skipping this step is the single most common cause of migration failure.

### Data Audit Checklist

Inventory every board in your monday.com CRM workspace:

- **Accounts board** — How many items? What columns? Any Connect Boards links to Contacts, Deals?
- **Contacts board** — Email column populated? Linked to Accounts and Deals?
- **Leads board** — Are Leads separate from Contacts or just a board group? What's the conversion workflow?
- **Deals board** — How many pipeline stages? Are deal values stored in Number columns?
- **Activities board** — This board is auto-created by the Emails & Activities app. What custom activity types exist?
- **Custom boards** — Any boards that don't map to standard CRM objects (e.g., Products, Projects, Partners)?

Identify every column type that changes migration logic: Email, Phone, Status, Dropdown, Date, Numbers, Connect Boards, Mirror, Formula, Subitems.

### Identify What Not to Migrate

Pruning reduces complexity and cost:

- Archived items and boards (monday.com's account export excludes these anyway)
- Test or demo data
- Duplicate contacts (deduplicate *before* migration, not after)
- Unused custom columns
- Internal-only boards with no CRM relevance
- **Mirror and Formula columns** — These are derived values. Do not treat them as authoritative data. monday.com's API returns `null` for the `value` field on Connect Boards, Dependency, and Subtasks columns in API versions `2025-04` and later. ([developer.monday.com](https://developer.monday.com/api-reference/changelog/value-field-now-returns-null-on-connect-boards-dependency-and-subtasks-columns)) Recompute them in Attio, or store them as one-time snapshot fields with a `legacy_` prefix.

### Attio's Object and Record Limits

This is a hard constraint that shapes your entire migration design. Attio restricts the total number of objects (standard + custom) per workspace by plan:

| Attio Plan | Object Limit | Record Limit |
|---|---|---|
| Free | 3 | 50,000 |
| Plus | 5 | 250,000 |
| Pro | 12 | 1,000,000 |
| Enterprise | Unlimited | Custom |

> [!WARNING]
> Companies and People count toward these object limits. Custom objects are available on Pro and Enterprise plans only. On Plus, the 5-object cap covers standard objects like Companies, People, and Deals — with no room for custom objects. If your monday.com workspace has many custom boards that need separate objects in Attio, you'll need at minimum the Pro plan. ([attio.com](https://attio.com/help/reference/workspace-settings-billing/attio-plans-and-features))

### Migration Strategy

- **Big bang:** Export everything, transform, import in a single cutover window. Works for small datasets (<5,000 records). Risk: any missed mapping errors affect the entire dataset.
- **Phased:** Migrate Companies first, then People, then Deals, then Activities. Validates each layer before adding the next. Preferred for datasets >5,000 records or complex relationship graphs.
- **Incremental / Delta:** Run a full migration, then sync delta changes until cutover. Required when teams can't stop working in monday.com during the migration window.

## Migration Approaches: CSV vs. Import2 vs. API vs. iPaaS

There are four realistic paths. Each has hard constraints.

### Method 1: Native CSV Export → Attio CSV Import

**How it works:** Export each monday.com board to Excel/CSV. Clean the files. Import each CSV into the corresponding Attio object or list using Attio's built-in CSV importer.

**When to use:** Flat data, <2,000 records, no complex relationships, no activity history needed.

**Hard limits:**
- monday.com caps board exports at **10,000 items per export**. Subitem updates are excluded — only the parent item's updates export. ([support.monday.com](https://support.monday.com/hc/en-us/articles/26989749858578-Export-from-monday-to-Excel))
- monday.com's account-level export does not include the Emails & Activities timeline. Archived boards and workdocs are also excluded. ([support.monday.com](https://support.monday.com/hc/en-us/articles/360002543719-How-to-export-your-entire-account-s-data))
- Attio's CSV importer accepts up to **100,000 rows, 100 columns, and 100 MB per file**. It can target objects or lists, and it can auto-create missing select options during import. It does not support importing notes. ([attio.com](https://attio.com/help/reference/imports-and-exports/import-data-into-attio))
- Connect Boards columns export as text labels, not IDs. You lose the machine-readable link between records.

**Pros:** Zero code. Good for an initial data sanity check. Free.

**Cons:** Relationships are flattened — you'll need to manually rebuild Account → Contact → Deal links. No activity or email history. Duplicate risk if unique attributes aren't mapped. Not re-runnable for delta syncs.

For a deeper analysis, see [Using CSVs for SaaS Data Migrations: Pros and Cons](https://clonepartner.com/blog/blog/csv-saas-data-migration/).

### Method 2: Import2 (Attio's Native Migration Partner)

**How it works:** Import2 is integrated into Attio's onboarding flow on Plus, Pro, and Enterprise plans. Connect your source CRM (monday.com is supported), map objects and fields through Import2's UI, run a sample migration of ~100 records, then execute the full migration. ([attio.com](https://attio.com/help/reference/imports-exports/migrate-data-from-another-crm?source=footer_pipedrive))

**When to use:** Standard CRM objects (Companies, People, Deals, Notes), straightforward field mappings, teams without engineering resources, and workspaces where Attio is not yet populated.

**Hard limits:**
- Import2 maps source objects to Attio **objects** only — not to Attio **lists**. If you need records imported directly into a sales pipeline list, use CSV or the API.
- Import2 **creates new records but does not update** existing ones. No upsert capability.
- If your team has already connected email inboxes to Attio, Import2 will create duplicates because it cannot execute custom upserts based on email addresses.

> [!WARNING]
> If email sync has already auto-created Person and Company records, Attio's documentation recommends disabling mailbox sync, deleting those records, running Import2, then re-enabling sync. This is a destructive workflow — understand the implications before proceeding.

**Pros:** No code. Handles notes migration. Free (included with Attio). Sample migration for validation.

**Cons:** Cannot import into lists. Cannot upsert. Duplicate risk with email sync. Limited transformation control.

### Method 3: Custom API-Based Migration

**How it works:** Build a custom ETL pipeline that queries monday.com's GraphQL API, stages and transforms the data, and writes to Attio's REST API using assert (upsert) endpoints. For production use, this means a staging database, a transformation layer, and retry orchestration.

**When to use:** Any migration that needs to preserve relationships, activity history, or pipeline stages. Required for >10,000 records, complex schemas, workspaces where email sync is already active, or ongoing delta syncs.

**This is the only method that can:**
- Preserve Account → Contact → Deal relationship chains
- Migrate the Emails & Activities timeline (via monday.com's API)
- Upsert records using `matching_attribute` to avoid duplicates
- Handle custom objects and list-specific attributes
- Be re-run safely for delta/incremental migrations

**Cons:** Requires engineering time (Python, Node.js, or similar). Must handle rate limits on both sides. Error handling, retry logic, and logging are your responsibility.

### Method 4: iPaaS Middleware (Zapier, Make)

**How it works:** Build Zaps or Make scenarios that trigger on monday.com events and create/update records in Attio. Attio ships an official Zapier app. ([attio.com](https://attio.com/apps/zapier))

**When to use:** Ongoing real-time sync of *new* records after a bulk migration. Not suitable for historical data migration.

**Why it doesn't work for migration:** Zapier and Make are designed for single-record, event-driven automation — not bulk data transfer. No native support for paginating through thousands of existing records. Rate limit and error handling is abstracted away (which means you can't control it). Expensive at scale due to per-task pricing.

### Comparison Table

| Factor | CSV Export/Import | Import2 | Custom API | iPaaS (Zapier/Make) |
|---|---|---|---|---|
| Cost | Free | Free | Engineering time | Per-task pricing |
| Relationships preserved | ❌ | Partial | ✅ | Partial |
| Activity/email history | ❌ | Notes only | ✅ | ❌ |
| Imports into Attio Lists | ✅ (manual) | ❌ | ✅ | ✅ |
| Upsert (dedup with email sync) | Manual | ❌ | ✅ | Partial |
| Handles >10K records | ❌ (export cap) | ✅ | ✅ | ❌ (cost/speed) |
| Re-runnable for deltas | ❌ | ❌ | ✅ | Partial |
| Complexity | Low | Low | High | Medium |

**Pick by scenario:**

- **Small business, <2K records, flat data:** CSV or Import2
- **Mid-market, 2K–50K records, relationships matter:** Custom API or managed service
- **Enterprise, >50K records, ongoing sync needed:** Custom API with delta sync, or managed service
- **Attio already populated with email sync data:** Custom API with assert endpoints — Import2 will create duplicates
- **One-time migration plus short coexistence:** Bulk load with API, then iPaaS for deltas until final cutover

## Deep Dive: Exporting Data from monday.com

### Board-Level Export

monday.com lets you export any board to Excel from the board menu. The limit is **10,000 items per export**. Only table views export — board views, dashboards, and workdocs do not. Subitem updates are excluded — only the parent item's updates export.

### Account-Level Export

Account admins can export the entire account as a ZIP file containing all boards (including private and shareable boards) as Excel files. This takes up to 24 hours for large accounts and can only be triggered once every 24 hours. The download link expires after 24 hours. ([support.monday.com](https://support.monday.com/hc/en-us/articles/360002543719-How-to-export-your-entire-account-s-data))

**What's excluded from the account export:**
- The Emails & Activities timeline
- Archived boards
- Workdocs (export these as PDF separately before running the account export)
- Dashboards (though their underlying board data is included)

> [!NOTE]
> Backups are two-part on monday.com. The account export gives you boards and files, but not the Emails & Activities timeline. If activity history matters, extract it separately via API before cutover.

### API-Based Extraction (Recommended)

monday.com exposes a **GraphQL API**. This is the only way to extract complete data including column values, Connect Boards relationships, and activity data. ([developer.monday.com](https://developer.monday.com/api-reference/docs/introduction-to-graphql))

> [!CAUTION]
> **Pin your monday API version.** Recent versions changed request formatting and Connect Boards response behavior. In API versions `2025-04` and later, the `value` field returns `null` for Connect Boards, Dependency, and Subtasks columns. Query `linked_item_ids` directly instead. Unpinned extraction code is risky. ([developer.monday.com](https://developer.monday.com/api-reference/docs/api-versioning))

monday.com's API uses a **complexity-based rate limit system**: ([developer.monday.com](https://developer.monday.com/api-reference/docs/rate-limits))

| Limit Type | Value |
|---|---|
| Per-query complexity | 5,000,000 points |
| Per-minute complexity (personal token) | 10,000,000 combined read+write |
| Per-minute complexity (app token) | 5,000,000 each for read and write |
| Minute limit (Pro) | 2,500 queries/min |
| Minute limit (Enterprise) | 5,000 queries/min |
| Concurrency limit (Pro) | 100 concurrent requests |

Deeply nested queries — fetching items with all column values, their linked items via Connect Boards, then *those* items' column values — can burn through complexity budget fast. A single query can hit the 5M per-query cap. Always include the `complexity` field in your queries to monitor budget consumption in real time.

Example: extracting items with Connect Boards relationships from a board:

```graphql
query GetBoardPage($boardId: [ID!]) {
  complexity {
    query
    before
    after
  }
  boards(ids: $boardId) {
    id
    name
    items_page(limit: 500) {
      cursor
      items {
        id
        name
        updated_at
        column_values {
          id
          text
          ... on BoardRelationValue {
            linked_item_ids
          }
        }
      }
    }
  }
}
```

Use cursor-based pagination to iterate through all items. The API returns a `cursor` value; pass it to subsequent requests to fetch the next page. monday.com recommends 200–500 items per call to avoid complexity budget exhaustion.

## Deep Dive: Importing to Attio & Handling API Limits

Attio's API is REST-based. The key migration endpoints: ([docs.attio.com](https://docs.attio.com/rest-api/endpoint-reference/records/assert-a-record))

- `PUT /v2/objects/{object}/records` — Assert (upsert) a record. Uses `matching_attribute` to find existing records and update them, or create new ones. This is your primary dedup mechanism.
- `POST /v2/lists/{list_id}/entries` — Add a record to a list as an entry.
- `POST /v2/notes` — Create a note attached to a record.
- `GET /v2/objects/{object}/records` — List records with filters (tops out at 500 results per page).

### Rate Limits

Attio enforces strict rate limits: ([docs.attio.com](https://docs.attio.com/rest-api/guides/rate-limiting))

| Limit Type | Value |
|---|---|
| Read requests | 100 per second |
| Write requests | 25 per second |
| HTTP response on limit | 429 Too Many Requests with `Retry-After` header |
| List query scoring | Score-based, 10-second sliding window |

At 25 writes/second, importing 50,000 records takes a minimum of ~33 minutes of pure write time — not counting reads, retries, or relationship linking. Plan accordingly.

Attio's rate limits are **per-workspace**, not per-integration. If another tool (Zapier, a webhook, another script) is writing to the same Attio workspace during your migration, you share the 25 writes/sec budget. Pause other integrations during bulk import windows.

List record and list entry endpoints also use **score-based rate limits** where complex filters and sorts consume more budget. If a single query exceeds the per-query score limit, simplify your filter — waiting and retrying won't help.

### The Assert (Upsert) Pattern

This is the most important pattern for avoiding duplicates. Attio's assert endpoint checks for an existing record based on a matching attribute (like `email_addresses` for People or `domains` for Companies) before creating a new one.

```javascript
// Upserting a Person in Attio using the Assert Endpoint
const response = await fetch(
  'https://api.attio.com/v2/objects/people/records?matching_attribute=email_addresses',
  {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${ATTIO_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      data: {
        values: {
          email_addresses: [{ email_address: 'jane.doe@example.com' }],
          first_name: [{ value: 'Jane' }],
          last_name: [{ value: 'Doe' }],
          // Link to Company using the Attio ID saved in staging
          company: [{ target_object: 'companies', target_record_id: 'attio_company_123' }]
        }
      }
    })
  }
);
```

> [!TIP]
> Attio doesn't automatically create referenced records. If you're asserting a Person with a relationship to a Company, the Company record must already exist. Always import in dependency order: Companies → People → Deals → List entries → Notes.

For custom objects and Deals, create a custom unique attribute (e.g., `legacy_monday_item_id`) in Attio to use as the matching key. This gives you idempotent reruns, stable joins, and a rollback key. See [Pipedrive to Attio Migration: The CTO's Guide to Data Mapping & APIs](https://clonepartner.com/blog/blog/pipedrive-to-attio-migration-the-ctos-guide-to-data-mapping-apis/) for a deeper look at the assert endpoint mechanics.

## Data Model & Object Mapping

Don't map board-for-board. Decide whether each monday.com board is a canonical entity, a workflow subset, or a derived surface. In Attio terms: canonical entities become objects; workflow subsets become lists; derived surfaces are rebuilt as views or computed fields.

### Object-Level Mapping

| monday.com CRM | Attio Target | Notes |
|---|---|---|
| Accounts board (items) | **Companies** (records) | Map company name, domain, address, industry. Use `domains` as dedupe key. |
| Contacts board (items) | **People** (records) | Map name, email, phone, job title. Use `email_addresses` as dedupe key. |
| Leads board (items) | **People** + Sales Pipeline **List** | Leads become People records added to a pipeline list with a pre-qualified stage. Don't force every lead into Deals too early. |
| Deals board (items) | **Deals** (records) on a Pipeline **List** | Map deal name, value, stage, close date. Pipeline stages map to list status attributes. |
| Activities (Emails & Activities app) | **Notes** + email sync | Activity types (calls, meetings) → Notes. Email history → reconnect via Attio email sync. Attio's public docs don't document a bulk backfill of historical email events. |
| Custom boards | **Custom Objects** or **Lists** | Depends on whether the data is entity-level or workflow-level. Custom objects are Pro/Enterprise only. |
| Connect Boards links | **Relationship attributes** | Extract `linked_item_ids` from the API. Do not rely on the raw `value` field. |
| Mirror columns | Recompute or snapshot | Derived field — not a source of truth. |
| Formula columns | Recompute or snapshot | Derived field — especially risky when built on Mirror/Connect data. |

### Field-Level Mapping

| monday.com Column | Column Type | Attio Attribute | Attio Type | Transform |
|---|---|---|---|---|
| Company Name | Text | Name | Text | Direct |
| Domain | Text/Link | Domains | Domain | Extract domain from URL if needed |
| Email | Email | Email addresses | Email | Direct |
| Phone | Phone | Phone numbers | Phone | Normalize with country code |
| Industry | Dropdown | Industry | Select | Map picklist values before loading |
| Deal Value | Number | Deal value | Currency | Set currency code |
| Deal Stage | Status | Stage (list attribute) | Status | Map status labels to Attio pipeline stages |
| Close Date | Date | Projected close date | Date | Format to ISO 8601 |
| Owner | People | Deal owner | Workspace member | Map by email address |
| Notes | Long Text/Updates | Notes (via API) | Note | Each update → separate Attio note |
| Files | File column | — | — | Download from monday.com, re-upload or link |

> [!WARNING]
> Do not migrate Mirror and Formula columns as authoritative data. They are derived values. Recompute them in Attio, or store as snapshot fields with a `legacy_` prefix. monday.com's API returns `null` for formulas that depend on Mirror and Connect Boards columns in recent API versions. ([developer.monday.com](https://developer.monday.com/api-reference/changelog/value-field-now-returns-null-on-connect-boards-dependency-and-subtasks-columns))

### Handling Relationships

In monday.com, the **Connect Boards column** creates a visual link between items on different boards. When you export to CSV, this column exports as a comma-separated list of item names — not IDs. If two items on the linked board share the same name (two "John Smith" contacts), the CSV can't distinguish them.

For API-based migration:

1. Extract Connect Boards data via the API to get `linked_item_ids`
2. Build a mapping table: `monday_item_id → attio_record_id`
3. Import Companies and People first, storing Attio record IDs in the mapping table
4. Set relationship attributes on People records pointing to their Company
5. Import Deals, linking them to the correct People and Companies

This is the step where CSV-based migrations break. Without the API, you're doing string matching on company names — which fails on duplicates, abbreviations, and typos.

## Step-by-Step API Migration Process

### Phase 1: Extract from monday.com

1. **Freeze source schema changes.** Stop teams from adding new columns, automations, or boards mid-project.
2. **Enumerate all CRM boards** — Identify Accounts, Contacts, Leads, Deals, Activities, and custom boards by board ID.
3. **Extract items with column values** — Paginate through each board using the GraphQL API with cursor pagination. Pin the API version header.
4. **Extract Connect Boards relationships** — For each item, resolve Connect Boards column values to get `linked_item_ids`.
5. **Extract Emails & Activities** — This data lives in the Emails & Activities app, not in standard board columns. The API is the only extraction path.
6. **Store raw data** — Write to JSON files or a staging database (PostgreSQL works). Do not attempt to transform and load in a single stream.

### Phase 2: Transform

1. **Deduplicate** — Identify duplicate contacts across Contacts and Leads boards (common in monday.com).
2. **Normalize fields** — Standardize phone formats (include country code), email casing, date formats (ISO 8601), domains (root domain only). Attio validates these strictly.
3. **Map picklist values** — monday.com Status/Label values → Attio Select options. Standardize before loading — Attio can auto-create missing options during CSV import, but that's how you end up with three near-identical pipeline stages.
4. **Build relationship graph** — Create a lookup: `{monday_item_id: {object_type, attio_record_id}}` (attio_record_id populated during load phase).
5. **Split Leads** — Decide which Leads become People records only vs. People + Deal entries on a pipeline list.

### Phase 3: Load into Attio

Load in strict dependency order:

1. **Companies** — Assert using `domains` as matching attribute. Store returned Attio record IDs in your mapping table alongside original monday.com item IDs.
2. **People** — Assert using `email_addresses`. Look up the parent Company's Attio ID from the mapping table and pass it in the relationship attribute.
3. **Deals** — Create Deal records. Because Deals don't have a natural unique identifier like email, match on a custom `legacy_monday_item_id` attribute. Link to both People and Companies.
4. **List entries** — Add Deals to pipeline lists with stage attributes. Add People to relevant lists.
5. **Notes** — Attach to the appropriate record via the Notes API.
6. **Custom objects** — Create custom object records and set relationships.

Implement exponential backoff for 429 responses:

```python
import time
import requests

def attio_write(url, payload, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.put(url, json=payload, headers=headers)
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 2))
            wait = retry_after * (2 ** attempt)
            time.sleep(wait)
            continue
        response.raise_for_status()
        return response.json()
    raise Exception(f"Failed after {max_retries} retries")
```

### Phase 4: Validate

1. **Record count comparison** — Total records per object in monday.com vs. Attio.
2. **Field-level spot checks** — Sample 5% of records, verify field values match.
3. **Relationship validation** — For sampled records, verify Company → People → Deal chains are intact.
4. **Pipeline stage distribution** — Compare deal stage counts between source and target.
5. **Notes verification** — Confirm notes are attached to the correct records.
6. **Duplicate search** — Search People by email, Companies by domain. Zero duplicates expected.

## Automation Script Outline (Python)

High-level structure for teams building in-house:

```python
# migration.py — monday.com → Attio migration outline

import requests
import time
import json
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("migration")

# --- Config ---
MONDAY_API_KEY = "your_monday_api_key"
ATTIO_API_KEY = "your_attio_api_key"
MONDAY_API_URL = "https://api.monday.com/v2"
ATTIO_API_URL = "https://api.attio.com/v2"

BOARDS = {
    "accounts": 123456789,
    "contacts": 234567890,
    "deals":    345678901,
}

# --- Phase 1: Extract ---
def extract_monday_items(board_id):
    """Paginate through all items on a monday.com board."""
    items = []
    cursor = None
    while True:
        query = build_items_query(board_id, cursor)
        response = monday_request(query)
        page = response["data"]["boards"][0]["items_page"]
        items.extend(page["items"])
        cursor = page.get("cursor")
        if not cursor:
            break
    return items

# --- Phase 2: Transform ---
def transform_account_to_company(monday_item):
    """Map monday.com Account item to Attio Company payload."""
    cols = {cv["id"]: cv["text"] for cv in monday_item["column_values"]}
    return {
        "data": {
            "values": {
                "name": monday_item["name"],
                "domains": [cols.get("domain", "")],
                "description": cols.get("description", ""),
            }
        }
    }

# --- Phase 3: Load ---
def assert_attio_record(object_slug, matching_attr, payload):
    """Upsert a record in Attio using the assert endpoint."""
    url = f"{ATTIO_API_URL}/objects/{object_slug}/records?matching_attribute={matching_attr}"
    return attio_write(url, payload)

def attio_write(url, payload, max_retries=5):
    headers = {
        "Authorization": f"Bearer {ATTIO_API_KEY}",
        "Content-Type": "application/json",
    }
    for attempt in range(max_retries):
        resp = requests.put(url, json=payload, headers=headers)
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 2)) * (2 ** attempt)
            logger.warning(f"Rate limited. Waiting {wait}s...")
            time.sleep(wait)
            continue
        resp.raise_for_status()
        return resp.json()
    raise Exception("Max retries exceeded")

# --- Main ---
def run_migration():
    # 1. Extract
    accounts = extract_monday_items(BOARDS["accounts"])
    contacts = extract_monday_items(BOARDS["contacts"])
    deals = extract_monday_items(BOARDS["deals"])

    # 2. Load Companies (dependency order: first)
    id_map = {}  # monday_item_id -> attio_record_id
    for acct in accounts:
        payload = transform_account_to_company(acct)
        result = assert_attio_record("companies", "domains", payload)
        id_map[acct["id"]] = result["data"]["id"]["record_id"]

    # 3. Load People (with company relationships)
    for contact in contacts:
        payload = transform_contact_to_person(contact, id_map)
        result = assert_attio_record("people", "email_addresses", payload)
        id_map[contact["id"]] = result["data"]["id"]["record_id"]

    # 4. Load Deals (with people + company relationships)
    for deal in deals:
        payload = transform_deal(deal, id_map)
        result = assert_attio_record("deals", "legacy_monday_item_id", payload)
        id_map[deal["id"]] = result["data"]["id"]["record_id"]

    logger.info(f"Migration complete. {len(id_map)} records migrated.")

if __name__ == "__main__":
    run_migration()
```

This is a skeleton. A production script needs: comprehensive error logging, a persistent ID map (database or JSON file that survives restarts), parallel workers with shared rate limit tracking, field-level validation, separate phases for notes and list entries, and dead-letter handling for transformation rejects.

**Logging rules worth enforcing:**
- Log source board ID, item ID, and update ID with every migrated record
- Persist transformation rejects to a dead-letter table instead of dropping them
- Retry 429 and transient 5xx responses with backoff and jitter
- Separate validation failures from transport failures so you don't hide bad data behind retry noise

## Common Edge Cases & Failure Modes

### Duplicate Records from Email Sync

The #1 post-migration issue with Attio. If your team connects email inboxes before or during migration, Attio's email sync auto-creates Person and Company records from email metadata. Your migration script then creates *additional* records for the same people.

**Prevention:** Either disable email sync until migration is complete, or use the assert endpoint with `matching_attribute=email_addresses` to upsert. Import2 cannot do this — it only creates, never updates.

### Connect Boards Data Loss on CSV Export

Connect Boards columns export as plain-text item names. If two items on the linked board share the same name (e.g., two "John Smith" contacts), the CSV can't distinguish them. The relationship is ambiguous or lost.

**Prevention:** Use the API to extract Connect Boards values with `linked_item_ids`, not display names.

### Emails & Activities Timeline Not Exportable

The account-level export explicitly excludes the Emails & Activities timeline. Emails, call logs, meeting notes, and activity summaries stored in the Emails & Activities app are **not available** in any native export format. The API is the only extraction path. ([support.monday.com](https://support.monday.com/hc/en-us/articles/360002543719-How-to-export-your-entire-account-s-data))

### Custom Object Budget Exhaustion

If your monday.com workspace has 8 custom boards that each represent a distinct entity (Products, Partners, Invoices, Projects), you'll need at minimum the Pro plan (12 objects). Custom objects aren't available on Plus at all. This often isn't discovered until mid-migration.

### monday.com API Version Breaking Changes

monday.com's API behavior changes by version. Request body requirements, variable format, and Connect Boards response structure have changed in recent releases. The `value` field returns `null` for Connect Boards, Dependency, and Subtasks columns in `2025-04` and later. Always pin your `API-Version` header. ([developer.monday.com](https://developer.monday.com/api-reference/docs/api-versioning))

### Rate Limit Interactions Between Multiple Apps

Attio's rate limits are per-workspace, not per-integration. If another tool is writing to the same Attio workspace during your migration, you share the 25 writes/sec budget. Coordinate with your team to pause other integrations during bulk import windows.

### Missing or Invalid Data Formats

Invalid phone numbers, email addresses, and domain formats will fail Attio imports or API writes. Attio validates these fields strictly. Normalize before loading: phones with country codes, domains as root domains only, emails lowercase.

## Attio Limitations & Constraints

Be aware of these before committing to Attio as your target:

- **Object limits are plan-gated.** 3 on Free, 5 on Plus, 12 on Pro, unlimited on Enterprise. Companies and People count toward the cap. Custom objects require Pro or Enterprise.
- **Record limits exist.** 50K (Free), 250K (Plus), 1M (Pro). Verify your monday.com record volume fits the target plan before migration.
- **No native bulk API.** Attio's API handles one record per request. At 25 writes/sec, large migrations require patience and careful batching — there's no Bulk API equivalent.
- **Notes cannot be imported via CSV.** The CSV importer handles records and list entries only. Notes require the API or Import2.
- **Attio doesn't automatically create referenced records.** If you assert a Person with a company relationship before that Company exists, the request fails. Strict dependency ordering is required.
- **Email sync and communication intelligence are centered on People and Companies.** Some monday.com activity history may need to be preserved as Notes or Tasks rather than recreated as native historical email events.

## Validation & Testing

### Pre-Migration Test Run

Always run a test migration against a separate Attio workspace (Attio's Free plan supports this). Validate:

- All objects created with correct attributes
- Relationship links resolve correctly
- Pipeline stages map to expected Attio status values
- Notes attach to the correct records
- No duplicate records

### Post-Migration QA

| Check | Method | Pass Criteria |
|---|---|---|
| Record counts | API query both systems | ±0 variance |
| Field values | 5% random sample spot check | 100% match on sampled records |
| Relationships | Verify 20 random Account→Contact→Deal chains | All links intact |
| Pipeline stages | Compare stage distribution | Counts match within ±1% |
| Notes | Verify 10 random records | Correct content, correct attachment |
| Duplicates | Search People by email, Companies by domain | 0 duplicates |

Run UAT with the people who actually use the CRM. Record-count comparisons are necessary but not sufficient.

### Rollback Plan

Before running the full migration, export your existing Attio workspace data as a backup. If the migration creates bad data, you can bulk-delete imported records (filter by `Created at` timestamp) and re-run after fixing the script. The `legacy_monday_item_id` attribute makes identifying and rolling back imported records straightforward.

## Post-Migration Tasks

1. **Rebuild automations.** monday.com automations don't transfer. Recreate critical workflows in Attio's workflow builder.
2. **Reconnect email sync.** If you disabled email sync for migration, re-enable it only after confirming your dedupe strategy is solid. Attio will begin ingesting new emails and enriching records.
3. **Configure lists and views.** Set up pipeline lists, filtered views, and reporting dashboards in Attio to match your team's workflow.
4. **Train users.** Attio's interface is fundamentally different from monday.com's board-centric model. Budget 1–2 hours per user for training on the object/list/record paradigm. Don't let teams recreate monday.com's board sprawl in Attio.
5. **Monitor for 2 weeks.** Watch for duplicate records (email sync catching up), missing relationships, orphaned deals, and pipeline stage mismatches.

## Best Practices

- **Back up everything before migration.** Export monday.com account data and Attio workspace data. Back up the Emails & Activities timeline separately via API — it's excluded from monday.com's account export.
- **Run test migrations on a free Attio workspace.** Validate mappings before touching production.
- **Disable Attio email sync during bulk import** if not using assert/upsert endpoints. Re-enable after.
- **Load in dependency order.** Companies → People → Deals → List entries → Notes. Never reverse.
- **Pin your monday.com API version.** Request formatting and Connect Boards behavior change between versions.
- **Monitor monday.com's complexity budget.** Add the `complexity` field to every GraphQL query.
- **Use idempotent writes.** Use Attio's `matching_attribute` parameter so re-running the script doesn't create duplicates. Add a `legacy_monday_item_id` attribute on every target object for stable joins and rollback.
- **Log everything.** Every API call, every response, every error. You'll need the audit trail during validation.
- **Plan for re-runs.** The first migration attempt rarely produces perfect results. Design your pipeline to be safely re-executable.
- **Separate validation failures from transport failures.** Don't hide bad data behind retry noise.
- **Prefer canonical source fields over Mirror or Formula columns.** Derived values are unreliable as migration sources.

## When a Managed Migration Service Makes Sense

Building a custom API pipeline is the right approach for preserving data fidelity. It's also a significant engineering investment:

- **monday.com's API complexity limits** require careful query design, pagination, and budget monitoring
- **Attio's 25 writes/sec limit** means large migrations take hours of continuous, monitored execution
- **Relationship rebuilding** requires maintaining a cross-system ID mapping table and loading in strict order
- **Edge cases** — picklist mismatches, multi-value columns, file attachments, duplicate resolution — add scope that's hard to estimate upfront

For a 20,000-record migration with relationships and activity history, teams typically underestimate the effort by 3–5x. What looks like a weekend project becomes a 2–3 week engineering sprint once you account for rate limit tuning, error handling, validation, and re-runs.

At ClonePartner, we've handled complex CRM migrations — including monday.com's board-based schema — across 1,200+ engagements. Our scripts handle monday.com's complexity limits and Attio's write ceiling without dropping records. We upsert using custom unique IDs so teams can keep their Attio email sync running during migration without duplicates. And we extract the full Emails & Activities timeline that monday.com's native export leaves behind.

See [how we completed Inuka's Attio migration in two days with zero errors](https://clonepartner.com/blog/blog/inuka-s-two-day-error-free-move-to-attio/) for a concrete example. For how we approach migrations in general, see [How We Run Migrations at ClonePartner](https://clonepartner.com/blog/blog/how-we-run-migrations-at-clonepartner/).

> Migrating from monday.com to Attio and want to preserve relationships, activity history, and pipeline data without the engineering overhead? Our team has done it — scope your migration in a 30-minute call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30&utm_source=blog&utm_medium=button&utm_campaign=demo_bookings&utm_content=cta_click&utm_term=demo_button_click)

## Frequently asked questions

### Can I migrate monday.com data to Attio using CSV export?

You can, but CSV exports flatten Connect Boards relationships into plain text and exclude the Emails & Activities timeline entirely. monday.com board exports are capped at 10,000 items. CSVs work for small, flat datasets under 2,000 records where relationship preservation isn't required. For anything more complex, use the API or a managed service.

### What are Attio's API rate limits for migration?

Attio enforces 100 read requests per second and 25 write requests per second, per workspace. Exceeding these returns HTTP 429 with a Retry-After header. List queries also use score-based rate limits on a 10-second sliding window. At 25 writes/sec, importing 50,000 records takes at minimum ~33 minutes of pure write time.

### Does Import2 work for monday.com to Attio migration?

Import2 is free and handles standard objects (Companies, People, Deals, Notes), but it cannot import into Attio Lists, cannot upsert existing records, and will create duplicates if email sync has already populated your workspace. You must delete email-sync-created records before running Import2, or use the API with assert endpoints instead.

### Can I export the Emails & Activities timeline from monday.com?

Not through native exports. monday.com's account export explicitly excludes the Emails & Activities timeline. The only way to extract email history, call logs, and activity data is through monday.com's GraphQL API. This is a key reason API-based migration is recommended over CSV.

### How many custom objects does Attio support?

Attio limits total objects (standard + custom) by plan: 3 on Free, 5 on Plus, 12 on Pro, and unlimited on Enterprise. Custom objects are only available on Pro and Enterprise. Companies and People count toward total object limits, so plan your monday.com board-to-object mapping accordingly.
