Skip to content

Commit 13eff4c

Browse files
feat: add Attio CRM integration
- Add Attio tools: list_records, get_record, create_record, update_record, search_records - Add AttioBlock with operations for managing CRM records - Add AttioIcon to icons.tsx - Register Attio OAuth provider with required scopes - Add ATTIO_CLIENT_ID and ATTIO_CLIENT_SECRET env variables - Support for people, companies, and custom objects Co-authored-by: Emir Karabeg <emir-karabeg@users.noreply.github.com>
1 parent 9e817bc commit 13eff4c

File tree

14 files changed

+1127
-0
lines changed

14 files changed

+1127
-0
lines changed

apps/sim/blocks/blocks/attio.ts

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { AttioIcon } from '@/components/icons'
2+
import type { BlockConfig } from '@/blocks/types'
3+
import { AuthMode } from '@/blocks/types'
4+
import type { AttioResponse } from '@/tools/attio/types'
5+
6+
export const AttioBlock: BlockConfig<AttioResponse> = {
7+
type: 'attio',
8+
name: 'Attio',
9+
description: 'Interact with Attio CRM to manage records',
10+
authMode: AuthMode.OAuth,
11+
longDescription:
12+
'Integrate Attio into your workflow. Manage people, companies, deals, and custom objects with powerful CRM automation capabilities. Create, update, search, and list records across your Attio workspace.',
13+
docsLink: 'https://docs.attio.com',
14+
category: 'tools',
15+
bgColor: '#000000',
16+
icon: AttioIcon,
17+
subBlocks: [
18+
{
19+
id: 'operation',
20+
title: 'Operation',
21+
type: 'dropdown',
22+
options: [
23+
{ label: 'List Records', id: 'list_records' },
24+
{ label: 'Get Record', id: 'get_record' },
25+
{ label: 'Create Record', id: 'create_record' },
26+
{ label: 'Update Record', id: 'update_record' },
27+
{ label: 'Search Records', id: 'search_records' },
28+
],
29+
value: () => 'list_records',
30+
},
31+
{
32+
id: 'credential',
33+
title: 'Attio Account',
34+
type: 'oauth-input',
35+
canonicalParamId: 'oauthCredential',
36+
mode: 'basic',
37+
serviceId: 'attio',
38+
requiredScopes: [
39+
'record_permission:read',
40+
'record_permission:read-write',
41+
'object_configuration:read',
42+
],
43+
placeholder: 'Select Attio account',
44+
required: true,
45+
},
46+
{
47+
id: 'manualCredential',
48+
title: 'Attio Account',
49+
type: 'short-input',
50+
canonicalParamId: 'oauthCredential',
51+
mode: 'advanced',
52+
placeholder: 'Enter credential ID',
53+
required: true,
54+
},
55+
{
56+
id: 'object',
57+
title: 'Object Type',
58+
type: 'short-input',
59+
placeholder: 'Object slug (e.g., "people", "companies", or custom object)',
60+
condition: {
61+
field: 'operation',
62+
value: ['list_records', 'get_record', 'create_record', 'update_record'],
63+
},
64+
required: {
65+
field: 'operation',
66+
value: ['list_records', 'get_record', 'create_record', 'update_record'],
67+
},
68+
},
69+
{
70+
id: 'recordId',
71+
title: 'Record ID',
72+
type: 'short-input',
73+
placeholder: 'The unique record ID',
74+
condition: { field: 'operation', value: ['get_record', 'update_record'] },
75+
required: { field: 'operation', value: ['get_record', 'update_record'] },
76+
},
77+
{
78+
id: 'values',
79+
title: 'Record Values',
80+
type: 'long-input',
81+
placeholder:
82+
'JSON object with attribute values (e.g., {"name": "John Doe", "email_addresses": "john@example.com"})',
83+
condition: { field: 'operation', value: ['create_record', 'update_record'] },
84+
required: { field: 'operation', value: ['create_record', 'update_record'] },
85+
wandConfig: {
86+
enabled: true,
87+
maintainHistory: true,
88+
prompt: `You are an expert Attio CRM developer. Generate Attio record values as JSON based on the user's request.
89+
90+
### CONTEXT
91+
{context}
92+
93+
### CRITICAL INSTRUCTION
94+
Return ONLY the JSON object with Attio attribute values. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw JSON object that can be used directly in Attio API create/update operations.
95+
96+
### ATTIO VALUES STRUCTURE
97+
Attio record values are defined as a JSON object with attribute slugs as keys. Values can be simple types or arrays for multi-value attributes.
98+
99+
### COMMON PEOPLE ATTRIBUTES
100+
- **name**: Full name (text)
101+
- **email_addresses**: Email address(es) - string or array
102+
- **phone_numbers**: Phone number(s) - string or array
103+
- **job_title**: Job title (text)
104+
- **description**: Description/notes (text)
105+
- **linkedin_url**: LinkedIn profile URL (text)
106+
- **twitter_url**: Twitter/X profile URL (text)
107+
108+
### COMMON COMPANY ATTRIBUTES
109+
- **name**: Company name (text)
110+
- **domains**: Domain(s) - string or array (e.g., "example.com")
111+
- **description**: Company description (text)
112+
- **industry**: Industry (text)
113+
- **employee_count**: Number of employees (number)
114+
- **linkedin_url**: LinkedIn company page (text)
115+
- **twitter_url**: Twitter/X handle (text)
116+
117+
### EXAMPLES
118+
119+
**Simple Person**: "Create a person named John Doe with email john@example.com"
120+
→ {
121+
"name": "John Doe",
122+
"email_addresses": "john@example.com"
123+
}
124+
125+
**Complete Person**: "Create a person with full details"
126+
→ {
127+
"name": "Jane Smith",
128+
"email_addresses": ["jane@company.com", "jane.personal@email.com"],
129+
"phone_numbers": "+1-555-123-4567",
130+
"job_title": "Marketing Director",
131+
"description": "Key decision maker for marketing initiatives"
132+
}
133+
134+
**Simple Company**: "Create a company called Acme Corp"
135+
→ {
136+
"name": "Acme Corp",
137+
"domains": "acme.com"
138+
}
139+
140+
**Complete Company**: "Create a tech company with full details"
141+
→ {
142+
"name": "TechStart Inc",
143+
"domains": ["techstart.io", "techstart.com"],
144+
"industry": "Technology",
145+
"employee_count": 50,
146+
"description": "Innovative software solutions company"
147+
}
148+
149+
### REMEMBER
150+
Return ONLY the JSON object with attribute values - no explanations, no markdown, no extra text.`,
151+
placeholder: 'Describe the record values you want to set...',
152+
generationType: 'json-object',
153+
},
154+
},
155+
{
156+
id: 'query',
157+
title: 'Search Query',
158+
type: 'short-input',
159+
placeholder: 'Search term (names, domains, emails, phone numbers)',
160+
condition: { field: 'operation', value: 'search_records' },
161+
required: { field: 'operation', value: 'search_records' },
162+
},
163+
{
164+
id: 'objects',
165+
title: 'Object Types to Search',
166+
type: 'short-input',
167+
placeholder: 'Comma-separated object slugs (e.g., "people,companies") or leave empty for all',
168+
condition: { field: 'operation', value: 'search_records' },
169+
},
170+
{
171+
id: 'limit',
172+
title: 'Limit',
173+
type: 'short-input',
174+
placeholder: 'Max results (default: 25, max: 500 for list, 25 for search)',
175+
condition: {
176+
field: 'operation',
177+
value: ['list_records', 'search_records'],
178+
},
179+
},
180+
{
181+
id: 'offset',
182+
title: 'Offset',
183+
type: 'short-input',
184+
placeholder: 'Number of records to skip for pagination',
185+
condition: { field: 'operation', value: 'list_records' },
186+
},
187+
{
188+
id: 'attributes',
189+
title: 'Attributes to Return',
190+
type: 'short-input',
191+
placeholder: 'Comma-separated attribute slugs (e.g., "name,email_addresses")',
192+
condition: { field: 'operation', value: 'list_records' },
193+
},
194+
],
195+
tools: {
196+
access: [
197+
'attio_list_records',
198+
'attio_get_record',
199+
'attio_create_record',
200+
'attio_update_record',
201+
'attio_search_records',
202+
],
203+
config: {
204+
tool: (params) => {
205+
switch (params.operation) {
206+
case 'list_records':
207+
return 'attio_list_records'
208+
case 'get_record':
209+
return 'attio_get_record'
210+
case 'create_record':
211+
return 'attio_create_record'
212+
case 'update_record':
213+
return 'attio_update_record'
214+
case 'search_records':
215+
return 'attio_search_records'
216+
default:
217+
throw new Error(`Unknown operation: ${params.operation}`)
218+
}
219+
},
220+
params: (params) => {
221+
const { oauthCredential, operation, attributes, objects, ...rest } = params
222+
223+
const cleanParams: Record<string, any> = {
224+
oauthCredential,
225+
}
226+
227+
if (attributes && operation === 'list_records') {
228+
const parsedAttributes =
229+
typeof attributes === 'string'
230+
? attributes.split(',').map((a: string) => a.trim())
231+
: attributes
232+
cleanParams.attributes = parsedAttributes
233+
}
234+
235+
if (objects && operation === 'search_records') {
236+
const parsedObjects =
237+
typeof objects === 'string' ? objects.split(',').map((o: string) => o.trim()) : objects
238+
cleanParams.objects = parsedObjects
239+
}
240+
241+
Object.entries(rest).forEach(([key, value]) => {
242+
if (value !== undefined && value !== null && value !== '') {
243+
if (key === 'limit' || key === 'offset') {
244+
cleanParams[key] = Number(value)
245+
} else {
246+
cleanParams[key] = value
247+
}
248+
}
249+
})
250+
251+
return cleanParams
252+
},
253+
},
254+
},
255+
inputs: {
256+
operation: { type: 'string', description: 'Operation to perform' },
257+
oauthCredential: { type: 'string', description: 'Attio access token' },
258+
object: { type: 'string', description: 'Object type slug (e.g., people, companies)' },
259+
recordId: { type: 'string', description: 'Record ID for get/update operations' },
260+
values: { type: 'json', description: 'Record values to create/update (JSON object)' },
261+
query: { type: 'string', description: 'Search query string' },
262+
objects: { type: 'string', description: 'Comma-separated object types to search' },
263+
limit: { type: 'number', description: 'Maximum results to return' },
264+
offset: { type: 'number', description: 'Number of records to skip' },
265+
attributes: { type: 'string', description: 'Comma-separated attribute slugs to return' },
266+
},
267+
outputs: {
268+
records: { type: 'json', description: 'Array of record objects' },
269+
record: { type: 'json', description: 'Single record object' },
270+
recordId: { type: 'string', description: 'Record ID' },
271+
total: { type: 'number', description: 'Total number of matching results' },
272+
paging: { type: 'json', description: 'Pagination info' },
273+
metadata: { type: 'json', description: 'Operation metadata' },
274+
success: { type: 'boolean', description: 'Operation success status' },
275+
},
276+
}

apps/sim/blocks/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ApifyBlock } from '@/blocks/blocks/apify'
1010
import { ApolloBlock } from '@/blocks/blocks/apollo'
1111
import { ArxivBlock } from '@/blocks/blocks/arxiv'
1212
import { AsanaBlock } from '@/blocks/blocks/asana'
13+
import { AttioBlock } from '@/blocks/blocks/attio'
1314
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
1415
import { CalComBlock } from '@/blocks/blocks/calcom'
1516
import { CalendlyBlock } from '@/blocks/blocks/calendly'
@@ -187,6 +188,7 @@ export const registry: Record<string, BlockConfig> = {
187188
apollo: ApolloBlock,
188189
arxiv: ArxivBlock,
189190
asana: AsanaBlock,
191+
attio: AttioBlock,
190192
browser_use: BrowserUseBlock,
191193
calcom: CalComBlock,
192194
calendly: CalendlyBlock,

apps/sim/components/icons.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,18 @@ export function AlgoliaIcon(props: SVGProps<SVGSVGElement>) {
11791179
)
11801180
}
11811181

1182+
export function AttioIcon(props: SVGProps<SVGSVGElement>) {
1183+
return (
1184+
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'>
1185+
<rect width='24' height='24' rx='4' fill='#000000' />
1186+
<path
1187+
d='M12 4L4 20h4l4-8 4 8h4L12 4z'
1188+
fill='#FFFFFF'
1189+
/>
1190+
</svg>
1191+
)
1192+
}
1193+
11821194
export function GoogleBooksIcon(props: SVGProps<SVGSVGElement>) {
11831195
return (
11841196
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 478.633 540.068'>

apps/sim/lib/core/config/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ export const env = createEnv({
240240
JIRA_CLIENT_SECRET: z.string().optional(), // Atlassian Jira OAuth client secret
241241
ASANA_CLIENT_ID: z.string().optional(), // Asana OAuth client ID
242242
ASANA_CLIENT_SECRET: z.string().optional(), // Asana OAuth client secret
243+
ATTIO_CLIENT_ID: z.string().optional(), // Attio OAuth client ID
244+
ATTIO_CLIENT_SECRET: z.string().optional(), // Attio OAuth client secret
243245
AIRTABLE_CLIENT_ID: z.string().optional(), // Airtable OAuth client ID
244246
AIRTABLE_CLIENT_SECRET: z.string().optional(), // Airtable OAuth client secret
245247
APOLLO_API_KEY: z.string().optional(), // Apollo API key (optional system-wide config)

apps/sim/lib/oauth/oauth.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
22
import {
33
AirtableIcon,
44
AsanaIcon,
5+
AttioIcon,
56
CalComIcon,
67
ConfluenceIcon,
78
DropboxIcon,
@@ -629,6 +630,25 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
629630
},
630631
defaultService: 'asana',
631632
},
633+
attio: {
634+
name: 'Attio',
635+
icon: AttioIcon,
636+
services: {
637+
attio: {
638+
name: 'Attio',
639+
description: 'Manage people, companies, and custom objects in Attio CRM.',
640+
providerId: 'attio',
641+
icon: AttioIcon,
642+
baseProviderIcon: AttioIcon,
643+
scopes: [
644+
'record_permission:read',
645+
'record_permission:read-write',
646+
'object_configuration:read',
647+
],
648+
},
649+
},
650+
defaultService: 'attio',
651+
},
632652
calcom: {
633653
name: 'Cal.com',
634654
icon: CalComIcon,
@@ -1045,6 +1065,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
10451065
supportsRefreshTokenRotation: true,
10461066
}
10471067
}
1068+
case 'attio': {
1069+
const { clientId, clientSecret } = getCredentials(
1070+
env.ATTIO_CLIENT_ID,
1071+
env.ATTIO_CLIENT_SECRET
1072+
)
1073+
return {
1074+
tokenEndpoint: 'https://app.attio.com/oauth/token',
1075+
clientId,
1076+
clientSecret,
1077+
useBasicAuth: false,
1078+
supportsRefreshTokenRotation: true,
1079+
}
1080+
}
10481081
case 'pipedrive': {
10491082
const { clientId, clientSecret } = getCredentials(
10501083
env.PIPEDRIVE_CLIENT_ID,

apps/sim/lib/oauth/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type OAuthProvider =
3434
| 'wealthbox'
3535
| 'webflow'
3636
| 'asana'
37+
| 'attio'
3738
| 'pipedrive'
3839
| 'hubspot'
3940
| 'salesforce'
@@ -76,6 +77,7 @@ export type OAuthService =
7677
| 'webflow'
7778
| 'trello'
7879
| 'asana'
80+
| 'attio'
7981
| 'pipedrive'
8082
| 'hubspot'
8183
| 'salesforce'

0 commit comments

Comments
 (0)