diff --git a/apps/docs/content/docs/en/integrations/salesforce.mdx b/apps/docs/content/docs/en/integrations/salesforce.mdx index 7c15ea20e14..1125dc36cdd 100644 --- a/apps/docs/content/docs/en/integrations/salesforce.mdx +++ b/apps/docs/content/docs/en/integrations/salesforce.mdx @@ -276,7 +276,7 @@ Delete a contact from Salesforce CRM ### `salesforce_get_leads` -Get lead(s) from Salesforce +Retrieve lead(s) from Salesforce CRM #### Input @@ -571,6 +571,9 @@ Update an existing case | `subject` | string | No | Case subject | | `status` | string | No | Status \(e.g., New, Working, Escalated, Closed\) | | `priority` | string | No | Priority \(e.g., Low, Medium, High\) | +| `origin` | string | No | Origin \(e.g., Phone, Email, Web\) | +| `contactId` | string | No | Salesforce Contact ID \(18-character string starting with 003\) | +| `accountId` | string | No | Salesforce Account ID \(18-character string starting with 001\) | | `description` | string | No | Case description | #### Output @@ -678,6 +681,8 @@ Update an existing task | `status` | string | No | Status \(e.g., Not Started, In Progress, Completed\) | | `priority` | string | No | Priority \(e.g., Low, Normal, High\) | | `activityDate` | string | No | Due date in YYYY-MM-DD format | +| `whoId` | string | No | Related Contact ID \(003...\) or Lead ID \(00Q...\) | +| `whatId` | string | No | Related Account ID \(001...\) or Opportunity ID \(006...\) | | `description` | string | No | Task description | #### Output @@ -720,8 +725,7 @@ Get a list of reports accessible by the current user | --------- | ---- | -------- | ----------- | | `idToken` | string | No | No description | | `instanceUrl` | string | No | No description | -| `folderName` | string | No | Filter reports by folder name \(case-insensitive partial match\) | -| `searchTerm` | string | No | Search term to filter reports by name or description | +| `searchTerm` | string | No | Filter reports by name \(case-insensitive partial match\) | #### Output @@ -735,7 +739,7 @@ Get a list of reports accessible by the current user ### `salesforce_get_report` -Get metadata and describe information for a specific report +Get the describe (definition and metadata) for a specific report #### Input @@ -818,7 +822,6 @@ Get a list of dashboards accessible by the current user | --------- | ---- | -------- | ----------- | | `idToken` | string | No | No description | | `instanceUrl` | string | No | No description | -| `folderName` | string | No | Filter dashboards by folder name \(case-insensitive partial match\) | #### Output @@ -852,7 +855,7 @@ Get details and results for a specific dashboard | ↳ `dashboardId` | string | Dashboard ID | | ↳ `components` | array | Array of dashboard component data with visualizations and filters | | ↳ `dashboardName` | string | Display name of the dashboard | -| ↳ `folderId` | string | ID of the folder containing the dashboard | +| ↳ `dashboardMetadata` | object | Structured dashboard metadata \(attributes, component definitions, layout\) | | ↳ `runningUser` | object | User context under which the dashboard data was retrieved | | ↳ `success` | boolean | Salesforce operation success | @@ -877,9 +880,10 @@ Refresh a dashboard to get the latest data | ↳ `dashboard` | object | Full dashboard details object | | ↳ `dashboardId` | string | Dashboard ID | | ↳ `components` | array | Array of dashboard component data with fresh visualizations | -| ↳ `status` | object | Dashboard refresh status information | +| ↳ `status` | object | Dashboard refresh status \(dashboardStatus\), when returned by the refresh | +| ↳ `statusUrl` | string | URL of the status resource to poll for refresh completion | | ↳ `dashboardName` | string | Display name of the dashboard | -| ↳ `refreshDate` | string | ISO 8601 timestamp when the dashboard was last refreshed | +| ↳ `dashboardMetadata` | object | Structured dashboard metadata \(attributes, component definitions, layout\) | | ↳ `success` | boolean | Salesforce operation success | ### `salesforce_query` diff --git a/apps/sim/blocks/blocks/salesforce.ts b/apps/sim/blocks/blocks/salesforce.ts index 23f605cb628..11cbc2b58d1 100644 --- a/apps/sim/blocks/blocks/salesforce.ts +++ b/apps/sim/blocks/blocks/salesforce.ts @@ -98,6 +98,7 @@ export const SalesforceBlock: BlockConfig = { title: 'Fields to Return', type: 'short-input', placeholder: 'Comma-separated fields', + mode: 'advanced', condition: { field: 'operation', value: [ @@ -115,6 +116,7 @@ export const SalesforceBlock: BlockConfig = { title: 'Limit', type: 'short-input', placeholder: 'Max results (default: 100)', + mode: 'advanced', condition: { field: 'operation', value: [ @@ -132,6 +134,7 @@ export const SalesforceBlock: BlockConfig = { title: 'Order By', type: 'short-input', placeholder: 'Field and direction (e.g., "Name ASC")', + mode: 'advanced', condition: { field: 'operation', value: [ @@ -158,8 +161,12 @@ export const SalesforceBlock: BlockConfig = { 'create_contact', 'update_contact', 'create_case', + 'update_case', + 'create_opportunity', + 'update_opportunity', ], }, + required: { field: 'operation', value: ['update_account', 'delete_account'] }, }, { id: 'name', @@ -170,12 +177,14 @@ export const SalesforceBlock: BlockConfig = { field: 'operation', value: ['create_account', 'update_account', 'create_opportunity', 'update_opportunity'], }, + required: { field: 'operation', value: ['create_account', 'create_opportunity'] }, }, { id: 'type', title: 'Type', type: 'short-input', placeholder: 'Type', + mode: 'advanced', condition: { field: 'operation', value: ['create_account', 'update_account'] }, }, { @@ -183,6 +192,7 @@ export const SalesforceBlock: BlockConfig = { title: 'Industry', type: 'short-input', placeholder: 'Industry', + mode: 'advanced', condition: { field: 'operation', value: ['create_account', 'update_account'] }, }, { @@ -207,6 +217,63 @@ export const SalesforceBlock: BlockConfig = { title: 'Website', type: 'short-input', placeholder: 'Website', + mode: 'advanced', + condition: { field: 'operation', value: ['create_account', 'update_account'] }, + }, + { + id: 'billingStreet', + title: 'Billing Street', + type: 'short-input', + placeholder: 'Billing street address', + mode: 'advanced', + condition: { field: 'operation', value: ['create_account', 'update_account'] }, + }, + { + id: 'billingCity', + title: 'Billing City', + type: 'short-input', + placeholder: 'Billing city', + mode: 'advanced', + condition: { field: 'operation', value: ['create_account', 'update_account'] }, + }, + { + id: 'billingState', + title: 'Billing State', + type: 'short-input', + placeholder: 'Billing state/province', + mode: 'advanced', + condition: { field: 'operation', value: ['create_account', 'update_account'] }, + }, + { + id: 'billingPostalCode', + title: 'Billing Postal Code', + type: 'short-input', + placeholder: 'Billing postal code', + mode: 'advanced', + condition: { field: 'operation', value: ['create_account', 'update_account'] }, + }, + { + id: 'billingCountry', + title: 'Billing Country', + type: 'short-input', + placeholder: 'Billing country', + mode: 'advanced', + condition: { field: 'operation', value: ['create_account', 'update_account'] }, + }, + { + id: 'annualRevenue', + title: 'Annual Revenue', + type: 'short-input', + placeholder: 'Annual revenue (number)', + mode: 'advanced', + condition: { field: 'operation', value: ['create_account', 'update_account'] }, + }, + { + id: 'numberOfEmployees', + title: 'Number of Employees', + type: 'short-input', + placeholder: 'Employee count (integer)', + mode: 'advanced', condition: { field: 'operation', value: ['create_account', 'update_account'] }, }, // Contact fields @@ -217,8 +284,9 @@ export const SalesforceBlock: BlockConfig = { placeholder: 'Contact ID', condition: { field: 'operation', - value: ['get_contacts', 'update_contact', 'delete_contact', 'create_case'], + value: ['get_contacts', 'update_contact', 'delete_contact', 'create_case', 'update_case'], }, + required: { field: 'operation', value: ['update_contact', 'delete_contact'] }, }, { id: 'lastName', @@ -229,6 +297,7 @@ export const SalesforceBlock: BlockConfig = { field: 'operation', value: ['create_contact', 'update_contact', 'create_lead', 'update_lead'], }, + required: { field: 'operation', value: ['create_contact', 'create_lead'] }, }, { id: 'firstName', @@ -260,6 +329,54 @@ export const SalesforceBlock: BlockConfig = { value: ['create_contact', 'update_contact', 'create_lead', 'update_lead'], }, }, + { + id: 'department', + title: 'Department', + type: 'short-input', + placeholder: 'Department', + mode: 'advanced', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + { + id: 'mailingStreet', + title: 'Mailing Street', + type: 'short-input', + placeholder: 'Mailing street address', + mode: 'advanced', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + { + id: 'mailingCity', + title: 'Mailing City', + type: 'short-input', + placeholder: 'Mailing city', + mode: 'advanced', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + { + id: 'mailingState', + title: 'Mailing State', + type: 'short-input', + placeholder: 'Mailing state/province', + mode: 'advanced', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + { + id: 'mailingPostalCode', + title: 'Mailing Postal Code', + type: 'short-input', + placeholder: 'Mailing postal code', + mode: 'advanced', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + { + id: 'mailingCountry', + title: 'Mailing Country', + type: 'short-input', + placeholder: 'Mailing country', + mode: 'advanced', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, // Lead fields { id: 'leadId', @@ -267,6 +384,7 @@ export const SalesforceBlock: BlockConfig = { type: 'short-input', placeholder: 'Lead ID', condition: { field: 'operation', value: ['get_leads', 'update_lead', 'delete_lead'] }, + required: { field: 'operation', value: ['update_lead', 'delete_lead'] }, }, { id: 'company', @@ -274,6 +392,7 @@ export const SalesforceBlock: BlockConfig = { type: 'short-input', placeholder: 'Company name', condition: { field: 'operation', value: ['create_lead', 'update_lead'] }, + required: { field: 'operation', value: ['create_lead'] }, }, { id: 'status', @@ -297,6 +416,7 @@ export const SalesforceBlock: BlockConfig = { title: 'Lead Source', type: 'short-input', placeholder: 'Lead source', + mode: 'advanced', condition: { field: 'operation', value: ['create_lead', 'update_lead'] }, }, // Opportunity fields @@ -309,6 +429,7 @@ export const SalesforceBlock: BlockConfig = { field: 'operation', value: ['get_opportunities', 'update_opportunity', 'delete_opportunity'], }, + required: { field: 'operation', value: ['update_opportunity', 'delete_opportunity'] }, }, { id: 'stageName', @@ -316,6 +437,7 @@ export const SalesforceBlock: BlockConfig = { type: 'short-input', placeholder: 'Stage name', condition: { field: 'operation', value: ['create_opportunity', 'update_opportunity'] }, + required: { field: 'operation', value: ['create_opportunity'] }, }, { id: 'closeDate', @@ -349,6 +471,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'Probability', type: 'short-input', placeholder: 'Win probability (0-100)', + mode: 'advanced', condition: { field: 'operation', value: ['create_opportunity', 'update_opportunity'] }, }, // Case fields @@ -358,6 +481,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n type: 'short-input', placeholder: 'Case ID', condition: { field: 'operation', value: ['get_cases', 'update_case', 'delete_case'] }, + required: { field: 'operation', value: ['update_case', 'delete_case'] }, }, { id: 'subject', @@ -368,6 +492,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n field: 'operation', value: ['create_case', 'update_case', 'create_task', 'update_task'], }, + required: { field: 'operation', value: ['create_case', 'create_task'] }, }, { id: 'priority', @@ -384,7 +509,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'Origin', type: 'short-input', placeholder: 'Origin (e.g., Phone, Email, Web)', - condition: { field: 'operation', value: ['create_case'] }, + condition: { field: 'operation', value: ['create_case', 'update_case'] }, + mode: 'advanced', }, // Task fields { @@ -393,6 +519,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n type: 'short-input', placeholder: 'Task ID', condition: { field: 'operation', value: ['get_tasks', 'update_task', 'delete_task'] }, + required: { field: 'operation', value: ['update_task', 'delete_task'] }, }, { id: 'activityDate', @@ -418,14 +545,16 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'Related Contact/Lead ID', type: 'short-input', placeholder: 'Contact or Lead ID', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'update_task'] }, + mode: 'advanced', }, { id: 'whatId', title: 'Related Account/Opportunity ID', type: 'short-input', placeholder: 'Account or Opportunity ID', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'update_task'] }, + mode: 'advanced', }, // Report fields { @@ -436,25 +565,24 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n condition: { field: 'operation', value: ['get_report', 'run_report'] }, required: true, }, - { - id: 'folderName', - title: 'Folder Name', - type: 'short-input', - placeholder: 'Filter by folder name', - condition: { field: 'operation', value: ['list_reports', 'list_dashboards'] }, - }, { id: 'searchTerm', title: 'Search Term', type: 'short-input', placeholder: 'Search reports by name', + mode: 'advanced', condition: { field: 'operation', value: ['list_reports'] }, }, { id: 'includeDetails', title: 'Include Details', - type: 'short-input', - placeholder: 'Include detail rows (true/false)', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + mode: 'advanced', condition: { field: 'operation', value: ['run_report'] }, }, { @@ -462,6 +590,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'Report Filters', type: 'long-input', placeholder: 'JSON array of report filters', + mode: 'advanced', condition: { field: 'operation', value: ['run_report'] }, }, // Dashboard fields @@ -504,6 +633,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'Description', type: 'long-input', placeholder: 'Description', + mode: 'advanced', condition: { field: 'operation', value: [ @@ -662,7 +792,11 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n }, outputs: { success: { type: 'boolean', description: 'Operation success status' }, - output: { type: 'json', description: 'Operation result data' }, + output: { + type: 'json', + description: + 'Operation result: sObject record(s) for get/create/update/delete ops (accounts, contacts, leads, opportunities, cases, tasks); report/dashboard payloads for analytics ops; records[] + paging for SOQL query ops; sObject schema for describe/list-objects ops', + }, }, } diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index dbad1771b0e..c846bcdaf35 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -13153,7 +13153,7 @@ }, { "name": "Get Leads", - "description": "Get lead(s) from Salesforce" + "description": "Retrieve lead(s) from Salesforce CRM" }, { "name": "Create Lead", @@ -13221,7 +13221,7 @@ }, { "name": "Get Report", - "description": "Get metadata and describe information for a specific report" + "description": "Get the describe (definition and metadata) for a specific report" }, { "name": "Run Report", diff --git a/apps/sim/tools/salesforce/create_account.ts b/apps/sim/tools/salesforce/create_account.ts index 275375f531d..7bb6036dd1f 100644 --- a/apps/sim/tools/salesforce/create_account.ts +++ b/apps/sim/tools/salesforce/create_account.ts @@ -1,13 +1,11 @@ -import { createLogger } from '@sim/logger' import type { SalesforceCreateAccountParams, SalesforceCreateAccountResponse, } from '@/tools/salesforce/types' import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' +import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' -const logger = createLogger('SalesforceCreateAccount') - export const salesforceCreateAccountTool: ToolConfig< SalesforceCreateAccountParams, SalesforceCreateAccountResponse @@ -120,39 +118,7 @@ export const salesforceCreateAccountTool: ToolConfig< request: { url: (params) => { - let instanceUrl = params.instanceUrl - - if (!instanceUrl && params.idToken) { - try { - const base64Url = params.idToken.split('.')[1] - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') - const jsonPayload = decodeURIComponent( - atob(base64) - .split('') - .map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`) - .join('') - ) - const decoded = JSON.parse(jsonPayload) - - if (decoded.profile) { - const match = decoded.profile.match(/^(https:\/\/[^/]+)/) - if (match) { - instanceUrl = match[1] - } - } else if (decoded.sub) { - const match = decoded.sub.match(/^(https:\/\/[^/]+)/) - if (match && match[1] !== 'https://login.salesforce.com') { - instanceUrl = match[1] - } - } - } catch (error) { - logger.error('Failed to decode Salesforce idToken', { error }) - } - } - - if (!instanceUrl) { - throw new Error('Salesforce instance URL is required but not provided') - } + const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) return `${instanceUrl}/services/data/v59.0/sobjects/Account` }, @@ -194,16 +160,17 @@ export const salesforceCreateAccountTool: ToolConfig< const data = await response.json() if (!response.ok) { - logger.error('Salesforce API request failed', { data, status: response.status }) - throw new Error(data[0]?.message || data.message || 'Failed to create account in Salesforce') + throw new Error( + extractErrorMessage(data, response.status, 'Failed to create account in Salesforce') + ) } return { success: true, output: { id: data.id, - success: data.success, - created: true, + success: data.success === true, + created: data.success === true, }, } }, diff --git a/apps/sim/tools/salesforce/create_case.ts b/apps/sim/tools/salesforce/create_case.ts index 33e60d0da9c..364605e610c 100644 --- a/apps/sim/tools/salesforce/create_case.ts +++ b/apps/sim/tools/salesforce/create_case.ts @@ -3,7 +3,7 @@ import type { SalesforceCreateCaseResponse, } from '@/tools/salesforce/types' import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceCreateCaseTool: ToolConfig< @@ -93,8 +93,8 @@ export const salesforceCreateCaseTool: ToolConfig< if (params.status) body.Status = params.status if (params.priority) body.Priority = params.priority if (params.origin) body.Origin = params.origin - if (params.contactId) body.ContactId = params.contactId - if (params.accountId) body.AccountId = params.accountId + if (params.contactId) body.ContactId = params.contactId.trim() + if (params.accountId) body.AccountId = params.accountId.trim() if (params.description) body.Description = params.description return body }, @@ -102,13 +102,14 @@ export const salesforceCreateCaseTool: ToolConfig< transformResponse: async (response) => { const data = await response.json() - if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to create case') + if (!response.ok) + throw new Error(extractErrorMessage(data, response.status, 'Failed to create case')) return { success: true, output: { id: data.id, - success: data.success, - created: true, + success: data.success === true, + created: data.success === true, }, } }, diff --git a/apps/sim/tools/salesforce/create_contact.ts b/apps/sim/tools/salesforce/create_contact.ts index 01c8c4831ee..0ba539f2471 100644 --- a/apps/sim/tools/salesforce/create_contact.ts +++ b/apps/sim/tools/salesforce/create_contact.ts @@ -4,7 +4,7 @@ import type { SalesforceCreateContactResponse, } from '@/tools/salesforce/types' import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -115,7 +115,7 @@ export const salesforceCreateContactTool: ToolConfig< if (params.firstName) body.FirstName = params.firstName if (params.email) body.Email = params.email if (params.phone) body.Phone = params.phone - if (params.accountId) body.AccountId = params.accountId + if (params.accountId) body.AccountId = params.accountId.trim() if (params.title) body.Title = params.title if (params.department) body.Department = params.department if (params.mailingStreet) body.MailingStreet = params.mailingStreet @@ -134,15 +134,17 @@ export const salesforceCreateContactTool: ToolConfig< if (!response.ok) { logger.error('Salesforce API request failed', { data, status: response.status }) - throw new Error(data[0]?.message || data.message || 'Failed to create contact in Salesforce') + throw new Error( + extractErrorMessage(data, response.status, 'Failed to create contact in Salesforce') + ) } return { success: true, output: { id: data.id, - success: data.success, - created: true, + success: data.success === true, + created: data.success === true, }, } }, diff --git a/apps/sim/tools/salesforce/create_lead.ts b/apps/sim/tools/salesforce/create_lead.ts index c6d0d4ed5c9..666673d8800 100644 --- a/apps/sim/tools/salesforce/create_lead.ts +++ b/apps/sim/tools/salesforce/create_lead.ts @@ -3,7 +3,7 @@ import type { SalesforceCreateLeadResponse, } from '@/tools/salesforce/types' import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceCreateLeadTool: ToolConfig< @@ -98,13 +98,14 @@ export const salesforceCreateLeadTool: ToolConfig< transformResponse: async (response) => { const data = await response.json() - if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to create lead') + if (!response.ok) + throw new Error(extractErrorMessage(data, response.status, 'Failed to create lead')) return { success: true, output: { id: data.id, - success: data.success, - created: true, + success: data.success === true, + created: data.success === true, }, } }, diff --git a/apps/sim/tools/salesforce/create_opportunity.ts b/apps/sim/tools/salesforce/create_opportunity.ts index a0d971cdca5..f1600a8907b 100644 --- a/apps/sim/tools/salesforce/create_opportunity.ts +++ b/apps/sim/tools/salesforce/create_opportunity.ts @@ -1,11 +1,14 @@ +import { createLogger } from '@sim/logger' import type { SalesforceCreateOpportunityParams, SalesforceCreateOpportunityResponse, } from '@/tools/salesforce/types' import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('SalesforceCreateOpportunity') + export const salesforceCreateOpportunityTool: ToolConfig< SalesforceCreateOpportunityParams, SalesforceCreateOpportunityResponse @@ -82,7 +85,7 @@ export const salesforceCreateOpportunityTool: ToolConfig< StageName: params.stageName, CloseDate: params.closeDate, } - if (params.accountId) body.AccountId = params.accountId + if (params.accountId) body.AccountId = params.accountId.trim() if (params.amount) body.Amount = Number.parseFloat(params.amount) if (params.probability) body.Probability = Number.parseInt(params.probability) if (params.description) body.Description = params.description @@ -92,14 +95,16 @@ export const salesforceCreateOpportunityTool: ToolConfig< transformResponse: async (response) => { const data = await response.json() - if (!response.ok) - throw new Error(data[0]?.message || data.message || 'Failed to create opportunity') + if (!response.ok) { + logger.error('Failed to create opportunity', { data, status: response.status }) + throw new Error(extractErrorMessage(data, response.status, 'Failed to create opportunity')) + } return { success: true, output: { id: data.id, - success: data.success, - created: true, + success: data.success === true, + created: data.success === true, }, } }, diff --git a/apps/sim/tools/salesforce/create_task.ts b/apps/sim/tools/salesforce/create_task.ts index 40286d0fcde..2eef3adc9c2 100644 --- a/apps/sim/tools/salesforce/create_task.ts +++ b/apps/sim/tools/salesforce/create_task.ts @@ -3,7 +3,7 @@ import type { SalesforceCreateTaskResponse, } from '@/tools/salesforce/types' import { SOBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceCreateTaskTool: ToolConfig< @@ -93,8 +93,8 @@ export const salesforceCreateTaskTool: ToolConfig< if (params.status) body.Status = params.status if (params.priority) body.Priority = params.priority if (params.activityDate) body.ActivityDate = params.activityDate - if (params.whoId) body.WhoId = params.whoId - if (params.whatId) body.WhatId = params.whatId + if (params.whoId) body.WhoId = params.whoId.trim() + if (params.whatId) body.WhatId = params.whatId.trim() if (params.description) body.Description = params.description return body }, @@ -102,13 +102,14 @@ export const salesforceCreateTaskTool: ToolConfig< transformResponse: async (response) => { const data = await response.json() - if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to create task') + if (!response.ok) + throw new Error(extractErrorMessage(data, response.status, 'Failed to create task')) return { success: true, output: { id: data.id, - success: data.success, - created: true, + success: data.success === true, + created: data.success === true, }, } }, diff --git a/apps/sim/tools/salesforce/delete_account.ts b/apps/sim/tools/salesforce/delete_account.ts index cb691bf5678..f43a15eb60b 100644 --- a/apps/sim/tools/salesforce/delete_account.ts +++ b/apps/sim/tools/salesforce/delete_account.ts @@ -1,13 +1,11 @@ -import { createLogger } from '@sim/logger' import type { SalesforceDeleteAccountParams, SalesforceDeleteAccountResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' -const logger = createLogger('SalesforceDeleteAccount') - export const salesforceDeleteAccountTool: ToolConfig< SalesforceDeleteAccountParams, SalesforceDeleteAccountResponse @@ -48,41 +46,10 @@ export const salesforceDeleteAccountTool: ToolConfig< request: { url: (params) => { - let instanceUrl = params.instanceUrl - - if (!instanceUrl && params.idToken) { - try { - const base64Url = params.idToken.split('.')[1] - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') - const jsonPayload = decodeURIComponent( - atob(base64) - .split('') - .map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`) - .join('') - ) - const decoded = JSON.parse(jsonPayload) - - if (decoded.profile) { - const match = decoded.profile.match(/^(https:\/\/[^/]+)/) - if (match) { - instanceUrl = match[1] - } - } else if (decoded.sub) { - const match = decoded.sub.match(/^(https:\/\/[^/]+)/) - if (match && match[1] !== 'https://login.salesforce.com') { - instanceUrl = match[1] - } - } - } catch (error) { - logger.error('Failed to decode Salesforce idToken', { error }) - } - } - - if (!instanceUrl) { - throw new Error('Salesforce instance URL is required but not provided') - } + const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) + const accountId = requireId(params.accountId, 'Account ID') - return `${instanceUrl}/services/data/v59.0/sobjects/Account/${params.accountId}` + return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, method: 'DELETE', headers: (params) => { @@ -99,16 +66,15 @@ export const salesforceDeleteAccountTool: ToolConfig< transformResponse: async (response: Response, params) => { if (!response.ok) { const data = await response.json().catch(() => ({})) - logger.error('Salesforce API request failed', { data, status: response.status }) throw new Error( - data[0]?.message || data.message || 'Failed to delete account from Salesforce' + extractErrorMessage(data, response.status, 'Failed to delete account from Salesforce') ) } return { success: true, output: { - id: params?.accountId || '', + id: params?.accountId?.trim() || '', deleted: true, }, } diff --git a/apps/sim/tools/salesforce/delete_case.ts b/apps/sim/tools/salesforce/delete_case.ts index ca30065ecfb..2a77f70d609 100644 --- a/apps/sim/tools/salesforce/delete_case.ts +++ b/apps/sim/tools/salesforce/delete_case.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteCaseResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteCaseTool: ToolConfig< @@ -45,8 +45,10 @@ export const salesforceDeleteCaseTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${params.caseId}`, + url: (params) => { + const caseId = requireId(params.caseId, 'Case ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -56,12 +58,12 @@ export const salesforceDeleteCaseTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json().catch(() => ({})) - throw new Error(data[0]?.message || data.message || 'Failed to delete case') + throw new Error(extractErrorMessage(data, response.status, 'Failed to delete case')) } return { success: true, output: { - id: params?.caseId || '', + id: params?.caseId?.trim() || '', deleted: true, }, } diff --git a/apps/sim/tools/salesforce/delete_contact.ts b/apps/sim/tools/salesforce/delete_contact.ts index 49d9026b862..22e9574666e 100644 --- a/apps/sim/tools/salesforce/delete_contact.ts +++ b/apps/sim/tools/salesforce/delete_contact.ts @@ -4,7 +4,7 @@ import type { SalesforceDeleteContactResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -35,7 +35,8 @@ export const salesforceDeleteContactTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${params.contactId}` + const contactId = requireId(params.contactId, 'Contact ID') + return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'DELETE', headers: (params) => ({ @@ -48,14 +49,14 @@ export const salesforceDeleteContactTool: ToolConfig< const data = await response.json().catch(() => ({})) logger.error('Salesforce API request failed', { data, status: response.status }) throw new Error( - data[0]?.message || data.message || 'Failed to delete contact from Salesforce' + extractErrorMessage(data, response.status, 'Failed to delete contact from Salesforce') ) } return { success: true, output: { - id: params?.contactId || '', + id: params?.contactId?.trim() || '', deleted: true, }, } diff --git a/apps/sim/tools/salesforce/delete_lead.ts b/apps/sim/tools/salesforce/delete_lead.ts index e33822e6426..d3d357b3790 100644 --- a/apps/sim/tools/salesforce/delete_lead.ts +++ b/apps/sim/tools/salesforce/delete_lead.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteLeadResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteLeadTool: ToolConfig< @@ -33,8 +33,10 @@ export const salesforceDeleteLeadTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${params.leadId}`, + url: (params) => { + const leadId = requireId(params.leadId, 'Lead ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), }, @@ -42,12 +44,12 @@ export const salesforceDeleteLeadTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json().catch(() => ({})) - throw new Error(data[0]?.message || data.message || 'Failed to delete lead') + throw new Error(extractErrorMessage(data, response.status, 'Failed to delete lead')) } return { success: true, output: { - id: params?.leadId || '', + id: params?.leadId?.trim() || '', deleted: true, }, } diff --git a/apps/sim/tools/salesforce/delete_opportunity.ts b/apps/sim/tools/salesforce/delete_opportunity.ts index 98b136601d8..4d52d7349a1 100644 --- a/apps/sim/tools/salesforce/delete_opportunity.ts +++ b/apps/sim/tools/salesforce/delete_opportunity.ts @@ -1,11 +1,14 @@ +import { createLogger } from '@sim/logger' import type { SalesforceDeleteOpportunityParams, SalesforceDeleteOpportunityResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('SalesforceDeleteOpportunity') + export const salesforceDeleteOpportunityTool: ToolConfig< SalesforceDeleteOpportunityParams, SalesforceDeleteOpportunityResponse @@ -33,8 +36,10 @@ export const salesforceDeleteOpportunityTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${params.opportunityId}`, + url: (params) => { + const opportunityId = requireId(params.opportunityId, 'Opportunity ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), }, @@ -42,12 +47,13 @@ export const salesforceDeleteOpportunityTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json().catch(() => ({})) - throw new Error(data[0]?.message || data.message || 'Failed to delete opportunity') + logger.error('Failed to delete opportunity', { data, status: response.status }) + throw new Error(extractErrorMessage(data, response.status, 'Failed to delete opportunity')) } return { success: true, output: { - id: params?.opportunityId || '', + id: params?.opportunityId?.trim() || '', deleted: true, }, } diff --git a/apps/sim/tools/salesforce/delete_task.ts b/apps/sim/tools/salesforce/delete_task.ts index 061205529d3..372a3dba11b 100644 --- a/apps/sim/tools/salesforce/delete_task.ts +++ b/apps/sim/tools/salesforce/delete_task.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteTaskResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteTaskTool: ToolConfig< @@ -45,8 +45,10 @@ export const salesforceDeleteTaskTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${params.taskId}`, + url: (params) => { + const taskId = requireId(params.taskId, 'Task ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -56,12 +58,12 @@ export const salesforceDeleteTaskTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json().catch(() => ({})) - throw new Error(data[0]?.message || data.message || 'Failed to delete task') + throw new Error(extractErrorMessage(data, response.status, 'Failed to delete task')) } return { success: true, output: { - id: params?.taskId || '', + id: params?.taskId?.trim() || '', deleted: true, }, } diff --git a/apps/sim/tools/salesforce/describe_object.ts b/apps/sim/tools/salesforce/describe_object.ts index 6c993706e93..20fd5efd57f 100644 --- a/apps/sim/tools/salesforce/describe_object.ts +++ b/apps/sim/tools/salesforce/describe_object.ts @@ -47,7 +47,8 @@ export const salesforceDescribeObjectTool: ToolConfig< ) } const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - return `${instanceUrl}/services/data/v59.0/sobjects/${params.objectName}/describe` + const objectName = params.objectName.trim() + return `${instanceUrl}/services/data/v59.0/sobjects/${objectName}/describe` }, method: 'GET', headers: (params) => ({ @@ -71,7 +72,7 @@ export const salesforceDescribeObjectTool: ToolConfig< return { success: true, output: { - objectName: params?.objectName || '', + objectName: data.name ?? params?.objectName ?? '', label: data.label, labelPlural: data.labelPlural, fields: data.fields, diff --git a/apps/sim/tools/salesforce/get_accounts.ts b/apps/sim/tools/salesforce/get_accounts.ts index c94d899aaf6..575039102b4 100644 --- a/apps/sim/tools/salesforce/get_accounts.ts +++ b/apps/sim/tools/salesforce/get_accounts.ts @@ -1,13 +1,11 @@ -import { createLogger } from '@sim/logger' import type { SalesforceGetAccountsParams, SalesforceGetAccountsResponse, } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' +import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' -const logger = createLogger('SalesforceGetAccounts') - export const salesforceGetAccountsTool: ToolConfig< SalesforceGetAccountsParams, SalesforceGetAccountsResponse @@ -63,39 +61,7 @@ export const salesforceGetAccountsTool: ToolConfig< request: { url: (params) => { - let instanceUrl = params.instanceUrl - - if (!instanceUrl && params.idToken) { - try { - const base64Url = params.idToken.split('.')[1] - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') - const jsonPayload = decodeURIComponent( - atob(base64) - .split('') - .map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`) - .join('') - ) - const decoded = JSON.parse(jsonPayload) - - if (decoded.profile) { - const match = decoded.profile.match(/^(https:\/\/[^/]+)/) - if (match) { - instanceUrl = match[1] - } - } else if (decoded.sub) { - const match = decoded.sub.match(/^(https:\/\/[^/]+)/) - if (match && match[1] !== 'https://login.salesforce.com') { - instanceUrl = match[1] - } - } - } catch (error) { - logger.error('Failed to decode Salesforce idToken', { error }) - } - } - - if (!instanceUrl) { - throw new Error('Salesforce instance URL is required but not provided') - } + const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) const limit = params.limit ? Number.parseInt(params.limit) : 100 const fields = @@ -126,9 +92,8 @@ export const salesforceGetAccountsTool: ToolConfig< const data = await response.json() if (!response.ok) { - logger.error('Salesforce API request failed', { data, status: response.status }) throw new Error( - data[0]?.message || data.message || 'Failed to fetch accounts from Salesforce' + extractErrorMessage(data, response.status, 'Failed to fetch accounts from Salesforce') ) } diff --git a/apps/sim/tools/salesforce/get_cases.ts b/apps/sim/tools/salesforce/get_cases.ts index a8e72dd0b04..cd45a33c064 100644 --- a/apps/sim/tools/salesforce/get_cases.ts +++ b/apps/sim/tools/salesforce/get_cases.ts @@ -1,6 +1,6 @@ import type { SalesforceGetCasesParams, SalesforceGetCasesResponse } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetCasesTool: ToolConfig< @@ -52,9 +52,10 @@ export const salesforceGetCasesTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.caseId) { + const caseId = requireId(params.caseId, 'Case ID') const fields = params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId' - return `${instanceUrl}/services/data/v59.0/sobjects/Case/${params.caseId}?fields=${fields}` + return `${instanceUrl}/services/data/v59.0/sobjects/Case/${caseId}?fields=${encodeURIComponent(fields)}` } const limit = params.limit ? Number.parseInt(params.limit) : 100 const fields = @@ -72,7 +73,8 @@ export const salesforceGetCasesTool: ToolConfig< transformResponse: async (response, params?) => { const data = await response.json() - if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to fetch cases') + if (!response.ok) + throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch cases')) if (params?.caseId) { return { success: true, diff --git a/apps/sim/tools/salesforce/get_contacts.ts b/apps/sim/tools/salesforce/get_contacts.ts index 34344630745..acfbdc50170 100644 --- a/apps/sim/tools/salesforce/get_contacts.ts +++ b/apps/sim/tools/salesforce/get_contacts.ts @@ -4,7 +4,7 @@ import type { SalesforceGetContactsResponse, } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -60,9 +60,10 @@ export const salesforceGetContactsTool: ToolConfig< // Single contact by ID if (params.contactId) { + const contactId = requireId(params.contactId, 'Contact ID') const fields = params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department' - return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${params.contactId}?fields=${fields}` + return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}?fields=${encodeURIComponent(fields)}` } // List contacts with SOQL query @@ -87,7 +88,7 @@ export const salesforceGetContactsTool: ToolConfig< if (!response.ok) { logger.error('Salesforce API request failed', { data, status: response.status }) throw new Error( - data[0]?.message || data.message || 'Failed to fetch contacts from Salesforce' + extractErrorMessage(data, response.status, 'Failed to fetch contacts from Salesforce') ) } diff --git a/apps/sim/tools/salesforce/get_dashboard.ts b/apps/sim/tools/salesforce/get_dashboard.ts index 9bf280107ef..41549a684a7 100644 --- a/apps/sim/tools/salesforce/get_dashboard.ts +++ b/apps/sim/tools/salesforce/get_dashboard.ts @@ -66,15 +66,18 @@ export const salesforceGetDashboardTool: ToolConfig< throw new Error(errorMessage) } + const meta = data.dashboardMetadata ?? {} + const attrs = meta.attributes ?? {} + return { success: true, output: { dashboard: data, - dashboardId: params?.dashboardId || '', + dashboardId: attrs.dashboardId ?? params?.dashboardId ?? '', components: data.componentData || [], - dashboardName: data.name ?? null, - folderId: data.folderId ?? null, - runningUser: data.runningUser ?? null, + dashboardName: attrs.dashboardName ?? null, + dashboardMetadata: data.dashboardMetadata ?? null, + runningUser: meta.runningUser ?? null, success: true, }, } diff --git a/apps/sim/tools/salesforce/get_leads.ts b/apps/sim/tools/salesforce/get_leads.ts index c028ccc5c54..e49afe9a0c5 100644 --- a/apps/sim/tools/salesforce/get_leads.ts +++ b/apps/sim/tools/salesforce/get_leads.ts @@ -1,6 +1,6 @@ import type { SalesforceGetLeadsParams, SalesforceGetLeadsResponse } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetLeadsTool: ToolConfig< @@ -9,7 +9,7 @@ export const salesforceGetLeadsTool: ToolConfig< > = { id: 'salesforce_get_leads', name: 'Get Leads from Salesforce', - description: 'Get lead(s) from Salesforce', + description: 'Retrieve lead(s) from Salesforce CRM', version: '1.0.0', oauth: { @@ -52,9 +52,10 @@ export const salesforceGetLeadsTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.leadId) { + const leadId = requireId(params.leadId, 'Lead ID') const fields = params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource' - return `${instanceUrl}/services/data/v59.0/sobjects/Lead/${params.leadId}?fields=${fields}` + return `${instanceUrl}/services/data/v59.0/sobjects/Lead/${leadId}?fields=${encodeURIComponent(fields)}` } const limit = params.limit ? Number.parseInt(params.limit) : 100 const fields = params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource' @@ -71,7 +72,8 @@ export const salesforceGetLeadsTool: ToolConfig< transformResponse: async (response, params?) => { const data = await response.json() - if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to fetch leads') + if (!response.ok) + throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch leads')) if (params?.leadId) { return { success: true, diff --git a/apps/sim/tools/salesforce/get_opportunities.ts b/apps/sim/tools/salesforce/get_opportunities.ts index fd46d97b5c9..b4a86a2dda5 100644 --- a/apps/sim/tools/salesforce/get_opportunities.ts +++ b/apps/sim/tools/salesforce/get_opportunities.ts @@ -1,11 +1,14 @@ +import { createLogger } from '@sim/logger' import type { SalesforceGetOpportunitiesParams, SalesforceGetOpportunitiesResponse, } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('SalesforceGetOpportunities') + export const salesforceGetOpportunitiesTool: ToolConfig< SalesforceGetOpportunitiesParams, SalesforceGetOpportunitiesResponse @@ -55,8 +58,9 @@ export const salesforceGetOpportunitiesTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.opportunityId) { + const opportunityId = requireId(params.opportunityId, 'Opportunity ID') const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability' - return `${instanceUrl}/services/data/v59.0/sobjects/Opportunity/${params.opportunityId}?fields=${fields}` + return `${instanceUrl}/services/data/v59.0/sobjects/Opportunity/${opportunityId}?fields=${encodeURIComponent(fields)}` } const limit = params.limit ? Number.parseInt(params.limit) : 100 const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability' @@ -73,8 +77,10 @@ export const salesforceGetOpportunitiesTool: ToolConfig< transformResponse: async (response, params?) => { const data = await response.json() - if (!response.ok) - throw new Error(data[0]?.message || data.message || 'Failed to fetch opportunities') + if (!response.ok) { + logger.error('Failed to fetch opportunities', { data, status: response.status }) + throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch opportunities')) + } if (params?.opportunityId) { return { success: true, diff --git a/apps/sim/tools/salesforce/get_report.ts b/apps/sim/tools/salesforce/get_report.ts index 48f4eca335c..2d68d7a8d1f 100644 --- a/apps/sim/tools/salesforce/get_report.ts +++ b/apps/sim/tools/salesforce/get_report.ts @@ -10,8 +10,8 @@ import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceReports') /** - * Get metadata for a specific report - * @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportmetadata.htm + * Get the describe (definition and metadata) for a specific report + * @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/analytics_api_report_describe.htm */ export const salesforceGetReportTool: ToolConfig< SalesforceGetReportParams, @@ -19,7 +19,7 @@ export const salesforceGetReportTool: ToolConfig< > = { id: 'salesforce_get_report', name: 'Get Report Metadata from Salesforce', - description: 'Get metadata and describe information for a specific report', + description: 'Get the describe (definition and metadata) for a specific report', version: '1.0.0', oauth: { diff --git a/apps/sim/tools/salesforce/get_tasks.ts b/apps/sim/tools/salesforce/get_tasks.ts index 36b4bc1093a..50817f9b12d 100644 --- a/apps/sim/tools/salesforce/get_tasks.ts +++ b/apps/sim/tools/salesforce/get_tasks.ts @@ -1,6 +1,6 @@ import type { SalesforceGetTasksParams, SalesforceGetTasksResponse } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetTasksTool: ToolConfig< @@ -64,9 +64,10 @@ export const salesforceGetTasksTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.taskId) { + const taskId = requireId(params.taskId, 'Task ID') const fields = params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId' - return `${instanceUrl}/services/data/v59.0/sobjects/Task/${params.taskId}?fields=${fields}` + return `${instanceUrl}/services/data/v59.0/sobjects/Task/${taskId}?fields=${encodeURIComponent(fields)}` } const limit = params.limit ? Number.parseInt(params.limit) : 100 const fields = params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId' @@ -83,7 +84,8 @@ export const salesforceGetTasksTool: ToolConfig< transformResponse: async (response, params?) => { const data = await response.json() - if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to fetch tasks') + if (!response.ok) + throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch tasks')) if (params?.taskId) { return { success: true, diff --git a/apps/sim/tools/salesforce/list_dashboards.ts b/apps/sim/tools/salesforce/list_dashboards.ts index 17f1be1666c..8cc519e3302 100644 --- a/apps/sim/tools/salesforce/list_dashboards.ts +++ b/apps/sim/tools/salesforce/list_dashboards.ts @@ -31,12 +31,6 @@ export const salesforceListDashboardsTool: ToolConfig< accessToken: { type: 'string', required: true, visibility: 'hidden' }, idToken: { type: 'string', required: false, visibility: 'hidden' }, instanceUrl: { type: 'string', required: false, visibility: 'hidden' }, - folderName: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Filter dashboards by folder name (case-insensitive partial match)', - }, }, request: { @@ -51,7 +45,7 @@ export const salesforceListDashboardsTool: ToolConfig< }), }, - transformResponse: async (response, params?) => { + transformResponse: async (response) => { const data = await response.json() if (!response.ok) { const errorMessage = extractErrorMessage( @@ -63,14 +57,13 @@ export const salesforceListDashboardsTool: ToolConfig< throw new Error(errorMessage) } - let dashboards = data.dashboards || data || [] - - // Filter by folder name if provided - if (params?.folderName) { - dashboards = dashboards.filter((dashboard: any) => - dashboard.folderName?.toLowerCase().includes(params.folderName!.toLowerCase()) - ) - } + // GET /analytics/dashboards returns a bare top-level array of dashboard objects; + // fall back to a `dashboards` wrapper defensively in case the shape varies by org. + const dashboards = Array.isArray(data) + ? data + : Array.isArray(data?.dashboards) + ? data.dashboards + : [] return { success: true, diff --git a/apps/sim/tools/salesforce/list_reports.ts b/apps/sim/tools/salesforce/list_reports.ts index 53492a0584a..211290f2e35 100644 --- a/apps/sim/tools/salesforce/list_reports.ts +++ b/apps/sim/tools/salesforce/list_reports.ts @@ -31,17 +31,11 @@ export const salesforceListReportsTool: ToolConfig< accessToken: { type: 'string', required: true, visibility: 'hidden' }, idToken: { type: 'string', required: false, visibility: 'hidden' }, instanceUrl: { type: 'string', required: false, visibility: 'hidden' }, - folderName: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Filter reports by folder name (case-insensitive partial match)', - }, searchTerm: { type: 'string', required: false, visibility: 'user-or-llm', - description: 'Search term to filter reports by name or description', + description: 'Filter reports by name (case-insensitive partial match)', }, }, @@ -69,21 +63,15 @@ export const salesforceListReportsTool: ToolConfig< throw new Error(errorMessage) } - let reports = data || [] + // GET /analytics/reports returns a bare top-level array of report objects, + // each with name, id, url, describeUrl, and instancesUrl. + let reports = Array.isArray(data) ? data : [] - // Filter by folder name if provided - if (params?.folderName) { - reports = reports.filter((report: any) => - report.folderName?.toLowerCase().includes(params.folderName!.toLowerCase()) - ) - } - - // Filter by search term if provided + // The list resource only returns the report name (no folder/description), + // so searchTerm can only match against the report name. if (params?.searchTerm) { - reports = reports.filter( - (report: any) => - report.name?.toLowerCase().includes(params.searchTerm!.toLowerCase()) || - report.description?.toLowerCase().includes(params.searchTerm!.toLowerCase()) + reports = reports.filter((report: any) => + report.name?.toLowerCase().includes(params.searchTerm!.toLowerCase()) ) } diff --git a/apps/sim/tools/salesforce/query.ts b/apps/sim/tools/salesforce/query.ts index f8906e97dee..b07c38cccb4 100644 --- a/apps/sim/tools/salesforce/query.ts +++ b/apps/sim/tools/salesforce/query.ts @@ -64,18 +64,19 @@ export const salesforceQueryTool: ToolConfig @@ -1175,9 +1153,9 @@ export const DASHBOARD_OUTPUT_PROPERTIES = { description: 'Display name of the dashboard', optional: true, }, - folderId: { - type: 'string', - description: 'ID of the folder containing the dashboard', + dashboardMetadata: { + type: 'object', + description: 'Structured dashboard metadata (attributes, component definitions, layout)', optional: true, }, runningUser: { @@ -1201,7 +1179,12 @@ export const REFRESH_DASHBOARD_OUTPUT_PROPERTIES = { }, status: { type: 'object', - description: 'Dashboard refresh status information', + description: 'Dashboard refresh status (dashboardStatus), when returned by the refresh', + optional: true, + }, + statusUrl: { + type: 'string', + description: 'URL of the status resource to poll for refresh completion', optional: true, }, dashboardName: { @@ -1209,9 +1192,9 @@ export const REFRESH_DASHBOARD_OUTPUT_PROPERTIES = { description: 'Display name of the dashboard', optional: true, }, - refreshDate: { - type: 'string', - description: 'ISO 8601 timestamp when the dashboard was last refreshed', + dashboardMetadata: { + type: 'object', + description: 'Structured dashboard metadata (attributes, component definitions, layout)', optional: true, }, success: { type: 'boolean', description: 'Salesforce operation success' }, @@ -1612,6 +1595,9 @@ export interface SalesforceUpdateCaseParams extends BaseSalesforceParams { subject?: string status?: string priority?: string + origin?: string + contactId?: string + accountId?: string description?: string } @@ -1681,6 +1667,8 @@ export interface SalesforceUpdateTaskParams extends BaseSalesforceParams { status?: string priority?: string activityDate?: string + whoId?: string + whatId?: string description?: string } @@ -1705,7 +1693,6 @@ export interface SalesforceDeleteTaskResponse { } export interface SalesforceListReportsParams extends BaseSalesforceParams { - folderName?: string searchTerm?: string } @@ -1765,9 +1752,7 @@ export interface SalesforceListReportTypesResponse { } } -export interface SalesforceListDashboardsParams extends BaseSalesforceParams { - folderName?: string -} +export type SalesforceListDashboardsParams = BaseSalesforceParams export interface SalesforceListDashboardsResponse { success: boolean @@ -1789,7 +1774,7 @@ export interface SalesforceGetDashboardResponse { dashboardId: string components: any[] dashboardName?: string - folderId?: string + dashboardMetadata?: any runningUser?: any success: boolean } @@ -1806,8 +1791,9 @@ export interface SalesforceRefreshDashboardResponse { dashboardId: string components: any[] status?: any + statusUrl?: string dashboardName?: string - refreshDate?: string + dashboardMetadata?: any success: boolean } } diff --git a/apps/sim/tools/salesforce/update_account.ts b/apps/sim/tools/salesforce/update_account.ts index debb61b417e..8fc579c8b96 100644 --- a/apps/sim/tools/salesforce/update_account.ts +++ b/apps/sim/tools/salesforce/update_account.ts @@ -1,13 +1,11 @@ -import { createLogger } from '@sim/logger' import type { SalesforceUpdateAccountParams, SalesforceUpdateAccountResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' -const logger = createLogger('SalesforceUpdateAccount') - export const salesforceUpdateAccountTool: ToolConfig< SalesforceUpdateAccountParams, SalesforceUpdateAccountResponse @@ -126,41 +124,10 @@ export const salesforceUpdateAccountTool: ToolConfig< request: { url: (params) => { - let instanceUrl = params.instanceUrl - - if (!instanceUrl && params.idToken) { - try { - const base64Url = params.idToken.split('.')[1] - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') - const jsonPayload = decodeURIComponent( - atob(base64) - .split('') - .map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`) - .join('') - ) - const decoded = JSON.parse(jsonPayload) - - if (decoded.profile) { - const match = decoded.profile.match(/^(https:\/\/[^/]+)/) - if (match) { - instanceUrl = match[1] - } - } else if (decoded.sub) { - const match = decoded.sub.match(/^(https:\/\/[^/]+)/) - if (match && match[1] !== 'https://login.salesforce.com') { - instanceUrl = match[1] - } - } - } catch (error) { - logger.error('Failed to decode Salesforce idToken', { error }) - } - } - - if (!instanceUrl) { - throw new Error('Salesforce instance URL is required but not provided') - } + const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) + const accountId = requireId(params.accountId, 'Account ID') - return `${instanceUrl}/services/data/v59.0/sobjects/Account/${params.accountId}` + return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, method: 'PATCH', headers: (params) => { @@ -198,14 +165,15 @@ export const salesforceUpdateAccountTool: ToolConfig< transformResponse: async (response: Response, params) => { if (!response.ok) { const data = await response.json() - logger.error('Salesforce API request failed', { data, status: response.status }) - throw new Error(data[0]?.message || data.message || 'Failed to update account in Salesforce') + throw new Error( + extractErrorMessage(data, response.status, 'Failed to update account in Salesforce') + ) } return { success: true, output: { - id: params?.accountId || '', + id: params?.accountId?.trim() || '', updated: true, }, } diff --git a/apps/sim/tools/salesforce/update_case.ts b/apps/sim/tools/salesforce/update_case.ts index ed997c5eddc..604285f0923 100644 --- a/apps/sim/tools/salesforce/update_case.ts +++ b/apps/sim/tools/salesforce/update_case.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateCaseResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateCaseTool: ToolConfig< @@ -60,6 +60,24 @@ export const salesforceUpdateCaseTool: ToolConfig< visibility: 'user-or-llm', description: 'Priority (e.g., Low, Medium, High)', }, + origin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Origin (e.g., Phone, Email, Web)', + }, + contactId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Salesforce Contact ID (18-character string starting with 003)', + }, + accountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Salesforce Account ID (18-character string starting with 001)', + }, description: { type: 'string', required: false, @@ -69,8 +87,10 @@ export const salesforceUpdateCaseTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${params.caseId}`, + url: (params) => { + const caseId = requireId(params.caseId, 'Case ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}` + }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -81,6 +101,9 @@ export const salesforceUpdateCaseTool: ToolConfig< if (params.subject) body.Subject = params.subject if (params.status) body.Status = params.status if (params.priority) body.Priority = params.priority + if (params.origin) body.Origin = params.origin + if (params.contactId) body.ContactId = params.contactId.trim() + if (params.accountId) body.AccountId = params.accountId.trim() if (params.description) body.Description = params.description return body }, @@ -89,12 +112,12 @@ export const salesforceUpdateCaseTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json() - throw new Error(data[0]?.message || data.message || 'Failed to update case') + throw new Error(extractErrorMessage(data, response.status, 'Failed to update case')) } return { success: true, output: { - id: params?.caseId || '', + id: params?.caseId?.trim() || '', updated: true, }, } diff --git a/apps/sim/tools/salesforce/update_contact.ts b/apps/sim/tools/salesforce/update_contact.ts index 49729823cac..ff745bb80df 100644 --- a/apps/sim/tools/salesforce/update_contact.ts +++ b/apps/sim/tools/salesforce/update_contact.ts @@ -4,7 +4,7 @@ import type { SalesforceUpdateContactResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -108,7 +108,8 @@ export const salesforceUpdateContactTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${params.contactId}` + const contactId = requireId(params.contactId, 'Contact ID') + return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'PATCH', headers: (params) => ({ @@ -122,7 +123,7 @@ export const salesforceUpdateContactTool: ToolConfig< if (params.firstName) body.FirstName = params.firstName if (params.email) body.Email = params.email if (params.phone) body.Phone = params.phone - if (params.accountId) body.AccountId = params.accountId + if (params.accountId) body.AccountId = params.accountId.trim() if (params.title) body.Title = params.title if (params.department) body.Department = params.department if (params.mailingStreet) body.MailingStreet = params.mailingStreet @@ -140,13 +141,15 @@ export const salesforceUpdateContactTool: ToolConfig< if (!response.ok) { const data = await response.json() logger.error('Salesforce API request failed', { data, status: response.status }) - throw new Error(data[0]?.message || data.message || 'Failed to update contact in Salesforce') + throw new Error( + extractErrorMessage(data, response.status, 'Failed to update contact in Salesforce') + ) } return { success: true, output: { - id: params?.contactId || '', + id: params?.contactId?.trim() || '', updated: true, }, } diff --git a/apps/sim/tools/salesforce/update_lead.ts b/apps/sim/tools/salesforce/update_lead.ts index 1575e813cf6..c0c44f8e2ef 100644 --- a/apps/sim/tools/salesforce/update_lead.ts +++ b/apps/sim/tools/salesforce/update_lead.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateLeadResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateLeadTool: ToolConfig< @@ -82,8 +82,10 @@ export const salesforceUpdateLeadTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${params.leadId}`, + url: (params) => { + const leadId = requireId(params.leadId, 'Lead ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` + }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -107,12 +109,12 @@ export const salesforceUpdateLeadTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json() - throw new Error(data[0]?.message || data.message || 'Failed to update lead') + throw new Error(extractErrorMessage(data, response.status, 'Failed to update lead')) } return { success: true, output: { - id: params?.leadId || '', + id: params?.leadId?.trim() || '', updated: true, }, } diff --git a/apps/sim/tools/salesforce/update_opportunity.ts b/apps/sim/tools/salesforce/update_opportunity.ts index 0d5bbdbc65e..bdfa007f801 100644 --- a/apps/sim/tools/salesforce/update_opportunity.ts +++ b/apps/sim/tools/salesforce/update_opportunity.ts @@ -1,11 +1,14 @@ +import { createLogger } from '@sim/logger' import type { SalesforceUpdateOpportunityParams, SalesforceUpdateOpportunityResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('SalesforceUpdateOpportunity') + export const salesforceUpdateOpportunityTool: ToolConfig< SalesforceUpdateOpportunityParams, SalesforceUpdateOpportunityResponse @@ -75,8 +78,10 @@ export const salesforceUpdateOpportunityTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${params.opportunityId}`, + url: (params) => { + const opportunityId = requireId(params.opportunityId, 'Opportunity ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` + }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -87,7 +92,7 @@ export const salesforceUpdateOpportunityTool: ToolConfig< if (params.name) body.Name = params.name if (params.stageName) body.StageName = params.stageName if (params.closeDate) body.CloseDate = params.closeDate - if (params.accountId) body.AccountId = params.accountId + if (params.accountId) body.AccountId = params.accountId.trim() if (params.amount) body.Amount = Number.parseFloat(params.amount) if (params.probability) body.Probability = Number.parseInt(params.probability) if (params.description) body.Description = params.description @@ -98,12 +103,13 @@ export const salesforceUpdateOpportunityTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json() - throw new Error(data[0]?.message || data.message || 'Failed to update opportunity') + logger.error('Failed to update opportunity', { data, status: response.status }) + throw new Error(extractErrorMessage(data, response.status, 'Failed to update opportunity')) } return { success: true, output: { - id: params?.opportunityId || '', + id: params?.opportunityId?.trim() || '', updated: true, }, } diff --git a/apps/sim/tools/salesforce/update_task.ts b/apps/sim/tools/salesforce/update_task.ts index e9b4627fe32..f711cad167f 100644 --- a/apps/sim/tools/salesforce/update_task.ts +++ b/apps/sim/tools/salesforce/update_task.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateTaskResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateTaskTool: ToolConfig< @@ -66,6 +66,18 @@ export const salesforceUpdateTaskTool: ToolConfig< visibility: 'user-or-llm', description: 'Due date in YYYY-MM-DD format', }, + whoId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Related Contact ID (003...) or Lead ID (00Q...)', + }, + whatId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Related Account ID (001...) or Opportunity ID (006...)', + }, description: { type: 'string', required: false, @@ -75,8 +87,10 @@ export const salesforceUpdateTaskTool: ToolConfig< }, request: { - url: (params) => - `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${params.taskId}`, + url: (params) => { + const taskId = requireId(params.taskId, 'Task ID') + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}` + }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -88,6 +102,8 @@ export const salesforceUpdateTaskTool: ToolConfig< if (params.status) body.Status = params.status if (params.priority) body.Priority = params.priority if (params.activityDate) body.ActivityDate = params.activityDate + if (params.whoId) body.WhoId = params.whoId.trim() + if (params.whatId) body.WhatId = params.whatId.trim() if (params.description) body.Description = params.description return body }, @@ -96,12 +112,12 @@ export const salesforceUpdateTaskTool: ToolConfig< transformResponse: async (response, params?) => { if (!response.ok) { const data = await response.json() - throw new Error(data[0]?.message || data.message || 'Failed to update task') + throw new Error(extractErrorMessage(data, response.status, 'Failed to update task')) } return { success: true, output: { - id: params?.taskId || '', + id: params?.taskId?.trim() || '', updated: true, }, } diff --git a/apps/sim/tools/salesforce/utils.ts b/apps/sim/tools/salesforce/utils.ts index 985a6a6f363..83a549ad16e 100644 --- a/apps/sim/tools/salesforce/utils.ts +++ b/apps/sim/tools/salesforce/utils.ts @@ -36,6 +36,23 @@ export function getInstanceUrl(idToken?: string, instanceUrl?: string): string { throw new Error('Salesforce instance URL is required but not provided') } +/** + * Trims a record ID and throws if it is missing or whitespace-only. + * Prevents whitespace-only IDs from collapsing into an empty URL path segment + * (e.g. `/sobjects/Account/`) and hitting Salesforce with a malformed request. + * @param value - The raw ID value from params + * @param label - Human-readable field name used in the error message + * @returns The trimmed, non-empty ID + * @throws Error if the ID is absent or whitespace-only + */ +export function requireId(value: string | undefined, label: string): string { + const trimmed = value?.trim() + if (!trimmed) { + throw new Error(`${label} is required. Please provide a valid Salesforce ${label}.`) + } + return trimmed +} + /** * Extracts a descriptive error message from Salesforce API responses * @param data - The response data from Salesforce API