diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 3016a905700..e991d1a5dee 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -64,6 +64,10 @@ export const AttioBlock: BlockConfig = { { label: 'Create Webhook', id: 'create_webhook' }, { label: 'Update Webhook', id: 'update_webhook' }, { label: 'Delete Webhook', id: 'delete_webhook' }, + { label: 'List Attributes', id: 'list_attributes' }, + { label: 'Get Attribute', id: 'get_attribute' }, + { label: 'Create Attribute', id: 'create_attribute' }, + { label: 'Update Attribute', id: 'update_attribute' }, ], value: () => 'list_records', }, @@ -430,7 +434,19 @@ YYYY-MM-DDTHH:mm:ss.SSSZ { label: 'Yes', id: 'true' }, ], value: () => 'false', - condition: { field: 'operation', value: ['create_task', 'update_task'] }, + condition: { field: 'operation', value: 'create_task' }, + }, + { + id: 'taskIsCompletedUpdate', + title: 'Completed', + type: 'dropdown', + options: [ + { label: 'Leave unchanged', id: 'unchanged' }, + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'unchanged', + condition: { field: 'operation', value: 'update_task' }, }, { id: 'taskLinkedRecords', @@ -662,7 +678,20 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. { label: 'Read Only', id: 'read-only' }, ], value: () => 'full-access', - condition: { field: 'operation', value: ['create_list', 'update_list'] }, + condition: { field: 'operation', value: 'create_list' }, + }, + { + id: 'listWorkspaceAccessUpdate', + title: 'Workspace Access', + type: 'dropdown', + options: [ + { label: 'Leave unchanged', id: 'unchanged' }, + { label: 'Full Access', id: 'full-access' }, + { label: 'Read & Write', id: 'read-and-write' }, + { label: 'Read Only', id: 'read-only' }, + ], + value: () => 'unchanged', + condition: { field: 'operation', value: 'update_list' }, }, // List entry fields @@ -823,29 +852,97 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. condition: { field: 'operation', value: 'create_comment' }, required: { field: 'operation', value: 'create_comment' }, }, + { + id: 'commentTarget', + title: 'Comment On', + type: 'dropdown', + options: [ + { label: 'List Entry', id: 'entry' }, + { label: 'Record', id: 'record' }, + { label: 'Reply to Thread', id: 'thread' }, + ], + value: () => 'entry', + condition: { field: 'operation', value: 'create_comment' }, + }, { id: 'commentList', title: 'List', type: 'short-input', placeholder: 'List ID or slug', - condition: { field: 'operation', value: 'create_comment' }, - required: { field: 'operation', value: 'create_comment' }, + condition: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'entry' }, + }, + required: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'entry' }, + }, }, { id: 'commentEntryId', title: 'Entry ID', type: 'short-input', placeholder: 'List entry ID to comment on', - condition: { field: 'operation', value: 'create_comment' }, - required: { field: 'operation', value: 'create_comment' }, + condition: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'entry' }, + }, + required: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'entry' }, + }, + }, + { + id: 'commentRecordObject', + title: 'Record Object', + type: 'short-input', + placeholder: 'Object ID or slug the record belongs to', + condition: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'record' }, + }, + required: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'record' }, + }, + }, + { + id: 'commentRecordId', + title: 'Record ID', + type: 'short-input', + placeholder: 'Record ID to comment on directly', + condition: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'record' }, + }, + required: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'record' }, + }, }, { id: 'commentThreadId', title: 'Thread ID', type: 'short-input', - placeholder: 'Reply to thread (optional, omit to start new)', - condition: { field: 'operation', value: 'create_comment' }, - mode: 'advanced', + placeholder: 'Thread ID to reply to', + condition: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'thread' }, + }, + required: { + field: 'operation', + value: 'create_comment', + and: { field: 'commentTarget', value: 'thread' }, + }, }, { id: 'commentCreatedAt', @@ -983,6 +1080,208 @@ workspace-member.created }, }, + // Attribute fields + { + id: 'attributeTarget', + title: 'Target', + type: 'dropdown', + options: [ + { label: 'Object', id: 'objects' }, + { label: 'List', id: 'lists' }, + ], + value: () => 'objects', + condition: { + field: 'operation', + value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'], + }, + required: { + field: 'operation', + value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'], + }, + }, + { + id: 'attributeIdentifier', + title: 'Object or List ID/Slug', + type: 'short-input', + placeholder: 'e.g. people, companies', + condition: { + field: 'operation', + value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'], + }, + required: { + field: 'operation', + value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'], + }, + }, + { + id: 'attributeId', + title: 'Attribute ID or Slug', + type: 'short-input', + placeholder: 'e.g. email_addresses', + condition: { field: 'operation', value: ['get_attribute', 'update_attribute'] }, + required: { field: 'operation', value: ['get_attribute', 'update_attribute'] }, + }, + { + id: 'attributeTitle', + title: 'Title', + type: 'short-input', + placeholder: 'e.g. Lead Source', + condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] }, + required: { field: 'operation', value: 'create_attribute' }, + }, + { + id: 'attributeApiSlug', + title: 'API Slug', + type: 'short-input', + placeholder: 'e.g. lead_source', + condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] }, + required: { field: 'operation', value: 'create_attribute' }, + }, + { + id: 'attributeType', + title: 'Type', + type: 'dropdown', + options: [ + { label: 'Text', id: 'text' }, + { label: 'Number', id: 'number' }, + { label: 'Checkbox', id: 'checkbox' }, + { label: 'Currency', id: 'currency' }, + { label: 'Date', id: 'date' }, + { label: 'Timestamp', id: 'timestamp' }, + { label: 'Rating', id: 'rating' }, + { label: 'Status', id: 'status' }, + { label: 'Select', id: 'select' }, + { label: 'Record Reference', id: 'record-reference' }, + { label: 'Actor Reference', id: 'actor-reference' }, + { label: 'Location', id: 'location' }, + { label: 'Domain', id: 'domain' }, + { label: 'Email Address', id: 'email-address' }, + { label: 'Phone Number', id: 'phone-number' }, + ], + condition: { field: 'operation', value: 'create_attribute' }, + required: { field: 'operation', value: 'create_attribute' }, + }, + { + id: 'attributeDescription', + title: 'Description', + type: 'long-input', + placeholder: 'Describe the attribute', + condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] }, + mode: 'advanced', + }, + { + id: 'attributeIsRequired', + title: 'Required', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'create_attribute' }, + mode: 'advanced', + }, + { + id: 'attributeIsRequiredUpdate', + title: 'Required', + type: 'dropdown', + options: [ + { label: 'Leave unchanged', id: 'unchanged' }, + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'unchanged', + condition: { field: 'operation', value: 'update_attribute' }, + mode: 'advanced', + }, + { + id: 'attributeIsUnique', + title: 'Unique', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'create_attribute' }, + mode: 'advanced', + }, + { + id: 'attributeIsUniqueUpdate', + title: 'Unique', + type: 'dropdown', + options: [ + { label: 'Leave unchanged', id: 'unchanged' }, + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'unchanged', + condition: { field: 'operation', value: 'update_attribute' }, + mode: 'advanced', + }, + { + id: 'attributeIsMultiselect', + title: 'Multiselect', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'create_attribute' }, + mode: 'advanced', + }, + { + id: 'attributeIsArchived', + title: 'Archived', + type: 'dropdown', + options: [ + { label: 'Leave unchanged', id: 'unchanged' }, + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'unchanged', + condition: { field: 'operation', value: 'update_attribute' }, + mode: 'advanced', + }, + { + id: 'attributeConfig', + title: 'Config', + type: 'code', + placeholder: '{"default_currency_code": "USD", "display_type": "text"}', + condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] }, + mode: 'advanced', + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `Generate Attio attribute type configuration as a JSON object. + +### CONTEXT +{context} + +### CRITICAL INSTRUCTION +Return ONLY the JSON object. No explanations, no markdown, no extra text. + +### EXAMPLES +Currency: {"default_currency_code": "USD", "display_type": "text"} +Record reference: {"allowed_objects": ["people", "companies"]}`, + placeholder: 'Describe the attribute configuration...', + generationType: 'json-object', + }, + }, + { + id: 'attributeShowArchived', + title: 'Show Archived', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'list_attributes' }, + mode: 'advanced', + }, + // Shared limit { id: 'limit', @@ -1000,6 +1299,7 @@ workspace-member.created 'query_list_entries', 'list_threads', 'list_webhooks', + 'list_attributes', ], }, }, @@ -1018,9 +1318,23 @@ workspace-member.created 'query_list_entries', 'list_threads', 'list_webhooks', + 'list_attributes', ], }, }, + { + id: 'taskSort', + title: 'Sort', + type: 'dropdown', + options: [ + { label: 'Created At (asc)', id: 'created_at:asc' }, + { label: 'Created At (desc)', id: 'created_at:desc' }, + { label: 'Completed At (asc)', id: 'completed_at:asc' }, + { label: 'Completed At (desc)', id: 'completed_at:desc' }, + ], + condition: { field: 'operation', value: 'list_tasks' }, + mode: 'advanced', + }, ...getTrigger('attio_record_created').subBlocks, ...getTrigger('attio_record_updated').subBlocks, ...getTrigger('attio_record_deleted').subBlocks, @@ -1116,6 +1430,10 @@ workspace-member.created 'attio_create_webhook', 'attio_update_webhook', 'attio_delete_webhook', + 'attio_list_attributes', + 'attio_get_attribute', + 'attio_create_attribute', + 'attio_update_attribute', ], config: { tool: (params) => `attio_${params.operation}`, @@ -1123,92 +1441,285 @@ workspace-member.created const cleanParams: Record = { oauthCredential: params.oauthCredential, } - - // Record params - if (params.objectType) cleanParams.objectType = params.objectType - if (params.recordId) cleanParams.recordId = params.recordId - if (params.matchingAttribute) cleanParams.matchingAttribute = params.matchingAttribute - if (params.values) cleanParams.values = params.values - if (params.filter) cleanParams.filter = params.filter - if (params.sorts) cleanParams.sorts = params.sorts - if (params.query) cleanParams.query = params.query - if (params.objects) cleanParams.objects = params.objects + const op = params.operation + + // Record params — each field is only sent for the operations whose subBlock condition + // actually exposes it, so a stale value left over from switching operations never leaks + // into an unrelated request (cleanParams keys are reused across several operation families). + if ( + [ + 'list_records', + 'get_record', + 'create_record', + 'update_record', + 'delete_record', + 'assert_record', + ].includes(op) && + params.objectType + ) + cleanParams.objectType = params.objectType + if (['get_record', 'update_record', 'delete_record'].includes(op) && params.recordId) + cleanParams.recordId = params.recordId + if (op === 'assert_record' && params.matchingAttribute) + cleanParams.matchingAttribute = params.matchingAttribute + if (['create_record', 'update_record', 'assert_record'].includes(op) && params.values) + cleanParams.values = params.values + if (op === 'list_records' && params.filter) cleanParams.filter = params.filter + if (op === 'list_records' && params.sorts) cleanParams.sorts = params.sorts + if (op === 'search_records' && params.query) cleanParams.query = params.query + if (op === 'search_records' && params.objects) cleanParams.objects = params.objects // Note params - if (params.noteParentObject) cleanParams.parentObject = params.noteParentObject - if (params.noteParentRecordId) cleanParams.parentRecordId = params.noteParentRecordId - if (params.noteTitle) cleanParams.title = params.noteTitle - if (params.noteContent) cleanParams.content = params.noteContent - if (params.noteFormat) cleanParams.format = params.noteFormat - if (params.noteId) cleanParams.noteId = params.noteId - if (params.noteCreatedAt) cleanParams.createdAt = params.noteCreatedAt - if (params.noteMeetingId) cleanParams.meetingId = params.noteMeetingId + if (['list_notes', 'create_note'].includes(op) && params.noteParentObject) + cleanParams.parentObject = params.noteParentObject + if (['list_notes', 'create_note'].includes(op) && params.noteParentRecordId) + cleanParams.parentRecordId = params.noteParentRecordId + if (op === 'create_note' && params.noteTitle) cleanParams.title = params.noteTitle + if (op === 'create_note' && params.noteContent) cleanParams.content = params.noteContent + if (op === 'create_note' && params.noteFormat) cleanParams.format = params.noteFormat + if (op === 'create_note' && params.noteCreatedAt) + cleanParams.createdAt = params.noteCreatedAt + if (op === 'create_note' && params.noteMeetingId) + cleanParams.meetingId = params.noteMeetingId + if (['get_note', 'delete_note'].includes(op) && params.noteId) + cleanParams.noteId = params.noteId // Task params - if (params.taskContent) cleanParams.content = params.taskContent - if (params.taskDeadline) cleanParams.deadlineAt = params.taskDeadline - if (params.taskIsCompleted !== undefined) + if (op === 'create_task' && params.taskContent) cleanParams.content = params.taskContent + if (['create_task', 'update_task'].includes(op) && params.taskDeadline) + cleanParams.deadlineAt = params.taskDeadline + if (op === 'create_task' && params.taskIsCompleted !== undefined) cleanParams.isCompleted = params.taskIsCompleted === 'true' || params.taskIsCompleted === true - if (params.taskLinkedRecords) cleanParams.linkedRecords = params.taskLinkedRecords - if (params.taskAssignees) cleanParams.assignees = params.taskAssignees - if (params.taskId) cleanParams.taskId = params.taskId - if (params.taskFilterObject) cleanParams.linkedObject = params.taskFilterObject - if (params.taskFilterRecordId) cleanParams.linkedRecordId = params.taskFilterRecordId - if (params.taskFilterAssignee) cleanParams.assignee = params.taskFilterAssignee - if (params.taskFilterCompleted && params.taskFilterCompleted !== 'all') + if (op === 'update_task') { + // taskIsCompletedUpdate is the current control; taskIsCompleted is a fallback for blocks + // saved before the split, when that field applied to both create_task and update_task. + if ( + params.taskIsCompletedUpdate !== undefined && + params.taskIsCompletedUpdate !== 'unchanged' + ) { + cleanParams.isCompleted = + params.taskIsCompletedUpdate === 'true' || params.taskIsCompletedUpdate === true + } else if ( + params.taskIsCompletedUpdate === undefined && + params.taskIsCompleted !== undefined + ) { + cleanParams.isCompleted = + params.taskIsCompleted === 'true' || params.taskIsCompleted === true + } + } + if (['create_task', 'update_task'].includes(op) && params.taskLinkedRecords) + cleanParams.linkedRecords = params.taskLinkedRecords + if (['create_task', 'update_task'].includes(op) && params.taskAssignees) + cleanParams.assignees = params.taskAssignees + if (['get_task', 'update_task', 'delete_task'].includes(op) && params.taskId) + cleanParams.taskId = params.taskId + if (op === 'list_tasks' && params.taskFilterObject) + cleanParams.linkedObject = params.taskFilterObject + if (op === 'list_tasks' && params.taskFilterRecordId) + cleanParams.linkedRecordId = params.taskFilterRecordId + if (op === 'list_tasks' && params.taskFilterAssignee) + cleanParams.assignee = params.taskFilterAssignee + if ( + op === 'list_tasks' && + params.taskFilterCompleted && + params.taskFilterCompleted !== 'all' + ) cleanParams.isCompleted = params.taskFilterCompleted === 'true' + if (op === 'list_tasks' && params.taskSort) cleanParams.sort = params.taskSort // Object params - if (params.objectIdOrSlug) cleanParams.object = params.objectIdOrSlug - if (params.objectApiSlug) cleanParams.apiSlug = params.objectApiSlug - if (params.objectSingularNoun) cleanParams.singularNoun = params.objectSingularNoun - if (params.objectPluralNoun) cleanParams.pluralNoun = params.objectPluralNoun + if (['get_object', 'update_object'].includes(op) && params.objectIdOrSlug) + cleanParams.object = params.objectIdOrSlug + if (['create_object', 'update_object'].includes(op) && params.objectApiSlug) + cleanParams.apiSlug = params.objectApiSlug + if (['create_object', 'update_object'].includes(op) && params.objectSingularNoun) + cleanParams.singularNoun = params.objectSingularNoun + if (['create_object', 'update_object'].includes(op) && params.objectPluralNoun) + cleanParams.pluralNoun = params.objectPluralNoun // List params - if (params.listIdOrSlug) cleanParams.list = params.listIdOrSlug - if (params.listName) cleanParams.name = params.listName - if (params.listParentObject) cleanParams.parentObject = params.listParentObject - if (params.listApiSlug) cleanParams.apiSlug = params.listApiSlug - if (params.listWorkspaceAccess) cleanParams.workspaceAccess = params.listWorkspaceAccess + if ( + [ + 'get_list', + 'update_list', + 'query_list_entries', + 'get_list_entry', + 'create_list_entry', + 'update_list_entry', + 'delete_list_entry', + ].includes(op) && + params.listIdOrSlug + ) + cleanParams.list = params.listIdOrSlug + if (['create_list', 'update_list'].includes(op) && params.listName) + cleanParams.name = params.listName + if (op === 'create_list' && params.listParentObject) + cleanParams.parentObject = params.listParentObject + if (['create_list', 'update_list'].includes(op) && params.listApiSlug) + cleanParams.apiSlug = params.listApiSlug + if (op === 'create_list' && params.listWorkspaceAccess) + cleanParams.workspaceAccess = params.listWorkspaceAccess + if (op === 'update_list') { + // listWorkspaceAccessUpdate is the current control; listWorkspaceAccess is a fallback + // for blocks saved before the split, when that field applied to both create and update. + if ( + params.listWorkspaceAccessUpdate && + params.listWorkspaceAccessUpdate !== 'unchanged' + ) { + cleanParams.workspaceAccess = params.listWorkspaceAccessUpdate + } else if (!params.listWorkspaceAccessUpdate && params.listWorkspaceAccess) { + cleanParams.workspaceAccess = params.listWorkspaceAccess + } + } // List entry params - if (params.entryId) cleanParams.entryId = params.entryId - if (params.entryParentRecordId) cleanParams.parentRecordId = params.entryParentRecordId - if (params.entryParentObject) cleanParams.parentObject = params.entryParentObject - if (params.entryValues) cleanParams.entryValues = params.entryValues - if (params.entryFilter) cleanParams.filter = params.entryFilter - if (params.entrySorts) cleanParams.sorts = params.entrySorts + if ( + ['get_list_entry', 'update_list_entry', 'delete_list_entry'].includes(op) && + params.entryId + ) + cleanParams.entryId = params.entryId + if (op === 'create_list_entry' && params.entryParentRecordId) + cleanParams.parentRecordId = params.entryParentRecordId + if (op === 'create_list_entry' && params.entryParentObject) + cleanParams.parentObject = params.entryParentObject + if (['create_list_entry', 'update_list_entry'].includes(op) && params.entryValues) + cleanParams.entryValues = params.entryValues + if (op === 'query_list_entries' && params.entryFilter) + cleanParams.filter = params.entryFilter + if (op === 'query_list_entries' && params.entrySorts) cleanParams.sorts = params.entrySorts // Member params - if (params.memberId) cleanParams.memberId = params.memberId + if (op === 'get_member' && params.memberId) cleanParams.memberId = params.memberId // Comment params - if (params.commentContent) cleanParams.content = params.commentContent - if (params.commentFormat) cleanParams.format = params.commentFormat - if (params.commentAuthorType) cleanParams.authorType = params.commentAuthorType - if (params.commentAuthorId) cleanParams.authorId = params.commentAuthorId - if (params.commentList) cleanParams.list = params.commentList - if (params.commentEntryId) cleanParams.entryId = params.commentEntryId - if (params.commentThreadId) cleanParams.threadId = params.commentThreadId - if (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt - if (params.commentId) cleanParams.commentId = params.commentId + if (op === 'create_comment') { + if (params.commentContent) cleanParams.content = params.commentContent + if (params.commentFormat) cleanParams.format = params.commentFormat + if (params.commentAuthorType) cleanParams.authorType = params.commentAuthorType + if (params.commentAuthorId) cleanParams.authorId = params.commentAuthorId + // Blocks saved before commentTarget existed have no value for it — fall back to the + // pre-existing behavior (entry-based comment, or thread reply if a thread ID was set). + const commentTarget = + params.commentTarget ?? (params.commentThreadId ? 'thread' : 'entry') + if (commentTarget === 'entry') { + if (params.commentList) cleanParams.list = params.commentList + if (params.commentEntryId) cleanParams.entryId = params.commentEntryId + } else if (commentTarget === 'record') { + if (params.commentRecordObject) cleanParams.recordObject = params.commentRecordObject + if (params.commentRecordId) cleanParams.recordId = params.commentRecordId + } else if (commentTarget === 'thread') { + if (params.commentThreadId) cleanParams.threadId = params.commentThreadId + } + if (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt + } + if (['get_comment', 'delete_comment'].includes(op) && params.commentId) + cleanParams.commentId = params.commentId // Thread params - if (params.threadId) cleanParams.threadId = params.threadId - if (params.threadFilterRecordId) cleanParams.recordId = params.threadFilterRecordId - if (params.threadFilterObject) cleanParams.object = params.threadFilterObject - if (params.threadFilterEntryId) cleanParams.entryId = params.threadFilterEntryId - if (params.threadFilterList) cleanParams.list = params.threadFilterList + if (op === 'get_thread' && params.threadId) cleanParams.threadId = params.threadId + if (op === 'list_threads' && params.threadFilterRecordId) + cleanParams.recordId = params.threadFilterRecordId + if (op === 'list_threads' && params.threadFilterObject) + cleanParams.object = params.threadFilterObject + if (op === 'list_threads' && params.threadFilterEntryId) + cleanParams.entryId = params.threadFilterEntryId + if (op === 'list_threads' && params.threadFilterList) + cleanParams.list = params.threadFilterList // Webhook params - if (params.webhookId) cleanParams.webhookId = params.webhookId - if (params.webhookTargetUrl) cleanParams.targetUrl = params.webhookTargetUrl - if (params.webhookSubscriptions) cleanParams.subscriptions = params.webhookSubscriptions - - // Shared params - if (params.limit) cleanParams.limit = Number(params.limit) - if (params.offset) cleanParams.offset = Number(params.offset) + if (['get_webhook', 'update_webhook', 'delete_webhook'].includes(op) && params.webhookId) + cleanParams.webhookId = params.webhookId + if (['create_webhook', 'update_webhook'].includes(op) && params.webhookTargetUrl) + cleanParams.targetUrl = params.webhookTargetUrl + if (['create_webhook', 'update_webhook'].includes(op) && params.webhookSubscriptions) + cleanParams.subscriptions = params.webhookSubscriptions + + // Attribute params + const attributeOps = [ + 'list_attributes', + 'get_attribute', + 'create_attribute', + 'update_attribute', + ] + if (attributeOps.includes(op) && params.attributeTarget) + cleanParams.target = params.attributeTarget + if (attributeOps.includes(op) && params.attributeIdentifier) + cleanParams.identifier = params.attributeIdentifier + if (['get_attribute', 'update_attribute'].includes(op) && params.attributeId) + cleanParams.attribute = params.attributeId + if (['create_attribute', 'update_attribute'].includes(op) && params.attributeTitle) + cleanParams.title = params.attributeTitle + if (['create_attribute', 'update_attribute'].includes(op) && params.attributeApiSlug) + cleanParams.apiSlug = params.attributeApiSlug + if (op === 'create_attribute' && params.attributeType) + cleanParams.type = params.attributeType + if (['create_attribute', 'update_attribute'].includes(op) && params.attributeDescription) + cleanParams.description = params.attributeDescription + if (op === 'create_attribute' && params.attributeIsRequired !== undefined) + cleanParams.isRequired = + params.attributeIsRequired === 'true' || params.attributeIsRequired === true + if ( + op === 'update_attribute' && + params.attributeIsRequiredUpdate !== undefined && + params.attributeIsRequiredUpdate !== 'unchanged' + ) + cleanParams.isRequired = + params.attributeIsRequiredUpdate === 'true' || params.attributeIsRequiredUpdate === true + if (op === 'create_attribute' && params.attributeIsUnique !== undefined) + cleanParams.isUnique = + params.attributeIsUnique === 'true' || params.attributeIsUnique === true + if ( + op === 'update_attribute' && + params.attributeIsUniqueUpdate !== undefined && + params.attributeIsUniqueUpdate !== 'unchanged' + ) + cleanParams.isUnique = + params.attributeIsUniqueUpdate === 'true' || params.attributeIsUniqueUpdate === true + if (op === 'create_attribute' && params.attributeIsMultiselect !== undefined) + cleanParams.isMultiselect = + params.attributeIsMultiselect === 'true' || params.attributeIsMultiselect === true + if ( + op === 'update_attribute' && + params.attributeIsArchived !== undefined && + params.attributeIsArchived !== 'unchanged' + ) + cleanParams.isArchived = + params.attributeIsArchived === 'true' || params.attributeIsArchived === true + if (['create_attribute', 'update_attribute'].includes(op) && params.attributeConfig) + cleanParams.config = params.attributeConfig + if (op === 'list_attributes' && params.attributeShowArchived !== undefined) + cleanParams.showArchived = + params.attributeShowArchived === 'true' || params.attributeShowArchived === true + + // Shared pagination params — only meaningful for list-style operations + if ( + [ + 'list_records', + 'search_records', + 'list_notes', + 'list_tasks', + 'query_list_entries', + 'list_threads', + 'list_webhooks', + 'list_attributes', + ].includes(op) && + params.limit + ) + cleanParams.limit = Number(params.limit) + if ( + [ + 'list_records', + 'list_notes', + 'list_tasks', + 'query_list_entries', + 'list_threads', + 'list_webhooks', + 'list_attributes', + ].includes(op) && + params.offset + ) + cleanParams.offset = Number(params.offset) return cleanParams }, @@ -1237,6 +1748,7 @@ workspace-member.created taskContent: { type: 'string', description: 'Task content' }, taskDeadline: { type: 'string', description: 'Task deadline' }, taskIsCompleted: { type: 'string', description: 'Task completion status' }, + taskIsCompletedUpdate: { type: 'string', description: 'Task completion status (update)' }, taskLinkedRecords: { type: 'json', description: 'Linked records JSON array' }, taskAssignees: { type: 'json', description: 'Assignees JSON array' }, taskId: { type: 'string', description: 'Task ID' }, @@ -1249,6 +1761,10 @@ workspace-member.created listParentObject: { type: 'string', description: 'List parent object' }, listApiSlug: { type: 'string', description: 'List API slug' }, listWorkspaceAccess: { type: 'string', description: 'List workspace-level access' }, + listWorkspaceAccessUpdate: { + type: 'string', + description: 'List workspace-level access (update)', + }, entryId: { type: 'string', description: 'List entry ID' }, entryParentRecordId: { type: 'string', description: 'Record ID for list entry' }, entryParentObject: { type: 'string', description: 'Record object type for list entry' }, @@ -1260,8 +1776,11 @@ workspace-member.created commentFormat: { type: 'string', description: 'Comment format' }, commentAuthorType: { type: 'string', description: 'Comment author type' }, commentAuthorId: { type: 'string', description: 'Comment author ID' }, + commentTarget: { type: 'string', description: 'What the comment is attached to' }, commentList: { type: 'string', description: 'List for comment' }, commentEntryId: { type: 'string', description: 'Entry ID for comment' }, + commentRecordObject: { type: 'string', description: 'Object for record comment' }, + commentRecordId: { type: 'string', description: 'Record ID for record comment' }, commentThreadId: { type: 'string', description: 'Thread ID to reply to' }, commentCreatedAt: { type: 'string', description: 'Comment creation timestamp (backdate)' }, commentId: { type: 'string', description: 'Comment ID' }, @@ -1269,6 +1788,34 @@ workspace-member.created webhookId: { type: 'string', description: 'Webhook ID' }, webhookTargetUrl: { type: 'string', description: 'Webhook target URL' }, webhookSubscriptions: { type: 'json', description: 'Webhook event subscriptions' }, + attributeTarget: { + type: 'string', + description: 'Whether the attribute is on an object or list', + }, + attributeIdentifier: { type: 'string', description: 'The object or list ID or slug' }, + attributeId: { type: 'string', description: 'The attribute ID or slug' }, + attributeTitle: { type: 'string', description: 'The attribute display title' }, + attributeApiSlug: { type: 'string', description: 'The attribute API slug' }, + attributeType: { type: 'string', description: 'The attribute value type' }, + attributeDescription: { type: 'string', description: 'The attribute description' }, + attributeIsRequired: { type: 'string', description: 'Whether the attribute is required' }, + attributeIsRequiredUpdate: { + type: 'string', + description: 'Whether the attribute is required (update)', + }, + attributeIsUnique: { type: 'string', description: 'Whether the attribute is unique' }, + attributeIsUniqueUpdate: { + type: 'string', + description: 'Whether the attribute is unique (update)', + }, + attributeIsMultiselect: { type: 'string', description: 'Whether the attribute is multiselect' }, + attributeIsArchived: { type: 'string', description: 'Whether the attribute is archived' }, + attributeConfig: { type: 'json', description: 'Type-dependent attribute configuration' }, + attributeShowArchived: { + type: 'string', + description: 'Whether to include archived attributes', + }, + taskSort: { type: 'string', description: 'Task list sort order' }, limit: { type: 'string', description: 'Maximum number of results' }, offset: { type: 'string', description: 'Number of results to skip for pagination' }, }, @@ -1289,6 +1836,7 @@ workspace-member.created content: { type: 'string', description: 'Task or note content' }, deadlineAt: { type: 'string', description: 'Task deadline' }, isCompleted: { type: 'boolean', description: 'Task completion status' }, + completedAt: { type: 'string', description: 'When the task was completed' }, linkedRecords: { type: 'json', description: 'Linked records' }, assignees: { type: 'json', description: 'Task assignees' }, objects: { type: 'json', description: 'Array of objects' }, @@ -1322,6 +1870,32 @@ workspace-member.created deleted: { type: 'boolean', description: 'Whether the item was deleted' }, createdAt: { type: 'string', description: 'When the item was created' }, success: { type: 'boolean', description: 'Whether the operation succeeded' }, + attributes: { type: 'json', description: 'Array of attributes' }, + attributeId: { type: 'string', description: 'The attribute ID' }, + description: { type: 'string', description: 'The attribute description' }, + type: { type: 'string', description: 'The attribute value type' }, + isSystemAttribute: { + type: 'boolean', + description: 'Whether this is a built-in system attribute', + }, + isWritable: { type: 'boolean', description: 'Whether the attribute can be written to' }, + isRequired: { type: 'boolean', description: 'Whether the attribute is required' }, + isUnique: { type: 'boolean', description: 'Whether the attribute enforces uniqueness' }, + isMultiselect: { type: 'boolean', description: 'Whether the attribute is multiselect' }, + isDefaultValueEnabled: { + type: 'boolean', + description: 'Whether this attribute has a default value enabled', + }, + isArchived: { type: 'boolean', description: 'Whether the attribute is archived' }, + defaultValue: { + type: 'json', + description: 'The default value for this attribute, if enabled', + }, + relationship: { + type: 'json', + description: 'The related attribute, if this attribute is part of a relationship', + }, + config: { type: 'json', description: 'Type-dependent attribute configuration' }, }, } diff --git a/apps/sim/tools/attio/assert_record.ts b/apps/sim/tools/attio/assert_record.ts index 00864d740a0..71707cdae99 100644 --- a/apps/sim/tools/attio/assert_record.ts +++ b/apps/sim/tools/attio/assert_record.ts @@ -56,11 +56,11 @@ export const attioAssertRecordTool: ToolConfig { - let values: Record = {} + let values: Record try { values = typeof params.values === 'string' ? JSON.parse(params.values) : params.values } catch { - values = {} + throw new Error('Invalid JSON provided for record values') } return { data: { values } } }, diff --git a/apps/sim/tools/attio/create_attribute.ts b/apps/sim/tools/attio/create_attribute.ts new file mode 100644 index 00000000000..fe362358658 --- /dev/null +++ b/apps/sim/tools/attio/create_attribute.ts @@ -0,0 +1,157 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { AttioCreateAttributeParams, AttioCreateAttributeResponse } from './types' +import { ATTRIBUTE_OUTPUT_PROPERTIES } from './types' + +const logger = createLogger('AttioCreateAttribute') + +export const attioCreateAttributeTool: ToolConfig< + AttioCreateAttributeParams, + AttioCreateAttributeResponse +> = { + id: 'attio_create_attribute', + name: 'Attio Create Attribute', + description: 'Create a new attribute (schema field) on an Attio object or 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', + }, + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether to create the attribute on an object or a list: objects or lists', + }, + identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object or list ID or slug (e.g. people, companies)', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The attribute display title', + }, + apiSlug: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The attribute API slug (unique, snake_case)', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The attribute value type (e.g. text, number, checkbox, currency, date, timestamp, rating, status, select, record-reference, actor-reference, location, domain, email-address, phone-number)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'A description of the attribute', + }, + isRequired: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether new records must provide a value (default false)', + }, + isUnique: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the attribute enforces uniqueness on new data (default false)', + }, + isMultiselect: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the attribute supports multiple values (default false)', + }, + config: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object of type-dependent configuration (e.g. currency or record-reference settings)', + }, + }, + + request: { + url: (params) => + `https://api.attio.com/v2/${params.target.trim()}/${params.identifier.trim()}/attributes`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const data: Record = { + title: params.title, + api_slug: params.apiSlug, + description: params.description ?? null, + type: params.type, + is_required: params.isRequired ?? false, + is_unique: params.isUnique ?? false, + is_multiselect: params.isMultiselect ?? false, + // `config` is a required key on Attio's create-attribute request body (even though its + // nested fields are only required for type-dependent configs like currency/record-reference). + config: {}, + } + if (params.config) { + try { + data.config = + typeof params.config === 'string' ? JSON.parse(params.config) : params.config + } catch { + throw new Error('Invalid JSON provided for attribute config') + } + } + 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 attribute') + } + const attr = data.data + return { + success: true, + output: { + attributeId: attr.id?.attribute_id ?? null, + title: attr.title ?? null, + apiSlug: attr.api_slug ?? null, + description: attr.description ?? null, + type: attr.type ?? null, + isSystemAttribute: attr.is_system_attribute ?? false, + isWritable: attr.is_writable ?? false, + isRequired: attr.is_required ?? false, + isUnique: attr.is_unique ?? false, + isMultiselect: attr.is_multiselect ?? false, + isDefaultValueEnabled: attr.is_default_value_enabled ?? false, + isArchived: attr.is_archived ?? false, + defaultValue: attr.default_value ?? null, + relationship: attr.relationship ?? null, + config: attr.config ?? null, + createdAt: attr.created_at ?? null, + }, + } + }, + + outputs: ATTRIBUTE_OUTPUT_PROPERTIES, +} diff --git a/apps/sim/tools/attio/create_comment.ts b/apps/sim/tools/attio/create_comment.ts index e9fe596ed9a..b7fec326125 100644 --- a/apps/sim/tools/attio/create_comment.ts +++ b/apps/sim/tools/attio/create_comment.ts @@ -52,21 +52,37 @@ export const attioCreateCommentTool: ToolConfig< }, list: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'The list ID or slug the entry belongs to', + description: + 'The list ID or slug the entry belongs to (used with entryId; omit if threadId or recordId is set)', }, entryId: { type: 'string', - required: true, + required: false, + visibility: 'user-or-llm', + description: + 'The list entry ID to comment on (used with list; omit if threadId or recordId is set)', + }, + recordObject: { + type: 'string', + required: false, visibility: 'user-or-llm', - description: 'The entry ID to comment on', + description: + 'The object ID or slug the record belongs to (used with recordId; omit if threadId or entryId is set)', + }, + recordId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The record ID to comment on directly (used with recordObject; omit if threadId or entryId is set)', }, threadId: { type: 'string', required: false, visibility: 'user-or-llm', - description: 'Thread ID to reply to (omit to start a new thread)', + description: 'Thread ID to reply to (omit to start a new thread on a record or list entry)', }, createdAt: { type: 'string', @@ -91,12 +107,25 @@ export const attioCreateCommentTool: ToolConfig< type: params.authorType, id: params.authorId, }, - entry: { + } + // Attio's comment body accepts exactly one of `thread_id`, `record`, or `entry` — mutually exclusive. + if (params.threadId) { + data.thread_id = params.threadId + } else if (params.recordObject && params.recordId) { + data.record = { + object: params.recordObject, + record_id: params.recordId, + } + } else if (params.list && params.entryId) { + data.entry = { list: params.list, entry_id: params.entryId, - }, + } + } else { + throw new Error( + 'Must provide either threadId, both recordObject and recordId, or both list and entryId' + ) } - if (params.threadId) data.thread_id = params.threadId if (params.createdAt) data.created_at = params.createdAt return { data } }, diff --git a/apps/sim/tools/attio/create_note.ts b/apps/sim/tools/attio/create_note.ts index 3124959a92f..792366db663 100644 --- a/apps/sim/tools/attio/create_note.ts +++ b/apps/sim/tools/attio/create_note.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { AttioCreateNoteParams, AttioCreateNoteResponse } from './types' -import { NOTE_OUTPUT_PROPERTIES } from './types' +import { mapNoteTags, NOTE_OUTPUT_PROPERTIES } from './types' const logger = createLogger('AttioCreateNote') @@ -105,7 +105,7 @@ export const attioCreateNoteTool: ToolConfig = + { + id: 'attio_get_attribute', + name: 'Attio Get Attribute', + description: 'Get a single attribute (schema field) on an Attio object or 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', + }, + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether the attribute belongs to an object or a list: objects or lists', + }, + identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object or list ID or slug (e.g. people, companies)', + }, + attribute: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The attribute ID or slug', + }, + }, + + request: { + url: (params) => + `https://api.attio.com/v2/${params.target.trim()}/${params.identifier.trim()}/attributes/${params.attribute.trim()}`, + 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 attribute') + } + const attr = data.data + return { + success: true, + output: { + attributeId: attr.id?.attribute_id ?? null, + title: attr.title ?? null, + apiSlug: attr.api_slug ?? null, + description: attr.description ?? null, + type: attr.type ?? null, + isSystemAttribute: attr.is_system_attribute ?? false, + isWritable: attr.is_writable ?? false, + isRequired: attr.is_required ?? false, + isUnique: attr.is_unique ?? false, + isMultiselect: attr.is_multiselect ?? false, + isDefaultValueEnabled: attr.is_default_value_enabled ?? false, + isArchived: attr.is_archived ?? false, + defaultValue: attr.default_value ?? null, + relationship: attr.relationship ?? null, + config: attr.config ?? null, + createdAt: attr.created_at ?? null, + }, + } + }, + + outputs: ATTRIBUTE_OUTPUT_PROPERTIES, + } diff --git a/apps/sim/tools/attio/get_note.ts b/apps/sim/tools/attio/get_note.ts index 0a9ad4a8e76..b253e975bc0 100644 --- a/apps/sim/tools/attio/get_note.ts +++ b/apps/sim/tools/attio/get_note.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { AttioGetNoteParams, AttioGetNoteResponse } from './types' -import { NOTE_OUTPUT_PROPERTIES } from './types' +import { mapNoteTags, NOTE_OUTPUT_PROPERTIES } from './types' const logger = createLogger('AttioGetNote') @@ -56,7 +56,7 @@ export const attioGetNoteTool: ToolConfig ({ Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', }), }, @@ -66,6 +65,7 @@ export const attioGetTaskTool: ToolConfig = { + id: 'attio_list_attributes', + name: 'Attio List Attributes', + description: 'List the attributes (schema fields) defined on an Attio object or 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', + }, + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether the attributes belong to an object or a list: objects or lists', + }, + identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object or list ID or slug (e.g. people, companies)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of attributes to return', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of attributes to skip for pagination', + }, + showArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include archived attributes (default false)', + }, + }, + + request: { + url: (params) => { + const searchParams = new URLSearchParams() + if (params.limit != null) searchParams.set('limit', String(params.limit)) + if (params.offset != null) searchParams.set('offset', String(params.offset)) + if (params.showArchived != null) + searchParams.set('show_archived', String(params.showArchived)) + const qs = searchParams.toString() + return `https://api.attio.com/v2/${params.target.trim()}/${params.identifier.trim()}/attributes${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 attributes') + } + const attributes = (data.data ?? []).map((attr: Record) => { + const id = attr.id as { attribute_id?: string } | undefined + return { + attributeId: id?.attribute_id ?? null, + title: (attr.title as string) ?? null, + apiSlug: (attr.api_slug as string) ?? null, + description: (attr.description as string) ?? null, + type: (attr.type as string) ?? null, + isSystemAttribute: (attr.is_system_attribute as boolean) ?? false, + isWritable: (attr.is_writable as boolean) ?? false, + isRequired: (attr.is_required as boolean) ?? false, + isUnique: (attr.is_unique as boolean) ?? false, + isMultiselect: (attr.is_multiselect as boolean) ?? false, + isDefaultValueEnabled: (attr.is_default_value_enabled as boolean) ?? false, + isArchived: (attr.is_archived as boolean) ?? false, + defaultValue: (attr.default_value as Record) ?? null, + relationship: (attr.relationship as Record) ?? null, + config: (attr.config as Record) ?? null, + createdAt: (attr.created_at as string) ?? null, + } + }) + return { + success: true, + output: { + attributes, + count: attributes.length, + }, + } + }, + + outputs: { + attributes: { + type: 'array', + description: 'Array of attributes', + items: { + type: 'object', + properties: ATTRIBUTE_OUTPUT_PROPERTIES, + }, + }, + count: { type: 'number', description: 'Number of attributes returned' }, + }, +} diff --git a/apps/sim/tools/attio/list_notes.ts b/apps/sim/tools/attio/list_notes.ts index fe7230752d8..ffd013cfec3 100644 --- a/apps/sim/tools/attio/list_notes.ts +++ b/apps/sim/tools/attio/list_notes.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { AttioListNotesParams, AttioListNotesResponse } from './types' -import { NOTE_OUTPUT_PROPERTIES } from './types' +import { mapNoteTags, NOTE_OUTPUT_PROPERTIES } from './types' const logger = createLogger('AttioListNotes') @@ -81,7 +81,7 @@ export const attioListNotesTool: ToolConfig ({ + type: tag.type ?? null, + workspaceMemberId: tag.workspace_member_id ?? null, + object: tag.object ?? null, + recordId: tag.record_id ?? null, + })) +} + /** Reusable actor shape returned by the Attio API */ export const ACTOR_OUTPUT_PROPERTIES = { type: { @@ -70,8 +96,22 @@ export const NOTE_OUTPUT_PROPERTIES = { items: { type: 'object', properties: { - type: { type: 'string', description: 'The tag type (e.g. workspace-member)' }, - workspaceMemberId: { type: 'string', description: 'The workspace member ID of the tagger' }, + type: { type: 'string', description: 'The tag type (workspace-member or record)' }, + workspaceMemberId: { + type: 'string', + description: 'The workspace member ID (present when type is workspace-member)', + optional: true, + }, + object: { + type: 'string', + description: 'The tagged object slug (present when type is record)', + optional: true, + }, + recordId: { + type: 'string', + description: 'The tagged record ID (present when type is record)', + optional: true, + }, }, }, }, @@ -89,6 +129,7 @@ export const TASK_OUTPUT_PROPERTIES = { content: { type: 'string', description: 'The task content' }, deadlineAt: { type: 'string', description: 'The task deadline', optional: true }, isCompleted: { type: 'boolean', description: 'Whether the task is completed' }, + completedAt: { type: 'string', description: 'When the task was completed', optional: true }, linkedRecords: { type: 'array', description: 'Records linked to this task', @@ -162,6 +203,43 @@ export const MEMBER_OUTPUT_PROPERTIES = { createdAt: { type: 'string', description: 'When the member was added' }, } as const satisfies Record +/** Shared output properties for Attio attributes */ +export const ATTRIBUTE_OUTPUT_PROPERTIES = { + attributeId: { type: 'string', description: 'The attribute ID' }, + title: { type: 'string', description: 'The attribute display title' }, + apiSlug: { type: 'string', description: 'The attribute API slug' }, + description: { type: 'string', description: 'The attribute description', optional: true }, + type: { + type: 'string', + description: 'The attribute value type (e.g. text, number, select, record-reference)', + }, + isSystemAttribute: { + type: 'boolean', + description: 'Whether this is a built-in system attribute', + }, + isWritable: { type: 'boolean', description: 'Whether the attribute can be written to' }, + isRequired: { type: 'boolean', description: 'Whether new records must provide a value' }, + isUnique: { type: 'boolean', description: 'Whether the attribute enforces uniqueness' }, + isMultiselect: { type: 'boolean', description: 'Whether the attribute supports multiple values' }, + isDefaultValueEnabled: { + type: 'boolean', + description: 'Whether this attribute has a default value enabled', + }, + isArchived: { type: 'boolean', description: 'Whether the attribute is archived' }, + defaultValue: { + type: 'json', + description: 'The default value for this attribute, if enabled', + optional: true, + }, + relationship: { + type: 'json', + description: 'The related attribute, if this attribute is part of a relationship', + optional: true, + }, + config: { type: 'json', description: 'Type-dependent attribute configuration', optional: true }, + createdAt: { type: 'string', description: 'When the attribute was created' }, +} as const satisfies Record + /** Shared output properties for Attio comments */ export const COMMENT_OUTPUT_PROPERTIES = { commentId: { type: 'string', description: 'The comment ID' }, @@ -248,32 +326,6 @@ interface AttioRecord { values: Record } -/** Raw Attio note shape from the API */ -interface AttioNote { - id: { workspace_id: string; note_id: string } - parent_object: string - parent_record_id: string - title: string - content_plaintext: string - content_markdown: string - meeting_id: string | null - tags: unknown[] - created_by_actor: unknown - created_at: string -} - -/** Raw Attio task shape from the API */ -interface AttioTask { - id: { workspace_id: string; task_id: string } - content_plaintext: string - deadline_at: string | null - is_completed: boolean - linked_records: Array<{ target_object_id: string; target_record_id: string }> - assignees: Array<{ referenced_actor_type: string; referenced_actor_id: string }> - created_by_actor: unknown - created_at: string -} - /** Params for listing/querying records */ export interface AttioListRecordsParams { accessToken: string @@ -453,7 +505,7 @@ export interface AttioListNotesResponse extends ToolResponse { contentPlaintext: string | null contentMarkdown: string | null meetingId: string | null - tags: unknown[] + tags: NoteTagOutput[] createdByActor: unknown createdAt: string | null }> @@ -471,7 +523,7 @@ export interface AttioCreateNoteResponse extends ToolResponse { contentPlaintext: string | null contentMarkdown: string | null meetingId: string | null - tags: unknown[] + tags: NoteTagOutput[] createdByActor: unknown createdAt: string | null } @@ -492,6 +544,7 @@ export interface AttioListTasksResponse extends ToolResponse { content: string | null deadlineAt: string | null isCompleted: boolean + completedAt: string | null linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }> assignees: Array<{ type: string | null; id: string | null }> createdByActor: unknown @@ -508,6 +561,7 @@ export interface AttioCreateTaskResponse extends ToolResponse { content: string | null deadlineAt: string | null isCompleted: boolean + completedAt: string | null linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }> assignees: Array<{ type: string | null; id: string | null }> createdByActor: unknown @@ -522,6 +576,7 @@ export interface AttioUpdateTaskResponse extends ToolResponse { content: string | null deadlineAt: string | null isCompleted: boolean + completedAt: string | null linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }> assignees: Array<{ type: string | null; id: string | null }> createdByActor: unknown @@ -542,6 +597,7 @@ export interface AttioGetTaskResponse extends ToolResponse { content: string | null deadlineAt: string | null isCompleted: boolean + completedAt: string | null linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }> assignees: Array<{ type: string | null; id: string | null }> createdByActor: unknown @@ -572,7 +628,7 @@ export interface AttioGetNoteResponse extends ToolResponse { contentPlaintext: string | null contentMarkdown: string | null meetingId: string | null - tags: unknown[] + tags: NoteTagOutput[] createdByActor: unknown createdAt: string | null } @@ -684,7 +740,7 @@ export interface AttioListListsResponse extends ToolResponse { name: string | null parentObject: string | null workspaceAccess: string | null - workspaceMemberAccess: string | null + workspaceMemberAccess: unknown createdByActor: { type: string | null; id: string | null } | null createdAt: string | null }> @@ -706,7 +762,7 @@ export interface AttioGetListResponse extends ToolResponse { name: string | null parentObject: string | null workspaceAccess: string | null - workspaceMemberAccess: string | null + workspaceMemberAccess: unknown createdByActor: { type: string | null; id: string | null } | null createdAt: string | null } @@ -730,7 +786,7 @@ export interface AttioCreateListResponse extends ToolResponse { name: string | null parentObject: string | null workspaceAccess: string | null - workspaceMemberAccess: string | null + workspaceMemberAccess: unknown createdByActor: { type: string | null; id: string | null } | null createdAt: string | null } @@ -754,7 +810,7 @@ export interface AttioUpdateListResponse extends ToolResponse { name: string | null parentObject: string | null workspaceAccess: string | null - workspaceMemberAccess: string | null + workspaceMemberAccess: unknown createdByActor: { type: string | null; id: string | null } | null createdAt: string | null } @@ -906,8 +962,10 @@ export interface AttioCreateCommentParams { format?: string authorType: string authorId: string - list: string - entryId: string + list?: string + entryId?: string + recordObject?: string + recordId?: string threadId?: string createdAt?: string } @@ -1098,6 +1156,97 @@ export interface AttioDeleteWebhookResponse extends ToolResponse { } } +/** Params for listing attributes on an object or list */ +export interface AttioListAttributesParams { + accessToken: string + target: string + identifier: string + limit?: number + offset?: number + showArchived?: boolean +} + +/** Attribute shape as returned in tool outputs (camelCase) */ +interface AttioAttributeOutput { + attributeId: string | null + title: string | null + apiSlug: string | null + description: string | null + type: string | null + isSystemAttribute: boolean + isWritable: boolean + isRequired: boolean + isUnique: boolean + isMultiselect: boolean + isDefaultValueEnabled: boolean + isArchived: boolean + defaultValue: Record | null + relationship: Record | null + config: Record | null + createdAt: string | null +} + +/** Response for listing attributes */ +export interface AttioListAttributesResponse extends ToolResponse { + output: { + attributes: AttioAttributeOutput[] + count: number + } +} + +/** Params for getting a single attribute */ +export interface AttioGetAttributeParams { + accessToken: string + target: string + identifier: string + attribute: string +} + +/** Response for getting a single attribute */ +export interface AttioGetAttributeResponse extends ToolResponse { + output: AttioAttributeOutput +} + +/** Params for creating an attribute */ +export interface AttioCreateAttributeParams { + accessToken: string + target: string + identifier: string + title: string + apiSlug: string + type: string + description?: string + isRequired?: boolean + isUnique?: boolean + isMultiselect?: boolean + config?: string +} + +/** Response for creating an attribute */ +export interface AttioCreateAttributeResponse extends ToolResponse { + output: AttioAttributeOutput +} + +/** Params for updating an attribute */ +export interface AttioUpdateAttributeParams { + accessToken: string + target: string + identifier: string + attribute: string + title?: string + apiSlug?: string + description?: string + isRequired?: boolean + isUnique?: boolean + isArchived?: boolean + config?: string +} + +/** Response for updating an attribute */ +export interface AttioUpdateAttributeResponse extends ToolResponse { + output: AttioAttributeOutput +} + export type AttioResponse = | AttioListRecordsResponse | AttioGetRecordResponse @@ -1140,3 +1289,7 @@ export type AttioResponse = | AttioCreateWebhookResponse | AttioUpdateWebhookResponse | AttioDeleteWebhookResponse + | AttioListAttributesResponse + | AttioGetAttributeResponse + | AttioCreateAttributeResponse + | AttioUpdateAttributeResponse diff --git a/apps/sim/tools/attio/update_attribute.ts b/apps/sim/tools/attio/update_attribute.ts new file mode 100644 index 00000000000..9cfd9c5d9d5 --- /dev/null +++ b/apps/sim/tools/attio/update_attribute.ts @@ -0,0 +1,150 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { AttioUpdateAttributeParams, AttioUpdateAttributeResponse } from './types' +import { ATTRIBUTE_OUTPUT_PROPERTIES } from './types' + +const logger = createLogger('AttioUpdateAttribute') + +export const attioUpdateAttributeTool: ToolConfig< + AttioUpdateAttributeParams, + AttioUpdateAttributeResponse +> = { + id: 'attio_update_attribute', + name: 'Attio Update Attribute', + description: 'Update an attribute (schema field) on an Attio object or 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', + }, + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether the attribute belongs to an object or a list: objects or lists', + }, + identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object or list ID or slug (e.g. people, companies)', + }, + attribute: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The attribute ID or slug to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New attribute display title', + }, + apiSlug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New attribute API slug', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New attribute description', + }, + isRequired: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether new records must provide a value', + }, + isUnique: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the attribute enforces uniqueness on new data', + }, + isArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Archive or unarchive the attribute', + }, + config: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of type-dependent configuration', + }, + }, + + request: { + url: (params) => + `https://api.attio.com/v2/${params.target.trim()}/${params.identifier.trim()}/attributes/${params.attribute.trim()}`, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const data: Record = {} + if (params.title != null) data.title = params.title + if (params.apiSlug != null) data.api_slug = params.apiSlug + if (params.description != null) data.description = params.description + if (params.isRequired != null) data.is_required = params.isRequired + if (params.isUnique != null) data.is_unique = params.isUnique + if (params.isArchived != null) data.is_archived = params.isArchived + if (params.config) { + try { + data.config = + typeof params.config === 'string' ? JSON.parse(params.config) : params.config + } catch { + throw new Error('Invalid JSON provided for attribute config') + } + } + 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 attribute') + } + const attr = data.data + return { + success: true, + output: { + attributeId: attr.id?.attribute_id ?? null, + title: attr.title ?? null, + apiSlug: attr.api_slug ?? null, + description: attr.description ?? null, + type: attr.type ?? null, + isSystemAttribute: attr.is_system_attribute ?? false, + isWritable: attr.is_writable ?? false, + isRequired: attr.is_required ?? false, + isUnique: attr.is_unique ?? false, + isMultiselect: attr.is_multiselect ?? false, + isDefaultValueEnabled: attr.is_default_value_enabled ?? false, + isArchived: attr.is_archived ?? false, + defaultValue: attr.default_value ?? null, + relationship: attr.relationship ?? null, + config: attr.config ?? null, + createdAt: attr.created_at ?? null, + }, + } + }, + + outputs: ATTRIBUTE_OUTPUT_PROPERTIES, +} diff --git a/apps/sim/tools/attio/update_record.ts b/apps/sim/tools/attio/update_record.ts index 21d613f9292..25548f46287 100644 --- a/apps/sim/tools/attio/update_record.ts +++ b/apps/sim/tools/attio/update_record.ts @@ -57,7 +57,7 @@ export const attioUpdateRecordTool: ToolConfig = { airtable_update_record: airtableUpdateRecordTool, airtable_upsert_records: airtableUpsertRecordsTool, attio_assert_record: attioAssertRecordTool, + attio_create_attribute: attioCreateAttributeTool, attio_create_comment: attioCreateCommentTool, attio_create_list: attioCreateListTool, attio_create_list_entry: attioCreateListEntryTool, @@ -6784,6 +6789,7 @@ export const tools: Record = { attio_delete_record: attioDeleteRecordTool, attio_delete_task: attioDeleteTaskTool, attio_delete_webhook: attioDeleteWebhookTool, + attio_get_attribute: attioGetAttributeTool, attio_get_comment: attioGetCommentTool, attio_get_list: attioGetListTool, attio_get_list_entry: attioGetListEntryTool, @@ -6794,6 +6800,7 @@ export const tools: Record = { attio_get_task: attioGetTaskTool, attio_get_thread: attioGetThreadTool, attio_get_webhook: attioGetWebhookTool, + attio_list_attributes: attioListAttributesTool, attio_list_lists: attioListListsTool, attio_list_members: attioListMembersTool, attio_list_notes: attioListNotesTool, @@ -6804,6 +6811,7 @@ export const tools: Record = { attio_list_webhooks: attioListWebhooksTool, attio_query_list_entries: attioQueryListEntriesTool, attio_search_records: attioSearchRecordsTool, + attio_update_attribute: attioUpdateAttributeTool, attio_update_list: attioUpdateListTool, attio_update_list_entry: attioUpdateListEntryTool, attio_update_object: attioUpdateObjectTool,