feat(attio): add Attio CRM integration with 40 tools and 18 webhook triggers (#3324)

* feat(attio): add Attio CRM integration with 40 tools and 18 webhook triggers

* update docs

* fix(attio): use timestamp generationType for date wandConfig fields
This commit is contained in:
Waleed
2026-02-24 13:56:42 -08:00
committed by GitHub
parent 9a31c7d8ad
commit 60f9eb21bf
77 changed files with 8390 additions and 0 deletions

View File

@@ -3552,6 +3552,15 @@ export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function AttioIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60.9 50' fill='currentColor'>
<path d='M60.3,34.8l-5.1-8.1c0,0,0,0,0,0L54.7,26c-0.8-1.2-2.1-1.9-3.5-1.9L43,24L42.5,25l-9.8,15.7l-0.5,0.9l4.1,6.6c0.8,1.2,2.1,1.9,3.5,1.9h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.6c0,0,0,0,0,0l5.1-8.2C61.1,37.9,61.1,36.2,60.3,34.8L60.3,34.8z M58.7,38.3l-5.1,8.2c0,0,0,0.1-0.1,0.1c-0.2,0.2-0.4,0.2-0.5,0.2c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.1-0.1-0.1-0.2-0.2-0.3c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.4-0.1-0.8,0-1.3c0.1-0.2,0.1-0.4,0.3-0.6l5.1-8.1c0,0,0,0,0,0c0.1-0.2,0.3-0.3,0.4-0.3c0.1,0,0.1,0,0.1,0c0,0,0,0,0.1,0c0.1,0,0.4,0,0.6,0.3l5.1,8.1C59.2,36.6,59.2,37.5,58.7,38.3L58.7,38.3z' />
<path d='M45.2,15.1c0.8-1.3,0.8-3.1,0-4.4l-5.1-8.1l-0.4-0.7C38.9,0.7,37.6,0,36.2,0H24.7c-1.4,0-2.7,0.7-3.5,1.9L0.6,34.9C0.2,35.5,0,36.3,0,37c0,0.8,0.2,1.5,0.6,2.2l5.5,8.8C6.9,49.3,8.2,50,9.7,50h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.7c0,0,0,0,0,0c0,0,0,0,0,0l4.1-6.6l12.1-19.4L45.2,15.1L45.2,15.1z M44,13c0,0.4-0.1,0.8-0.4,1.2L23.5,46.4c-0.2,0.3-0.5,0.3-0.6,0.3c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.5-0.7-0.5-1.7,0-2.4L37.4,3.6c0.2-0.3,0.5-0.3,0.6-0.3c0.1,0,0.4,0,0.6,0.3l5.1,8.1C43.9,12.1,44,12.5,44,13z' />
</svg>
)
}
export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'>

View File

@@ -13,6 +13,7 @@ import {
ApolloIcon,
ArxivIcon,
AsanaIcon,
AttioIcon,
BrainIcon,
BrowserUseIcon,
CalComIcon,
@@ -159,6 +160,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
apollo: ApolloIcon,
arxiv: ArxivIcon,
asana: AsanaIcon,
attio: AttioIcon,
browser_use: BrowserUseIcon,
calcom: CalComIcon,
calendly: CalendlyIcon,

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
"apollo",
"arxiv",
"asana",
"attio",
"browser_use",
"calcom",
"calendly",

View File

@@ -301,6 +301,16 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'user-follow-modify': 'Follow and unfollow artists and users',
'user-read-playback-position': 'View playback position in podcasts',
'ugc-image-upload': 'Upload images to Spotify playlists',
// Attio
'record_permission:read-write': 'Read and write CRM records',
'object_configuration:read-write': 'Read and manage object schemas',
'list_configuration:read-write': 'Read and manage list configurations',
'list_entry:read-write': 'Read and write list entries',
'note:read-write': 'Read and write notes',
'task:read-write': 'Read and write tasks',
'comment:read-write': 'Read and write comments and threads',
'user_management:read': 'View workspace members',
'webhook:read-write': 'Manage webhooks',
}
function getScopeDescription(scope: string): string {

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import { ApifyBlock } from '@/blocks/blocks/apify'
import { ApolloBlock } from '@/blocks/blocks/apollo'
import { ArxivBlock } from '@/blocks/blocks/arxiv'
import { AsanaBlock } from '@/blocks/blocks/asana'
import { AttioBlock } from '@/blocks/blocks/attio'
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
import { CalComBlock } from '@/blocks/blocks/calcom'
import { CalendlyBlock } from '@/blocks/blocks/calendly'
@@ -187,6 +188,7 @@ export const registry: Record<string, BlockConfig> = {
apollo: ApolloBlock,
arxiv: ArxivBlock,
asana: AsanaBlock,
attio: AttioBlock,
browser_use: BrowserUseBlock,
calcom: CalComBlock,
calendly: CalendlyBlock,

View File

@@ -3552,6 +3552,15 @@ export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function AttioIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60.9 50' fill='currentColor'>
<path d='M60.3,34.8l-5.1-8.1c0,0,0,0,0,0L54.7,26c-0.8-1.2-2.1-1.9-3.5-1.9L43,24L42.5,25l-9.8,15.7l-0.5,0.9l4.1,6.6c0.8,1.2,2.1,1.9,3.5,1.9h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.6c0,0,0,0,0,0l5.1-8.2C61.1,37.9,61.1,36.2,60.3,34.8L60.3,34.8z M58.7,38.3l-5.1,8.2c0,0,0,0.1-0.1,0.1c-0.2,0.2-0.4,0.2-0.5,0.2c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.1-0.1-0.1-0.2-0.2-0.3c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.4-0.1-0.8,0-1.3c0.1-0.2,0.1-0.4,0.3-0.6l5.1-8.1c0,0,0,0,0,0c0.1-0.2,0.3-0.3,0.4-0.3c0.1,0,0.1,0,0.1,0c0,0,0,0,0.1,0c0.1,0,0.4,0,0.6,0.3l5.1,8.1C59.2,36.6,59.2,37.5,58.7,38.3L58.7,38.3z' />
<path d='M45.2,15.1c0.8-1.3,0.8-3.1,0-4.4l-5.1-8.1l-0.4-0.7C38.9,0.7,37.6,0,36.2,0H24.7c-1.4,0-2.7,0.7-3.5,1.9L0.6,34.9C0.2,35.5,0,36.3,0,37c0,0.8,0.2,1.5,0.6,2.2l5.5,8.8C6.9,49.3,8.2,50,9.7,50h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.7c0,0,0,0,0,0c0,0,0,0,0,0l4.1-6.6l12.1-19.4L45.2,15.1L45.2,15.1z M44,13c0,0.4-0.1,0.8-0.4,1.2L23.5,46.4c-0.2,0.3-0.5,0.3-0.6,0.3c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.5-0.7-0.5-1.7,0-2.4L37.4,3.6c0.2-0.3,0.5-0.3,0.6-0.3c0.1,0,0.4,0,0.6,0.3l5.1,8.1C43.9,12.1,44,12.5,44,13z' />
</svg>
)
}
export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'>

View File

@@ -503,6 +503,7 @@ export const auth = betterAuth({
'zoom',
'wordpress',
'linear',
'attio',
'shopify',
'trello',
'calcom',
@@ -2237,6 +2238,69 @@ export const auth = betterAuth({
},
},
{
providerId: 'attio',
clientId: env.ATTIO_CLIENT_ID as string,
clientSecret: env.ATTIO_CLIENT_SECRET as string,
authorizationUrl: 'https://app.attio.com/authorize',
tokenUrl: 'https://app.attio.com/oauth/token',
scopes: [
'record_permission:read-write',
'object_configuration:read-write',
'list_configuration:read-write',
'list_entry:read-write',
'note:read-write',
'task:read-write',
'comment:read-write',
'user_management:read',
'webhook:read-write',
],
responseType: 'code',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/attio`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://api.attio.com/v2/workspace_members', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Attio API error:', {
status: response.status,
statusText: response.statusText,
body: errorText,
})
throw new Error(`Attio API error: ${response.status} ${response.statusText}`)
}
const { data } = await response.json()
if (!data || data.length === 0) {
throw new Error('No workspace members found in Attio response')
}
const member = data[0]
return {
id: `${member.id.workspace_member_id}-${crypto.randomUUID()}`,
email: member.email_address,
name:
`${member.first_name ?? ''} ${member.last_name ?? ''}`.trim() ||
member.email_address,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
image: member.avatar_url || undefined,
}
} catch (error) {
logger.error('Error in Attio getUserInfo:', error)
throw error
}
},
},
{
providerId: 'dropbox',
clientId: env.DROPBOX_CLIENT_ID as string,

View File

@@ -281,6 +281,8 @@ export const env = createEnv({
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
CALCOM_CLIENT_ID: z.string().optional(), // Cal.com OAuth client ID
ATTIO_CLIENT_ID: z.string().optional(), // Attio OAuth client ID
ATTIO_CLIENT_SECRET: z.string().optional(), // Attio OAuth client secret
// E2B Remote Code Execution
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import {
AirtableIcon,
AsanaIcon,
AttioIcon,
CalComIcon,
ConfluenceIcon,
DropboxIcon,
@@ -629,6 +630,31 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'asana',
},
attio: {
name: 'Attio',
icon: AttioIcon,
services: {
attio: {
name: 'Attio',
description: 'Manage records, notes, tasks, lists, comments, and more in Attio CRM.',
providerId: 'attio',
icon: AttioIcon,
baseProviderIcon: AttioIcon,
scopes: [
'record_permission:read-write',
'object_configuration:read-write',
'list_configuration:read-write',
'list_entry:read-write',
'note:read-write',
'task:read-write',
'comment:read-write',
'user_management:read',
'webhook:read-write',
],
},
},
defaultService: 'attio',
},
calcom: {
name: 'Cal.com',
icon: CalComIcon,
@@ -966,6 +992,18 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
supportsRefreshTokenRotation: true,
}
}
case 'attio': {
const { clientId, clientSecret } = getCredentials(
env.ATTIO_CLIENT_ID,
env.ATTIO_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://app.attio.com/oauth/token',
clientId,
clientSecret,
useBasicAuth: false,
}
}
case 'dropbox': {
const { clientId, clientSecret } = getCredentials(
env.DROPBOX_CLIENT_ID,

View File

@@ -34,6 +34,7 @@ export type OAuthProvider =
| 'wealthbox'
| 'webflow'
| 'asana'
| 'attio'
| 'pipedrive'
| 'hubspot'
| 'salesforce'
@@ -76,6 +77,7 @@ export type OAuthService =
| 'webflow'
| 'trello'
| 'asana'
| 'attio'
| 'pipedrive'
| 'hubspot'
| 'salesforce'

View File

@@ -946,6 +946,28 @@ export async function queueWebhookExecution(
}
}
if (foundWebhook.provider === 'attio') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined
if (triggerId && triggerId !== 'attio_webhook') {
const { isAttioPayloadMatch } = await import('@/triggers/attio/utils')
if (!isAttioPayloadMatch(triggerId, body)) {
const eventType = body?.event_type as string | undefined
logger.debug(
`[${options.requestId}] Attio event mismatch for trigger ${triggerId}. Event: ${eventType}. Skipping execution.`,
{
webhookId: foundWebhook.id,
workflowId: foundWorkflow.id,
triggerId,
receivedEvent: eventType,
}
)
return NextResponse.json({ status: 'skipped', reason: 'event_type_mismatch' })
}
}
}
if (foundWebhook.provider === 'hubspot') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined

View File

@@ -0,0 +1,95 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioAssertRecordParams, AttioAssertRecordResponse } from './types'
import { RECORD_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioAssertRecord')
export const attioAssertRecordTool: ToolConfig<AttioAssertRecordParams, AttioAssertRecordResponse> =
{
id: 'attio_assert_record',
name: 'Attio Assert Record',
description:
'Upsert a record in Attio — creates it if no match is found, updates it if a match exists',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
objectType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type slug (e.g. people, companies)',
},
matchingAttribute: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The attribute slug to match on for upsert (e.g. email_addresses for people, domains for companies)',
},
values: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON object of attribute values (e.g. {"email_addresses":[{"email_address":"test@example.com"}]})',
},
},
request: {
url: (params) =>
`https://api.attio.com/v2/objects/${params.objectType}/records?matching_attribute=${params.matchingAttribute}`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let values: Record<string, unknown> = {}
try {
values = typeof params.values === 'string' ? JSON.parse(params.values) : params.values
} catch {
values = {}
}
return { data: { values } }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to assert record')
}
const record = data.data
return {
success: true,
output: {
record,
recordId: record.id?.record_id ?? null,
webUrl: record.web_url ?? null,
},
}
},
outputs: {
record: {
type: 'object',
description: 'The upserted record',
properties: RECORD_OUTPUT_PROPERTIES,
},
recordId: { type: 'string', description: 'The record ID' },
webUrl: { type: 'string', description: 'URL to view the record in Attio' },
},
}

View File

@@ -0,0 +1,137 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateCommentParams, AttioCreateCommentResponse } from './types'
import { COMMENT_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateComment')
export const attioCreateCommentTool: ToolConfig<
AttioCreateCommentParams,
AttioCreateCommentResponse
> = {
id: 'attio_create_comment',
name: 'Attio Create Comment',
description: 'Create a comment on a list entry in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The comment content',
},
format: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Content format: plaintext or markdown (default plaintext)',
},
authorType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Author type (e.g. workspace-member)',
},
authorId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Author workspace member ID',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug the entry belongs to',
},
entryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The entry ID to comment on',
},
threadId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Thread ID to reply to (omit to start a new thread)',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Backdate the comment (ISO 8601 format)',
},
},
request: {
url: 'https://api.attio.com/v2/comments',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {
format: params.format || 'plaintext',
content: params.content,
author: {
type: params.authorType,
id: params.authorId,
},
entry: {
list: params.list,
entry_id: params.entryId,
},
}
if (params.threadId) data.thread_id = params.threadId
if (params.createdAt) data.created_at = params.createdAt
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create comment')
}
const c = data.data
const author = c.author as { type?: string; id?: string } | undefined
const entry = c.entry as { list_id?: string; entry_id?: string } | undefined
const record = c.record as { object_id?: string; record_id?: string } | undefined
const resolvedBy = c.resolved_by as { type?: string; id?: string } | undefined
return {
success: true,
output: {
commentId: c.id?.comment_id ?? null,
threadId: c.thread_id ?? null,
contentPlaintext: c.content_plaintext ?? null,
author: author ? { type: author.type ?? null, id: author.id ?? null } : null,
entry: entry ? { listId: entry.list_id ?? null, entryId: entry.entry_id ?? null } : null,
record: record
? { objectId: record.object_id ?? null, recordId: record.record_id ?? null }
: null,
resolvedAt: c.resolved_at ?? null,
resolvedBy: resolvedBy
? { type: resolvedBy.type ?? null, id: resolvedBy.id ?? null }
: null,
createdAt: c.created_at ?? null,
},
}
},
outputs: COMMENT_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,114 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateListParams, AttioCreateListResponse } from './types'
import { LIST_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateList')
export const attioCreateListTool: ToolConfig<AttioCreateListParams, AttioCreateListResponse> = {
id: 'attio_create_list',
name: 'Attio Create List',
description: 'Create a new list in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list name',
},
apiSlug: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The API slug for the list (auto-generated from name if omitted)',
},
parentObject: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The parent object slug (e.g. people, companies)',
},
workspaceAccess: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Workspace-level access: full-access, read-and-write, or read-only (omit for private)',
},
workspaceMemberAccess: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of member access entries, e.g. [{"workspace_member_id":"...","level":"read-and-write"}]',
},
},
request: {
url: 'https://api.attio.com/v2/lists',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {
name: params.name,
parent_object: params.parentObject,
}
if (params.apiSlug) data.api_slug = params.apiSlug
if (params.workspaceAccess) data.workspace_access = params.workspaceAccess
if (params.workspaceMemberAccess) {
try {
data.workspace_member_access =
typeof params.workspaceMemberAccess === 'string'
? JSON.parse(params.workspaceMemberAccess)
: params.workspaceMemberAccess
} catch {
data.workspace_member_access = params.workspaceMemberAccess
}
}
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create list')
}
const list = data.data
const actor = list.created_by_actor as { type?: string; id?: string } | undefined
return {
success: true,
output: {
listId: list.id?.list_id ?? null,
apiSlug: list.api_slug ?? null,
name: list.name ?? null,
parentObject: Array.isArray(list.parent_object)
? (list.parent_object[0] ?? null)
: (list.parent_object ?? null),
workspaceAccess: list.workspace_access ?? null,
workspaceMemberAccess: list.workspace_member_access ?? null,
createdByActor: actor ? { type: actor.type ?? null, id: actor.id ?? null } : null,
createdAt: list.created_at ?? null,
},
}
},
outputs: LIST_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,102 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateListEntryParams, AttioCreateListEntryResponse } from './types'
import { LIST_ENTRY_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateListEntry')
export const attioCreateListEntryTool: ToolConfig<
AttioCreateListEntryParams,
AttioCreateListEntryResponse
> = {
id: 'attio_create_list_entry',
name: 'Attio Create List Entry',
description: 'Add a record to an Attio list as a new entry',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug',
},
parentRecordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The record ID to add to the list',
},
parentObject: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type slug of the record (e.g. people, companies)',
},
entryValues: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object of entry attribute values',
},
},
request: {
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {
parent_record_id: params.parentRecordId,
parent_object: params.parentObject,
}
if (params.entryValues) {
try {
data.entry_values =
typeof params.entryValues === 'string'
? JSON.parse(params.entryValues)
: params.entryValues
} catch {
data.entry_values = {}
}
}
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create list entry')
}
const entry = data.data
return {
success: true,
output: {
entryId: entry.id?.entry_id ?? null,
listId: entry.id?.list_id ?? null,
parentRecordId: entry.parent_record_id ?? null,
parentObject: entry.parent_object ?? null,
createdAt: entry.created_at ?? null,
entryValues: entry.entry_values ?? {},
},
}
},
outputs: LIST_ENTRY_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,116 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateNoteParams, AttioCreateNoteResponse } from './types'
import { NOTE_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateNote')
export const attioCreateNoteTool: ToolConfig<AttioCreateNoteParams, AttioCreateNoteResponse> = {
id: 'attio_create_note',
name: 'Attio Create Note',
description: 'Create a note on a record in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
parentObject: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The parent object type slug (e.g. people, companies)',
},
parentRecordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The parent record ID to attach the note to',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The note title',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The note content',
},
format: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Content format: plaintext or markdown (default plaintext)',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Backdate the note creation time (ISO 8601 format)',
},
meetingId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Associate the note with a meeting ID',
},
},
request: {
url: 'https://api.attio.com/v2/notes',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
parent_object: params.parentObject,
parent_record_id: params.parentRecordId,
title: params.title,
format: params.format || 'plaintext',
content: params.content,
}
if (params.createdAt) body.created_at = params.createdAt
if (params.meetingId !== undefined) body.meeting_id = params.meetingId || null
return { data: body }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create note')
}
const note = data.data
return {
success: true,
output: {
noteId: note.id?.note_id ?? null,
parentObject: note.parent_object ?? null,
parentRecordId: note.parent_record_id ?? null,
title: note.title ?? null,
contentPlaintext: note.content_plaintext ?? null,
contentMarkdown: note.content_markdown ?? null,
meetingId: note.meeting_id ?? null,
tags: note.tags ?? [],
createdByActor: note.created_by_actor ?? null,
createdAt: note.created_at ?? null,
},
}
},
outputs: NOTE_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,83 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateObjectParams, AttioCreateObjectResponse } from './types'
import { OBJECT_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateObject')
export const attioCreateObjectTool: ToolConfig<AttioCreateObjectParams, AttioCreateObjectResponse> =
{
id: 'attio_create_object',
name: 'Attio Create Object',
description: 'Create a custom object in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
apiSlug: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The API slug for the object (e.g. projects)',
},
singularNoun: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Singular display name (e.g. Project)',
},
pluralNoun: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Plural display name (e.g. Projects)',
},
},
request: {
url: 'https://api.attio.com/v2/objects',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
data: {
api_slug: params.apiSlug,
singular_noun: params.singularNoun,
plural_noun: params.pluralNoun,
},
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create object')
}
const obj = data.data
return {
success: true,
output: {
objectId: obj.id?.object_id ?? null,
apiSlug: obj.api_slug ?? null,
singularNoun: obj.singular_noun ?? null,
pluralNoun: obj.plural_noun ?? null,
createdAt: obj.created_at ?? null,
},
}
},
outputs: OBJECT_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,81 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateRecordParams, AttioCreateRecordResponse } from './types'
import { RECORD_OBJECT_OUTPUT } from './types'
const logger = createLogger('AttioCreateRecord')
export const attioCreateRecordTool: ToolConfig<AttioCreateRecordParams, AttioCreateRecordResponse> =
{
id: 'attio_create_record',
name: 'Attio Create Record',
description: 'Create a new record in Attio for a given object type',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
objectType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type slug (e.g. people, companies)',
},
values: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'JSON object of attribute values to set on the record',
},
},
request: {
url: (params) => `https://api.attio.com/v2/objects/${params.objectType}/records`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let values: Record<string, unknown>
try {
values = typeof params.values === 'string' ? JSON.parse(params.values) : params.values
} catch {
values = {}
}
return { data: { values } }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create record')
}
const record = data.data
return {
success: true,
output: {
record,
recordId: record.id?.record_id ?? null,
webUrl: record.web_url ?? null,
},
}
},
outputs: {
record: RECORD_OBJECT_OUTPUT,
recordId: { type: 'string', description: 'The ID of the created record' },
webUrl: { type: 'string', description: 'URL to view the record in Attio' },
},
}

View File

@@ -0,0 +1,136 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateTaskParams, AttioCreateTaskResponse } from './types'
import { TASK_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateTask')
export const attioCreateTaskTool: ToolConfig<AttioCreateTaskParams, AttioCreateTaskResponse> = {
id: 'attio_create_task',
name: 'Attio Create Task',
description: 'Create a task in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The task content (max 2000 characters)',
},
deadlineAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Deadline in ISO 8601 format (e.g. 2024-12-01T15:00:00.000Z)',
},
isCompleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the task is completed (default false)',
},
linkedRecords: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of linked records (e.g. [{"target_object":"people","target_record_id":"..."}])',
},
assignees: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of assignees (e.g. [{"referenced_actor_type":"workspace-member","referenced_actor_id":"..."}])',
},
},
request: {
url: 'https://api.attio.com/v2/tasks',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let linkedRecords: unknown[] = []
let assignees: unknown[] = []
try {
if (params.linkedRecords) {
linkedRecords =
typeof params.linkedRecords === 'string'
? JSON.parse(params.linkedRecords)
: params.linkedRecords
}
} catch {
linkedRecords = []
}
try {
if (params.assignees) {
assignees =
typeof params.assignees === 'string' ? JSON.parse(params.assignees) : params.assignees
}
} catch {
assignees = []
}
return {
data: {
content: params.content,
format: 'plaintext',
deadline_at: params.deadlineAt || null,
is_completed: params.isCompleted ?? false,
linked_records: linkedRecords,
assignees,
},
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create task')
}
const task = data.data
const linkedRecords = (task.linked_records ?? []).map(
(r: { target_object_id?: string; target_record_id?: string }) => ({
targetObjectId: r.target_object_id ?? null,
targetRecordId: r.target_record_id ?? null,
})
)
const assignees = (task.assignees ?? []).map(
(a: { referenced_actor_type?: string; referenced_actor_id?: string }) => ({
type: a.referenced_actor_type ?? null,
id: a.referenced_actor_id ?? null,
})
)
return {
success: true,
output: {
taskId: task.id?.task_id ?? null,
content: task.content_plaintext ?? null,
deadlineAt: task.deadline_at ?? null,
isCompleted: task.is_completed ?? false,
linkedRecords,
assignees,
createdByActor: task.created_by_actor ?? null,
createdAt: task.created_at ?? null,
},
}
},
outputs: TASK_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,104 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateWebhookParams, AttioCreateWebhookResponse } from './types'
import { WEBHOOK_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateWebhook')
export const attioCreateWebhookTool: ToolConfig<
AttioCreateWebhookParams,
AttioCreateWebhookResponse
> = {
id: 'attio_create_webhook',
name: 'Attio Create Webhook',
description: 'Create a webhook in Attio to receive event notifications',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
targetUrl: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The HTTPS URL to receive webhook events',
},
subscriptions: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON array of subscriptions (e.g. [{"event_type":"record.created","filter":{"object_id":"..."}}])',
},
},
request: {
url: 'https://api.attio.com/v2/webhooks',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let subscriptions: unknown[] = []
try {
subscriptions =
typeof params.subscriptions === 'string'
? JSON.parse(params.subscriptions)
: params.subscriptions
} catch {
subscriptions = []
}
return {
data: {
target_url: params.targetUrl,
subscriptions,
},
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create webhook')
}
const w = data.data
const subs =
(w.subscriptions as Array<{ event_type?: string; filter?: unknown }>)?.map(
(s: { event_type?: string; filter?: unknown }) => ({
eventType: s.event_type ?? null,
filter: s.filter ?? null,
})
) ?? []
return {
success: true,
output: {
webhookId: w.id?.webhook_id ?? null,
targetUrl: w.target_url ?? null,
subscriptions: subs,
status: w.status ?? null,
secret: w.secret ?? null,
createdAt: w.created_at ?? null,
},
}
},
outputs: {
...WEBHOOK_OUTPUT_PROPERTIES,
secret: {
type: 'string',
description: 'The webhook signing secret (only returned on creation)',
},
},
}

View File

@@ -0,0 +1,61 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioDeleteCommentParams, AttioDeleteCommentResponse } from './types'
const logger = createLogger('AttioDeleteComment')
export const attioDeleteCommentTool: ToolConfig<
AttioDeleteCommentParams,
AttioDeleteCommentResponse
> = {
id: 'attio_delete_comment',
name: 'Attio Delete Comment',
description: 'Delete a comment in Attio (if head of thread, deletes entire thread)',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
commentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The comment ID to delete',
},
},
request: {
url: (params) => `https://api.attio.com/v2/comments/${params.commentId}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json()
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to delete comment')
}
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the comment was deleted' },
},
}

View File

@@ -0,0 +1,67 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioDeleteListEntryParams, AttioDeleteListEntryResponse } from './types'
const logger = createLogger('AttioDeleteListEntry')
export const attioDeleteListEntryTool: ToolConfig<
AttioDeleteListEntryParams,
AttioDeleteListEntryResponse
> = {
id: 'attio_delete_list_entry',
name: 'Attio Delete List Entry',
description: 'Remove an entry from an Attio list',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug',
},
entryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The entry ID to delete',
},
},
request: {
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries/${params.entryId}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json()
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to delete list entry')
}
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the entry was deleted' },
},
}

View File

@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioDeleteNoteParams, AttioDeleteNoteResponse } from './types'
const logger = createLogger('AttioDeleteNote')
export const attioDeleteNoteTool: ToolConfig<AttioDeleteNoteParams, AttioDeleteNoteResponse> = {
id: 'attio_delete_note',
name: 'Attio Delete Note',
description: 'Delete a note from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
noteId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the note to delete',
},
},
request: {
url: (params) => `https://api.attio.com/v2/notes/${params.noteId}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json()
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to delete note')
}
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the note was deleted' },
},
}

View File

@@ -0,0 +1,66 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioDeleteRecordParams, AttioDeleteRecordResponse } from './types'
const logger = createLogger('AttioDeleteRecord')
export const attioDeleteRecordTool: ToolConfig<AttioDeleteRecordParams, AttioDeleteRecordResponse> =
{
id: 'attio_delete_record',
name: 'Attio Delete Record',
description: 'Delete a record from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
objectType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type slug (e.g. people, companies)',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the record to delete',
},
},
request: {
url: (params) =>
`https://api.attio.com/v2/objects/${params.objectType}/records/${params.recordId}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json()
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to delete record')
}
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the record was deleted' },
},
}

View File

@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioDeleteTaskParams, AttioDeleteTaskResponse } from './types'
const logger = createLogger('AttioDeleteTask')
export const attioDeleteTaskTool: ToolConfig<AttioDeleteTaskParams, AttioDeleteTaskResponse> = {
id: 'attio_delete_task',
name: 'Attio Delete Task',
description: 'Delete a task from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to delete',
},
},
request: {
url: (params) => `https://api.attio.com/v2/tasks/${params.taskId}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json()
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to delete task')
}
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the task was deleted' },
},
}

View File

@@ -0,0 +1,61 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioDeleteWebhookParams, AttioDeleteWebhookResponse } from './types'
const logger = createLogger('AttioDeleteWebhook')
export const attioDeleteWebhookTool: ToolConfig<
AttioDeleteWebhookParams,
AttioDeleteWebhookResponse
> = {
id: 'attio_delete_webhook',
name: 'Attio Delete Webhook',
description: 'Delete a webhook from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
webhookId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The webhook ID to delete',
},
},
request: {
url: (params) => `https://api.attio.com/v2/webhooks/${params.webhookId}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json()
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to delete webhook')
}
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the webhook was deleted' },
},
}

View File

@@ -0,0 +1,74 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetCommentParams, AttioGetCommentResponse } from './types'
import { COMMENT_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetComment')
export const attioGetCommentTool: ToolConfig<AttioGetCommentParams, AttioGetCommentResponse> = {
id: 'attio_get_comment',
name: 'Attio Get Comment',
description: 'Get a single comment by ID from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
commentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The comment ID',
},
},
request: {
url: (params) => `https://api.attio.com/v2/comments/${params.commentId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get comment')
}
const c = data.data
const author = c.author as { type?: string; id?: string } | undefined
const entry = c.entry as { list_id?: string; entry_id?: string } | undefined
const record = c.record as { object_id?: string; record_id?: string } | undefined
const resolvedBy = c.resolved_by as { type?: string; id?: string } | undefined
return {
success: true,
output: {
commentId: c.id?.comment_id ?? null,
threadId: c.thread_id ?? null,
contentPlaintext: c.content_plaintext ?? null,
author: author ? { type: author.type ?? null, id: author.id ?? null } : null,
entry: entry ? { listId: entry.list_id ?? null, entryId: entry.entry_id ?? null } : null,
record: record
? { objectId: record.object_id ?? null, recordId: record.record_id ?? null }
: null,
resolvedAt: c.resolved_at ?? null,
resolvedBy: resolvedBy
? { type: resolvedBy.type ?? null, id: resolvedBy.id ?? null }
: null,
createdAt: c.created_at ?? null,
},
}
},
outputs: COMMENT_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,68 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetListParams, AttioGetListResponse } from './types'
import { LIST_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetList')
export const attioGetListTool: ToolConfig<AttioGetListParams, AttioGetListResponse> = {
id: 'attio_get_list',
name: 'Attio Get List',
description: 'Get a single list by ID or slug',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug',
},
},
request: {
url: (params) => `https://api.attio.com/v2/lists/${params.list}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get list')
}
const list = data.data
const actor = list.created_by_actor as { type?: string; id?: string } | undefined
return {
success: true,
output: {
listId: list.id?.list_id ?? null,
apiSlug: list.api_slug ?? null,
name: list.name ?? null,
parentObject: Array.isArray(list.parent_object)
? (list.parent_object[0] ?? null)
: (list.parent_object ?? null),
workspaceAccess: list.workspace_access ?? null,
workspaceMemberAccess: list.workspace_member_access ?? null,
createdByActor: actor ? { type: actor.type ?? null, id: actor.id ?? null } : null,
createdAt: list.created_at ?? null,
},
}
},
outputs: LIST_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,70 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetListEntryParams, AttioGetListEntryResponse } from './types'
import { LIST_ENTRY_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetListEntry')
export const attioGetListEntryTool: ToolConfig<AttioGetListEntryParams, AttioGetListEntryResponse> =
{
id: 'attio_get_list_entry',
name: 'Attio Get List Entry',
description: 'Get a single list entry by ID',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug',
},
entryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The entry ID',
},
},
request: {
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries/${params.entryId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get list entry')
}
const entry = data.data
return {
success: true,
output: {
entryId: entry.id?.entry_id ?? null,
listId: entry.id?.list_id ?? null,
parentRecordId: entry.parent_record_id ?? null,
parentObject: entry.parent_object ?? null,
createdAt: entry.created_at ?? null,
entryValues: entry.entry_values ?? {},
},
}
},
outputs: LIST_ENTRY_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,64 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetMemberParams, AttioGetMemberResponse } from './types'
import { MEMBER_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetMember')
export const attioGetMemberTool: ToolConfig<AttioGetMemberParams, AttioGetMemberResponse> = {
id: 'attio_get_member',
name: 'Attio Get Member',
description: 'Get a single workspace member by ID',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
memberId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The workspace member ID',
},
},
request: {
url: (params) => `https://api.attio.com/v2/workspace_members/${params.memberId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get workspace member')
}
const m = data.data
return {
success: true,
output: {
memberId: m.id?.workspace_member_id ?? null,
firstName: m.first_name ?? null,
lastName: m.last_name ?? null,
avatarUrl: m.avatar_url ?? null,
emailAddress: m.email_address ?? null,
accessLevel: m.access_level ?? null,
createdAt: m.created_at ?? null,
},
}
},
outputs: MEMBER_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,67 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetNoteParams, AttioGetNoteResponse } from './types'
import { NOTE_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetNote')
export const attioGetNoteTool: ToolConfig<AttioGetNoteParams, AttioGetNoteResponse> = {
id: 'attio_get_note',
name: 'Attio Get Note',
description: 'Get a single note by ID from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
noteId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the note to retrieve',
},
},
request: {
url: (params) => `https://api.attio.com/v2/notes/${params.noteId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get note')
}
const note = data.data
return {
success: true,
output: {
noteId: note.id?.note_id ?? null,
parentObject: note.parent_object ?? null,
parentRecordId: note.parent_record_id ?? null,
title: note.title ?? null,
contentPlaintext: note.content_plaintext ?? null,
contentMarkdown: note.content_markdown ?? null,
meetingId: note.meeting_id ?? null,
tags: note.tags ?? [],
createdByActor: note.created_by_actor ?? null,
createdAt: note.created_at ?? null,
},
}
},
outputs: NOTE_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,62 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetObjectParams, AttioGetObjectResponse } from './types'
import { OBJECT_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetObject')
export const attioGetObjectTool: ToolConfig<AttioGetObjectParams, AttioGetObjectResponse> = {
id: 'attio_get_object',
name: 'Attio Get Object',
description: 'Get a single object by ID or slug',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
object: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID or slug (e.g. people, companies)',
},
},
request: {
url: (params) => `https://api.attio.com/v2/objects/${params.object}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get object')
}
const obj = data.data
return {
success: true,
output: {
objectId: obj.id?.object_id ?? null,
apiSlug: obj.api_slug ?? null,
singularNoun: obj.singular_noun ?? null,
pluralNoun: obj.plural_noun ?? null,
createdAt: obj.created_at ?? null,
},
}
},
outputs: OBJECT_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,71 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetRecordParams, AttioGetRecordResponse } from './types'
import { RECORD_OBJECT_OUTPUT } from './types'
const logger = createLogger('AttioGetRecord')
export const attioGetRecordTool: ToolConfig<AttioGetRecordParams, AttioGetRecordResponse> = {
id: 'attio_get_record',
name: 'Attio Get Record',
description: 'Get a single record by ID from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
objectType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type slug (e.g. people, companies)',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the record to retrieve',
},
},
request: {
url: (params) =>
`https://api.attio.com/v2/objects/${params.objectType}/records/${params.recordId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get record')
}
const record = data.data
return {
success: true,
output: {
record,
recordId: record.id?.record_id ?? null,
webUrl: record.web_url ?? null,
},
}
},
outputs: {
record: RECORD_OBJECT_OUTPUT,
recordId: { type: 'string', description: 'The record ID' },
webUrl: { type: 'string', description: 'URL to view the record in Attio' },
},
}

View File

@@ -0,0 +1,74 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetThreadParams, AttioGetThreadResponse } from './types'
import { THREAD_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetThread')
export const attioGetThreadTool: ToolConfig<AttioGetThreadParams, AttioGetThreadResponse> = {
id: 'attio_get_thread',
name: 'Attio Get Thread',
description: 'Get a single comment thread by ID from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
threadId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The thread ID',
},
},
request: {
url: (params) => `https://api.attio.com/v2/threads/${params.threadId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get thread')
}
const t = data.data
const comments =
(
t.comments as Array<{
id?: { comment_id?: string }
content_plaintext?: string
author?: { type?: string; id?: string }
created_at?: string
}>
)?.map((c) => ({
commentId: c.id?.comment_id ?? null,
contentPlaintext: c.content_plaintext ?? null,
author: c.author ? { type: c.author.type ?? null, id: c.author.id ?? null } : null,
createdAt: c.created_at ?? null,
})) ?? []
return {
success: true,
output: {
threadId: t.id?.thread_id ?? null,
comments,
createdAt: t.created_at ?? null,
},
}
},
outputs: THREAD_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,69 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetWebhookParams, AttioGetWebhookResponse } from './types'
import { WEBHOOK_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetWebhook')
export const attioGetWebhookTool: ToolConfig<AttioGetWebhookParams, AttioGetWebhookResponse> = {
id: 'attio_get_webhook',
name: 'Attio Get Webhook',
description: 'Get a single webhook by ID from Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
webhookId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The webhook ID',
},
},
request: {
url: (params) => `https://api.attio.com/v2/webhooks/${params.webhookId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get webhook')
}
const w = data.data
const subs =
(w.subscriptions as Array<{ event_type?: string; filter?: unknown }>)?.map(
(s: { event_type?: string; filter?: unknown }) => ({
eventType: s.event_type ?? null,
filter: s.filter ?? null,
})
) ?? []
return {
success: true,
output: {
webhookId: w.id?.webhook_id ?? null,
targetUrl: w.target_url ?? null,
subscriptions: subs,
status: w.status ?? null,
createdAt: w.created_at ?? null,
},
}
},
outputs: WEBHOOK_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,40 @@
export { attioAssertRecordTool } from './assert_record'
export { attioCreateCommentTool } from './create_comment'
export { attioCreateListTool } from './create_list'
export { attioCreateListEntryTool } from './create_list_entry'
export { attioCreateNoteTool } from './create_note'
export { attioCreateObjectTool } from './create_object'
export { attioCreateRecordTool } from './create_record'
export { attioCreateTaskTool } from './create_task'
export { attioCreateWebhookTool } from './create_webhook'
export { attioDeleteCommentTool } from './delete_comment'
export { attioDeleteListEntryTool } from './delete_list_entry'
export { attioDeleteNoteTool } from './delete_note'
export { attioDeleteRecordTool } from './delete_record'
export { attioDeleteTaskTool } from './delete_task'
export { attioDeleteWebhookTool } from './delete_webhook'
export { attioGetCommentTool } from './get_comment'
export { attioGetListTool } from './get_list'
export { attioGetListEntryTool } from './get_list_entry'
export { attioGetMemberTool } from './get_member'
export { attioGetNoteTool } from './get_note'
export { attioGetObjectTool } from './get_object'
export { attioGetRecordTool } from './get_record'
export { attioGetThreadTool } from './get_thread'
export { attioGetWebhookTool } from './get_webhook'
export { attioListListsTool } from './list_lists'
export { attioListMembersTool } from './list_members'
export { attioListNotesTool } from './list_notes'
export { attioListObjectsTool } from './list_objects'
export { attioListRecordsTool } from './list_records'
export { attioListTasksTool } from './list_tasks'
export { attioListThreadsTool } from './list_threads'
export { attioListWebhooksTool } from './list_webhooks'
export { attioQueryListEntriesTool } from './query_list_entries'
export { attioSearchRecordsTool } from './search_records'
export { attioUpdateListTool } from './update_list'
export { attioUpdateListEntryTool } from './update_list_entry'
export { attioUpdateObjectTool } from './update_object'
export { attioUpdateRecordTool } from './update_record'
export { attioUpdateTaskTool } from './update_task'
export { attioUpdateWebhookTool } from './update_webhook'

View File

@@ -0,0 +1,78 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListListsParams, AttioListListsResponse } from './types'
import { LIST_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioListLists')
export const attioListListsTool: ToolConfig<AttioListListsParams, AttioListListsResponse> = {
id: 'attio_list_lists',
name: 'Attio List Lists',
description: 'List all lists in the Attio workspace',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
},
request: {
url: 'https://api.attio.com/v2/lists',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list lists')
}
const lists = (data.data ?? []).map((list: Record<string, unknown>) => {
const id = list.id as { list_id?: string } | undefined
const actor = list.created_by_actor as { type?: string; id?: string } | undefined
return {
listId: id?.list_id ?? null,
apiSlug: (list.api_slug as string) ?? null,
name: (list.name as string) ?? null,
parentObject: Array.isArray(list.parent_object)
? (list.parent_object[0] ?? null)
: ((list.parent_object as string) ?? null),
workspaceAccess: (list.workspace_access as string) ?? null,
workspaceMemberAccess: list.workspace_member_access ?? null,
createdByActor: actor ? { type: actor.type ?? null, id: actor.id ?? null } : null,
createdAt: (list.created_at as string) ?? null,
}
})
return {
success: true,
output: {
lists,
count: lists.length,
},
}
},
outputs: {
lists: {
type: 'array',
description: 'Array of lists',
items: {
type: 'object',
properties: LIST_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of lists returned' },
},
}

View File

@@ -0,0 +1,74 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListMembersParams, AttioListMembersResponse } from './types'
import { MEMBER_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioListMembers')
export const attioListMembersTool: ToolConfig<AttioListMembersParams, AttioListMembersResponse> = {
id: 'attio_list_members',
name: 'Attio List Members',
description: 'List all workspace members in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
},
request: {
url: 'https://api.attio.com/v2/workspace_members',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list workspace members')
}
const members = (data.data ?? []).map((m: Record<string, unknown>) => {
const id = m.id as { workspace_member_id?: string } | undefined
return {
memberId: id?.workspace_member_id ?? null,
firstName: (m.first_name as string) ?? null,
lastName: (m.last_name as string) ?? null,
avatarUrl: (m.avatar_url as string) ?? null,
emailAddress: (m.email_address as string) ?? null,
accessLevel: (m.access_level as string) ?? null,
createdAt: (m.created_at as string) ?? null,
}
})
return {
success: true,
output: {
members,
count: members.length,
},
}
},
outputs: {
members: {
type: 'array',
description: 'Array of workspace members',
items: {
type: 'object',
properties: MEMBER_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of members returned' },
},
}

View File

@@ -0,0 +1,109 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListNotesParams, AttioListNotesResponse } from './types'
import { NOTE_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioListNotes')
export const attioListNotesTool: ToolConfig<AttioListNotesParams, AttioListNotesResponse> = {
id: 'attio_list_notes',
name: 'Attio List Notes',
description: 'List notes in Attio, optionally filtered by parent record',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
parentObject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Object type slug to filter notes by (e.g. people, companies)',
},
parentRecordId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Record ID to filter notes by',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of notes to return (default 10, max 50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of notes to skip for pagination',
},
},
request: {
url: (params) => {
const searchParams = new URLSearchParams()
if (params.parentObject) searchParams.set('parent_object', params.parentObject)
if (params.parentRecordId) searchParams.set('parent_record_id', params.parentRecordId)
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/notes${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list notes')
}
const notes = (data.data ?? []).map((note: Record<string, unknown>) => {
const noteId = note.id as { note_id?: string } | undefined
return {
noteId: noteId?.note_id ?? null,
parentObject: (note.parent_object as string) ?? null,
parentRecordId: (note.parent_record_id as string) ?? null,
title: (note.title as string) ?? null,
contentPlaintext: (note.content_plaintext as string) ?? null,
contentMarkdown: (note.content_markdown as string) ?? null,
meetingId: (note.meeting_id as string) ?? null,
tags: (note.tags as unknown[]) ?? [],
createdByActor: note.created_by_actor ?? null,
createdAt: (note.created_at as string) ?? null,
}
})
return {
success: true,
output: {
notes,
count: notes.length,
},
}
},
outputs: {
notes: {
type: 'array',
description: 'Array of notes',
items: {
type: 'object',
properties: NOTE_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of notes returned' },
},
}

View File

@@ -0,0 +1,77 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListObjectsParams, AttioListObjectsResponse } from './types'
import { OBJECT_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioListObjects')
export const attioListObjectsTool: ToolConfig<AttioListObjectsParams, AttioListObjectsResponse> = {
id: 'attio_list_objects',
name: 'Attio List Objects',
description: 'List all objects (system and custom) in the Attio workspace',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
},
request: {
url: 'https://api.attio.com/v2/objects',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list objects')
}
const objects = (data.data ?? []).map(
(obj: {
id?: { object_id?: string }
api_slug?: string
singular_noun?: string
plural_noun?: string
created_at?: string
}) => ({
objectId: obj.id?.object_id ?? null,
apiSlug: obj.api_slug ?? null,
singularNoun: obj.singular_noun ?? null,
pluralNoun: obj.plural_noun ?? null,
createdAt: obj.created_at ?? null,
})
)
return {
success: true,
output: {
objects,
count: objects.length,
},
}
},
outputs: {
objects: {
type: 'array',
description: 'Array of objects',
items: {
type: 'object',
properties: OBJECT_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of objects returned' },
},
}

View File

@@ -0,0 +1,107 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListRecordsParams, AttioListRecordsResponse } from './types'
import { RECORDS_ARRAY_OUTPUT } from './types'
const logger = createLogger('AttioListRecords')
export const attioListRecordsTool: ToolConfig<AttioListRecordsParams, AttioListRecordsResponse> = {
id: 'attio_list_records',
name: 'Attio List Records',
description: 'Query and list records for a given object type (e.g. people, companies)',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
objectType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type slug (e.g. people, companies)',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON filter object for querying records',
},
sorts: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of sort objects, e.g. [{"direction":"asc","attribute":"name"}]',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of records to return (default 500)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of records to skip for pagination',
},
},
request: {
url: (params) => `https://api.attio.com/v2/objects/${params.objectType}/records/query`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.filter) {
try {
body.filter = JSON.parse(params.filter)
} catch {
body.filter = params.filter
}
}
if (params.sorts) {
try {
body.sorts = JSON.parse(params.sorts)
} catch {
body.sorts = params.sorts
}
}
if (params.limit !== undefined) body.limit = params.limit
if (params.offset !== undefined) body.offset = params.offset
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list records')
}
const records = data.data ?? []
return {
success: true,
output: {
records,
count: records.length,
},
}
},
outputs: {
records: RECORDS_ARRAY_OUTPUT,
count: { type: 'number', description: 'Number of records returned' },
},
}

View File

@@ -0,0 +1,147 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListTasksParams, AttioListTasksResponse } from './types'
import { TASK_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioListTasks')
export const attioListTasksTool: ToolConfig<AttioListTasksParams, AttioListTasksResponse> = {
id: 'attio_list_tasks',
name: 'Attio List Tasks',
description: 'List tasks in Attio, optionally filtered by record, assignee, or completion status',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
linkedObject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Object type slug to filter tasks by (requires linkedRecordId)',
},
linkedRecordId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Record ID to filter tasks by (requires linkedObject)',
},
assignee: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assignee email or member ID to filter by',
},
isCompleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Filter by completion status',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: created_at:asc or created_at:desc',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of tasks to return (default 500)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of tasks to skip for pagination',
},
},
request: {
url: (params) => {
const searchParams = new URLSearchParams()
if (params.linkedObject) searchParams.set('linked_object', params.linkedObject)
if (params.linkedRecordId) searchParams.set('linked_record_id', params.linkedRecordId)
if (params.assignee) searchParams.set('assignee', params.assignee)
if (params.isCompleted !== undefined) {
searchParams.set('is_completed', String(params.isCompleted))
}
if (params.sort) searchParams.set('sort', params.sort)
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/tasks${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list tasks')
}
const tasks = (data.data ?? []).map((task: Record<string, unknown>) => {
const taskId = task.id as { task_id?: string } | undefined
const linkedRecords =
(
task.linked_records as Array<{ target_object_id?: string; target_record_id?: string }>
)?.map((r) => ({
targetObjectId: r.target_object_id ?? null,
targetRecordId: r.target_record_id ?? null,
})) ?? []
const assignees =
(
task.assignees as Array<{
referenced_actor_type?: string
referenced_actor_id?: string
}>
)?.map((a) => ({
type: a.referenced_actor_type ?? null,
id: a.referenced_actor_id ?? null,
})) ?? []
return {
taskId: taskId?.task_id ?? null,
content: (task.content_plaintext as string) ?? null,
deadlineAt: (task.deadline_at as string) ?? null,
isCompleted: (task.is_completed as boolean) ?? false,
linkedRecords,
assignees,
createdByActor: task.created_by_actor ?? null,
createdAt: (task.created_at as string) ?? null,
}
})
return {
success: true,
output: {
tasks,
count: tasks.length,
},
}
},
outputs: {
tasks: {
type: 'array',
description: 'Array of tasks',
items: {
type: 'object',
properties: TASK_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of tasks returned' },
},
}

View File

@@ -0,0 +1,152 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListThreadsParams, AttioListThreadsResponse } from './types'
const logger = createLogger('AttioListThreads')
export const attioListThreadsTool: ToolConfig<AttioListThreadsParams, AttioListThreadsResponse> = {
id: 'attio_list_threads',
name: 'Attio List Threads',
description: 'List comment threads in Attio, optionally filtered by record or list entry',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
recordId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by record ID (requires object)',
},
object: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Object slug to filter by (requires recordId)',
},
entryId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by list entry ID (requires list)',
},
list: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'List ID or slug to filter by (requires entryId)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of threads to return (max 50)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of threads to skip for pagination',
},
},
request: {
url: (params) => {
const searchParams = new URLSearchParams()
if (params.recordId) searchParams.set('record_id', params.recordId)
if (params.object) searchParams.set('object', params.object)
if (params.entryId) searchParams.set('entry_id', params.entryId)
if (params.list) searchParams.set('list', params.list)
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/threads${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list threads')
}
const threads = (data.data ?? []).map((t: Record<string, unknown>) => {
const id = t.id as { thread_id?: string } | undefined
const comments =
(
t.comments as Array<{
id?: { comment_id?: string }
content_plaintext?: string
author?: { type?: string; id?: string }
created_at?: string
}>
)?.map((c) => ({
commentId: c.id?.comment_id ?? null,
contentPlaintext: c.content_plaintext ?? null,
author: c.author ? { type: c.author.type ?? null, id: c.author.id ?? null } : null,
createdAt: c.created_at ?? null,
})) ?? []
return {
threadId: id?.thread_id ?? null,
comments,
createdAt: (t.created_at as string) ?? null,
}
})
return {
success: true,
output: {
threads,
count: threads.length,
},
}
},
outputs: {
threads: {
type: 'array',
description: 'Array of threads',
items: {
type: 'object',
properties: {
threadId: { type: 'string', description: 'The thread ID' },
comments: {
type: 'array',
description: 'Comments in the thread',
items: {
type: 'object',
properties: {
commentId: { type: 'string', description: 'The comment ID' },
contentPlaintext: { type: 'string', description: 'Comment content' },
author: {
type: 'object',
description: 'Comment author',
properties: {
type: { type: 'string', description: 'Actor type' },
id: { type: 'string', description: 'Actor ID' },
},
},
createdAt: { type: 'string', description: 'When the comment was created' },
},
},
},
createdAt: { type: 'string', description: 'When the thread was created' },
},
},
},
count: { type: 'number', description: 'Number of threads returned' },
},
}

View File

@@ -0,0 +1,96 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListWebhooksParams, AttioListWebhooksResponse } from './types'
import { WEBHOOK_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioListWebhooks')
export const attioListWebhooksTool: ToolConfig<AttioListWebhooksParams, AttioListWebhooksResponse> =
{
id: 'attio_list_webhooks',
name: 'Attio List Webhooks',
description: 'List all webhooks in the Attio workspace',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of webhooks to return',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of webhooks to skip for pagination',
},
},
request: {
url: (params) => {
const searchParams = new URLSearchParams()
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/webhooks${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list webhooks')
}
const webhooks = (data.data ?? []).map((w: Record<string, unknown>) => {
const id = w.id as { webhook_id?: string } | undefined
const subs =
(w.subscriptions as Array<{ event_type?: string; filter?: unknown }>)?.map((s) => ({
eventType: s.event_type ?? null,
filter: s.filter ?? null,
})) ?? []
return {
webhookId: id?.webhook_id ?? null,
targetUrl: (w.target_url as string) ?? null,
subscriptions: subs,
status: (w.status as string) ?? null,
createdAt: (w.created_at as string) ?? null,
}
})
return {
success: true,
output: {
webhooks,
count: webhooks.length,
},
}
},
outputs: {
webhooks: {
type: 'array',
description: 'Array of webhooks',
items: {
type: 'object',
properties: WEBHOOK_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of webhooks returned' },
},
}

View File

@@ -0,0 +1,129 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioQueryListEntriesParams, AttioQueryListEntriesResponse } from './types'
import { LIST_ENTRY_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioQueryListEntries')
export const attioQueryListEntriesTool: ToolConfig<
AttioQueryListEntriesParams,
AttioQueryListEntriesResponse
> = {
id: 'attio_query_list_entries',
name: 'Attio Query List Entries',
description: 'Query entries in an Attio list with optional filter, sort, and pagination',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON filter object for querying entries',
},
sorts: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of sort objects (e.g. [{"attribute":"created_at","direction":"desc"}])',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of entries to return (default 500)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of entries to skip for pagination',
},
},
request: {
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries/query`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.filter) {
try {
body.filter =
typeof params.filter === 'string' ? JSON.parse(params.filter) : params.filter
} catch {
body.filter = {}
}
}
if (params.sorts) {
try {
body.sorts = typeof params.sorts === 'string' ? JSON.parse(params.sorts) : params.sorts
} catch {
body.sorts = []
}
}
if (params.limit !== undefined) body.limit = params.limit
if (params.offset !== undefined) body.offset = params.offset
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to query list entries')
}
const entries = (data.data ?? []).map((entry: Record<string, unknown>) => {
const id = entry.id as { list_id?: string; entry_id?: string } | undefined
return {
entryId: id?.entry_id ?? null,
listId: id?.list_id ?? null,
parentRecordId: (entry.parent_record_id as string) ?? null,
parentObject: (entry.parent_object as string) ?? null,
createdAt: (entry.created_at as string) ?? null,
entryValues: (entry.entry_values as Record<string, unknown>) ?? {},
}
})
return {
success: true,
output: {
entries,
count: entries.length,
},
}
},
outputs: {
entries: {
type: 'array',
description: 'Array of list entries',
items: {
type: 'object',
properties: LIST_ENTRY_OUTPUT_PROPERTIES,
},
},
count: { type: 'number', description: 'Number of entries returned' },
},
}

View File

@@ -0,0 +1,115 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioSearchRecordsParams, AttioSearchRecordsResponse } from './types'
const logger = createLogger('AttioSearchRecords')
export const attioSearchRecordsTool: ToolConfig<
AttioSearchRecordsParams,
AttioSearchRecordsResponse
> = {
id: 'attio_search_records',
name: 'Attio Search Records',
description: 'Fuzzy search for records across object types in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The search query (max 256 characters)',
},
objects: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated object slugs to search (e.g. people,companies)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-25, default 25)',
},
},
request: {
url: 'https://api.attio.com/v2/objects/records/search',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const objects = params.objects
.split(',')
.map((s) => s.trim())
.filter(Boolean)
return {
query: params.query,
objects,
limit: params.limit ?? 25,
request_as: { type: 'workspace' },
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to search records')
}
const results = (data.data ?? []).map(
(item: {
id?: { workspace_id?: string; object_id?: string; record_id?: string }
object_slug?: string
record_text?: string
record_image?: string
}) => ({
recordId: item.id?.record_id ?? null,
objectId: item.id?.object_id ?? null,
objectSlug: item.object_slug ?? null,
recordText: item.record_text ?? null,
recordImage: item.record_image ?? null,
})
)
return {
success: true,
output: {
results,
count: results.length,
},
}
},
outputs: {
results: {
type: 'array',
description: 'Search results',
items: {
type: 'object',
properties: {
recordId: { type: 'string', description: 'The record ID' },
objectId: { type: 'string', description: 'The object type ID' },
objectSlug: { type: 'string', description: 'The object type slug' },
recordText: { type: 'string', description: 'Display text for the record' },
recordImage: { type: 'string', description: 'Image URL for the record', optional: true },
},
},
},
count: { type: 'number', description: 'Number of results returned' },
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioUpdateListParams, AttioUpdateListResponse } from './types'
import { LIST_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioUpdateList')
export const attioUpdateListTool: ToolConfig<AttioUpdateListParams, AttioUpdateListResponse> = {
id: 'attio_update_list',
name: 'Attio Update List',
description: 'Update a list in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug to update',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New name for the list',
},
apiSlug: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New API slug for the list',
},
workspaceAccess: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'New workspace-level access: full-access, read-and-write, or read-only (omit for private)',
},
workspaceMemberAccess: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of member access entries, e.g. [{"workspace_member_id":"...","level":"read-and-write"}]',
},
},
request: {
url: (params) => `https://api.attio.com/v2/lists/${params.list}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {}
if (params.name !== undefined) data.name = params.name
if (params.apiSlug !== undefined) data.api_slug = params.apiSlug
if (params.workspaceAccess !== undefined) data.workspace_access = params.workspaceAccess
if (params.workspaceMemberAccess !== undefined) {
try {
data.workspace_member_access =
typeof params.workspaceMemberAccess === 'string'
? JSON.parse(params.workspaceMemberAccess)
: params.workspaceMemberAccess
} catch {
data.workspace_member_access = params.workspaceMemberAccess
}
}
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update list')
}
const list = data.data
const actor = list.created_by_actor as { type?: string; id?: string } | undefined
return {
success: true,
output: {
listId: list.id?.list_id ?? null,
apiSlug: list.api_slug ?? null,
name: list.name ?? null,
parentObject: Array.isArray(list.parent_object)
? (list.parent_object[0] ?? null)
: (list.parent_object ?? null),
workspaceAccess: list.workspace_access ?? null,
workspaceMemberAccess: list.workspace_member_access ?? null,
createdByActor: actor ? { type: actor.type ?? null, id: actor.id ?? null } : null,
createdAt: list.created_at ?? null,
},
}
},
outputs: LIST_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,91 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioUpdateListEntryParams, AttioUpdateListEntryResponse } from './types'
import { LIST_ENTRY_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioUpdateListEntry')
export const attioUpdateListEntryTool: ToolConfig<
AttioUpdateListEntryParams,
AttioUpdateListEntryResponse
> = {
id: 'attio_update_list_entry',
name: 'Attio Update List Entry',
description: 'Update entry attribute values on an Attio list entry (appends multiselect values)',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
list: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The list ID or slug',
},
entryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The entry ID to update',
},
entryValues: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'JSON object of entry attribute values to update',
},
},
request: {
url: (params) => `https://api.attio.com/v2/lists/${params.list}/entries/${params.entryId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let entryValues: Record<string, unknown> = {}
try {
entryValues =
typeof params.entryValues === 'string'
? JSON.parse(params.entryValues)
: params.entryValues
} catch {
entryValues = {}
}
return { data: { entry_values: entryValues } }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update list entry')
}
const entry = data.data
return {
success: true,
output: {
entryId: entry.id?.entry_id ?? null,
listId: entry.id?.list_id ?? null,
parentRecordId: entry.parent_record_id ?? null,
parentObject: entry.parent_object ?? null,
createdAt: entry.created_at ?? null,
entryValues: entry.entry_values ?? {},
},
}
},
outputs: LIST_ENTRY_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,89 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioUpdateObjectParams, AttioUpdateObjectResponse } from './types'
import { OBJECT_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioUpdateObject')
export const attioUpdateObjectTool: ToolConfig<AttioUpdateObjectParams, AttioUpdateObjectResponse> =
{
id: 'attio_update_object',
name: 'Attio Update Object',
description: 'Update a custom object in Attio',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
object: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID or slug to update',
},
apiSlug: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New API slug',
},
singularNoun: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New singular display name',
},
pluralNoun: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New plural display name',
},
},
request: {
url: (params) => `https://api.attio.com/v2/objects/${params.object}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {}
if (params.apiSlug !== undefined) data.api_slug = params.apiSlug
if (params.singularNoun !== undefined) data.singular_noun = params.singularNoun
if (params.pluralNoun !== undefined) data.plural_noun = params.pluralNoun
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update object')
}
const obj = data.data
return {
success: true,
output: {
objectId: obj.id?.object_id ?? null,
apiSlug: obj.api_slug ?? null,
singularNoun: obj.singular_noun ?? null,
pluralNoun: obj.plural_noun ?? null,
createdAt: obj.created_at ?? null,
},
}
},
outputs: OBJECT_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,88 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioUpdateRecordParams, AttioUpdateRecordResponse } from './types'
import { RECORD_OBJECT_OUTPUT } from './types'
const logger = createLogger('AttioUpdateRecord')
export const attioUpdateRecordTool: ToolConfig<AttioUpdateRecordParams, AttioUpdateRecordResponse> =
{
id: 'attio_update_record',
name: 'Attio Update Record',
description: 'Update an existing record in Attio (appends multiselect values)',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
objectType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type slug (e.g. people, companies)',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the record to update',
},
values: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'JSON object of attribute values to update',
},
},
request: {
url: (params) =>
`https://api.attio.com/v2/objects/${params.objectType}/records/${params.recordId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let values: Record<string, unknown>
try {
values = typeof params.values === 'string' ? JSON.parse(params.values) : params.values
} catch {
values = {}
}
return { data: { values } }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update record')
}
const record = data.data
return {
success: true,
output: {
record,
recordId: record.id?.record_id ?? null,
webUrl: record.web_url ?? null,
},
}
},
outputs: {
record: RECORD_OBJECT_OUTPUT,
recordId: { type: 'string', description: 'The ID of the updated record' },
webUrl: { type: 'string', description: 'URL to view the record in Attio' },
},
}

View File

@@ -0,0 +1,126 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioUpdateTaskParams, AttioUpdateTaskResponse } from './types'
import { TASK_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioUpdateTask')
export const attioUpdateTaskTool: ToolConfig<AttioUpdateTaskParams, AttioUpdateTaskResponse> = {
id: 'attio_update_task',
name: 'Attio Update Task',
description: 'Update a task in Attio (deadline, completion status, linked records, assignees)',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to update',
},
deadlineAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New deadline in ISO 8601 format',
},
isCompleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the task is completed',
},
linkedRecords: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of linked records',
},
assignees: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of assignees',
},
},
request: {
url: (params) => `https://api.attio.com/v2/tasks/${params.taskId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {}
if (params.deadlineAt !== undefined) data.deadline_at = params.deadlineAt || null
if (params.isCompleted !== undefined) data.is_completed = params.isCompleted
if (params.linkedRecords) {
try {
data.linked_records =
typeof params.linkedRecords === 'string'
? JSON.parse(params.linkedRecords)
: params.linkedRecords
} catch {
data.linked_records = []
}
}
if (params.assignees) {
try {
data.assignees =
typeof params.assignees === 'string' ? JSON.parse(params.assignees) : params.assignees
} catch {
data.assignees = []
}
}
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update task')
}
const task = data.data
const linkedRecords = (task.linked_records ?? []).map(
(r: { target_object_id?: string; target_record_id?: string }) => ({
targetObjectId: r.target_object_id ?? null,
targetRecordId: r.target_record_id ?? null,
})
)
const assignees = (task.assignees ?? []).map(
(a: { referenced_actor_type?: string; referenced_actor_id?: string }) => ({
type: a.referenced_actor_type ?? null,
id: a.referenced_actor_id ?? null,
})
)
return {
success: true,
output: {
taskId: task.id?.task_id ?? null,
content: task.content_plaintext ?? null,
deadlineAt: task.deadline_at ?? null,
isCompleted: task.is_completed ?? false,
linkedRecords,
assignees,
createdByActor: task.created_by_actor ?? null,
createdAt: task.created_at ?? null,
},
}
},
outputs: TASK_OUTPUT_PROPERTIES,
}

View File

@@ -0,0 +1,100 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioUpdateWebhookParams, AttioUpdateWebhookResponse } from './types'
import { WEBHOOK_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioUpdateWebhook')
export const attioUpdateWebhookTool: ToolConfig<
AttioUpdateWebhookParams,
AttioUpdateWebhookResponse
> = {
id: 'attio_update_webhook',
name: 'Attio Update Webhook',
description: 'Update a webhook in Attio (target URL and/or subscriptions)',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
webhookId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The webhook ID to update',
},
targetUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New HTTPS target URL',
},
subscriptions: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New JSON array of subscriptions',
},
},
request: {
url: (params) => `https://api.attio.com/v2/webhooks/${params.webhookId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {}
if (params.targetUrl !== undefined) data.target_url = params.targetUrl
if (params.subscriptions) {
try {
data.subscriptions =
typeof params.subscriptions === 'string'
? JSON.parse(params.subscriptions)
: params.subscriptions
} catch {
data.subscriptions = []
}
}
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update webhook')
}
const w = data.data
const subs =
(w.subscriptions as Array<{ event_type?: string; filter?: unknown }>)?.map(
(s: { event_type?: string; filter?: unknown }) => ({
eventType: s.event_type ?? null,
filter: s.filter ?? null,
})
) ?? []
return {
success: true,
output: {
webhookId: w.id?.webhook_id ?? null,
targetUrl: w.target_url ?? null,
subscriptions: subs,
status: w.status ?? null,
createdAt: w.created_at ?? null,
},
}
},
outputs: WEBHOOK_OUTPUT_PROPERTIES,
}

View File

@@ -79,6 +79,48 @@ import {
asanaSearchTasksTool,
asanaUpdateTaskTool,
} from '@/tools/asana'
import {
attioAssertRecordTool,
attioCreateCommentTool,
attioCreateListEntryTool,
attioCreateListTool,
attioCreateNoteTool,
attioCreateObjectTool,
attioCreateRecordTool,
attioCreateTaskTool,
attioCreateWebhookTool,
attioDeleteCommentTool,
attioDeleteListEntryTool,
attioDeleteNoteTool,
attioDeleteRecordTool,
attioDeleteTaskTool,
attioDeleteWebhookTool,
attioGetCommentTool,
attioGetListEntryTool,
attioGetListTool,
attioGetMemberTool,
attioGetNoteTool,
attioGetObjectTool,
attioGetRecordTool,
attioGetThreadTool,
attioGetWebhookTool,
attioListListsTool,
attioListMembersTool,
attioListNotesTool,
attioListObjectsTool,
attioListRecordsTool,
attioListTasksTool,
attioListThreadsTool,
attioListWebhooksTool,
attioQueryListEntriesTool,
attioSearchRecordsTool,
attioUpdateListEntryTool,
attioUpdateListTool,
attioUpdateObjectTool,
attioUpdateRecordTool,
attioUpdateTaskTool,
attioUpdateWebhookTool,
} from '@/tools/attio'
import { browserUseRunTaskTool } from '@/tools/browser_use'
import {
calcomCancelBookingTool,
@@ -3060,6 +3102,46 @@ export const tools: Record<string, ToolConfig> = {
airtable_get_record: airtableGetRecordTool,
airtable_list_records: airtableListRecordsTool,
airtable_update_record: airtableUpdateRecordTool,
attio_assert_record: attioAssertRecordTool,
attio_create_comment: attioCreateCommentTool,
attio_create_list: attioCreateListTool,
attio_create_list_entry: attioCreateListEntryTool,
attio_create_note: attioCreateNoteTool,
attio_create_object: attioCreateObjectTool,
attio_create_record: attioCreateRecordTool,
attio_create_task: attioCreateTaskTool,
attio_create_webhook: attioCreateWebhookTool,
attio_delete_comment: attioDeleteCommentTool,
attio_delete_list_entry: attioDeleteListEntryTool,
attio_delete_note: attioDeleteNoteTool,
attio_delete_record: attioDeleteRecordTool,
attio_delete_task: attioDeleteTaskTool,
attio_delete_webhook: attioDeleteWebhookTool,
attio_get_comment: attioGetCommentTool,
attio_get_list: attioGetListTool,
attio_get_list_entry: attioGetListEntryTool,
attio_get_member: attioGetMemberTool,
attio_get_note: attioGetNoteTool,
attio_get_object: attioGetObjectTool,
attio_get_record: attioGetRecordTool,
attio_get_thread: attioGetThreadTool,
attio_get_webhook: attioGetWebhookTool,
attio_list_lists: attioListListsTool,
attio_list_members: attioListMembersTool,
attio_list_notes: attioListNotesTool,
attio_list_objects: attioListObjectsTool,
attio_list_records: attioListRecordsTool,
attio_list_tasks: attioListTasksTool,
attio_list_threads: attioListThreadsTool,
attio_list_webhooks: attioListWebhooksTool,
attio_query_list_entries: attioQueryListEntriesTool,
attio_search_records: attioSearchRecordsTool,
attio_update_list: attioUpdateListTool,
attio_update_list_entry: attioUpdateListEntryTool,
attio_update_object: attioUpdateObjectTool,
attio_update_record: attioUpdateRecordTool,
attio_update_task: attioUpdateTaskTool,
attio_update_webhook: attioUpdateWebhookTool,
ahrefs_domain_rating: ahrefsDomainRatingTool,
ahrefs_backlinks: ahrefsBacklinksTool,
ahrefs_backlinks_stats: ahrefsBacklinksStatsTool,

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Comment Created Trigger
*
* Triggers when a comment is created in Attio.
*/
export const attioCommentCreatedTrigger: TriggerConfig = {
id: 'attio_comment_created',
name: 'Attio Comment Created',
provider: 'attio',
description: 'Trigger workflow when a new comment is created in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_created',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.created'),
extraFields: buildAttioExtraFields('attio_comment_created'),
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Comment Deleted Trigger
*
* Triggers when a comment is deleted in Attio.
*/
export const attioCommentDeletedTrigger: TriggerConfig = {
id: 'attio_comment_deleted',
name: 'Attio Comment Deleted',
provider: 'attio',
description: 'Trigger workflow when a comment is deleted in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.deleted'),
extraFields: buildAttioExtraFields('attio_comment_deleted'),
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Comment Resolved Trigger
*
* Triggers when a comment thread is resolved in Attio.
*/
export const attioCommentResolvedTrigger: TriggerConfig = {
id: 'attio_comment_resolved',
name: 'Attio Comment Resolved',
provider: 'attio',
description: 'Trigger workflow when a comment thread is resolved in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_resolved',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.resolved'),
extraFields: buildAttioExtraFields('attio_comment_resolved'),
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Comment Unresolved Trigger
*
* Triggers when a comment thread is unresolved in Attio.
*/
export const attioCommentUnresolvedTrigger: TriggerConfig = {
id: 'attio_comment_unresolved',
name: 'Attio Comment Unresolved',
provider: 'attio',
description: 'Trigger workflow when a comment thread is unresolved in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_unresolved',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.unresolved'),
extraFields: buildAttioExtraFields('attio_comment_unresolved'),
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,18 @@
export { attioCommentCreatedTrigger } from './comment_created'
export { attioCommentDeletedTrigger } from './comment_deleted'
export { attioCommentResolvedTrigger } from './comment_resolved'
export { attioCommentUnresolvedTrigger } from './comment_unresolved'
export { attioListEntryCreatedTrigger } from './list_entry_created'
export { attioListEntryDeletedTrigger } from './list_entry_deleted'
export { attioListEntryUpdatedTrigger } from './list_entry_updated'
export { attioNoteCreatedTrigger } from './note_created'
export { attioNoteDeletedTrigger } from './note_deleted'
export { attioNoteUpdatedTrigger } from './note_updated'
export { attioRecordCreatedTrigger } from './record_created'
export { attioRecordDeletedTrigger } from './record_deleted'
export { attioRecordMergedTrigger } from './record_merged'
export { attioRecordUpdatedTrigger } from './record_updated'
export { attioTaskCreatedTrigger } from './task_created'
export { attioTaskDeletedTrigger } from './task_deleted'
export { attioTaskUpdatedTrigger } from './task_updated'
export { attioWebhookTrigger } from './webhook'

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildListEntryOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio List Entry Created Trigger
*
* Triggers when a list entry is created in Attio.
*/
export const attioListEntryCreatedTrigger: TriggerConfig = {
id: 'attio_list_entry_created',
name: 'Attio List Entry Created',
provider: 'attio',
description: 'Trigger workflow when a new list entry is created in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_list_entry_created',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('list-entry.created'),
extraFields: buildAttioExtraFields('attio_list_entry_created'),
}),
outputs: buildListEntryOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildListEntryOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio List Entry Deleted Trigger
*
* Triggers when a list entry is deleted in Attio.
*/
export const attioListEntryDeletedTrigger: TriggerConfig = {
id: 'attio_list_entry_deleted',
name: 'Attio List Entry Deleted',
provider: 'attio',
description: 'Trigger workflow when a list entry is deleted in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_list_entry_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('list-entry.deleted'),
extraFields: buildAttioExtraFields('attio_list_entry_deleted'),
}),
outputs: buildListEntryOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildListEntryUpdatedOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio List Entry Updated Trigger
*
* Triggers when a list entry is updated in Attio.
*/
export const attioListEntryUpdatedTrigger: TriggerConfig = {
id: 'attio_list_entry_updated',
name: 'Attio List Entry Updated',
provider: 'attio',
description: 'Trigger workflow when a list entry is updated in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_list_entry_updated',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('list-entry.updated'),
extraFields: buildAttioExtraFields('attio_list_entry_updated'),
}),
outputs: buildListEntryUpdatedOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildNoteOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Note Created Trigger
*
* Triggers when a note is created in Attio.
*/
export const attioNoteCreatedTrigger: TriggerConfig = {
id: 'attio_note_created',
name: 'Attio Note Created',
provider: 'attio',
description: 'Trigger workflow when a new note is created in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_note_created',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('note.created'),
extraFields: buildAttioExtraFields('attio_note_created'),
}),
outputs: buildNoteOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildNoteOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Note Deleted Trigger
*
* Triggers when a note is deleted in Attio.
*/
export const attioNoteDeletedTrigger: TriggerConfig = {
id: 'attio_note_deleted',
name: 'Attio Note Deleted',
provider: 'attio',
description: 'Trigger workflow when a note is deleted in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_note_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('note.deleted'),
extraFields: buildAttioExtraFields('attio_note_deleted'),
}),
outputs: buildNoteOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildNoteOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Note Updated Trigger
*
* Triggers when a note is updated in Attio.
*/
export const attioNoteUpdatedTrigger: TriggerConfig = {
id: 'attio_note_updated',
name: 'Attio Note Updated',
provider: 'attio',
description: 'Trigger workflow when a note is updated in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_note_updated',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('note.updated'),
extraFields: buildAttioExtraFields('attio_note_updated'),
}),
outputs: buildNoteOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,42 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildRecordOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Record Created Trigger
*
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
* Triggers when a new record is created in Attio.
*/
export const attioRecordCreatedTrigger: TriggerConfig = {
id: 'attio_record_created',
name: 'Attio Record Created',
provider: 'attio',
description: 'Trigger workflow when a new record is created in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_record_created',
triggerOptions: attioTriggerOptions,
includeDropdown: true,
setupInstructions: attioSetupInstructions('record.created'),
extraFields: buildAttioExtraFields('attio_record_created'),
}),
outputs: buildRecordOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildRecordOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Record Deleted Trigger
*
* Triggers when a record is deleted in Attio.
*/
export const attioRecordDeletedTrigger: TriggerConfig = {
id: 'attio_record_deleted',
name: 'Attio Record Deleted',
provider: 'attio',
description: 'Trigger workflow when a record is deleted in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_record_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('record.deleted'),
extraFields: buildAttioExtraFields('attio_record_deleted'),
}),
outputs: buildRecordOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildRecordMergedEventOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Record Merged Trigger
*
* Triggers when two records are merged in Attio.
*/
export const attioRecordMergedTrigger: TriggerConfig = {
id: 'attio_record_merged',
name: 'Attio Record Merged',
provider: 'attio',
description: 'Trigger workflow when two records are merged in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_record_merged',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('record.merged'),
extraFields: buildAttioExtraFields('attio_record_merged'),
}),
outputs: buildRecordMergedEventOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildRecordUpdatedOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Record Updated Trigger
*
* Triggers when a record is updated in Attio.
*/
export const attioRecordUpdatedTrigger: TriggerConfig = {
id: 'attio_record_updated',
name: 'Attio Record Updated',
provider: 'attio',
description: 'Trigger workflow when a record is updated in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_record_updated',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('record.updated'),
extraFields: buildAttioExtraFields('attio_record_updated'),
}),
outputs: buildRecordUpdatedOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildTaskOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Task Created Trigger
*
* Triggers when a task is created in Attio.
*/
export const attioTaskCreatedTrigger: TriggerConfig = {
id: 'attio_task_created',
name: 'Attio Task Created',
provider: 'attio',
description: 'Trigger workflow when a new task is created in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_task_created',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('task.created'),
extraFields: buildAttioExtraFields('attio_task_created'),
}),
outputs: buildTaskOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildTaskOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Task Deleted Trigger
*
* Triggers when a task is deleted in Attio.
*/
export const attioTaskDeletedTrigger: TriggerConfig = {
id: 'attio_task_deleted',
name: 'Attio Task Deleted',
provider: 'attio',
description: 'Trigger workflow when a task is deleted in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_task_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('task.deleted'),
extraFields: buildAttioExtraFields('attio_task_deleted'),
}),
outputs: buildTaskOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildTaskOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Attio Task Updated Trigger
*
* Triggers when a task is updated in Attio.
*/
export const attioTaskUpdatedTrigger: TriggerConfig = {
id: 'attio_task_updated',
name: 'Attio Task Updated',
provider: 'attio',
description: 'Trigger workflow when a task is updated in Attio',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_task_updated',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('task.updated'),
extraFields: buildAttioExtraFields('attio_task_updated'),
}),
outputs: buildTaskOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -0,0 +1,295 @@
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
export const attioTriggerOptions = [
{ label: 'Record Created', id: 'attio_record_created' },
{ label: 'Record Updated', id: 'attio_record_updated' },
{ label: 'Record Deleted', id: 'attio_record_deleted' },
{ label: 'Record Merged', id: 'attio_record_merged' },
{ label: 'Note Created', id: 'attio_note_created' },
{ label: 'Note Updated', id: 'attio_note_updated' },
{ label: 'Note Deleted', id: 'attio_note_deleted' },
{ label: 'Task Created', id: 'attio_task_created' },
{ label: 'Task Updated', id: 'attio_task_updated' },
{ label: 'Task Deleted', id: 'attio_task_deleted' },
{ label: 'Comment Created', id: 'attio_comment_created' },
{ label: 'Comment Resolved', id: 'attio_comment_resolved' },
{ label: 'Comment Unresolved', id: 'attio_comment_unresolved' },
{ label: 'Comment Deleted', id: 'attio_comment_deleted' },
{ label: 'List Entry Created', id: 'attio_list_entry_created' },
{ label: 'List Entry Updated', id: 'attio_list_entry_updated' },
{ label: 'List Entry Deleted', id: 'attio_list_entry_deleted' },
{ label: 'Generic Webhook (All Events)', id: 'attio_webhook' },
]
export function attioSetupInstructions(eventType: string): string {
const instructions = [
'<strong>Note:</strong> You need access to the Attio developer settings to create webhooks. See the <a href="https://docs.attio.com/rest-api/guides/webhooks" target="_blank" rel="noopener noreferrer">Attio webhook documentation</a> for details.',
'In Attio, navigate to <strong>Settings > Developers</strong> and select your integration.',
'Go to the <strong>Webhooks</strong> tab and click <strong>"Create Webhook"</strong>.',
'Paste the <strong>Webhook URL</strong> from above into the target URL field.',
`Add a subscription with the event type <strong>${eventType}</strong>. You can optionally add filters to scope the events.`,
'Save the webhook. Copy the <strong>signing secret</strong> shown and paste it in the field above for signature verification.',
'The webhook is now active. Attio will send events to the URL you configured.',
]
return instructions
.map(
(instruction, index) =>
`<div class="mb-3">${index === 0 ? instruction : `<strong>${index}.</strong> ${instruction}`}</div>`
)
.join('')
}
export function buildAttioExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'webhookSecret',
title: 'Webhook Secret',
type: 'short-input',
placeholder: 'Enter the webhook signing secret from Attio',
description:
'The signing secret from Attio used to verify webhook deliveries via HMAC-SHA256 signature',
password: true,
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
/**
* Base webhook outputs common to all Attio triggers.
*/
function buildBaseWebhookOutputs(): Record<string, TriggerOutput> {
return {
eventType: {
type: 'string',
description: 'The type of event (e.g. record.created, note.created)',
},
}
}
/**
* Record event outputs for record triggers.
*/
function buildRecordIdOutputs(): Record<string, TriggerOutput> {
return {
workspaceId: { type: 'string', description: 'The workspace ID' },
objectId: { type: 'string', description: 'The object type ID (e.g. people, companies)' },
recordId: { type: 'string', description: 'The record ID' },
}
}
/**
* Record updated event outputs (includes attributeId).
*/
function buildRecordUpdatedIdOutputs(): Record<string, TriggerOutput> {
return {
...buildRecordIdOutputs(),
attributeId: {
type: 'string',
description: 'The ID of the attribute that was updated on the record',
},
}
}
/**
* Record merged event outputs.
* Attio payload: id.record_id (winner), duplicate_object_id, duplicate_record_id (loser).
*/
function buildRecordMergedOutputs(): Record<string, TriggerOutput> {
return {
workspaceId: { type: 'string', description: 'The workspace ID' },
objectId: { type: 'string', description: 'The object type ID of the surviving record' },
recordId: { type: 'string', description: 'The surviving record ID after merge' },
duplicateObjectId: {
type: 'string',
description: 'The object type ID of the merged-away record',
},
duplicateRecordId: { type: 'string', description: 'The record ID that was merged away' },
}
}
/**
* Note event outputs.
*/
function buildNoteIdOutputs(): Record<string, TriggerOutput> {
return {
workspaceId: { type: 'string', description: 'The workspace ID' },
noteId: { type: 'string', description: 'The note ID' },
parentObjectId: { type: 'string', description: 'The parent object type ID' },
parentRecordId: { type: 'string', description: 'The parent record ID' },
}
}
/**
* Task event outputs.
* Attio task webhook payloads only contain workspace_id and task_id.
*/
function buildTaskIdOutputs(): Record<string, TriggerOutput> {
return {
workspaceId: { type: 'string', description: 'The workspace ID' },
taskId: { type: 'string', description: 'The task ID' },
}
}
/**
* Comment event outputs.
* Attio payload uses object_id/record_id (not parent_*), plus list_id/entry_id.
*/
function buildCommentIdOutputs(): Record<string, TriggerOutput> {
return {
workspaceId: { type: 'string', description: 'The workspace ID' },
threadId: { type: 'string', description: 'The thread ID' },
commentId: { type: 'string', description: 'The comment ID' },
objectId: { type: 'string', description: 'The object type ID' },
recordId: { type: 'string', description: 'The record ID' },
listId: { type: 'string', description: 'The list ID (if comment is on a list entry)' },
entryId: { type: 'string', description: 'The list entry ID (if comment is on a list entry)' },
}
}
/**
* List entry event outputs.
*/
function buildListEntryIdOutputs(): Record<string, TriggerOutput> {
return {
workspaceId: { type: 'string', description: 'The workspace ID' },
listId: { type: 'string', description: 'The list ID' },
entryId: { type: 'string', description: 'The list entry ID' },
}
}
/**
* List entry updated event outputs (includes attributeId).
*/
function buildListEntryUpdatedIdOutputs(): Record<string, TriggerOutput> {
return {
...buildListEntryIdOutputs(),
attributeId: {
type: 'string',
description: 'The ID of the attribute that was updated on the list entry',
},
}
}
/** Record created/deleted outputs. */
export function buildRecordOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildRecordIdOutputs(),
}
}
/** Record updated outputs (includes attributeId). */
export function buildRecordUpdatedOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildRecordUpdatedIdOutputs(),
}
}
/** Record merged outputs. */
export function buildRecordMergedEventOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildRecordMergedOutputs(),
}
}
/** Note event outputs. */
export function buildNoteOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildNoteIdOutputs(),
}
}
/** Task event outputs. */
export function buildTaskOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildTaskIdOutputs(),
}
}
/** Comment event outputs. */
export function buildCommentOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildCommentIdOutputs(),
}
}
/** List entry created/deleted outputs. */
export function buildListEntryOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildListEntryIdOutputs(),
}
}
/** List entry updated outputs (includes attributeId). */
export function buildListEntryUpdatedOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
...buildListEntryUpdatedIdOutputs(),
}
}
/** Generic webhook outputs covering all event types. */
export function buildGenericWebhookOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
id: { type: 'json', description: 'The event ID object containing resource identifiers' },
parentObjectId: {
type: 'string',
description: 'The parent object type ID (if applicable)',
},
parentRecordId: {
type: 'string',
description: 'The parent record ID (if applicable)',
},
}
}
/**
* Maps trigger IDs to the exact Attio event type strings.
*/
const TRIGGER_EVENT_MAP: Record<string, string[]> = {
attio_record_created: ['record.created'],
attio_record_updated: ['record.updated'],
attio_record_deleted: ['record.deleted'],
attio_record_merged: ['record.merged'],
attio_note_created: ['note.created'],
attio_note_updated: ['note.updated', 'note.content-updated'],
attio_note_deleted: ['note.deleted'],
attio_task_created: ['task.created'],
attio_task_updated: ['task.updated'],
attio_task_deleted: ['task.deleted'],
attio_comment_created: ['comment.created'],
attio_comment_resolved: ['comment.resolved'],
attio_comment_unresolved: ['comment.unresolved'],
attio_comment_deleted: ['comment.deleted'],
attio_list_entry_created: ['list-entry.created'],
attio_list_entry_updated: ['list-entry.updated'],
attio_list_entry_deleted: ['list-entry.deleted'],
}
/**
* Checks if an Attio webhook payload matches a trigger.
*/
export function isAttioPayloadMatch(triggerId: string, body: Record<string, unknown>): boolean {
if (triggerId === 'attio_webhook') {
return true
}
const eventType = body.event_type as string | undefined
if (!eventType) {
return false
}
const acceptedEvents = TRIGGER_EVENT_MAP[triggerId]
return acceptedEvents ? acceptedEvents.includes(eventType) : false
}

View File

@@ -0,0 +1,40 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildGenericWebhookOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Generic Attio Webhook Trigger
*
* Captures all Attio webhook events without filtering.
*/
export const attioWebhookTrigger: TriggerConfig = {
id: 'attio_webhook',
name: 'Attio Webhook (All Events)',
provider: 'attio',
description: 'Trigger workflow on any Attio webhook event',
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_webhook',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('All Events'),
extraFields: buildAttioExtraFields('attio_webhook'),
}),
outputs: buildGenericWebhookOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Attio-Signature': 'hmac-sha256-signature',
},
},
}

View File

@@ -1,4 +1,24 @@
import { airtableWebhookTrigger } from '@/triggers/airtable'
import {
attioCommentCreatedTrigger,
attioCommentDeletedTrigger,
attioCommentResolvedTrigger,
attioCommentUnresolvedTrigger,
attioListEntryCreatedTrigger,
attioListEntryDeletedTrigger,
attioListEntryUpdatedTrigger,
attioNoteCreatedTrigger,
attioNoteDeletedTrigger,
attioNoteUpdatedTrigger,
attioRecordCreatedTrigger,
attioRecordDeletedTrigger,
attioRecordMergedTrigger,
attioRecordUpdatedTrigger,
attioTaskCreatedTrigger,
attioTaskDeletedTrigger,
attioTaskUpdatedTrigger,
attioWebhookTrigger,
} from '@/triggers/attio'
import {
calcomBookingCancelledTrigger,
calcomBookingCreatedTrigger,
@@ -145,6 +165,24 @@ import { whatsappWebhookTrigger } from '@/triggers/whatsapp'
export const TRIGGER_REGISTRY: TriggerRegistry = {
slack_webhook: slackWebhookTrigger,
airtable_webhook: airtableWebhookTrigger,
attio_webhook: attioWebhookTrigger,
attio_record_created: attioRecordCreatedTrigger,
attio_record_updated: attioRecordUpdatedTrigger,
attio_record_deleted: attioRecordDeletedTrigger,
attio_record_merged: attioRecordMergedTrigger,
attio_note_created: attioNoteCreatedTrigger,
attio_note_updated: attioNoteUpdatedTrigger,
attio_note_deleted: attioNoteDeletedTrigger,
attio_task_created: attioTaskCreatedTrigger,
attio_task_updated: attioTaskUpdatedTrigger,
attio_task_deleted: attioTaskDeletedTrigger,
attio_comment_created: attioCommentCreatedTrigger,
attio_comment_resolved: attioCommentResolvedTrigger,
attio_comment_unresolved: attioCommentUnresolvedTrigger,
attio_comment_deleted: attioCommentDeletedTrigger,
attio_list_entry_created: attioListEntryCreatedTrigger,
attio_list_entry_updated: attioListEntryUpdatedTrigger,
attio_list_entry_deleted: attioListEntryDeletedTrigger,
calendly_webhook: calendlyWebhookTrigger,
calendly_invitee_created: calendlyInviteeCreatedTrigger,
calendly_invitee_canceled: calendlyInviteeCanceledTrigger,