fix(attio): automatic webhook lifecycle management and tool fixes (#3327)

* fix(attio): use code subblock type for JSON input fields

* fix(attio): correct people name attribute format in wand prompt example

* fix(attio): improve wand prompt with correct attribute formats for all field types

* fix(attio): use array format with full_name for personal-name attribute in wand prompt

* fix(attio): use loose null checks to prevent sending null params to API

* fix(attio): add offset param and make pagination fields advanced mode

* fix(attio): remove redundant (optional) from placeholders

* fix(attio): always send required workspace_access and workspace_member_access in create list

* fix(attio): always send api_slug in create list, auto-generate from name if not provided

* fix(attio): update api slug placeholder text

* fix(tools): manage lifecycle for attio tools

* updated docs

* fix(attio): remove incorrect save button reference from setup instructions

* fix(attio): log debug message when signature verification is skipped
This commit is contained in:
Waleed
2026-02-24 17:30:52 -08:00
committed by GitHub
parent 0574427d45
commit d06459f489
37 changed files with 457 additions and 324 deletions
+2 -2
View File
@@ -1012,8 +1012,8 @@ Update a webhook in Attio (target URL and/or subscriptions)
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `webhookId` | string | Yes | The webhook ID to update |
| `targetUrl` | string | No | New HTTPS target URL |
| `subscriptions` | string | No | New JSON array of subscriptions |
| `targetUrl` | string | Yes | HTTPS target URL for webhook delivery |
| `subscriptions` | string | Yes | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
#### Output
+57 -33
View File
@@ -139,7 +139,7 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
{
id: 'values',
title: 'Values',
type: 'long-input',
type: 'code',
placeholder: '{"name": "Acme Corp", "domains": [{"domain": "acme.com"}]}',
condition: {
field: 'operation',
@@ -161,21 +161,25 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
Return ONLY the JSON object with Attio attribute values. No explanations, no markdown, no extra text.
### ATTIO VALUES STRUCTURE
Keys are attribute slugs, values follow Attio's attribute format. Simple values can be strings; complex values are arrays of objects.
Keys are attribute slugs. Most text attributes use array-of-objects format [{"value": "..."}]. Special attributes have their own format.
### COMMON PEOPLE ATTRIBUTES
- name: [{"first_name": "...", "last_name": "..."}]
- name: [{"first_name": "...", "last_name": "...", "full_name": "..."}] (personal-name type, full_name is required)
- email_addresses: [{"email_address": "..."}]
- phone_numbers: [{"original_phone_number": "...", "country_code": "US"}]
- job_title, description, linkedin, twitter
- job_title: [{"value": "..."}]
- description: [{"value": "..."}]
- linkedin: [{"value": "https://linkedin.com/in/..."}]
- twitter: [{"value": "@handle"}]
### COMMON COMPANY ATTRIBUTES
- name: [{"value": "..."}]
- domains: [{"domain": "..."}]
- description, primary_location, categories
- description: [{"value": "..."}]
- primary_location: [{"line_1": "...", "locality": "...", "region": "...", "postcode": "...", "country_code": "US"}]
### EXAMPLES
Person: {"name": [{"first_name": "John", "last_name": "Doe"}], "email_addresses": [{"email_address": "john@example.com"}]}
Person: {"name": [{"first_name": "John", "last_name": "Doe", "full_name": "John Doe"}], "email_addresses": [{"email_address": "john@example.com"}], "job_title": [{"value": "Engineer"}]}
Company: {"name": [{"value": "Acme Corp"}], "domains": [{"domain": "acme.com"}]}`,
placeholder: 'Describe the record values you want to set...',
generationType: 'json-object',
@@ -184,8 +188,8 @@ Company: {"name": [{"value": "Acme Corp"}], "domains": [{"domain": "acme.com"}]}
{
id: 'filter',
title: 'Filter',
type: 'long-input',
placeholder: '{"name": "John Smith"} (optional)',
type: 'code',
placeholder: '{"name": "John Smith"}',
condition: { field: 'operation', value: 'list_records' },
wandConfig: {
enabled: true,
@@ -215,8 +219,8 @@ Empty (list all): {}`,
{
id: 'sorts',
title: 'Sort',
type: 'long-input',
placeholder: '[{"direction":"asc","attribute":"name"}] (optional)',
type: 'code',
placeholder: '[{"direction":"asc","attribute":"name"}]',
condition: { field: 'operation', value: 'list_records' },
wandConfig: {
enabled: true,
@@ -332,7 +336,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
id: 'noteMeetingId',
title: 'Meeting ID',
type: 'short-input',
placeholder: 'Link to a meeting (optional)',
placeholder: 'Link to a meeting',
condition: { field: 'operation', value: 'create_note' },
mode: 'advanced',
},
@@ -358,7 +362,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
id: 'taskDeadline',
title: 'Deadline',
type: 'short-input',
placeholder: '2024-12-01T15:00:00.000Z (optional)',
placeholder: '2024-12-01T15:00:00.000Z',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
wandConfig: {
enabled: true,
@@ -394,8 +398,8 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
{
id: 'taskLinkedRecords',
title: 'Linked Records',
type: 'long-input',
placeholder: '[{"target_object":"people","target_record_id":"..."}] (optional)',
type: 'code',
placeholder: '[{"target_object":"people","target_record_id":"..."}]',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
wandConfig: {
enabled: true,
@@ -420,9 +424,8 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
{
id: 'taskAssignees',
title: 'Assignees',
type: 'long-input',
placeholder:
'[{"referenced_actor_type":"workspace-member","referenced_actor_id":"..."}] (optional)',
type: 'code',
placeholder: '[{"referenced_actor_type":"workspace-member","referenced_actor_id":"..."}]',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
wandConfig: {
enabled: true,
@@ -456,21 +459,21 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
id: 'taskFilterObject',
title: 'Linked Object Type',
type: 'short-input',
placeholder: 'e.g. people, companies (optional)',
placeholder: 'e.g. people, companies',
condition: { field: 'operation', value: 'list_tasks' },
},
{
id: 'taskFilterRecordId',
title: 'Linked Record ID',
type: 'short-input',
placeholder: 'Filter by linked record ID (optional)',
placeholder: 'Filter by linked record ID',
condition: { field: 'operation', value: 'list_tasks' },
},
{
id: 'taskFilterAssignee',
title: 'Assignee',
type: 'short-input',
placeholder: 'Filter by assignee email or ID (optional)',
placeholder: 'Filter by assignee email or ID',
condition: { field: 'operation', value: 'list_tasks' },
},
{
@@ -571,7 +574,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
id: 'listApiSlug',
title: 'API Slug',
type: 'short-input',
placeholder: 'e.g. my_list (optional, auto-generated)',
placeholder: 'e.g. my_list (auto-generated from name)',
condition: { field: 'operation', value: ['create_list', 'update_list'] },
mode: 'advanced',
},
@@ -622,8 +625,8 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
{
id: 'entryValues',
title: 'Entry Values',
type: 'long-input',
placeholder: '{"attribute_slug": "value"} (optional)',
type: 'code',
placeholder: '{"attribute_slug": "value"}',
condition: {
field: 'operation',
value: ['create_list_entry', 'update_list_entry'],
@@ -652,8 +655,8 @@ Keys are list attribute slugs. Values follow Attio attribute format.
{
id: 'entryFilter',
title: 'Filter',
type: 'long-input',
placeholder: '{"attribute": {"$operator": "value"}} (optional)',
type: 'code',
placeholder: '{"attribute": {"$operator": "value"}}',
condition: { field: 'operation', value: 'query_list_entries' },
wandConfig: {
enabled: true,
@@ -679,8 +682,8 @@ Logical: $and, $or, $not
{
id: 'entrySorts',
title: 'Sort',
type: 'long-input',
placeholder: '[{"direction":"asc","attribute":"created_at"}] (optional)',
type: 'code',
placeholder: '[{"direction":"asc","attribute":"created_at"}]',
condition: { field: 'operation', value: 'query_list_entries' },
wandConfig: {
enabled: true,
@@ -819,7 +822,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
id: 'threadFilterRecordId',
title: 'Record ID',
type: 'short-input',
placeholder: 'Filter by record ID (optional)',
placeholder: 'Filter by record ID',
condition: { field: 'operation', value: 'list_threads' },
},
{
@@ -833,7 +836,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
id: 'threadFilterEntryId',
title: 'Entry ID',
type: 'short-input',
placeholder: 'Filter by entry ID (optional)',
placeholder: 'Filter by entry ID',
condition: { field: 'operation', value: 'list_threads' },
},
{
@@ -865,15 +868,15 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
type: 'short-input',
placeholder: 'https://example.com/webhook',
condition: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
required: { field: 'operation', value: 'create_webhook' },
required: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
},
{
id: 'webhookSubscriptions',
title: 'Subscriptions',
type: 'long-input',
type: 'code',
placeholder: '[{"event_type":"record.created","filter":{"object_id":"..."}}]',
condition: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
required: { field: 'operation', value: 'create_webhook' },
required: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
wandConfig: {
enabled: true,
maintainHistory: true,
@@ -911,7 +914,8 @@ workspace-member.created
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Max results (optional)',
placeholder: 'Max results',
mode: 'advanced',
condition: {
field: 'operation',
value: [
@@ -925,6 +929,24 @@ workspace-member.created
],
},
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: 'Number of results to skip',
mode: 'advanced',
condition: {
field: 'operation',
value: [
'list_records',
'list_notes',
'list_tasks',
'query_list_entries',
'list_threads',
'list_webhooks',
],
},
},
...getTrigger('attio_record_created').subBlocks,
...getTrigger('attio_record_updated').subBlocks,
...getTrigger('attio_record_deleted').subBlocks,
@@ -1103,6 +1125,7 @@ workspace-member.created
// Shared params
if (params.limit) cleanParams.limit = Number(params.limit)
if (params.offset) cleanParams.offset = Number(params.offset)
return cleanParams
},
@@ -1164,6 +1187,7 @@ workspace-member.created
webhookTargetUrl: { type: 'string', description: 'Webhook target URL' },
webhookSubscriptions: { type: 'json', description: 'Webhook event subscriptions' },
limit: { type: 'string', description: 'Maximum number of results' },
offset: { type: 'string', description: 'Number of results to skip for pagination' },
},
outputs: {
+28
View File
@@ -13,6 +13,7 @@ import { convertSquareBracketsToTwiML } from '@/lib/webhooks/utils'
import {
handleSlackChallenge,
handleWhatsAppVerification,
validateAttioSignature,
validateCalcomSignature,
validateCirclebackSignature,
validateFirefliesSignature,
@@ -597,6 +598,33 @@ export async function verifyProviderAuth(
}
}
if (foundWebhook.provider === 'attio') {
const secret = providerConfig.webhookSecret as string | undefined
if (!secret) {
logger.debug(
`[${requestId}] Attio webhook ${foundWebhook.id} has no signing secret, skipping signature verification`
)
} else {
const signature = request.headers.get('Attio-Signature')
if (!signature) {
logger.warn(`[${requestId}] Attio webhook missing signature header`)
return new NextResponse('Unauthorized - Missing Attio signature', { status: 401 })
}
const isValidSignature = validateAttioSignature(secret, signature, rawBody)
if (!isValidSignature) {
logger.warn(`[${requestId}] Attio signature verification failed`, {
signatureLength: signature.length,
secretLength: secret.length,
})
return new NextResponse('Unauthorized - Invalid Attio signature', { status: 401 })
}
}
}
if (foundWebhook.provider === 'linear') {
const secret = providerConfig.webhookSecret as string | undefined
+206 -1
View File
@@ -19,6 +19,7 @@ const calendlyLogger = createLogger('CalendlyWebhook')
const grainLogger = createLogger('GrainWebhook')
const lemlistLogger = createLogger('LemlistWebhook')
const webflowLogger = createLogger('WebflowWebhook')
const attioLogger = createLogger('AttioWebhook')
const providerSubscriptionsLogger = createLogger('WebhookProviderSubscriptions')
function getProviderConfig(webhook: any): Record<string, any> {
@@ -976,6 +977,196 @@ export async function deleteWebflowWebhook(
}
}
export async function createAttioWebhookSubscription(
userId: string,
webhookData: any,
requestId: string
): Promise<{ externalId: string; webhookSecret: string } | undefined> {
try {
const { path, providerConfig } = webhookData
const { triggerId, credentialId } = providerConfig || {}
if (!credentialId) {
attioLogger.warn(`[${requestId}] Missing credentialId for Attio webhook creation.`, {
webhookId: webhookData.id,
})
throw new Error(
'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
)
}
const credentialOwner = await getCredentialOwner(credentialId, requestId)
const accessToken = credentialOwner
? await refreshAccessTokenIfNeeded(
credentialOwner.accountId,
credentialOwner.userId,
requestId
)
: null
if (!accessToken) {
attioLogger.warn(
`[${requestId}] Could not retrieve Attio access token for user ${userId}. Cannot create webhook.`
)
throw new Error(
'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
)
}
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
const { TRIGGER_EVENT_MAP } = await import('@/triggers/attio/utils')
let subscriptions: Array<{ event_type: string }> = []
if (triggerId === 'attio_webhook') {
const allEvents = new Set<string>()
for (const events of Object.values(TRIGGER_EVENT_MAP)) {
for (const event of events) {
allEvents.add(event)
}
}
subscriptions = Array.from(allEvents).map((event_type) => ({ event_type }))
} else {
const events = TRIGGER_EVENT_MAP[triggerId]
if (!events || events.length === 0) {
attioLogger.warn(`[${requestId}] No event types mapped for trigger ${triggerId}`, {
webhookId: webhookData.id,
})
throw new Error(`Unknown Attio trigger type: ${triggerId}`)
}
subscriptions = events.map((event_type) => ({ event_type }))
}
const requestBody = {
target_url: notificationUrl,
subscriptions,
}
const attioResponse = await fetch('https://api.attio.com/v2/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
if (!attioResponse.ok) {
const errorBody = await attioResponse.json().catch(() => ({}))
attioLogger.error(
`[${requestId}] Failed to create webhook in Attio for webhook ${webhookData.id}. Status: ${attioResponse.status}`,
{ response: errorBody }
)
let userFriendlyMessage = 'Failed to create webhook subscription in Attio'
if (attioResponse.status === 401) {
userFriendlyMessage = 'Attio authentication failed. Please reconnect your Attio account.'
} else if (attioResponse.status === 403) {
userFriendlyMessage =
'Attio access denied. Please ensure your integration has webhook permissions.'
}
throw new Error(userFriendlyMessage)
}
const responseBody = await attioResponse.json()
const data = responseBody.data || responseBody
const webhookId = data.id?.webhook_id || data.webhook_id || data.id
const secret = data.secret
if (!webhookId) {
attioLogger.error(
`[${requestId}] Attio webhook created but no webhook_id returned for webhook ${webhookData.id}`,
{ response: responseBody }
)
throw new Error('Attio webhook creation succeeded but no webhook ID was returned')
}
if (!secret) {
attioLogger.warn(
`[${requestId}] Attio webhook created but no secret returned for webhook ${webhookData.id}. Signature verification will be skipped.`,
{ response: responseBody }
)
}
attioLogger.info(
`[${requestId}] Successfully created webhook in Attio for webhook ${webhookData.id}.`,
{ attioWebhookId: webhookId }
)
return { externalId: webhookId, webhookSecret: secret || '' }
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
attioLogger.error(
`[${requestId}] Exception during Attio webhook creation for webhook ${webhookData.id}.`,
{ message }
)
throw error
}
}
export async function deleteAttioWebhook(
webhook: any,
_workflow: any,
requestId: string
): Promise<void> {
try {
const config = getProviderConfig(webhook)
const externalId = config.externalId as string | undefined
const credentialId = config.credentialId as string | undefined
if (!externalId) {
attioLogger.warn(
`[${requestId}] Missing externalId for Attio webhook deletion ${webhook.id}, skipping cleanup`
)
return
}
if (!credentialId) {
attioLogger.warn(
`[${requestId}] Missing credentialId for Attio webhook deletion ${webhook.id}, skipping cleanup`
)
return
}
const credentialOwner = await getCredentialOwner(credentialId, requestId)
const accessToken = credentialOwner
? await refreshAccessTokenIfNeeded(
credentialOwner.accountId,
credentialOwner.userId,
requestId
)
: null
if (!accessToken) {
attioLogger.warn(
`[${requestId}] Could not retrieve Attio access token. Cannot delete webhook.`,
{ webhookId: webhook.id }
)
return
}
const attioResponse = await fetch(`https://api.attio.com/v2/webhooks/${externalId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!attioResponse.ok && attioResponse.status !== 404) {
const responseBody = await attioResponse.json().catch(() => ({}))
attioLogger.warn(
`[${requestId}] Failed to delete Attio webhook (non-fatal): ${attioResponse.status}`,
{ response: responseBody }
)
} else {
attioLogger.info(`[${requestId}] Successfully deleted Attio webhook ${externalId}`)
}
} catch (error) {
attioLogger.warn(`[${requestId}] Error deleting Attio webhook (non-fatal)`, error)
}
}
export async function createGrainWebhookSubscription(
_request: NextRequest,
webhookData: any,
@@ -1611,6 +1802,7 @@ type RecreateCheckInput = {
/** Providers that create external webhook subscriptions */
const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set([
'airtable',
'attio',
'calendly',
'webflow',
'typeform',
@@ -1626,6 +1818,7 @@ const SYSTEM_MANAGED_FIELDS = new Set([
'externalSubscriptionId',
'eventTypes',
'webhookTag',
'webhookSecret',
'historyId',
'lastCheckedTimestamp',
'setupCompleted',
@@ -1686,6 +1879,16 @@ export async function createExternalWebhookSubscription(
updatedProviderConfig = { ...updatedProviderConfig, externalId }
externalSubscriptionCreated = true
}
} else if (provider === 'attio') {
const result = await createAttioWebhookSubscription(userId, webhookData, requestId)
if (result) {
updatedProviderConfig = {
...updatedProviderConfig,
externalId: result.externalId,
webhookSecret: result.webhookSecret,
}
externalSubscriptionCreated = true
}
} else if (provider === 'calendly') {
const externalId = await createCalendlyWebhookSubscription(webhookData, requestId)
if (externalId) {
@@ -1736,7 +1939,7 @@ export async function createExternalWebhookSubscription(
/**
* Clean up external webhook subscriptions for a webhook
* Handles Airtable, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
* Handles Airtable, Attio, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
* Don't fail deletion if cleanup fails
*/
export async function cleanupExternalWebhook(
@@ -1746,6 +1949,8 @@ export async function cleanupExternalWebhook(
): Promise<void> {
if (webhook.provider === 'airtable') {
await deleteAirtableWebhook(webhook, workflow, requestId)
} else if (webhook.provider === 'attio') {
await deleteAttioWebhook(webhook, workflow, requestId)
} else if (webhook.provider === 'microsoft-teams') {
await deleteTeamsSubscription(webhook, workflow, requestId)
} else if (webhook.provider === 'telegram') {
+35
View File
@@ -1395,6 +1395,41 @@ export function validateLinearSignature(secret: string, signature: string, body:
}
}
/**
* Validates an Attio webhook request signature using HMAC SHA-256
* @param secret - Attio webhook signing secret (plain text)
* @param signature - Attio-Signature header value (hex-encoded HMAC SHA-256 signature)
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateAttioSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Attio signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Attio signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${signature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: signature.length,
match: computedHash === signature,
})
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Attio signature:', error)
return false
}
}
/**
* Validates a Circleback webhook request signature using HMAC SHA-256
* @param secret - Circleback signing secret (plain text)
+9 -2
View File
@@ -65,12 +65,19 @@ export const attioCreateListTool: ToolConfig<AttioCreateListParams, AttioCreateL
'Content-Type': 'application/json',
}),
body: (params) => {
const apiSlug =
params.apiSlug ||
params.name
?.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_|_$/g, '')
const data: Record<string, unknown> = {
name: params.name,
api_slug: apiSlug,
parent_object: params.parentObject,
workspace_access: params.workspaceAccess ?? null,
workspace_member_access: [] as unknown[],
}
if (params.apiSlug) data.api_slug = params.apiSlug
if (params.workspaceAccess) data.workspace_access = params.workspaceAccess
if (params.workspaceMemberAccess) {
try {
data.workspace_member_access =
+8 -6
View File
@@ -60,20 +60,22 @@ export const attioCreateListEntryTool: ToolConfig<
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {
parent_record_id: params.parentRecordId,
parent_object: params.parentObject,
}
let entryValues: unknown = {}
if (params.entryValues) {
try {
data.entry_values =
entryValues =
typeof params.entryValues === 'string'
? JSON.parse(params.entryValues)
: params.entryValues
} catch {
data.entry_values = {}
entryValues = {}
}
}
const data: Record<string, unknown> = {
parent_record_id: params.parentRecordId,
parent_object: params.parentObject,
entry_values: entryValues,
}
return { data }
},
},
+1 -1
View File
@@ -83,7 +83,7 @@ export const attioCreateNoteTool: ToolConfig<AttioCreateNoteParams, AttioCreateN
content: params.content,
}
if (params.createdAt) body.created_at = params.createdAt
if (params.meetingId !== undefined) body.meeting_id = params.meetingId || null
if (params.meetingId != null) body.meeting_id = params.meetingId || null
return { data: body }
},
},
+2 -2
View File
@@ -54,8 +54,8 @@ export const attioListNotesTool: ToolConfig<AttioListNotesParams, AttioListNotes
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))
if (params.limit != null) searchParams.set('limit', String(params.limit))
if (params.offset != null) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/notes${qs ? `?${qs}` : ''}`
},
+2 -2
View File
@@ -78,8 +78,8 @@ export const attioListRecordsTool: ToolConfig<AttioListRecordsParams, AttioListR
body.sorts = params.sorts
}
}
if (params.limit !== undefined) body.limit = params.limit
if (params.offset !== undefined) body.offset = params.offset
if (params.limit != null) body.limit = params.limit
if (params.offset != null) body.offset = params.offset
return body
},
},
+3 -3
View File
@@ -73,12 +73,12 @@ export const attioListTasksTool: ToolConfig<AttioListTasksParams, AttioListTasks
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) {
if (params.isCompleted != null) {
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))
if (params.limit != null) searchParams.set('limit', String(params.limit))
if (params.offset != null) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/tasks${qs ? `?${qs}` : ''}`
},
+2 -2
View File
@@ -67,8 +67,8 @@ export const attioListThreadsTool: ToolConfig<AttioListThreadsParams, AttioListT
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))
if (params.limit != null) searchParams.set('limit', String(params.limit))
if (params.offset != null) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/threads${qs ? `?${qs}` : ''}`
},
+2 -2
View File
@@ -41,8 +41,8 @@ export const attioListWebhooksTool: ToolConfig<AttioListWebhooksParams, AttioLis
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))
if (params.limit != null) searchParams.set('limit', String(params.limit))
if (params.offset != null) searchParams.set('offset', String(params.offset))
const qs = searchParams.toString()
return `https://api.attio.com/v2/webhooks${qs ? `?${qs}` : ''}`
},
+2 -2
View File
@@ -83,8 +83,8 @@ export const attioQueryListEntriesTool: ToolConfig<
body.sorts = []
}
}
if (params.limit !== undefined) body.limit = params.limit
if (params.offset !== undefined) body.offset = params.offset
if (params.limit != null) body.limit = params.limit
if (params.offset != null) body.offset = params.offset
return body
},
},
+4 -4
View File
@@ -66,10 +66,10 @@ export const attioUpdateListTool: ToolConfig<AttioUpdateListParams, AttioUpdateL
}),
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) {
if (params.name != null) data.name = params.name
if (params.apiSlug != null) data.api_slug = params.apiSlug
if (params.workspaceAccess != null) data.workspace_access = params.workspaceAccess
if (params.workspaceMemberAccess != null) {
try {
data.workspace_member_access =
typeof params.workspaceMemberAccess === 'string'
+3 -3
View File
@@ -59,9 +59,9 @@ export const attioUpdateObjectTool: ToolConfig<AttioUpdateObjectParams, AttioUpd
}),
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
if (params.apiSlug != null) data.api_slug = params.apiSlug
if (params.singularNoun != null) data.singular_noun = params.singularNoun
if (params.pluralNoun != null) data.plural_noun = params.pluralNoun
return { data }
},
},
+2 -2
View File
@@ -64,8 +64,8 @@ export const attioUpdateTaskTool: ToolConfig<AttioUpdateTaskParams, AttioUpdateT
}),
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.deadlineAt != null) data.deadline_at = params.deadlineAt || null
if (params.isCompleted != null) data.is_completed = params.isCompleted
if (params.linkedRecords) {
try {
data.linked_records =
+11 -8
View File
@@ -34,15 +34,15 @@ export const attioUpdateWebhookTool: ToolConfig<
},
targetUrl: {
type: 'string',
required: false,
required: true,
visibility: 'user-or-llm',
description: 'New HTTPS target URL',
description: 'HTTPS target URL for webhook delivery',
},
subscriptions: {
type: 'string',
required: false,
required: true,
visibility: 'user-or-llm',
description: 'New JSON array of subscriptions',
description: 'JSON array of subscriptions, e.g. [{"event_type":"note.created"}]',
},
},
@@ -54,18 +54,21 @@ export const attioUpdateWebhookTool: ToolConfig<
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {}
if (params.targetUrl !== undefined) data.target_url = params.targetUrl
let subscriptions: unknown = []
if (params.subscriptions) {
try {
data.subscriptions =
subscriptions =
typeof params.subscriptions === 'string'
? JSON.parse(params.subscriptions)
: params.subscriptions
} catch {
data.subscriptions = []
subscriptions = []
}
}
const data: Record<string, unknown> = {
target_url: params.targetUrl,
subscriptions,
}
return { data }
},
},
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioCommentCreatedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_created',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.created'),
extraFields: buildAttioExtraFields('attio_comment_created'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_comment_created'),
outputs: buildCommentOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioCommentDeletedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.deleted'),
extraFields: buildAttioExtraFields('attio_comment_deleted'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_comment_deleted'),
outputs: buildCommentOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioCommentResolvedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_resolved',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.resolved'),
extraFields: buildAttioExtraFields('attio_comment_resolved'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_comment_resolved'),
outputs: buildCommentOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildCommentOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioCommentUnresolvedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_comment_unresolved',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('comment.unresolved'),
extraFields: buildAttioExtraFields('attio_comment_unresolved'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_comment_unresolved'),
outputs: buildCommentOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildListEntryOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildListEntryOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioListEntryCreatedTrigger: TriggerConfig = {
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'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_list_entry_created'),
outputs: buildListEntryOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildListEntryOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildListEntryOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioListEntryDeletedTrigger: TriggerConfig = {
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'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_list_entry_deleted'),
outputs: buildListEntryOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildListEntryUpdatedOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildListEntryUpdatedOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioListEntryUpdatedTrigger: TriggerConfig = {
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'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_list_entry_updated'),
outputs: buildListEntryUpdatedOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildNoteOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildNoteOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioNoteCreatedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_note_created',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('note.created'),
extraFields: buildAttioExtraFields('attio_note_created'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_note_created'),
outputs: buildNoteOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildNoteOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildNoteOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioNoteDeletedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_note_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('note.deleted'),
extraFields: buildAttioExtraFields('attio_note_deleted'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_note_deleted'),
outputs: buildNoteOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildNoteOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildNoteOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioNoteUpdatedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_note_updated',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('note.updated'),
extraFields: buildAttioExtraFields('attio_note_updated'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_note_updated'),
outputs: buildNoteOutputs(),
+13 -10
View File
@@ -1,9 +1,7 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildAttioTriggerSubBlocks,
buildRecordOutputs,
} from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
@@ -22,13 +20,18 @@ export const attioRecordCreatedTrigger: TriggerConfig = {
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'),
}),
subBlocks: [
{
id: 'selectedTriggerId',
title: 'Trigger Type',
type: 'dropdown',
mode: 'trigger',
options: attioTriggerOptions,
value: () => 'attio_record_created',
required: true,
},
...buildAttioTriggerSubBlocks('attio_record_created'),
],
outputs: buildRecordOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildRecordOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildRecordOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioRecordDeletedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_record_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('record.deleted'),
extraFields: buildAttioExtraFields('attio_record_deleted'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_record_deleted'),
outputs: buildRecordOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildRecordMergedEventOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildRecordMergedEventOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioRecordMergedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_record_merged',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('record.merged'),
extraFields: buildAttioExtraFields('attio_record_merged'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_record_merged'),
outputs: buildRecordMergedEventOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildRecordUpdatedOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildRecordUpdatedOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioRecordUpdatedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_record_updated',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('record.updated'),
extraFields: buildAttioExtraFields('attio_record_updated'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_record_updated'),
outputs: buildRecordUpdatedOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildTaskOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildTaskOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioTaskCreatedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_task_created',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('task.created'),
extraFields: buildAttioExtraFields('attio_task_created'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_task_created'),
outputs: buildTaskOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildTaskOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildTaskOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioTaskDeletedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_task_deleted',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('task.deleted'),
extraFields: buildAttioExtraFields('attio_task_deleted'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_task_deleted'),
outputs: buildTaskOutputs(),
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildTaskOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildTaskOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioTaskUpdatedTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_task_updated',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('task.updated'),
extraFields: buildAttioExtraFields('attio_task_updated'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_task_updated'),
outputs: buildTaskOutputs(),
+31 -18
View File
@@ -22,15 +22,11 @@ export const attioTriggerOptions = [
{ label: 'Generic Webhook (All Events)', id: 'attio_webhook' },
]
export function attioSetupInstructions(eventType: string): string {
export function attioSetupInstructions(): 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.',
'<strong>Note:</strong> Webhooks are automatically created in Attio when you deploy this workflow, and deleted when you undeploy. See the <a href="https://docs.attio.com/rest-api/guides/webhooks" target="_blank" rel="noopener noreferrer">Attio webhook documentation</a> for details.',
'Connect your <strong>Attio account</strong> using the credential selector above.',
'<strong>Deploy</strong> the workflow — a webhook will be created automatically in your Attio workspace.',
]
return instructions
@@ -41,17 +37,34 @@ export function attioSetupInstructions(eventType: string): string {
.join('')
}
export function buildAttioExtraFields(triggerId: string): SubBlockConfig[] {
/**
* Builds subBlocks for an Attio trigger with OAuth credentials and automatic webhook lifecycle.
* Used by both the primary trigger (with dropdown) and secondary triggers.
*/
export function buildAttioTriggerSubBlocks(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,
id: 'triggerCredentials',
title: 'Attio Account',
type: 'oauth-input',
serviceId: 'attio',
mode: 'trigger',
required: true,
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'triggerSave',
title: 'Save',
type: 'trigger-save',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: attioSetupInstructions(),
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
@@ -257,7 +270,7 @@ export function buildGenericWebhookOutputs(): Record<string, TriggerOutput> {
/**
* Maps trigger IDs to the exact Attio event type strings.
*/
const TRIGGER_EVENT_MAP: Record<string, string[]> = {
export const TRIGGER_EVENT_MAP: Record<string, string[]> = {
attio_record_created: ['record.created'],
attio_record_updated: ['record.updated'],
attio_record_deleted: ['record.deleted'],
+2 -13
View File
@@ -1,11 +1,5 @@
import { AttioIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
attioSetupInstructions,
attioTriggerOptions,
buildAttioExtraFields,
buildGenericWebhookOutputs,
} from '@/triggers/attio/utils'
import { buildAttioTriggerSubBlocks, buildGenericWebhookOutputs } from '@/triggers/attio/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
@@ -21,12 +15,7 @@ export const attioWebhookTrigger: TriggerConfig = {
version: '1.0.0',
icon: AttioIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'attio_webhook',
triggerOptions: attioTriggerOptions,
setupInstructions: attioSetupInstructions('All Events'),
extraFields: buildAttioExtraFields('attio_webhook'),
}),
subBlocks: buildAttioTriggerSubBlocks('attio_webhook'),
outputs: buildGenericWebhookOutputs(),