---
name: attio
description: Use when building apps that extend Attio's CRM interface,
  integrating external services, querying workspace data, automating workflows,
  or syncing data via REST API. Reach for this skill when working with records,
  lists, webhooks, workflow automation, or custom UI extensions.
metadata:
  mintlify-proj: attio
  version: "1.0"
---

# Attio Developer Platform

## Product summary

Attio is a customizable CRM platform with a developer platform consisting of four parts: the App SDK (TypeScript React apps running in Attio's infrastructure), REST API (read/write workspace data via JSON over HTTPS), SQL (read-only queries), and MCP (AI-powered workspace management). Agents use Attio to build custom integrations, automate workflows, extend the UI with custom actions and widgets, and sync data between systems. Key files: `src/app/extensions/` (UI extensions), `src/app/blocks/` (workflow blocks), `.server.ts` files (server functions). Primary docs: https://docs.attio.com

## When to use

Reach for this skill when:
- Building custom UI extensions (record actions, widgets, tabs, pages) that run inside Attio
- Creating workflow automation blocks (triggers and steps) for the Attio Workflows builder
- Reading or writing records, lists, activities, or other workspace data via REST API
- Setting up webhooks to respond to real-time events (record created/updated, list entry changed, etc.)
- Authenticating to third-party services on behalf of users (OAuth or secrets)
- Querying workspace data with GraphQL or SQL
- Publishing apps to Attio's app store or managing app versions
- Debugging app behavior or handling errors in server functions

## Quick reference

### API Authentication

| Method | Use case | Token level |
|--------|----------|-------------|
| OAuth 2.0 | Multi-workspace apps, user-level access | `workspace` or `user` |
| API key | Single workspace, development | `workspace` only |
| Bearer token | All requests | Pass in `Authorization: Bearer <token>` header |

### REST API Endpoints (core operations)

| Task | Endpoint | Method |
|------|----------|--------|
| List records | `GET /v2/objects/{object}/records` | Query with filters, sorts, pagination |
| Create record | `POST /v2/objects/{object}/records` | Fails on unique attribute conflicts |
| Upsert record | `POST /v2/objects/{object}/records/upsert` | Creates or updates by matching attribute |
| Update record | `PATCH /v2/objects/{object}/records/{id}` | Appends multiselect values |
| Overwrite record | `PUT /v2/objects/{object}/records/{id}` | Overwrites multiselect values |
| Delete record | `DELETE /v2/objects/{object}/records/{id}` | Soft delete (archiving) |
| List webhooks | `GET /v2/webhooks` | Workspace-level only |
| Create webhook | `POST /v2/webhooks` | Subscribe to events with filters |

### App SDK File Structure

```
src/app/
├── extensions/          # UI extensions (record actions, widgets, tabs)
│   └── my-action/
│       ├── extension.tsx    # UI component
│       └── handler.server.ts # Server logic (optional)
├── blocks/              # Workflow blocks (triggers and steps)
│   └── my-block/
│       ├── block.ts         # Block definition
│       ├── configurator.tsx # Configuration UI
│       ├── execute.ts       # Step handler
│       └── activate.ts      # Trigger setup
├── pages/               # Full app pages
├── settings/            # Workspace settings
├── events/              # Connection lifecycle events
└── webhooks/            # Inbound webhook handlers
```

### Standard Objects

| Object | Default | Attributes |
|--------|---------|-----------|
| `people` | Always | name, email_addresses, phone_numbers, etc. |
| `companies` | Always | name, domain, etc. |
| `deals` | Optional | name, value, status, etc. |
| `users` | Optional | primary_email_address, etc. |
| `workspaces` | Optional | name, etc. |

### Scopes (permission model)

Common scopes: `record_permission:read`, `record_permission:read-write`, `object_configuration:read`, `webhook:read-write`, `activity_configuration:read`, `list_entry:read-write`

## Decision guidance

### When to use Create vs Upsert vs Update

| Scenario | Use | Behavior |
|----------|-----|----------|
| New record, fail on duplicates | Create | Throws 400 if unique attribute exists |
| New or existing, match by attribute | Upsert | Creates if no match, updates if match found |
| Modify existing record by ID | Update (PATCH) | Appends multiselect values, keeps existing |
| Replace multiselect values | Update (PUT) | Overwrites multiselect, removes old values |

### When to use App SDK vs REST API

| Scenario | Use | Why |
|----------|-----|-----|
| Custom UI in Attio interface | App SDK | Runs in Attio's sandbox, access to components, forms, navigation |
| External system integration | REST API | Standalone service, no UI needed, can call from anywhere |
| Workflow automation | Workflow blocks (SDK) | Integrates with Attio Workflows builder, triggers and steps |
| Real-time data sync | Webhooks (REST API) | Subscribe to events, push to external system |
| Querying workspace data | GraphQL (SDK) or SQL (REST API) | GraphQL for app UI, SQL for analytics/reporting |

### When to use Workspace vs User token level

| Scenario | Use | Constraint |
|----------|-----|-----------|
| App-wide operations (webhooks, config) | Workspace | Only admins can grant |
| User-specific data access | User | Any member can grant, requires PKCE |
| Single workspace development | Workspace | Simpler, no OAuth needed |
| Multi-workspace SaaS app | User | Respects member permissions |

## Workflow

### Building a record action with server function

1. **Create extension folder**: Make `src/app/extensions/my-action/` with `extension.tsx`
2. **Define extension**: Export `defineExtension({ type: "record-action", ... })` with `onTrigger` handler
3. **Create server function**: Add `my-action.server.ts` with `export default async function myAction() { ... }`
4. **Call from UI**: Import and call the server function from `onTrigger`, handle errors with try/catch
5. **Test locally**: Run `npm run dev`, choose workspace, install app, test on a record page
6. **Verify**: Check logs in developer console, confirm server function output

### Setting up a webhook for real-time events

1. **Identify event**: Choose event type (e.g., `record.created`, `record.updated`, `list-entry.created`)
2. **Create webhook**: POST to `/v2/webhooks` with `target_url`, `subscriptions` array with `event_type` and optional `filter`
3. **Add handler**: Create `src/app/webhooks/my-handler.ts` with `export default async function(event) { ... }`
4. **Verify signature**: Use `timingSafeEqual()` to compare webhook secret with request signature
5. **Test**: Trigger event in Attio, check webhook delivery status in `/v2/webhooks/{id}`
6. **Handle retries**: Attio retries failed webhooks; return 200 to acknowledge

### Querying records with filters and pagination

1. **Construct filter**: Use `filter` object with `$eq`, `$gt`, `$in`, `$or`, `$and` operators
2. **Add sort**: Include `sort` array with `attribute` and `direction` ("asc" or "desc")
3. **Paginate**: Use `limit` (default 50) and `offset` (start at 0)
4. **Make request**: GET `/v2/objects/{object}/records?filter=...&sort=...&limit=50&offset=0`
5. **Check results**: If results < limit, you've reached the end; otherwise increment offset and repeat
6. **Handle errors**: Catch 400 (invalid filter), 429 (rate limited), 403 (permission denied)

### Publishing an app to the app store

1. **Prepare code**: Ensure no experimental APIs, handle errors gracefully, validate form inputs
2. **Create version**: Run `npm run ship` to increment version and upload code
3. **Add listing**: Fill in app name, description, logo, listing images in developer console
4. **Submit for review**: Click "Submit for review" in Share tab
5. **Address feedback**: Attio team reviews code, security, UX; respond to comments
6. **Publish**: Once approved, app appears in Attio's app store
7. **Ship updates**: Run `npm run ship` again for new versions, no re-review needed for minor changes

## Common gotchas

- **Multiselect append vs overwrite**: PATCH appends to existing values; PUT replaces them. Choose the right endpoint or lose data.
- **Upsert matching**: Upsert uses a single matching attribute (e.g., email). If multiple records match, it fails. Use `if_multiple_matches: "match_all"` for activity records only.
- **Server function timeout**: 30-second hard limit. Long operations must defer and use callbacks; no way to extend.
- **JSON serialization**: Server functions only pass JSON-serializable data. Dates become strings; functions/symbols are lost.
- **Webhook uniqueness**: Target URL + event type + filter must be unique per workspace. Duplicate subscriptions are rejected with 409.
- **Rate limits**: 25 requests/second per webhook URL. REST API has per-endpoint limits. Implement exponential backoff and queue retries.
- **Token level confusion**: Workspace-level tokens can't be granted by non-admins. User-level tokens require PKCE and respect member permissions.
- **Scope mismatch**: Token scopes must match endpoint requirements. Missing scopes return 403 "unauthorized", not "insufficient_scopes".
- **Record reference creation**: Linking a non-existent record with `if_unmatched: "create"` counts as a write. Bulk creates can exhaust rate limits.
- **Experimental APIs**: Apps submitted for review must not use experimental APIs (marked with `Experimental_` prefix). They can change in minor versions.
- **Webhook secret rotation**: Store the secret returned on webhook creation; it's only shown once. Use it to verify webhook signatures.
- **GraphQL vs REST**: GraphQL queries in App SDK run sequentially if multiple `useQuery()` hooks are in the same component. Use `runQuery()` with `useAsyncCache()` for parallel queries.

## Verification checklist

Before submitting work:

- [ ] **Authentication**: Token has correct scopes for all endpoints used. Token level (workspace/user) matches endpoint requirements.
- [ ] **Data integrity**: Multiselect updates use correct endpoint (PATCH for append, PUT for overwrite). Upsert matching attribute is unique or handles multiple matches.
- [ ] **Error handling**: Server functions catch and re-throw errors. Webhooks return 200 on success. Rate limit errors are retried with backoff.
- [ ] **Pagination**: Offset increments correctly. Loop stops when results < limit. No infinite loops on empty result sets.
- [ ] **Webhook setup**: Target URL is reachable. Secret is stored securely. Signature verification uses `timingSafeEqual()`. Event filters are correct.
- [ ] **App SDK**: Extensions live in `src/app/extensions/` with `extension.tsx`. Server functions end in `.server.ts`. Workflow blocks in `src/app/blocks/` with required files.
- [ ] **UI/UX**: Forms validate inputs. Loading and error states are shown. No unhandled exceptions. Consistent with Attio design language.
- [ ] **Testing**: App tested locally with `npm run dev`. Installed in test workspace. Tested on actual records/lists. Logs checked in developer console.
- [ ] **Publishing**: No experimental APIs used. Code reviewed for security (no hardcoded secrets, proper auth). Listing images and metadata complete.

## Resources

- **Comprehensive navigation**: https://docs.attio.com/llms.txt — page-by-page reference for all documentation
- **REST API reference**: https://docs.attio.com/rest-api/overview — endpoints, scopes, authentication
- **App SDK overview**: https://docs.attio.com/sdk/overview — extensions, workflow blocks, server functions, components
- **Creating an app**: https://docs.attio.com/sdk/guides/creating-an-app — step-by-step setup and first app walkthrough

---

> For additional documentation and navigation, see: https://docs.attio.com/llms.txt