From 9d8e15782ca0f835a05a347d0fb0d9bd8778fece Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 14:49:14 -0700 Subject: [PATCH 1/8] feat(attio): add attribute tools + fix API alignment gaps - Add 4 new tools: attio_list_attributes, get_attribute, create_attribute, update_attribute - Add missing completed_at field to task tools - Fix note tags output to match Attio's actual response shape (workspace-member vs record tags) - Fix attribute outputs missing is_default_value_enabled, default_value, relationship - Fix create_comment sending both entry and thread_id (Attio requires exactly one) - Wire attribute pagination + task sort into the block --- apps/sim/blocks/blocks/attio.ts | 270 +++++++++++++++++++++++ apps/sim/tools/attio/create_attribute.ts | 156 +++++++++++++ apps/sim/tools/attio/create_comment.ts | 10 +- apps/sim/tools/attio/create_note.ts | 4 +- apps/sim/tools/attio/create_task.ts | 1 + apps/sim/tools/attio/get_attribute.ts | 87 ++++++++ apps/sim/tools/attio/get_note.ts | 4 +- apps/sim/tools/attio/get_task.ts | 1 + apps/sim/tools/attio/index.ts | 4 + apps/sim/tools/attio/list_attributes.ts | 124 +++++++++++ apps/sim/tools/attio/list_notes.ts | 4 +- apps/sim/tools/attio/list_tasks.ts | 1 + apps/sim/tools/attio/types.ts | 188 +++++++++++++++- apps/sim/tools/attio/update_attribute.ts | 150 +++++++++++++ apps/sim/tools/attio/update_task.ts | 1 + apps/sim/tools/registry.ts | 8 + 16 files changed, 999 insertions(+), 14 deletions(-) create mode 100644 apps/sim/tools/attio/create_attribute.ts create mode 100644 apps/sim/tools/attio/get_attribute.ts create mode 100644 apps/sim/tools/attio/list_attributes.ts create mode 100644 apps/sim/tools/attio/update_attribute.ts diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 3016a905700..6ec0a0bf8f2 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', }, @@ -983,6 +987,180 @@ 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', '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', '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: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + 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 +1178,7 @@ workspace-member.created 'query_list_entries', 'list_threads', 'list_webhooks', + 'list_attributes', ], }, }, @@ -1018,9 +1197,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 +1309,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}`, @@ -1158,6 +1355,7 @@ workspace-member.created if (params.taskFilterAssignee) cleanParams.assignee = params.taskFilterAssignee if (params.taskFilterCompleted && params.taskFilterCompleted !== 'all') cleanParams.isCompleted = params.taskFilterCompleted === 'true' + if (params.taskSort) cleanParams.sort = params.taskSort // Object params if (params.objectIdOrSlug) cleanParams.object = params.objectIdOrSlug @@ -1206,6 +1404,31 @@ workspace-member.created if (params.webhookTargetUrl) cleanParams.targetUrl = params.webhookTargetUrl if (params.webhookSubscriptions) cleanParams.subscriptions = params.webhookSubscriptions + // Attribute params + if (params.attributeTarget) cleanParams.target = params.attributeTarget + if (params.attributeIdentifier) cleanParams.identifier = params.attributeIdentifier + if (params.attributeId) cleanParams.attribute = params.attributeId + if (params.attributeTitle) cleanParams.title = params.attributeTitle + if (params.attributeApiSlug) cleanParams.apiSlug = params.attributeApiSlug + if (params.attributeType) cleanParams.type = params.attributeType + if (params.attributeDescription) cleanParams.description = params.attributeDescription + if (params.attributeIsRequired !== undefined) + cleanParams.isRequired = + params.attributeIsRequired === 'true' || params.attributeIsRequired === true + if (params.attributeIsUnique !== undefined) + cleanParams.isUnique = + params.attributeIsUnique === 'true' || params.attributeIsUnique === true + if (params.attributeIsMultiselect !== undefined) + cleanParams.isMultiselect = + params.attributeIsMultiselect === 'true' || params.attributeIsMultiselect === true + if (params.attributeIsArchived !== undefined) + cleanParams.isArchived = + params.attributeIsArchived === 'true' || params.attributeIsArchived === true + if (params.attributeConfig) cleanParams.config = params.attributeConfig + if (params.attributeShowArchived !== undefined) + cleanParams.showArchived = + params.attributeShowArchived === 'true' || params.attributeShowArchived === true + // Shared params if (params.limit) cleanParams.limit = Number(params.limit) if (params.offset) cleanParams.offset = Number(params.offset) @@ -1269,6 +1492,26 @@ 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' }, + attributeIsUnique: { type: 'string', description: 'Whether the attribute is unique' }, + 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 +1532,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 +1566,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/create_attribute.ts b/apps/sim/tools/attio/create_attribute.ts new file mode 100644 index 00000000000..1af5206c755 --- /dev/null +++ b/apps/sim/tools/attio/create_attribute.ts @@ -0,0 +1,156 @@ +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) => { + let config: Record = {} + if (params.config) { + try { + config = typeof params.config === 'string' ? JSON.parse(params.config) : params.config + } catch { + config = {} + } + } + return { + data: { + 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, + }, + } + }, + }, + + 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..31df20b1444 100644 --- a/apps/sim/tools/attio/create_comment.ts +++ b/apps/sim/tools/attio/create_comment.ts @@ -91,12 +91,16 @@ export const attioCreateCommentTool: ToolConfig< type: params.authorType, id: params.authorId, }, - entry: { + } + // Attio's comment body accepts exactly one of `thread_id` or `entry` — sending both is rejected. + if (params.threadId) { + data.thread_id = params.threadId + } else { + data.entry = { list: params.list, entry_id: params.entryId, - }, + } } - if (params.threadId) data.thread_id = params.threadId if (params.createdAt) data.created_at = params.createdAt return { data } }, 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 = { + 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' }, @@ -268,6 +346,7 @@ interface AttioTask { content_plaintext: string deadline_at: string | null is_completed: boolean + completed_at: string | null 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 @@ -453,7 +532,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 +550,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 +571,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 +588,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 +603,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 +624,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 +655,7 @@ export interface AttioGetNoteResponse extends ToolResponse { contentPlaintext: string | null contentMarkdown: string | null meetingId: string | null - tags: unknown[] + tags: NoteTagOutput[] createdByActor: unknown createdAt: string | null } @@ -1098,6 +1181,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 +1314,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..594f84bcc9c --- /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 { + data.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_task.ts b/apps/sim/tools/attio/update_task.ts index e55394b44ce..66620fa6eb3 100644 --- a/apps/sim/tools/attio/update_task.ts +++ b/apps/sim/tools/attio/update_task.ts @@ -114,6 +114,7 @@ export const attioUpdateTaskTool: 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, From a87e0fa2fa11f9480478615d8666394a9779c58e Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 15:02:30 -0700 Subject: [PATCH 2/8] fix(attio): address review feedback on attribute tools - Throw on invalid config JSON instead of silently overwriting with {} (create_attribute, update_attribute) - create_attribute: omit config field entirely when not provided instead of always sending {} - Add "Leave unchanged" option to Required/Unique dropdowns so update_attribute no longer clears existing constraints on unrelated field updates - Fix stale sort param description on list_tasks (was missing completed_at:asc/desc variants) --- apps/sim/blocks/blocks/attio.ts | 10 +++++---- apps/sim/tools/attio/create_attribute.ts | 28 +++++++++++------------- apps/sim/tools/attio/list_tasks.ts | 3 ++- apps/sim/tools/attio/update_attribute.ts | 2 +- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 6ec0a0bf8f2..9d1ff7efd15 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -1081,10 +1081,11 @@ workspace-member.created title: 'Required', type: 'dropdown', options: [ + { label: 'Leave unchanged', id: 'unchanged' }, { label: 'No', id: 'false' }, { label: 'Yes', id: 'true' }, ], - value: () => 'false', + value: () => 'unchanged', condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] }, mode: 'advanced', }, @@ -1093,10 +1094,11 @@ workspace-member.created title: 'Unique', type: 'dropdown', options: [ + { label: 'Leave unchanged', id: 'unchanged' }, { label: 'No', id: 'false' }, { label: 'Yes', id: 'true' }, ], - value: () => 'false', + value: () => 'unchanged', condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] }, mode: 'advanced', }, @@ -1412,10 +1414,10 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (params.attributeApiSlug) cleanParams.apiSlug = params.attributeApiSlug if (params.attributeType) cleanParams.type = params.attributeType if (params.attributeDescription) cleanParams.description = params.attributeDescription - if (params.attributeIsRequired !== undefined) + if (params.attributeIsRequired !== undefined && params.attributeIsRequired !== 'unchanged') cleanParams.isRequired = params.attributeIsRequired === 'true' || params.attributeIsRequired === true - if (params.attributeIsUnique !== undefined) + if (params.attributeIsUnique !== undefined && params.attributeIsUnique !== 'unchanged') cleanParams.isUnique = params.attributeIsUnique === 'true' || params.attributeIsUnique === true if (params.attributeIsMultiselect !== undefined) diff --git a/apps/sim/tools/attio/create_attribute.ts b/apps/sim/tools/attio/create_attribute.ts index 1af5206c755..6d96ebf1a5a 100644 --- a/apps/sim/tools/attio/create_attribute.ts +++ b/apps/sim/tools/attio/create_attribute.ts @@ -99,26 +99,24 @@ export const attioCreateAttributeTool: ToolConfig< 'Content-Type': 'application/json', }), body: (params) => { - let config: Record = {} + 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, + } if (params.config) { try { - config = typeof params.config === 'string' ? JSON.parse(params.config) : params.config + data.config = + typeof params.config === 'string' ? JSON.parse(params.config) : params.config } catch { - config = {} + throw new Error('Invalid JSON provided for attribute config') } } - return { - data: { - 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, - }, - } + return { data } }, }, diff --git a/apps/sim/tools/attio/list_tasks.ts b/apps/sim/tools/attio/list_tasks.ts index 83bcdcf8a23..a5ea1294c72 100644 --- a/apps/sim/tools/attio/list_tasks.ts +++ b/apps/sim/tools/attio/list_tasks.ts @@ -51,7 +51,8 @@ export const attioListTasksTool: ToolConfig Date: Mon, 6 Jul 2026 15:26:30 -0700 Subject: [PATCH 3/8] fix(attio): fix silent update-clobber bugs + complete comment mutual-exclusion - create_comment: support all 3 of Attio's mutually-exclusive comment targets (thread_id, record, entry), not just thread_id/entry - Fix update_task silently clearing is_completed on unrelated field updates (taskIsCompletedUpdate now defaults to "leave unchanged") - Fix update_list silently resetting workspace_access to full-access on unrelated field updates (listWorkspaceAccessUpdate, same pattern) - create_attribute: restore config:{} as always-required per Attio's schema (previously omitted it entirely, which is invalid on create) - create_record/update_record/assert_record/list_records: throw on invalid values/filter/sorts JSON instead of silently substituting {} (was a silent data-loss risk on malformed input) - get_task: drop stray Content-Type header on a GET request - types.ts: remove two dead unreferenced interfaces, fix workspaceMemberAccess type (was string, is actually an array) --- apps/sim/blocks/blocks/attio.ts | 68 +++++++++++++++++++++--- apps/sim/tools/attio/assert_record.ts | 4 +- apps/sim/tools/attio/create_attribute.ts | 3 ++ apps/sim/tools/attio/create_comment.ts | 39 +++++++++++--- apps/sim/tools/attio/create_record.ts | 2 +- apps/sim/tools/attio/get_task.ts | 1 - apps/sim/tools/attio/list_records.ts | 4 +- apps/sim/tools/attio/types.ts | 41 +++----------- apps/sim/tools/attio/update_record.ts | 2 +- 9 files changed, 111 insertions(+), 53 deletions(-) diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 9d1ff7efd15..fe7e0ec4ed0 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -434,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', @@ -666,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 @@ -831,17 +856,31 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. id: 'commentList', title: 'List', type: 'short-input', - placeholder: 'List ID or slug', + placeholder: 'List ID or slug (used with Entry ID)', condition: { field: 'operation', value: 'create_comment' }, - required: { field: 'operation', value: 'create_comment' }, }, { id: 'commentEntryId', title: 'Entry ID', type: 'short-input', - placeholder: 'List entry ID to comment on', + placeholder: 'List entry ID to comment on (used with List)', condition: { field: 'operation', value: 'create_comment' }, - required: { field: 'operation', value: 'create_comment' }, + }, + { + id: 'commentRecordObject', + title: 'Record Object', + type: 'short-input', + placeholder: 'Object ID or slug the record belongs to (used with Record ID)', + condition: { field: 'operation', value: 'create_comment' }, + mode: 'advanced', + }, + { + id: 'commentRecordId', + title: 'Record ID', + type: 'short-input', + placeholder: 'Record ID to comment on directly (used with Record Object)', + condition: { field: 'operation', value: 'create_comment' }, + mode: 'advanced', }, { id: 'commentThreadId', @@ -1349,6 +1388,12 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (params.taskIsCompleted !== undefined) cleanParams.isCompleted = params.taskIsCompleted === 'true' || params.taskIsCompleted === true + if ( + params.taskIsCompletedUpdate !== undefined && + params.taskIsCompletedUpdate !== 'unchanged' + ) + cleanParams.isCompleted = + params.taskIsCompletedUpdate === 'true' || params.taskIsCompletedUpdate === true if (params.taskLinkedRecords) cleanParams.linkedRecords = params.taskLinkedRecords if (params.taskAssignees) cleanParams.assignees = params.taskAssignees if (params.taskId) cleanParams.taskId = params.taskId @@ -1371,6 +1416,8 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (params.listParentObject) cleanParams.parentObject = params.listParentObject if (params.listApiSlug) cleanParams.apiSlug = params.listApiSlug if (params.listWorkspaceAccess) cleanParams.workspaceAccess = params.listWorkspaceAccess + if (params.listWorkspaceAccessUpdate && params.listWorkspaceAccessUpdate !== 'unchanged') + cleanParams.workspaceAccess = params.listWorkspaceAccessUpdate // List entry params if (params.entryId) cleanParams.entryId = params.entryId @@ -1390,6 +1437,8 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (params.commentAuthorId) cleanParams.authorId = params.commentAuthorId if (params.commentList) cleanParams.list = params.commentList if (params.commentEntryId) cleanParams.entryId = params.commentEntryId + if (params.commentRecordObject) cleanParams.recordObject = params.commentRecordObject + if (params.commentRecordId) cleanParams.recordId = params.commentRecordId if (params.commentThreadId) cleanParams.threadId = params.commentThreadId if (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt if (params.commentId) cleanParams.commentId = params.commentId @@ -1462,6 +1511,7 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, 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' }, @@ -1474,6 +1524,10 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, 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' }, @@ -1487,6 +1541,8 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, commentAuthorId: { type: 'string', description: 'Comment author ID' }, 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' }, 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 index 6d96ebf1a5a..fe362358658 100644 --- a/apps/sim/tools/attio/create_attribute.ts +++ b/apps/sim/tools/attio/create_attribute.ts @@ -107,6 +107,9 @@ export const attioCreateAttributeTool: ToolConfig< 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 { diff --git a/apps/sim/tools/attio/create_comment.ts b/apps/sim/tools/attio/create_comment.ts index 31df20b1444..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', @@ -92,14 +108,23 @@ export const attioCreateCommentTool: ToolConfig< id: params.authorId, }, } - // Attio's comment body accepts exactly one of `thread_id` or `entry` — sending both is rejected. + // Attio's comment body accepts exactly one of `thread_id`, `record`, or `entry` — mutually exclusive. if (params.threadId) { data.thread_id = params.threadId - } else { + } 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.createdAt) data.created_at = params.createdAt return { data } diff --git a/apps/sim/tools/attio/create_record.ts b/apps/sim/tools/attio/create_record.ts index 4ec2bb13d33..0594371ec0b 100644 --- a/apps/sim/tools/attio/create_record.ts +++ b/apps/sim/tools/attio/create_record.ts @@ -50,7 +50,7 @@ export const attioCreateRecordTool: ToolConfig ({ Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', }), }, diff --git a/apps/sim/tools/attio/list_records.ts b/apps/sim/tools/attio/list_records.ts index 39cfef28349..49b917a0f18 100644 --- a/apps/sim/tools/attio/list_records.ts +++ b/apps/sim/tools/attio/list_records.ts @@ -68,14 +68,14 @@ export const attioListRecordsTool: ToolConfig } -/** 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 - completed_at: string | null - 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 @@ -767,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 }> @@ -789,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 } @@ -813,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 } @@ -837,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 } @@ -989,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 } 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 Date: Mon, 6 Jul 2026 15:55:12 -0700 Subject: [PATCH 4/8] fix(attio): gate operation-specific param mapping on current operation Params like taskIsCompleted/listWorkspaceAccess/attributeIsMultiselect/ attributeIsArchived are only meant for one specific operation, but their mapping wasn't checking params.operation. A stale value persisted in block state from a prior operation selection (e.g. switching the dropdown from create_task to update_task) could leak through and override the "leave unchanged" default on the other operation. --- apps/sim/blocks/blocks/attio.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index fe7e0ec4ed0..1ca130f007d 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -1385,10 +1385,11 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, // Task params if (params.taskContent) cleanParams.content = params.taskContent if (params.taskDeadline) cleanParams.deadlineAt = params.taskDeadline - if (params.taskIsCompleted !== undefined) + if (params.operation === 'create_task' && params.taskIsCompleted !== undefined) cleanParams.isCompleted = params.taskIsCompleted === 'true' || params.taskIsCompleted === true if ( + params.operation === 'update_task' && params.taskIsCompletedUpdate !== undefined && params.taskIsCompletedUpdate !== 'unchanged' ) @@ -1415,8 +1416,13 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, 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 (params.listWorkspaceAccessUpdate && params.listWorkspaceAccessUpdate !== 'unchanged') + if (params.operation === 'create_list' && params.listWorkspaceAccess) + cleanParams.workspaceAccess = params.listWorkspaceAccess + if ( + params.operation === 'update_list' && + params.listWorkspaceAccessUpdate && + params.listWorkspaceAccessUpdate !== 'unchanged' + ) cleanParams.workspaceAccess = params.listWorkspaceAccessUpdate // List entry params @@ -1469,10 +1475,10 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (params.attributeIsUnique !== undefined && params.attributeIsUnique !== 'unchanged') cleanParams.isUnique = params.attributeIsUnique === 'true' || params.attributeIsUnique === true - if (params.attributeIsMultiselect !== undefined) + if (params.operation === 'create_attribute' && params.attributeIsMultiselect !== undefined) cleanParams.isMultiselect = params.attributeIsMultiselect === 'true' || params.attributeIsMultiselect === true - if (params.attributeIsArchived !== undefined) + if (params.operation === 'update_attribute' && params.attributeIsArchived !== undefined) cleanParams.isArchived = params.attributeIsArchived === 'true' || params.attributeIsArchived === true if (params.attributeConfig) cleanParams.config = params.attributeConfig From 7b23b14705cf860c74d0dc1accb6f08003e782e6 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 16:16:08 -0700 Subject: [PATCH 5/8] fix(attio): explicit comment target selector + attribute archived tri-state - Add "Leave unchanged" default to attributeIsArchived dropdown, same pattern as isRequired/isUnique, so a stale archived flag from an earlier edit can't unarchive an attribute on an unrelated update - create_comment: replace field-presence inference (which let stale record fields hijack a list-entry comment or vice versa) with an explicit commentTarget selector (List Entry / Record / Reply to Thread). Only the fields for the selected target are ever forwarded --- apps/sim/blocks/blocks/attio.ts | 103 ++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 1ca130f007d..966e125eac2 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -852,43 +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 (used with Entry ID)', - condition: { field: 'operation', value: 'create_comment' }, + placeholder: 'List ID or slug', + 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 (used with List)', - condition: { field: 'operation', value: 'create_comment' }, + placeholder: 'List entry ID to comment on', + 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 (used with Record ID)', - condition: { field: 'operation', value: 'create_comment' }, - mode: 'advanced', + 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 (used with Record Object)', - condition: { field: 'operation', value: 'create_comment' }, - mode: 'advanced', + 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', @@ -1158,9 +1212,11 @@ workspace-member.created 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', }, @@ -1441,11 +1497,15 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, 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.commentRecordObject) cleanParams.recordObject = params.commentRecordObject - if (params.commentRecordId) cleanParams.recordId = params.commentRecordId - if (params.commentThreadId) cleanParams.threadId = params.commentThreadId + if (params.commentTarget === 'entry') { + if (params.commentList) cleanParams.list = params.commentList + if (params.commentEntryId) cleanParams.entryId = params.commentEntryId + } else if (params.commentTarget === 'record') { + if (params.commentRecordObject) cleanParams.recordObject = params.commentRecordObject + if (params.commentRecordId) cleanParams.recordId = params.commentRecordId + } else if (params.commentTarget === 'thread') { + if (params.commentThreadId) cleanParams.threadId = params.commentThreadId + } if (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt if (params.commentId) cleanParams.commentId = params.commentId @@ -1478,7 +1538,11 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (params.operation === 'create_attribute' && params.attributeIsMultiselect !== undefined) cleanParams.isMultiselect = params.attributeIsMultiselect === 'true' || params.attributeIsMultiselect === true - if (params.operation === 'update_attribute' && params.attributeIsArchived !== undefined) + if ( + params.operation === 'update_attribute' && + params.attributeIsArchived !== undefined && + params.attributeIsArchived !== 'unchanged' + ) cleanParams.isArchived = params.attributeIsArchived === 'true' || params.attributeIsArchived === true if (params.attributeConfig) cleanParams.config = params.attributeConfig @@ -1545,6 +1609,7 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, 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' }, From c0fe19bdb07c7018a12ab50b8df914e8ef082746 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 16:28:42 -0700 Subject: [PATCH 6/8] fix(attio): gate remaining unscoped param mappings + comment-target back-compat - taskFilterCompleted (list_tasks filter) was mapped to isCompleted for every operation; gate it to list_tasks so it can't override the isCompleted value on an unrelated update_task call - Generic threadId (get_thread) was unconditionally forwarded and create_comment prioritizes thread_id first, so a leftover threadId from configuring get_thread could silently hijack a comment meant for a list entry or record; gate it to get_thread - Default commentTarget to the pre-existing behavior (entry, or thread if commentThreadId is set) when absent, so blocks saved before this field existed keep working instead of throwing --- apps/sim/blocks/blocks/attio.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 966e125eac2..d17351cec13 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -1457,7 +1457,11 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, 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 ( + params.operation === 'list_tasks' && + params.taskFilterCompleted && + params.taskFilterCompleted !== 'all' + ) cleanParams.isCompleted = params.taskFilterCompleted === 'true' if (params.taskSort) cleanParams.sort = params.taskSort @@ -1497,20 +1501,24 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (params.commentFormat) cleanParams.format = params.commentFormat if (params.commentAuthorType) cleanParams.authorType = params.commentAuthorType if (params.commentAuthorId) cleanParams.authorId = params.commentAuthorId - if (params.commentTarget === 'entry') { + // 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 (params.commentTarget === 'record') { + } else if (commentTarget === 'record') { if (params.commentRecordObject) cleanParams.recordObject = params.commentRecordObject if (params.commentRecordId) cleanParams.recordId = params.commentRecordId - } else if (params.commentTarget === 'thread') { + } else if (commentTarget === 'thread') { if (params.commentThreadId) cleanParams.threadId = params.commentThreadId } if (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt if (params.commentId) cleanParams.commentId = params.commentId // Thread params - if (params.threadId) cleanParams.threadId = params.threadId + if (params.operation === 'get_thread' && 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 From ba5132c14e9fcae82a5456a5692cc076a8965381 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 16:44:39 -0700 Subject: [PATCH 7/8] fix(attio): gate every param mapping by its operation, eliminate stale-value class of bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The params() function reuses cleanParams keys (title, content, apiSlug, filter, sorts, recordId, entryId, list, object, threadId, ...) across several unrelated operation families. Every mapping was unconditional on presence alone, so a stale value left in block state from a previously selected operation could silently leak into an unrelated request and overwrite the intended value (last-write-wins on a shared key). Rewrote the whole function to gate every line by params.operation against the exact operation set its subBlock's condition exposes it under, closing this entire bug class in one pass instead of patching individual instances as they were found in review. Also split attributeIsRequired/attributeIsUnique into create-only (default false) and update-only (tri-state, default "leave unchanged") variants — the shared field let an explicit Yes/No choice from create_attribute carry over and silently change constraints on an unrelated update_attribute call. --- apps/sim/blocks/blocks/attio.ts | 331 +++++++++++++++++++++++--------- 1 file changed, 241 insertions(+), 90 deletions(-) diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index d17351cec13..6adc6a1f070 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -1173,26 +1173,50 @@ workspace-member.created 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: ['create_attribute', 'update_attribute'] }, + 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: ['create_attribute', 'update_attribute'] }, + condition: { field: 'operation', value: 'update_attribute' }, mode: 'advanced', }, { @@ -1417,150 +1441,269 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, const cleanParams: Record = { oauthCredential: params.oauthCredential, } + const op = params.operation - // 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 + // 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.operation === 'create_task' && 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.operation === 'update_task' && + op === 'update_task' && params.taskIsCompletedUpdate !== undefined && params.taskIsCompletedUpdate !== 'unchanged' ) cleanParams.isCompleted = params.taskIsCompletedUpdate === 'true' || params.taskIsCompletedUpdate === 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 (['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 ( - params.operation === 'list_tasks' && + op === 'list_tasks' && params.taskFilterCompleted && params.taskFilterCompleted !== 'all' ) cleanParams.isCompleted = params.taskFilterCompleted === 'true' - if (params.taskSort) cleanParams.sort = params.taskSort + 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.operation === 'create_list' && 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 ( - params.operation === 'update_list' && + op === 'update_list' && params.listWorkspaceAccessUpdate && params.listWorkspaceAccessUpdate !== 'unchanged' ) cleanParams.workspaceAccess = params.listWorkspaceAccessUpdate // 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 - // 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 (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 (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt - if (params.commentId) cleanParams.commentId = params.commentId + if (['get_comment', 'delete_comment'].includes(op) && params.commentId) + cleanParams.commentId = params.commentId // Thread params - if (params.operation === 'get_thread' && 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 + 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 - if (params.attributeTarget) cleanParams.target = params.attributeTarget - if (params.attributeIdentifier) cleanParams.identifier = params.attributeIdentifier - if (params.attributeId) cleanParams.attribute = params.attributeId - if (params.attributeTitle) cleanParams.title = params.attributeTitle - if (params.attributeApiSlug) cleanParams.apiSlug = params.attributeApiSlug - if (params.attributeType) cleanParams.type = params.attributeType - if (params.attributeDescription) cleanParams.description = params.attributeDescription - if (params.attributeIsRequired !== undefined && params.attributeIsRequired !== 'unchanged') + 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 (params.attributeIsUnique !== undefined && params.attributeIsUnique !== 'unchanged') + 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 (params.operation === 'create_attribute' && params.attributeIsMultiselect !== undefined) + 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 ( - params.operation === 'update_attribute' && + op === 'update_attribute' && params.attributeIsArchived !== undefined && params.attributeIsArchived !== 'unchanged' ) cleanParams.isArchived = params.attributeIsArchived === 'true' || params.attributeIsArchived === true - if (params.attributeConfig) cleanParams.config = params.attributeConfig - if (params.attributeShowArchived !== undefined) + 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 params - if (params.limit) cleanParams.limit = Number(params.limit) - if (params.offset) cleanParams.offset = Number(params.offset) + // 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 }, @@ -1640,7 +1783,15 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, 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' }, From 43f16625d7b7a08429f14c28608b839a715efda7 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 6 Jul 2026 16:55:05 -0700 Subject: [PATCH 8/8] fix(attio): preserve legacy taskIsCompleted/listWorkspaceAccess on update taskIsCompleted and listWorkspaceAccess pre-date this PR as fields shared between create and update operations (confirmed present on origin/staging). Splitting them into create/update variants earlier this session meant existing saved blocks with a value stored under the legacy field name would silently stop applying it on update_task / update_list once the new -Update field (always undefined for old blocks) took over. Fall back to the legacy field when the new field is untouched, so old saved workflows keep behaving exactly as before. --- apps/sim/blocks/blocks/attio.ts | 42 +++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 6adc6a1f070..e991d1a5dee 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -1491,13 +1491,23 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, if (op === 'create_task' && params.taskIsCompleted !== undefined) cleanParams.isCompleted = params.taskIsCompleted === 'true' || params.taskIsCompleted === true - if ( - op === 'update_task' && - params.taskIsCompletedUpdate !== undefined && - params.taskIsCompletedUpdate !== 'unchanged' - ) - cleanParams.isCompleted = - params.taskIsCompletedUpdate === 'true' || params.taskIsCompletedUpdate === true + 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) @@ -1550,12 +1560,18 @@ Record reference: {"allowed_objects": ["people", "companies"]}`, cleanParams.apiSlug = params.listApiSlug if (op === 'create_list' && params.listWorkspaceAccess) cleanParams.workspaceAccess = params.listWorkspaceAccess - if ( - op === 'update_list' && - params.listWorkspaceAccessUpdate && - params.listWorkspaceAccessUpdate !== 'unchanged' - ) - cleanParams.workspaceAccess = params.listWorkspaceAccessUpdate + 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 (