feat(attio): add attribute tools + fix API alignment gaps (#5445)

* 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

* 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)

* 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)

* 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.

* 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

* 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

* fix(attio): gate every param mapping by its operation, eliminate stale-value class of bugs

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.

* 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.
This commit is contained in:
Waleed
2026-07-06 17:01:15 -07:00
committed by GitHub
parent a50b373e9f
commit 586b97fd17
20 changed files with 1421 additions and 131 deletions
+646 -72
View File
@@ -64,6 +64,10 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
{ label: 'Create Webhook', id: 'create_webhook' },
{ label: 'Update Webhook', id: 'update_webhook' },
{ label: 'Delete Webhook', id: 'delete_webhook' },
{ label: 'List Attributes', id: 'list_attributes' },
{ label: 'Get Attribute', id: 'get_attribute' },
{ label: 'Create Attribute', id: 'create_attribute' },
{ label: 'Update Attribute', id: 'update_attribute' },
],
value: () => 'list_records',
},
@@ -430,7 +434,19 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
condition: { field: 'operation', value: 'create_task' },
},
{
id: 'taskIsCompletedUpdate',
title: 'Completed',
type: 'dropdown',
options: [
{ label: 'Leave unchanged', id: 'unchanged' },
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'unchanged',
condition: { field: 'operation', value: 'update_task' },
},
{
id: 'taskLinkedRecords',
@@ -662,7 +678,20 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
{ label: 'Read Only', id: 'read-only' },
],
value: () => 'full-access',
condition: { field: 'operation', value: ['create_list', 'update_list'] },
condition: { field: 'operation', value: 'create_list' },
},
{
id: 'listWorkspaceAccessUpdate',
title: 'Workspace Access',
type: 'dropdown',
options: [
{ label: 'Leave unchanged', id: 'unchanged' },
{ label: 'Full Access', id: 'full-access' },
{ label: 'Read & Write', id: 'read-and-write' },
{ label: 'Read Only', id: 'read-only' },
],
value: () => 'unchanged',
condition: { field: 'operation', value: 'update_list' },
},
// List entry fields
@@ -823,29 +852,97 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
condition: { field: 'operation', value: 'create_comment' },
required: { field: 'operation', value: 'create_comment' },
},
{
id: 'commentTarget',
title: 'Comment On',
type: 'dropdown',
options: [
{ label: 'List Entry', id: 'entry' },
{ label: 'Record', id: 'record' },
{ label: 'Reply to Thread', id: 'thread' },
],
value: () => 'entry',
condition: { field: 'operation', value: 'create_comment' },
},
{
id: 'commentList',
title: 'List',
type: 'short-input',
placeholder: 'List ID or slug',
condition: { field: 'operation', value: 'create_comment' },
required: { field: 'operation', value: 'create_comment' },
condition: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'entry' },
},
required: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'entry' },
},
},
{
id: 'commentEntryId',
title: 'Entry ID',
type: 'short-input',
placeholder: 'List entry ID to comment on',
condition: { field: 'operation', value: 'create_comment' },
required: { field: 'operation', value: 'create_comment' },
condition: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'entry' },
},
required: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'entry' },
},
},
{
id: 'commentRecordObject',
title: 'Record Object',
type: 'short-input',
placeholder: 'Object ID or slug the record belongs to',
condition: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'record' },
},
required: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'record' },
},
},
{
id: 'commentRecordId',
title: 'Record ID',
type: 'short-input',
placeholder: 'Record ID to comment on directly',
condition: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'record' },
},
required: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'record' },
},
},
{
id: 'commentThreadId',
title: 'Thread ID',
type: 'short-input',
placeholder: 'Reply to thread (optional, omit to start new)',
condition: { field: 'operation', value: 'create_comment' },
mode: 'advanced',
placeholder: 'Thread ID to reply to',
condition: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'thread' },
},
required: {
field: 'operation',
value: 'create_comment',
and: { field: 'commentTarget', value: 'thread' },
},
},
{
id: 'commentCreatedAt',
@@ -983,6 +1080,208 @@ workspace-member.created
},
},
// Attribute fields
{
id: 'attributeTarget',
title: 'Target',
type: 'dropdown',
options: [
{ label: 'Object', id: 'objects' },
{ label: 'List', id: 'lists' },
],
value: () => 'objects',
condition: {
field: 'operation',
value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'],
},
required: {
field: 'operation',
value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'],
},
},
{
id: 'attributeIdentifier',
title: 'Object or List ID/Slug',
type: 'short-input',
placeholder: 'e.g. people, companies',
condition: {
field: 'operation',
value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'],
},
required: {
field: 'operation',
value: ['list_attributes', 'get_attribute', 'create_attribute', 'update_attribute'],
},
},
{
id: 'attributeId',
title: 'Attribute ID or Slug',
type: 'short-input',
placeholder: 'e.g. email_addresses',
condition: { field: 'operation', value: ['get_attribute', 'update_attribute'] },
required: { field: 'operation', value: ['get_attribute', 'update_attribute'] },
},
{
id: 'attributeTitle',
title: 'Title',
type: 'short-input',
placeholder: 'e.g. Lead Source',
condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] },
required: { field: 'operation', value: 'create_attribute' },
},
{
id: 'attributeApiSlug',
title: 'API Slug',
type: 'short-input',
placeholder: 'e.g. lead_source',
condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] },
required: { field: 'operation', value: 'create_attribute' },
},
{
id: 'attributeType',
title: 'Type',
type: 'dropdown',
options: [
{ label: 'Text', id: 'text' },
{ label: 'Number', id: 'number' },
{ label: 'Checkbox', id: 'checkbox' },
{ label: 'Currency', id: 'currency' },
{ label: 'Date', id: 'date' },
{ label: 'Timestamp', id: 'timestamp' },
{ label: 'Rating', id: 'rating' },
{ label: 'Status', id: 'status' },
{ label: 'Select', id: 'select' },
{ label: 'Record Reference', id: 'record-reference' },
{ label: 'Actor Reference', id: 'actor-reference' },
{ label: 'Location', id: 'location' },
{ label: 'Domain', id: 'domain' },
{ label: 'Email Address', id: 'email-address' },
{ label: 'Phone Number', id: 'phone-number' },
],
condition: { field: 'operation', value: 'create_attribute' },
required: { field: 'operation', value: 'create_attribute' },
},
{
id: 'attributeDescription',
title: 'Description',
type: 'long-input',
placeholder: 'Describe the attribute',
condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] },
mode: 'advanced',
},
{
id: 'attributeIsRequired',
title: 'Required',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'create_attribute' },
mode: 'advanced',
},
{
id: 'attributeIsRequiredUpdate',
title: 'Required',
type: 'dropdown',
options: [
{ label: 'Leave unchanged', id: 'unchanged' },
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'unchanged',
condition: { field: 'operation', value: 'update_attribute' },
mode: 'advanced',
},
{
id: 'attributeIsUnique',
title: 'Unique',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'create_attribute' },
mode: 'advanced',
},
{
id: 'attributeIsUniqueUpdate',
title: 'Unique',
type: 'dropdown',
options: [
{ label: 'Leave unchanged', id: 'unchanged' },
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'unchanged',
condition: { field: 'operation', value: 'update_attribute' },
mode: 'advanced',
},
{
id: 'attributeIsMultiselect',
title: 'Multiselect',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'create_attribute' },
mode: 'advanced',
},
{
id: 'attributeIsArchived',
title: 'Archived',
type: 'dropdown',
options: [
{ label: 'Leave unchanged', id: 'unchanged' },
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'unchanged',
condition: { field: 'operation', value: 'update_attribute' },
mode: 'advanced',
},
{
id: 'attributeConfig',
title: 'Config',
type: 'code',
placeholder: '{"default_currency_code": "USD", "display_type": "text"}',
condition: { field: 'operation', value: ['create_attribute', 'update_attribute'] },
mode: 'advanced',
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `Generate Attio attribute type configuration as a JSON object.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the JSON object. No explanations, no markdown, no extra text.
### EXAMPLES
Currency: {"default_currency_code": "USD", "display_type": "text"}
Record reference: {"allowed_objects": ["people", "companies"]}`,
placeholder: 'Describe the attribute configuration...',
generationType: 'json-object',
},
},
{
id: 'attributeShowArchived',
title: 'Show Archived',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'list_attributes' },
mode: 'advanced',
},
// Shared limit
{
id: 'limit',
@@ -1000,6 +1299,7 @@ workspace-member.created
'query_list_entries',
'list_threads',
'list_webhooks',
'list_attributes',
],
},
},
@@ -1018,9 +1318,23 @@ workspace-member.created
'query_list_entries',
'list_threads',
'list_webhooks',
'list_attributes',
],
},
},
{
id: 'taskSort',
title: 'Sort',
type: 'dropdown',
options: [
{ label: 'Created At (asc)', id: 'created_at:asc' },
{ label: 'Created At (desc)', id: 'created_at:desc' },
{ label: 'Completed At (asc)', id: 'completed_at:asc' },
{ label: 'Completed At (desc)', id: 'completed_at:desc' },
],
condition: { field: 'operation', value: 'list_tasks' },
mode: 'advanced',
},
...getTrigger('attio_record_created').subBlocks,
...getTrigger('attio_record_updated').subBlocks,
...getTrigger('attio_record_deleted').subBlocks,
@@ -1116,6 +1430,10 @@ workspace-member.created
'attio_create_webhook',
'attio_update_webhook',
'attio_delete_webhook',
'attio_list_attributes',
'attio_get_attribute',
'attio_create_attribute',
'attio_update_attribute',
],
config: {
tool: (params) => `attio_${params.operation}`,
@@ -1123,92 +1441,285 @@ workspace-member.created
const cleanParams: Record<string, unknown> = {
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.taskIsCompleted !== undefined)
if (op === 'create_task' && params.taskContent) cleanParams.content = params.taskContent
if (['create_task', 'update_task'].includes(op) && params.taskDeadline)
cleanParams.deadlineAt = params.taskDeadline
if (op === 'create_task' && params.taskIsCompleted !== undefined)
cleanParams.isCompleted =
params.taskIsCompleted === 'true' || params.taskIsCompleted === true
if (params.taskLinkedRecords) cleanParams.linkedRecords = params.taskLinkedRecords
if (params.taskAssignees) cleanParams.assignees = params.taskAssignees
if (params.taskId) cleanParams.taskId = params.taskId
if (params.taskFilterObject) cleanParams.linkedObject = params.taskFilterObject
if (params.taskFilterRecordId) cleanParams.linkedRecordId = params.taskFilterRecordId
if (params.taskFilterAssignee) cleanParams.assignee = params.taskFilterAssignee
if (params.taskFilterCompleted && params.taskFilterCompleted !== 'all')
if (op === 'update_task') {
// taskIsCompletedUpdate is the current control; taskIsCompleted is a fallback for blocks
// saved before the split, when that field applied to both create_task and update_task.
if (
params.taskIsCompletedUpdate !== undefined &&
params.taskIsCompletedUpdate !== 'unchanged'
) {
cleanParams.isCompleted =
params.taskIsCompletedUpdate === 'true' || params.taskIsCompletedUpdate === true
} else if (
params.taskIsCompletedUpdate === undefined &&
params.taskIsCompleted !== undefined
) {
cleanParams.isCompleted =
params.taskIsCompleted === 'true' || params.taskIsCompleted === true
}
}
if (['create_task', 'update_task'].includes(op) && params.taskLinkedRecords)
cleanParams.linkedRecords = params.taskLinkedRecords
if (['create_task', 'update_task'].includes(op) && params.taskAssignees)
cleanParams.assignees = params.taskAssignees
if (['get_task', 'update_task', 'delete_task'].includes(op) && params.taskId)
cleanParams.taskId = params.taskId
if (op === 'list_tasks' && params.taskFilterObject)
cleanParams.linkedObject = params.taskFilterObject
if (op === 'list_tasks' && params.taskFilterRecordId)
cleanParams.linkedRecordId = params.taskFilterRecordId
if (op === 'list_tasks' && params.taskFilterAssignee)
cleanParams.assignee = params.taskFilterAssignee
if (
op === 'list_tasks' &&
params.taskFilterCompleted &&
params.taskFilterCompleted !== 'all'
)
cleanParams.isCompleted = params.taskFilterCompleted === 'true'
if (op === 'list_tasks' && params.taskSort) cleanParams.sort = params.taskSort
// Object params
if (params.objectIdOrSlug) cleanParams.object = params.objectIdOrSlug
if (params.objectApiSlug) cleanParams.apiSlug = params.objectApiSlug
if (params.objectSingularNoun) cleanParams.singularNoun = params.objectSingularNoun
if (params.objectPluralNoun) cleanParams.pluralNoun = params.objectPluralNoun
if (['get_object', 'update_object'].includes(op) && params.objectIdOrSlug)
cleanParams.object = params.objectIdOrSlug
if (['create_object', 'update_object'].includes(op) && params.objectApiSlug)
cleanParams.apiSlug = params.objectApiSlug
if (['create_object', 'update_object'].includes(op) && params.objectSingularNoun)
cleanParams.singularNoun = params.objectSingularNoun
if (['create_object', 'update_object'].includes(op) && params.objectPluralNoun)
cleanParams.pluralNoun = params.objectPluralNoun
// List params
if (params.listIdOrSlug) cleanParams.list = params.listIdOrSlug
if (params.listName) cleanParams.name = params.listName
if (params.listParentObject) cleanParams.parentObject = params.listParentObject
if (params.listApiSlug) cleanParams.apiSlug = params.listApiSlug
if (params.listWorkspaceAccess) cleanParams.workspaceAccess = params.listWorkspaceAccess
if (
[
'get_list',
'update_list',
'query_list_entries',
'get_list_entry',
'create_list_entry',
'update_list_entry',
'delete_list_entry',
].includes(op) &&
params.listIdOrSlug
)
cleanParams.list = params.listIdOrSlug
if (['create_list', 'update_list'].includes(op) && params.listName)
cleanParams.name = params.listName
if (op === 'create_list' && params.listParentObject)
cleanParams.parentObject = params.listParentObject
if (['create_list', 'update_list'].includes(op) && params.listApiSlug)
cleanParams.apiSlug = params.listApiSlug
if (op === 'create_list' && params.listWorkspaceAccess)
cleanParams.workspaceAccess = params.listWorkspaceAccess
if (op === 'update_list') {
// listWorkspaceAccessUpdate is the current control; listWorkspaceAccess is a fallback
// for blocks saved before the split, when that field applied to both create and update.
if (
params.listWorkspaceAccessUpdate &&
params.listWorkspaceAccessUpdate !== 'unchanged'
) {
cleanParams.workspaceAccess = params.listWorkspaceAccessUpdate
} else if (!params.listWorkspaceAccessUpdate && params.listWorkspaceAccess) {
cleanParams.workspaceAccess = params.listWorkspaceAccess
}
}
// List entry params
if (params.entryId) cleanParams.entryId = params.entryId
if (params.entryParentRecordId) cleanParams.parentRecordId = params.entryParentRecordId
if (params.entryParentObject) cleanParams.parentObject = params.entryParentObject
if (params.entryValues) cleanParams.entryValues = params.entryValues
if (params.entryFilter) cleanParams.filter = params.entryFilter
if (params.entrySorts) cleanParams.sorts = params.entrySorts
if (
['get_list_entry', 'update_list_entry', 'delete_list_entry'].includes(op) &&
params.entryId
)
cleanParams.entryId = params.entryId
if (op === 'create_list_entry' && params.entryParentRecordId)
cleanParams.parentRecordId = params.entryParentRecordId
if (op === 'create_list_entry' && params.entryParentObject)
cleanParams.parentObject = params.entryParentObject
if (['create_list_entry', 'update_list_entry'].includes(op) && params.entryValues)
cleanParams.entryValues = params.entryValues
if (op === 'query_list_entries' && params.entryFilter)
cleanParams.filter = params.entryFilter
if (op === 'query_list_entries' && params.entrySorts) cleanParams.sorts = params.entrySorts
// Member params
if (params.memberId) cleanParams.memberId = params.memberId
if (op === 'get_member' && params.memberId) cleanParams.memberId = params.memberId
// Comment params
if (params.commentContent) cleanParams.content = params.commentContent
if (params.commentFormat) cleanParams.format = params.commentFormat
if (params.commentAuthorType) cleanParams.authorType = params.commentAuthorType
if (params.commentAuthorId) cleanParams.authorId = params.commentAuthorId
if (params.commentList) cleanParams.list = params.commentList
if (params.commentEntryId) cleanParams.entryId = params.commentEntryId
if (params.commentThreadId) cleanParams.threadId = params.commentThreadId
if (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt
if (params.commentId) cleanParams.commentId = params.commentId
if (op === 'create_comment') {
if (params.commentContent) cleanParams.content = params.commentContent
if (params.commentFormat) cleanParams.format = params.commentFormat
if (params.commentAuthorType) cleanParams.authorType = params.commentAuthorType
if (params.commentAuthorId) cleanParams.authorId = params.commentAuthorId
// Blocks saved before commentTarget existed have no value for it — fall back to the
// pre-existing behavior (entry-based comment, or thread reply if a thread ID was set).
const commentTarget =
params.commentTarget ?? (params.commentThreadId ? 'thread' : 'entry')
if (commentTarget === 'entry') {
if (params.commentList) cleanParams.list = params.commentList
if (params.commentEntryId) cleanParams.entryId = params.commentEntryId
} else if (commentTarget === 'record') {
if (params.commentRecordObject) cleanParams.recordObject = params.commentRecordObject
if (params.commentRecordId) cleanParams.recordId = params.commentRecordId
} else if (commentTarget === 'thread') {
if (params.commentThreadId) cleanParams.threadId = params.commentThreadId
}
if (params.commentCreatedAt) cleanParams.createdAt = params.commentCreatedAt
}
if (['get_comment', 'delete_comment'].includes(op) && params.commentId)
cleanParams.commentId = params.commentId
// Thread params
if (params.threadId) cleanParams.threadId = params.threadId
if (params.threadFilterRecordId) cleanParams.recordId = params.threadFilterRecordId
if (params.threadFilterObject) cleanParams.object = params.threadFilterObject
if (params.threadFilterEntryId) cleanParams.entryId = params.threadFilterEntryId
if (params.threadFilterList) cleanParams.list = params.threadFilterList
if (op === 'get_thread' && params.threadId) cleanParams.threadId = params.threadId
if (op === 'list_threads' && params.threadFilterRecordId)
cleanParams.recordId = params.threadFilterRecordId
if (op === 'list_threads' && params.threadFilterObject)
cleanParams.object = params.threadFilterObject
if (op === 'list_threads' && params.threadFilterEntryId)
cleanParams.entryId = params.threadFilterEntryId
if (op === 'list_threads' && params.threadFilterList)
cleanParams.list = params.threadFilterList
// Webhook params
if (params.webhookId) cleanParams.webhookId = params.webhookId
if (params.webhookTargetUrl) cleanParams.targetUrl = params.webhookTargetUrl
if (params.webhookSubscriptions) cleanParams.subscriptions = params.webhookSubscriptions
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
// Shared params
if (params.limit) cleanParams.limit = Number(params.limit)
if (params.offset) cleanParams.offset = Number(params.offset)
// Attribute params
const attributeOps = [
'list_attributes',
'get_attribute',
'create_attribute',
'update_attribute',
]
if (attributeOps.includes(op) && params.attributeTarget)
cleanParams.target = params.attributeTarget
if (attributeOps.includes(op) && params.attributeIdentifier)
cleanParams.identifier = params.attributeIdentifier
if (['get_attribute', 'update_attribute'].includes(op) && params.attributeId)
cleanParams.attribute = params.attributeId
if (['create_attribute', 'update_attribute'].includes(op) && params.attributeTitle)
cleanParams.title = params.attributeTitle
if (['create_attribute', 'update_attribute'].includes(op) && params.attributeApiSlug)
cleanParams.apiSlug = params.attributeApiSlug
if (op === 'create_attribute' && params.attributeType)
cleanParams.type = params.attributeType
if (['create_attribute', 'update_attribute'].includes(op) && params.attributeDescription)
cleanParams.description = params.attributeDescription
if (op === 'create_attribute' && params.attributeIsRequired !== undefined)
cleanParams.isRequired =
params.attributeIsRequired === 'true' || params.attributeIsRequired === true
if (
op === 'update_attribute' &&
params.attributeIsRequiredUpdate !== undefined &&
params.attributeIsRequiredUpdate !== 'unchanged'
)
cleanParams.isRequired =
params.attributeIsRequiredUpdate === 'true' || params.attributeIsRequiredUpdate === true
if (op === 'create_attribute' && params.attributeIsUnique !== undefined)
cleanParams.isUnique =
params.attributeIsUnique === 'true' || params.attributeIsUnique === true
if (
op === 'update_attribute' &&
params.attributeIsUniqueUpdate !== undefined &&
params.attributeIsUniqueUpdate !== 'unchanged'
)
cleanParams.isUnique =
params.attributeIsUniqueUpdate === 'true' || params.attributeIsUniqueUpdate === true
if (op === 'create_attribute' && params.attributeIsMultiselect !== undefined)
cleanParams.isMultiselect =
params.attributeIsMultiselect === 'true' || params.attributeIsMultiselect === true
if (
op === 'update_attribute' &&
params.attributeIsArchived !== undefined &&
params.attributeIsArchived !== 'unchanged'
)
cleanParams.isArchived =
params.attributeIsArchived === 'true' || params.attributeIsArchived === true
if (['create_attribute', 'update_attribute'].includes(op) && params.attributeConfig)
cleanParams.config = params.attributeConfig
if (op === 'list_attributes' && params.attributeShowArchived !== undefined)
cleanParams.showArchived =
params.attributeShowArchived === 'true' || params.attributeShowArchived === true
// Shared pagination params — only meaningful for list-style operations
if (
[
'list_records',
'search_records',
'list_notes',
'list_tasks',
'query_list_entries',
'list_threads',
'list_webhooks',
'list_attributes',
].includes(op) &&
params.limit
)
cleanParams.limit = Number(params.limit)
if (
[
'list_records',
'list_notes',
'list_tasks',
'query_list_entries',
'list_threads',
'list_webhooks',
'list_attributes',
].includes(op) &&
params.offset
)
cleanParams.offset = Number(params.offset)
return cleanParams
},
@@ -1237,6 +1748,7 @@ workspace-member.created
taskContent: { type: 'string', description: 'Task content' },
taskDeadline: { type: 'string', description: 'Task deadline' },
taskIsCompleted: { type: 'string', description: 'Task completion status' },
taskIsCompletedUpdate: { type: 'string', description: 'Task completion status (update)' },
taskLinkedRecords: { type: 'json', description: 'Linked records JSON array' },
taskAssignees: { type: 'json', description: 'Assignees JSON array' },
taskId: { type: 'string', description: 'Task ID' },
@@ -1249,6 +1761,10 @@ workspace-member.created
listParentObject: { type: 'string', description: 'List parent object' },
listApiSlug: { type: 'string', description: 'List API slug' },
listWorkspaceAccess: { type: 'string', description: 'List workspace-level access' },
listWorkspaceAccessUpdate: {
type: 'string',
description: 'List workspace-level access (update)',
},
entryId: { type: 'string', description: 'List entry ID' },
entryParentRecordId: { type: 'string', description: 'Record ID for list entry' },
entryParentObject: { type: 'string', description: 'Record object type for list entry' },
@@ -1260,8 +1776,11 @@ workspace-member.created
commentFormat: { type: 'string', description: 'Comment format' },
commentAuthorType: { type: 'string', description: 'Comment author type' },
commentAuthorId: { type: 'string', description: 'Comment author ID' },
commentTarget: { type: 'string', description: 'What the comment is attached to' },
commentList: { type: 'string', description: 'List for comment' },
commentEntryId: { type: 'string', description: 'Entry ID for comment' },
commentRecordObject: { type: 'string', description: 'Object for record comment' },
commentRecordId: { type: 'string', description: 'Record ID for record comment' },
commentThreadId: { type: 'string', description: 'Thread ID to reply to' },
commentCreatedAt: { type: 'string', description: 'Comment creation timestamp (backdate)' },
commentId: { type: 'string', description: 'Comment ID' },
@@ -1269,6 +1788,34 @@ workspace-member.created
webhookId: { type: 'string', description: 'Webhook ID' },
webhookTargetUrl: { type: 'string', description: 'Webhook target URL' },
webhookSubscriptions: { type: 'json', description: 'Webhook event subscriptions' },
attributeTarget: {
type: 'string',
description: 'Whether the attribute is on an object or list',
},
attributeIdentifier: { type: 'string', description: 'The object or list ID or slug' },
attributeId: { type: 'string', description: 'The attribute ID or slug' },
attributeTitle: { type: 'string', description: 'The attribute display title' },
attributeApiSlug: { type: 'string', description: 'The attribute API slug' },
attributeType: { type: 'string', description: 'The attribute value type' },
attributeDescription: { type: 'string', description: 'The attribute description' },
attributeIsRequired: { type: 'string', description: 'Whether the attribute is required' },
attributeIsRequiredUpdate: {
type: 'string',
description: 'Whether the attribute is required (update)',
},
attributeIsUnique: { type: 'string', description: 'Whether the attribute is unique' },
attributeIsUniqueUpdate: {
type: 'string',
description: 'Whether the attribute is unique (update)',
},
attributeIsMultiselect: { type: 'string', description: 'Whether the attribute is multiselect' },
attributeIsArchived: { type: 'string', description: 'Whether the attribute is archived' },
attributeConfig: { type: 'json', description: 'Type-dependent attribute configuration' },
attributeShowArchived: {
type: 'string',
description: 'Whether to include archived attributes',
},
taskSort: { type: 'string', description: 'Task list sort order' },
limit: { type: 'string', description: 'Maximum number of results' },
offset: { type: 'string', description: 'Number of results to skip for pagination' },
},
@@ -1289,6 +1836,7 @@ workspace-member.created
content: { type: 'string', description: 'Task or note content' },
deadlineAt: { type: 'string', description: 'Task deadline' },
isCompleted: { type: 'boolean', description: 'Task completion status' },
completedAt: { type: 'string', description: 'When the task was completed' },
linkedRecords: { type: 'json', description: 'Linked records' },
assignees: { type: 'json', description: 'Task assignees' },
objects: { type: 'json', description: 'Array of objects' },
@@ -1322,6 +1870,32 @@ workspace-member.created
deleted: { type: 'boolean', description: 'Whether the item was deleted' },
createdAt: { type: 'string', description: 'When the item was created' },
success: { type: 'boolean', description: 'Whether the operation succeeded' },
attributes: { type: 'json', description: 'Array of attributes' },
attributeId: { type: 'string', description: 'The attribute ID' },
description: { type: 'string', description: 'The attribute description' },
type: { type: 'string', description: 'The attribute value type' },
isSystemAttribute: {
type: 'boolean',
description: 'Whether this is a built-in system attribute',
},
isWritable: { type: 'boolean', description: 'Whether the attribute can be written to' },
isRequired: { type: 'boolean', description: 'Whether the attribute is required' },
isUnique: { type: 'boolean', description: 'Whether the attribute enforces uniqueness' },
isMultiselect: { type: 'boolean', description: 'Whether the attribute is multiselect' },
isDefaultValueEnabled: {
type: 'boolean',
description: 'Whether this attribute has a default value enabled',
},
isArchived: { type: 'boolean', description: 'Whether the attribute is archived' },
defaultValue: {
type: 'json',
description: 'The default value for this attribute, if enabled',
},
relationship: {
type: 'json',
description: 'The related attribute, if this attribute is part of a relationship',
},
config: { type: 'json', description: 'Type-dependent attribute configuration' },
},
}
+2 -2
View File
@@ -56,11 +56,11 @@ export const attioAssertRecordTool: ToolConfig<AttioAssertRecordParams, AttioAss
'Content-Type': 'application/json',
}),
body: (params) => {
let values: Record<string, unknown> = {}
let values: Record<string, unknown>
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 } }
},
+157
View File
@@ -0,0 +1,157 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioCreateAttributeParams, AttioCreateAttributeResponse } from './types'
import { ATTRIBUTE_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioCreateAttribute')
export const attioCreateAttributeTool: ToolConfig<
AttioCreateAttributeParams,
AttioCreateAttributeResponse
> = {
id: 'attio_create_attribute',
name: 'Attio Create Attribute',
description: 'Create a new attribute (schema field) on an Attio object or list',
version: '1.0.0',
oauth: {
required: true,
provider: 'attio',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The OAuth access token for the Attio API',
},
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Whether to create the attribute on an object or a list: objects or lists',
},
identifier: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object or list ID or slug (e.g. people, companies)',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The attribute display title',
},
apiSlug: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The attribute API slug (unique, snake_case)',
},
type: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The attribute value type (e.g. text, number, checkbox, currency, date, timestamp, rating, status, select, record-reference, actor-reference, location, domain, email-address, phone-number)',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'A description of the attribute',
},
isRequired: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether new records must provide a value (default false)',
},
isUnique: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the attribute enforces uniqueness on new data (default false)',
},
isMultiselect: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the attribute supports multiple values (default false)',
},
config: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON object of type-dependent configuration (e.g. currency or record-reference settings)',
},
},
request: {
url: (params) =>
`https://api.attio.com/v2/${params.target.trim()}/${params.identifier.trim()}/attributes`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {
title: params.title,
api_slug: params.apiSlug,
description: params.description ?? null,
type: params.type,
is_required: params.isRequired ?? false,
is_unique: params.isUnique ?? false,
is_multiselect: params.isMultiselect ?? false,
// `config` is a required key on Attio's create-attribute request body (even though its
// nested fields are only required for type-dependent configs like currency/record-reference).
config: {},
}
if (params.config) {
try {
data.config =
typeof params.config === 'string' ? JSON.parse(params.config) : params.config
} catch {
throw new Error('Invalid JSON provided for attribute config')
}
}
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create attribute')
}
const attr = data.data
return {
success: true,
output: {
attributeId: attr.id?.attribute_id ?? null,
title: attr.title ?? null,
apiSlug: attr.api_slug ?? null,
description: attr.description ?? null,
type: attr.type ?? null,
isSystemAttribute: attr.is_system_attribute ?? false,
isWritable: attr.is_writable ?? false,
isRequired: attr.is_required ?? false,
isUnique: attr.is_unique ?? false,
isMultiselect: attr.is_multiselect ?? false,
isDefaultValueEnabled: attr.is_default_value_enabled ?? false,
isArchived: attr.is_archived ?? false,
defaultValue: attr.default_value ?? null,
relationship: attr.relationship ?? null,
config: attr.config ?? null,
createdAt: attr.created_at ?? null,
},
}
},
outputs: ATTRIBUTE_OUTPUT_PROPERTIES,
}
+37 -8
View File
@@ -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 entry ID to comment on',
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 object ID or slug the record belongs to (used with recordId; omit if threadId or entryId is set)',
},
recordId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The record ID to comment on directly (used with recordObject; omit if threadId or entryId is set)',
},
threadId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Thread ID to reply to (omit to start a new thread)',
description: 'Thread ID to reply to (omit to start a new thread on a record or list entry)',
},
createdAt: {
type: 'string',
@@ -91,12 +107,25 @@ export const attioCreateCommentTool: ToolConfig<
type: params.authorType,
id: params.authorId,
},
entry: {
}
// Attio's comment body accepts exactly one of `thread_id`, `record`, or `entry` — mutually exclusive.
if (params.threadId) {
data.thread_id = params.threadId
} else if (params.recordObject && params.recordId) {
data.record = {
object: params.recordObject,
record_id: params.recordId,
}
} else if (params.list && params.entryId) {
data.entry = {
list: params.list,
entry_id: params.entryId,
},
}
} else {
throw new Error(
'Must provide either threadId, both recordObject and recordId, or both list and entryId'
)
}
if (params.threadId) data.thread_id = params.threadId
if (params.createdAt) data.created_at = params.createdAt
return { data }
},
+2 -2
View File
@@ -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<AttioCreateNoteParams, AttioCreateN
contentPlaintext: note.content_plaintext ?? null,
contentMarkdown: note.content_markdown ?? null,
meetingId: note.meeting_id ?? null,
tags: note.tags ?? [],
tags: mapNoteTags(note.tags),
createdByActor: note.created_by_actor ?? null,
createdAt: note.created_at ?? null,
},
+1 -1
View File
@@ -50,7 +50,7 @@ export const attioCreateRecordTool: ToolConfig<AttioCreateRecordParams, AttioCre
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 } }
},
+1
View File
@@ -124,6 +124,7 @@ export const attioCreateTaskTool: ToolConfig<AttioCreateTaskParams, AttioCreateT
content: task.content_plaintext ?? null,
deadlineAt: task.deadline_at ?? null,
isCompleted: task.is_completed ?? false,
completedAt: task.completed_at ?? null,
linkedRecords,
assignees,
createdByActor: task.created_by_actor ?? null,
+87
View File
@@ -0,0 +1,87 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioGetAttributeParams, AttioGetAttributeResponse } from './types'
import { ATTRIBUTE_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioGetAttribute')
export const attioGetAttributeTool: ToolConfig<AttioGetAttributeParams, AttioGetAttributeResponse> =
{
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,
}
+2 -2
View File
@@ -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<AttioGetNoteParams, AttioGetNoteRespon
contentPlaintext: note.content_plaintext ?? null,
contentMarkdown: note.content_markdown ?? null,
meetingId: note.meeting_id ?? null,
tags: note.tags ?? [],
tags: mapNoteTags(note.tags),
createdByActor: note.created_by_actor ?? null,
createdAt: note.created_at ?? null,
},
+1 -1
View File
@@ -36,7 +36,6 @@ export const attioGetTaskTool: ToolConfig<AttioGetTaskParams, AttioGetTaskRespon
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
@@ -66,6 +65,7 @@ export const attioGetTaskTool: ToolConfig<AttioGetTaskParams, AttioGetTaskRespon
content: task.content_plaintext ?? null,
deadlineAt: task.deadline_at ?? null,
isCompleted: task.is_completed ?? false,
completedAt: task.completed_at ?? null,
linkedRecords,
assignees,
createdByActor: task.created_by_actor ?? null,
+4
View File
@@ -1,4 +1,5 @@
export { attioAssertRecordTool } from './assert_record'
export { attioCreateAttributeTool } from './create_attribute'
export { attioCreateCommentTool } from './create_comment'
export { attioCreateListTool } from './create_list'
export { attioCreateListEntryTool } from './create_list_entry'
@@ -13,6 +14,7 @@ export { attioDeleteNoteTool } from './delete_note'
export { attioDeleteRecordTool } from './delete_record'
export { attioDeleteTaskTool } from './delete_task'
export { attioDeleteWebhookTool } from './delete_webhook'
export { attioGetAttributeTool } from './get_attribute'
export { attioGetCommentTool } from './get_comment'
export { attioGetListTool } from './get_list'
export { attioGetListEntryTool } from './get_list_entry'
@@ -23,6 +25,7 @@ export { attioGetRecordTool } from './get_record'
export { attioGetTaskTool } from './get_task'
export { attioGetThreadTool } from './get_thread'
export { attioGetWebhookTool } from './get_webhook'
export { attioListAttributesTool } from './list_attributes'
export { attioListListsTool } from './list_lists'
export { attioListMembersTool } from './list_members'
export { attioListNotesTool } from './list_notes'
@@ -33,6 +36,7 @@ export { attioListThreadsTool } from './list_threads'
export { attioListWebhooksTool } from './list_webhooks'
export { attioQueryListEntriesTool } from './query_list_entries'
export { attioSearchRecordsTool } from './search_records'
export { attioUpdateAttributeTool } from './update_attribute'
export { attioUpdateListTool } from './update_list'
export { attioUpdateListEntryTool } from './update_list_entry'
export { attioUpdateObjectTool } from './update_object'
+124
View File
@@ -0,0 +1,124 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { AttioListAttributesParams, AttioListAttributesResponse } from './types'
import { ATTRIBUTE_OUTPUT_PROPERTIES } from './types'
const logger = createLogger('AttioListAttributes')
export const attioListAttributesTool: ToolConfig<
AttioListAttributesParams,
AttioListAttributesResponse
> = {
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<string, unknown>) => {
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<string, unknown>) ?? null,
relationship: (attr.relationship as Record<string, unknown>) ?? null,
config: (attr.config as Record<string, unknown>) ?? 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' },
},
}
+2 -2
View File
@@ -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<AttioListNotesParams, AttioListNotes
contentPlaintext: (note.content_plaintext as string) ?? null,
contentMarkdown: (note.content_markdown as string) ?? null,
meetingId: (note.meeting_id as string) ?? null,
tags: (note.tags as unknown[]) ?? [],
tags: mapNoteTags(note.tags),
createdByActor: note.created_by_actor ?? null,
createdAt: (note.created_at as string) ?? null,
}
+2 -2
View File
@@ -68,14 +68,14 @@ export const attioListRecordsTool: ToolConfig<AttioListRecordsParams, AttioListR
try {
body.filter = JSON.parse(params.filter)
} catch {
body.filter = params.filter
throw new Error('Invalid JSON provided for filter')
}
}
if (params.sorts) {
try {
body.sorts = JSON.parse(params.sorts)
} catch {
body.sorts = params.sorts
throw new Error('Invalid JSON provided for sorts')
}
}
if (params.limit != null) body.limit = params.limit
+3 -1
View File
@@ -51,7 +51,8 @@ export const attioListTasksTool: ToolConfig<AttioListTasksParams, AttioListTasks
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: created_at:asc or created_at:desc',
description:
'Sort order: created_at:asc, created_at:desc, completed_at:asc, or completed_at:desc',
},
limit: {
type: 'number',
@@ -118,6 +119,7 @@ export const attioListTasksTool: ToolConfig<AttioListTasksParams, AttioListTasks
content: (task.content_plaintext as string) ?? null,
deadlineAt: (task.deadline_at as string) ?? null,
isCompleted: (task.is_completed as boolean) ?? false,
completedAt: (task.completed_at as string) ?? null,
linkedRecords,
assignees,
createdByActor: task.created_by_actor ?? null,
+190 -37
View File
@@ -1,5 +1,31 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
/** Raw Attio note tag shape (workspace-member or record variant) */
export interface AttioNoteTag {
type?: string
workspace_member_id?: string
object?: string
record_id?: string
}
/** Camelcase note tag shape returned in tool outputs */
export interface NoteTagOutput {
type: string | null
workspaceMemberId: string | null
object: string | null
recordId: string | null
}
/** Maps a raw Attio note tag to the camelCase output shape declared in NOTE_OUTPUT_PROPERTIES */
export function mapNoteTags(tags: unknown): NoteTagOutput[] {
return ((tags as AttioNoteTag[] | undefined) ?? []).map((tag) => ({
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<string, OutputProperty>
/** 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<string, OutputProperty>
/** Shared output properties for Attio comments */
export const COMMENT_OUTPUT_PROPERTIES = {
commentId: { type: 'string', description: 'The comment ID' },
@@ -248,32 +326,6 @@ interface AttioRecord {
values: Record<string, unknown>
}
/** Raw Attio note shape from the API */
interface AttioNote {
id: { workspace_id: string; note_id: string }
parent_object: string
parent_record_id: string
title: string
content_plaintext: string
content_markdown: string
meeting_id: string | null
tags: unknown[]
created_by_actor: unknown
created_at: string
}
/** Raw Attio task shape from the API */
interface AttioTask {
id: { workspace_id: string; task_id: string }
content_plaintext: string
deadline_at: string | null
is_completed: boolean
linked_records: Array<{ target_object_id: string; target_record_id: string }>
assignees: Array<{ referenced_actor_type: string; referenced_actor_id: string }>
created_by_actor: unknown
created_at: string
}
/** Params for listing/querying records */
export interface AttioListRecordsParams {
accessToken: string
@@ -453,7 +505,7 @@ export interface AttioListNotesResponse extends ToolResponse {
contentPlaintext: string | null
contentMarkdown: string | null
meetingId: string | null
tags: unknown[]
tags: NoteTagOutput[]
createdByActor: unknown
createdAt: string | null
}>
@@ -471,7 +523,7 @@ export interface AttioCreateNoteResponse extends ToolResponse {
contentPlaintext: string | null
contentMarkdown: string | null
meetingId: string | null
tags: unknown[]
tags: NoteTagOutput[]
createdByActor: unknown
createdAt: string | null
}
@@ -492,6 +544,7 @@ export interface AttioListTasksResponse extends ToolResponse {
content: string | null
deadlineAt: string | null
isCompleted: boolean
completedAt: string | null
linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }>
assignees: Array<{ type: string | null; id: string | null }>
createdByActor: unknown
@@ -508,6 +561,7 @@ export interface AttioCreateTaskResponse extends ToolResponse {
content: string | null
deadlineAt: string | null
isCompleted: boolean
completedAt: string | null
linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }>
assignees: Array<{ type: string | null; id: string | null }>
createdByActor: unknown
@@ -522,6 +576,7 @@ export interface AttioUpdateTaskResponse extends ToolResponse {
content: string | null
deadlineAt: string | null
isCompleted: boolean
completedAt: string | null
linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }>
assignees: Array<{ type: string | null; id: string | null }>
createdByActor: unknown
@@ -542,6 +597,7 @@ export interface AttioGetTaskResponse extends ToolResponse {
content: string | null
deadlineAt: string | null
isCompleted: boolean
completedAt: string | null
linkedRecords: Array<{ targetObjectId: string | null; targetRecordId: string | null }>
assignees: Array<{ type: string | null; id: string | null }>
createdByActor: unknown
@@ -572,7 +628,7 @@ export interface AttioGetNoteResponse extends ToolResponse {
contentPlaintext: string | null
contentMarkdown: string | null
meetingId: string | null
tags: unknown[]
tags: NoteTagOutput[]
createdByActor: unknown
createdAt: string | null
}
@@ -684,7 +740,7 @@ export interface AttioListListsResponse extends ToolResponse {
name: string | null
parentObject: string | null
workspaceAccess: string | null
workspaceMemberAccess: string | null
workspaceMemberAccess: unknown
createdByActor: { type: string | null; id: string | null } | null
createdAt: string | null
}>
@@ -706,7 +762,7 @@ export interface AttioGetListResponse extends ToolResponse {
name: string | null
parentObject: string | null
workspaceAccess: string | null
workspaceMemberAccess: string | null
workspaceMemberAccess: unknown
createdByActor: { type: string | null; id: string | null } | null
createdAt: string | null
}
@@ -730,7 +786,7 @@ export interface AttioCreateListResponse extends ToolResponse {
name: string | null
parentObject: string | null
workspaceAccess: string | null
workspaceMemberAccess: string | null
workspaceMemberAccess: unknown
createdByActor: { type: string | null; id: string | null } | null
createdAt: string | null
}
@@ -754,7 +810,7 @@ export interface AttioUpdateListResponse extends ToolResponse {
name: string | null
parentObject: string | null
workspaceAccess: string | null
workspaceMemberAccess: string | null
workspaceMemberAccess: unknown
createdByActor: { type: string | null; id: string | null } | null
createdAt: string | null
}
@@ -906,8 +962,10 @@ export interface AttioCreateCommentParams {
format?: string
authorType: string
authorId: string
list: string
entryId: string
list?: string
entryId?: string
recordObject?: string
recordId?: string
threadId?: string
createdAt?: string
}
@@ -1098,6 +1156,97 @@ export interface AttioDeleteWebhookResponse extends ToolResponse {
}
}
/** Params for listing attributes on an object or list */
export interface AttioListAttributesParams {
accessToken: string
target: string
identifier: string
limit?: number
offset?: number
showArchived?: boolean
}
/** Attribute shape as returned in tool outputs (camelCase) */
interface AttioAttributeOutput {
attributeId: string | null
title: string | null
apiSlug: string | null
description: string | null
type: string | null
isSystemAttribute: boolean
isWritable: boolean
isRequired: boolean
isUnique: boolean
isMultiselect: boolean
isDefaultValueEnabled: boolean
isArchived: boolean
defaultValue: Record<string, unknown> | null
relationship: Record<string, unknown> | null
config: Record<string, unknown> | null
createdAt: string | null
}
/** Response for listing attributes */
export interface AttioListAttributesResponse extends ToolResponse {
output: {
attributes: AttioAttributeOutput[]
count: number
}
}
/** Params for getting a single attribute */
export interface AttioGetAttributeParams {
accessToken: string
target: string
identifier: string
attribute: string
}
/** Response for getting a single attribute */
export interface AttioGetAttributeResponse extends ToolResponse {
output: AttioAttributeOutput
}
/** Params for creating an attribute */
export interface AttioCreateAttributeParams {
accessToken: string
target: string
identifier: string
title: string
apiSlug: string
type: string
description?: string
isRequired?: boolean
isUnique?: boolean
isMultiselect?: boolean
config?: string
}
/** Response for creating an attribute */
export interface AttioCreateAttributeResponse extends ToolResponse {
output: AttioAttributeOutput
}
/** Params for updating an attribute */
export interface AttioUpdateAttributeParams {
accessToken: string
target: string
identifier: string
attribute: string
title?: string
apiSlug?: string
description?: string
isRequired?: boolean
isUnique?: boolean
isArchived?: boolean
config?: string
}
/** Response for updating an attribute */
export interface AttioUpdateAttributeResponse extends ToolResponse {
output: AttioAttributeOutput
}
export type AttioResponse =
| AttioListRecordsResponse
| AttioGetRecordResponse
@@ -1140,3 +1289,7 @@ export type AttioResponse =
| AttioCreateWebhookResponse
| AttioUpdateWebhookResponse
| AttioDeleteWebhookResponse
| AttioListAttributesResponse
| AttioGetAttributeResponse
| AttioCreateAttributeResponse
| AttioUpdateAttributeResponse
+150
View File
@@ -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<string, unknown> = {}
if (params.title != null) data.title = params.title
if (params.apiSlug != null) data.api_slug = params.apiSlug
if (params.description != null) data.description = params.description
if (params.isRequired != null) data.is_required = params.isRequired
if (params.isUnique != null) data.is_unique = params.isUnique
if (params.isArchived != null) data.is_archived = params.isArchived
if (params.config) {
try {
data.config =
typeof params.config === 'string' ? JSON.parse(params.config) : params.config
} catch {
throw new Error('Invalid JSON provided for attribute config')
}
}
return { data }
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Attio API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update attribute')
}
const attr = data.data
return {
success: true,
output: {
attributeId: attr.id?.attribute_id ?? null,
title: attr.title ?? null,
apiSlug: attr.api_slug ?? null,
description: attr.description ?? null,
type: attr.type ?? null,
isSystemAttribute: attr.is_system_attribute ?? false,
isWritable: attr.is_writable ?? false,
isRequired: attr.is_required ?? false,
isUnique: attr.is_unique ?? false,
isMultiselect: attr.is_multiselect ?? false,
isDefaultValueEnabled: attr.is_default_value_enabled ?? false,
isArchived: attr.is_archived ?? false,
defaultValue: attr.default_value ?? null,
relationship: attr.relationship ?? null,
config: attr.config ?? null,
createdAt: attr.created_at ?? null,
},
}
},
outputs: ATTRIBUTE_OUTPUT_PROPERTIES,
}
+1 -1
View File
@@ -57,7 +57,7 @@ export const attioUpdateRecordTool: ToolConfig<AttioUpdateRecordParams, AttioUpd
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 } }
},
+1
View File
@@ -114,6 +114,7 @@ export const attioUpdateTaskTool: ToolConfig<AttioUpdateTaskParams, AttioUpdateT
content: task.content_plaintext ?? null,
deadlineAt: task.deadline_at ?? null,
isCompleted: task.is_completed ?? false,
completedAt: task.completed_at ?? null,
linkedRecords,
assignees,
createdByActor: task.created_by_actor ?? null,
+8
View File
@@ -257,6 +257,7 @@ import {
} from '@/tools/athena'
import {
attioAssertRecordTool,
attioCreateAttributeTool,
attioCreateCommentTool,
attioCreateListEntryTool,
attioCreateListTool,
@@ -271,6 +272,7 @@ import {
attioDeleteRecordTool,
attioDeleteTaskTool,
attioDeleteWebhookTool,
attioGetAttributeTool,
attioGetCommentTool,
attioGetListEntryTool,
attioGetListTool,
@@ -281,6 +283,7 @@ import {
attioGetTaskTool,
attioGetThreadTool,
attioGetWebhookTool,
attioListAttributesTool,
attioListListsTool,
attioListMembersTool,
attioListNotesTool,
@@ -291,6 +294,7 @@ import {
attioListWebhooksTool,
attioQueryListEntriesTool,
attioSearchRecordsTool,
attioUpdateAttributeTool,
attioUpdateListEntryTool,
attioUpdateListTool,
attioUpdateObjectTool,
@@ -6798,6 +6802,7 @@ export const tools: Record<string, 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,
@@ -6812,6 +6817,7 @@ export const tools: Record<string, ToolConfig> = {
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,
@@ -6822,6 +6828,7 @@ export const tools: Record<string, ToolConfig> = {
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,
@@ -6832,6 +6839,7 @@ export const tools: Record<string, ToolConfig> = {
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,