From 4be2293d259f7c265e4cdfd427c3f8f5a7d255fe Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 19:05:40 -0700 Subject: [PATCH 1/5] improvement(salesforce): align tools + block with Salesforce API and harden CRUD/analytics - Migrate all 4 Account tools to shared getInstanceUrl + extractErrorMessage helpers (drop ~40 lines of inlined idToken decode per file) - Use extractErrorMessage consistently across every CRUD tool; add loggers to the opportunity tools - Trim ID path params on all update/delete/single-get tools; URL-encode single-record field lists - Fix dashboard tools: read name/metadata from dashboardMetadata.attributes (was always null); refresh now returns status/statusUrl defensively; drop no-op list_dashboards folderName filter - Expose update_case origin/contact/account and update_task who/what fields end-to-end - Block: mark create-required (Name, LastName, Company, StageName, Subject) and update/delete IDs conditionally required; add account billing*/revenue/employees and contact mailing*/department subBlocks; convert includeDetails to a Yes/No dropdown; move optional fields to advanced mode - Trim over-declared dashboard output types; make Task.Status optional - Regenerate integration docs --- .../docs/en/integrations/salesforce.mdx | 17 +- apps/sim/blocks/blocks/salesforce.ts | 158 +++++++++++++++++- apps/sim/lib/integrations/integrations.json | 4 +- apps/sim/tools/salesforce/create_account.ts | 45 +---- apps/sim/tools/salesforce/create_case.ts | 5 +- apps/sim/tools/salesforce/create_contact.ts | 6 +- apps/sim/tools/salesforce/create_lead.ts | 9 +- .../tools/salesforce/create_opportunity.ts | 11 +- apps/sim/tools/salesforce/create_task.ts | 5 +- apps/sim/tools/salesforce/delete_account.ts | 44 +---- apps/sim/tools/salesforce/delete_case.ts | 10 +- apps/sim/tools/salesforce/delete_contact.ts | 7 +- apps/sim/tools/salesforce/delete_lead.ts | 10 +- .../tools/salesforce/delete_opportunity.ts | 14 +- apps/sim/tools/salesforce/delete_task.ts | 10 +- apps/sim/tools/salesforce/describe_object.ts | 5 +- apps/sim/tools/salesforce/get_accounts.ts | 41 +---- apps/sim/tools/salesforce/get_cases.ts | 8 +- apps/sim/tools/salesforce/get_contacts.ts | 7 +- apps/sim/tools/salesforce/get_dashboard.ts | 11 +- apps/sim/tools/salesforce/get_leads.ts | 10 +- .../sim/tools/salesforce/get_opportunities.ts | 14 +- apps/sim/tools/salesforce/get_report.ts | 6 +- apps/sim/tools/salesforce/get_tasks.ts | 8 +- apps/sim/tools/salesforce/list_dashboards.ts | 17 +- apps/sim/tools/salesforce/query.ts | 5 +- apps/sim/tools/salesforce/query_more.ts | 5 +- .../sim/tools/salesforce/refresh_dashboard.ts | 14 +- apps/sim/tools/salesforce/run_report.ts | 4 +- apps/sim/tools/salesforce/types.ts | 63 +++---- apps/sim/tools/salesforce/update_account.ts | 46 +---- apps/sim/tools/salesforce/update_case.ts | 31 +++- apps/sim/tools/salesforce/update_contact.ts | 9 +- apps/sim/tools/salesforce/update_lead.ts | 10 +- .../tools/salesforce/update_opportunity.ts | 14 +- apps/sim/tools/salesforce/update_task.ts | 24 ++- 36 files changed, 395 insertions(+), 312 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/salesforce.mdx b/apps/docs/content/docs/en/integrations/salesforce.mdx index 7c15ea20e14..fa23d6cd5e2 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 @@ -735,7 +740,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 +823,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 +856,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 +881,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..1b93209f177 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 { @@ -441,20 +570,27 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'Folder Name', type: 'short-input', placeholder: 'Filter by folder name', - condition: { field: 'operation', value: ['list_reports', 'list_dashboards'] }, + mode: 'advanced', + condition: { field: 'operation', value: ['list_reports'] }, }, { 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 +598,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 +641,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 +800,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..f3f1c7bfbdc 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,15 +160,16 @@ 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, + success: true, created: true, }, } diff --git a/apps/sim/tools/salesforce/create_case.ts b/apps/sim/tools/salesforce/create_case.ts index 33e60d0da9c..72569ae748e 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< @@ -102,7 +102,8 @@ 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: { diff --git a/apps/sim/tools/salesforce/create_contact.ts b/apps/sim/tools/salesforce/create_contact.ts index 01c8c4831ee..b94ce89e42b 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') @@ -134,7 +134,9 @@ 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 { 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..61d416834a9 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 @@ -92,8 +95,10 @@ 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: { diff --git a/apps/sim/tools/salesforce/create_task.ts b/apps/sim/tools/salesforce/create_task.ts index 40286d0fcde..3f29f6f120b 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< @@ -102,7 +102,8 @@ 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: { diff --git a/apps/sim/tools/salesforce/delete_account.ts b/apps/sim/tools/salesforce/delete_account.ts index cb691bf5678..6093f139bf3 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 } 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 = params.accountId.trim() - return `${instanceUrl}/services/data/v59.0/sobjects/Account/${params.accountId}` + return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, method: 'DELETE', headers: (params) => { @@ -99,9 +66,8 @@ 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') ) } diff --git a/apps/sim/tools/salesforce/delete_case.ts b/apps/sim/tools/salesforce/delete_case.ts index ca30065ecfb..17c4abaf7c0 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 } 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 = params.caseId.trim() + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -56,7 +58,7 @@ 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, diff --git a/apps/sim/tools/salesforce/delete_contact.ts b/apps/sim/tools/salesforce/delete_contact.ts index 49d9026b862..1edd6e64160 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 } 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 = params.contactId.trim() + return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'DELETE', headers: (params) => ({ @@ -48,7 +49,7 @@ 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') ) } diff --git a/apps/sim/tools/salesforce/delete_lead.ts b/apps/sim/tools/salesforce/delete_lead.ts index e33822e6426..5ff6a31edb1 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 } 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 = params.leadId.trim() + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), }, @@ -42,7 +44,7 @@ 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, diff --git a/apps/sim/tools/salesforce/delete_opportunity.ts b/apps/sim/tools/salesforce/delete_opportunity.ts index 98b136601d8..56fbac09b4b 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 } 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 = params.opportunityId.trim() + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), }, @@ -42,7 +47,8 @@ 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, diff --git a/apps/sim/tools/salesforce/delete_task.ts b/apps/sim/tools/salesforce/delete_task.ts index 061205529d3..98596c88a9a 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 } 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 = params.taskId.trim() + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}` + }, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -56,7 +58,7 @@ 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, 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..13f9a2e7e84 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 } 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 = params.caseId.trim() 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..b8112809f1f 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 } 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 = params.contactId.trim() 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..d4cd0a2fad4 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 } 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 = params.leadId.trim() 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..f6d67229961 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 } 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 = params.opportunityId.trim() 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=${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..1bab76d9b0d 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 } 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 = params.taskId.trim() 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=${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..26f2ffe77de 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,7 @@ 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()) - ) - } + const dashboards = Array.isArray(data.dashboards) ? data.dashboards : [] return { success: true, 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 } @@ -1765,9 +1753,7 @@ export interface SalesforceListReportTypesResponse { } } -export interface SalesforceListDashboardsParams extends BaseSalesforceParams { - folderName?: string -} +export type SalesforceListDashboardsParams = BaseSalesforceParams export interface SalesforceListDashboardsResponse { success: boolean @@ -1789,7 +1775,7 @@ export interface SalesforceGetDashboardResponse { dashboardId: string components: any[] dashboardName?: string - folderId?: string + dashboardMetadata?: any runningUser?: any success: boolean } @@ -1806,8 +1792,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..5ea1f84fdb5 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 } 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 = params.accountId.trim() - return `${instanceUrl}/services/data/v59.0/sobjects/Account/${params.accountId}` + return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, method: 'PATCH', headers: (params) => { @@ -198,8 +165,9 @@ 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 { diff --git a/apps/sim/tools/salesforce/update_case.ts b/apps/sim/tools/salesforce/update_case.ts index ed997c5eddc..7acd6a84754 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 } 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 = params.caseId.trim() + 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 + if (params.accountId) body.AccountId = params.accountId if (params.description) body.Description = params.description return body }, @@ -89,7 +112,7 @@ 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, diff --git a/apps/sim/tools/salesforce/update_contact.ts b/apps/sim/tools/salesforce/update_contact.ts index 49729823cac..8e81c93266c 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 } 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 = params.contactId.trim() + return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'PATCH', headers: (params) => ({ @@ -140,7 +141,9 @@ 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 { diff --git a/apps/sim/tools/salesforce/update_lead.ts b/apps/sim/tools/salesforce/update_lead.ts index 1575e813cf6..d77162220ad 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 } 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 = params.leadId.trim() + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` + }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -107,7 +109,7 @@ 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, diff --git a/apps/sim/tools/salesforce/update_opportunity.ts b/apps/sim/tools/salesforce/update_opportunity.ts index 0d5bbdbc65e..a5fae5e2807 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 } 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 = params.opportunityId.trim() + return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` + }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -98,7 +103,8 @@ 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, diff --git a/apps/sim/tools/salesforce/update_task.ts b/apps/sim/tools/salesforce/update_task.ts index e9b4627fe32..27b8f406d0b 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 } 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 = params.taskId.trim() + 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 + if (params.whatId) body.WhatId = params.whatId if (params.description) body.Description = params.description return body }, @@ -96,7 +112,7 @@ 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, From 6b6349b377b48ebd018a9217cd29dd8d0453b880 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 19:39:24 -0700 Subject: [PATCH 2/5] fix(salesforce): correct list/refresh response shapes per live API docs - list_dashboards: GET /analytics/dashboards returns a bare top-level array, not a {dashboards} wrapper (fixes always-empty result) - refresh_dashboard: statusUrl is returned at the top level of the PUT response, read it there first - list_reports: the list resource only returns report name/id/url/describeUrl/instancesUrl, so drop the no-op folderName filter and match searchTerm on name only Validated tool-by-tool against the live Salesforce REST/Analytics/Object-Reference docs (API v67.0). --- .../docs/en/integrations/salesforce.mdx | 3 +- apps/sim/blocks/blocks/salesforce.ts | 8 ------ apps/sim/tools/salesforce/list_dashboards.ts | 8 +++++- apps/sim/tools/salesforce/list_reports.ts | 28 ++++++------------- .../sim/tools/salesforce/refresh_dashboard.ts | 2 +- apps/sim/tools/salesforce/types.ts | 1 - 6 files changed, 17 insertions(+), 33 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/salesforce.mdx b/apps/docs/content/docs/en/integrations/salesforce.mdx index fa23d6cd5e2..1125dc36cdd 100644 --- a/apps/docs/content/docs/en/integrations/salesforce.mdx +++ b/apps/docs/content/docs/en/integrations/salesforce.mdx @@ -725,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 diff --git a/apps/sim/blocks/blocks/salesforce.ts b/apps/sim/blocks/blocks/salesforce.ts index 1b93209f177..11cbc2b58d1 100644 --- a/apps/sim/blocks/blocks/salesforce.ts +++ b/apps/sim/blocks/blocks/salesforce.ts @@ -565,14 +565,6 @@ 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', - mode: 'advanced', - condition: { field: 'operation', value: ['list_reports'] }, - }, { id: 'searchTerm', title: 'Search Term', diff --git a/apps/sim/tools/salesforce/list_dashboards.ts b/apps/sim/tools/salesforce/list_dashboards.ts index 26f2ffe77de..8cc519e3302 100644 --- a/apps/sim/tools/salesforce/list_dashboards.ts +++ b/apps/sim/tools/salesforce/list_dashboards.ts @@ -57,7 +57,13 @@ export const salesforceListDashboardsTool: ToolConfig< throw new Error(errorMessage) } - const dashboards = Array.isArray(data.dashboards) ? data.dashboards : [] + // 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/refresh_dashboard.ts b/apps/sim/tools/salesforce/refresh_dashboard.ts index c7dfd9dbe6f..0179b707dfe 100644 --- a/apps/sim/tools/salesforce/refresh_dashboard.ts +++ b/apps/sim/tools/salesforce/refresh_dashboard.ts @@ -77,7 +77,7 @@ export const salesforceRefreshDashboardTool: ToolConfig< dashboardId: attrs.dashboardId ?? params?.dashboardId ?? '', components: data.componentData ?? data.componentStatus ?? [], status: data.dashboardStatus ?? data.status ?? null, - statusUrl: attrs.statusUrl ?? null, + statusUrl: data.statusUrl ?? attrs.statusUrl ?? null, dashboardName: attrs.dashboardName ?? null, dashboardMetadata: data.dashboardMetadata ?? null, success: true, diff --git a/apps/sim/tools/salesforce/types.ts b/apps/sim/tools/salesforce/types.ts index a9c410a2a1e..ce58df28413 100644 --- a/apps/sim/tools/salesforce/types.ts +++ b/apps/sim/tools/salesforce/types.ts @@ -1693,7 +1693,6 @@ export interface SalesforceDeleteTaskResponse { } export interface SalesforceListReportsParams extends BaseSalesforceParams { - folderName?: string searchTerm?: string } From a0c593f61c2e915acb3f5d42802cbcfd5d3c7057 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 19:53:09 -0700 Subject: [PATCH 3/5] fix(salesforce): address Greptile/Cursor review - Add shared requireId() guard so whitespace-only IDs fail fast instead of producing malformed /sobjects/Object/ empty-path requests (all update/delete/single-get tools) - URL-encode the fields query value in get_opportunities and get_tasks single-record GETs (matching the other get_* tools) - Reflect the Salesforce API success flag consistently across all create tools (success/created use data.success === true) --- apps/sim/tools/salesforce/create_account.ts | 4 ++-- apps/sim/tools/salesforce/create_case.ts | 4 ++-- apps/sim/tools/salesforce/create_contact.ts | 4 ++-- apps/sim/tools/salesforce/create_opportunity.ts | 4 ++-- apps/sim/tools/salesforce/create_task.ts | 4 ++-- apps/sim/tools/salesforce/delete_account.ts | 4 ++-- apps/sim/tools/salesforce/delete_case.ts | 4 ++-- apps/sim/tools/salesforce/delete_contact.ts | 4 ++-- apps/sim/tools/salesforce/delete_lead.ts | 4 ++-- apps/sim/tools/salesforce/delete_opportunity.ts | 4 ++-- apps/sim/tools/salesforce/delete_task.ts | 4 ++-- apps/sim/tools/salesforce/get_cases.ts | 4 ++-- apps/sim/tools/salesforce/get_contacts.ts | 4 ++-- apps/sim/tools/salesforce/get_leads.ts | 4 ++-- apps/sim/tools/salesforce/get_opportunities.ts | 6 +++--- apps/sim/tools/salesforce/get_tasks.ts | 6 +++--- apps/sim/tools/salesforce/update_account.ts | 4 ++-- apps/sim/tools/salesforce/update_case.ts | 4 ++-- apps/sim/tools/salesforce/update_contact.ts | 4 ++-- apps/sim/tools/salesforce/update_lead.ts | 4 ++-- apps/sim/tools/salesforce/update_opportunity.ts | 4 ++-- apps/sim/tools/salesforce/update_task.ts | 4 ++-- apps/sim/tools/salesforce/utils.ts | 17 +++++++++++++++++ 23 files changed, 63 insertions(+), 46 deletions(-) diff --git a/apps/sim/tools/salesforce/create_account.ts b/apps/sim/tools/salesforce/create_account.ts index f3f1c7bfbdc..7bb6036dd1f 100644 --- a/apps/sim/tools/salesforce/create_account.ts +++ b/apps/sim/tools/salesforce/create_account.ts @@ -169,8 +169,8 @@ export const salesforceCreateAccountTool: ToolConfig< success: true, output: { id: data.id, - success: true, - 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 72569ae748e..ea7e4626e1e 100644 --- a/apps/sim/tools/salesforce/create_case.ts +++ b/apps/sim/tools/salesforce/create_case.ts @@ -108,8 +108,8 @@ export const salesforceCreateCaseTool: ToolConfig< 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 b94ce89e42b..0a4921e5a2c 100644 --- a/apps/sim/tools/salesforce/create_contact.ts +++ b/apps/sim/tools/salesforce/create_contact.ts @@ -143,8 +143,8 @@ export const salesforceCreateContactTool: ToolConfig< 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 61d416834a9..974e35d179d 100644 --- a/apps/sim/tools/salesforce/create_opportunity.ts +++ b/apps/sim/tools/salesforce/create_opportunity.ts @@ -103,8 +103,8 @@ export const salesforceCreateOpportunityTool: ToolConfig< 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 3f29f6f120b..719206531a5 100644 --- a/apps/sim/tools/salesforce/create_task.ts +++ b/apps/sim/tools/salesforce/create_task.ts @@ -108,8 +108,8 @@ export const salesforceCreateTaskTool: ToolConfig< 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 6093f139bf3..7cb07564468 100644 --- a/apps/sim/tools/salesforce/delete_account.ts +++ b/apps/sim/tools/salesforce/delete_account.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteAccountResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteAccountTool: ToolConfig< @@ -47,7 +47,7 @@ export const salesforceDeleteAccountTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const accountId = params.accountId.trim() + const accountId = requireId(params.accountId, 'Account ID') return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, diff --git a/apps/sim/tools/salesforce/delete_case.ts b/apps/sim/tools/salesforce/delete_case.ts index 17c4abaf7c0..b1fff18f413 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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteCaseTool: ToolConfig< @@ -46,7 +46,7 @@ export const salesforceDeleteCaseTool: ToolConfig< request: { url: (params) => { - const caseId = params.caseId.trim() + const caseId = requireId(params.caseId, 'Case ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_contact.ts b/apps/sim/tools/salesforce/delete_contact.ts index 1edd6e64160..b0eb12d96c4 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 { extractErrorMessage, 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,7 @@ export const salesforceDeleteContactTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const contactId = params.contactId.trim() + const contactId = requireId(params.contactId, 'Contact ID') return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_lead.ts b/apps/sim/tools/salesforce/delete_lead.ts index 5ff6a31edb1..23c06399364 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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteLeadTool: ToolConfig< @@ -34,7 +34,7 @@ export const salesforceDeleteLeadTool: ToolConfig< request: { url: (params) => { - const leadId = params.leadId.trim() + const leadId = requireId(params.leadId, 'Lead ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_opportunity.ts b/apps/sim/tools/salesforce/delete_opportunity.ts index 56fbac09b4b..efb875eaa8b 100644 --- a/apps/sim/tools/salesforce/delete_opportunity.ts +++ b/apps/sim/tools/salesforce/delete_opportunity.ts @@ -4,7 +4,7 @@ import type { SalesforceDeleteOpportunityResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceDeleteOpportunity') @@ -37,7 +37,7 @@ export const salesforceDeleteOpportunityTool: ToolConfig< request: { url: (params) => { - const opportunityId = params.opportunityId.trim() + const opportunityId = requireId(params.opportunityId, 'Opportunity ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_task.ts b/apps/sim/tools/salesforce/delete_task.ts index 98596c88a9a..f058d052a32 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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteTaskTool: ToolConfig< @@ -46,7 +46,7 @@ export const salesforceDeleteTaskTool: ToolConfig< request: { url: (params) => { - const taskId = params.taskId.trim() + const taskId = requireId(params.taskId, 'Task ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/get_cases.ts b/apps/sim/tools/salesforce/get_cases.ts index 13f9a2e7e84..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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetCasesTool: ToolConfig< @@ -52,7 +52,7 @@ export const salesforceGetCasesTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.caseId) { - const caseId = params.caseId.trim() + 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/${caseId}?fields=${encodeURIComponent(fields)}` diff --git a/apps/sim/tools/salesforce/get_contacts.ts b/apps/sim/tools/salesforce/get_contacts.ts index b8112809f1f..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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -60,7 +60,7 @@ export const salesforceGetContactsTool: ToolConfig< // Single contact by ID if (params.contactId) { - const contactId = params.contactId.trim() + 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/${contactId}?fields=${encodeURIComponent(fields)}` diff --git a/apps/sim/tools/salesforce/get_leads.ts b/apps/sim/tools/salesforce/get_leads.ts index d4cd0a2fad4..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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetLeadsTool: ToolConfig< @@ -52,7 +52,7 @@ export const salesforceGetLeadsTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.leadId) { - const leadId = params.leadId.trim() + 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/${leadId}?fields=${encodeURIComponent(fields)}` diff --git a/apps/sim/tools/salesforce/get_opportunities.ts b/apps/sim/tools/salesforce/get_opportunities.ts index f6d67229961..b4a86a2dda5 100644 --- a/apps/sim/tools/salesforce/get_opportunities.ts +++ b/apps/sim/tools/salesforce/get_opportunities.ts @@ -4,7 +4,7 @@ import type { SalesforceGetOpportunitiesResponse, } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceGetOpportunities') @@ -58,9 +58,9 @@ export const salesforceGetOpportunitiesTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.opportunityId) { - const opportunityId = params.opportunityId.trim() + 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/${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' diff --git a/apps/sim/tools/salesforce/get_tasks.ts b/apps/sim/tools/salesforce/get_tasks.ts index 1bab76d9b0d..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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetTasksTool: ToolConfig< @@ -64,10 +64,10 @@ export const salesforceGetTasksTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.taskId) { - const taskId = params.taskId.trim() + 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/${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' diff --git a/apps/sim/tools/salesforce/update_account.ts b/apps/sim/tools/salesforce/update_account.ts index 5ea1f84fdb5..0a96ad61bbd 100644 --- a/apps/sim/tools/salesforce/update_account.ts +++ b/apps/sim/tools/salesforce/update_account.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateAccountResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateAccountTool: ToolConfig< @@ -125,7 +125,7 @@ export const salesforceUpdateAccountTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const accountId = params.accountId.trim() + const accountId = requireId(params.accountId, 'Account ID') return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, diff --git a/apps/sim/tools/salesforce/update_case.ts b/apps/sim/tools/salesforce/update_case.ts index 7acd6a84754..f95bfa33850 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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateCaseTool: ToolConfig< @@ -88,7 +88,7 @@ export const salesforceUpdateCaseTool: ToolConfig< request: { url: (params) => { - const caseId = params.caseId.trim() + const caseId = requireId(params.caseId, 'Case ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_contact.ts b/apps/sim/tools/salesforce/update_contact.ts index 8e81c93266c..aa61c90588d 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 { extractErrorMessage, 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,7 @@ export const salesforceUpdateContactTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const contactId = params.contactId.trim() + const contactId = requireId(params.contactId, 'Contact ID') return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_lead.ts b/apps/sim/tools/salesforce/update_lead.ts index d77162220ad..0ff3b373136 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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateLeadTool: ToolConfig< @@ -83,7 +83,7 @@ export const salesforceUpdateLeadTool: ToolConfig< request: { url: (params) => { - const leadId = params.leadId.trim() + const leadId = requireId(params.leadId, 'Lead ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_opportunity.ts b/apps/sim/tools/salesforce/update_opportunity.ts index a5fae5e2807..f02a5e2b4cf 100644 --- a/apps/sim/tools/salesforce/update_opportunity.ts +++ b/apps/sim/tools/salesforce/update_opportunity.ts @@ -4,7 +4,7 @@ import type { SalesforceUpdateOpportunityResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceUpdateOpportunity') @@ -79,7 +79,7 @@ export const salesforceUpdateOpportunityTool: ToolConfig< request: { url: (params) => { - const opportunityId = params.opportunityId.trim() + const opportunityId = requireId(params.opportunityId, 'Opportunity ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_task.ts b/apps/sim/tools/salesforce/update_task.ts index 27b8f406d0b..3de99e3dd02 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 { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateTaskTool: ToolConfig< @@ -88,7 +88,7 @@ export const salesforceUpdateTaskTool: ToolConfig< request: { url: (params) => { - const taskId = params.taskId.trim() + const taskId = requireId(params.taskId, 'Task ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}` }, method: 'PATCH', 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 From b9d35582e5b6c02cc2704da02393d2947dcc4e1d Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 20:00:46 -0700 Subject: [PATCH 4/5] fix(salesforce): trim echoed output IDs and relation reference IDs - update/delete tools now return output.id from the trimmed ID so chained workflows never receive whitespace-padded IDs - trim relation reference IDs (AccountId, ContactId, WhoId, WhatId) in create/update bodies to avoid Salesforce reference errors from copy-pasted whitespace --- apps/sim/tools/salesforce/create_case.ts | 4 ++-- apps/sim/tools/salesforce/create_contact.ts | 2 +- apps/sim/tools/salesforce/create_opportunity.ts | 2 +- apps/sim/tools/salesforce/create_task.ts | 4 ++-- apps/sim/tools/salesforce/delete_account.ts | 2 +- apps/sim/tools/salesforce/delete_case.ts | 2 +- apps/sim/tools/salesforce/delete_contact.ts | 2 +- apps/sim/tools/salesforce/delete_lead.ts | 2 +- apps/sim/tools/salesforce/delete_opportunity.ts | 2 +- apps/sim/tools/salesforce/delete_task.ts | 2 +- apps/sim/tools/salesforce/update_account.ts | 2 +- apps/sim/tools/salesforce/update_case.ts | 6 +++--- apps/sim/tools/salesforce/update_contact.ts | 2 +- apps/sim/tools/salesforce/update_lead.ts | 2 +- apps/sim/tools/salesforce/update_opportunity.ts | 4 ++-- apps/sim/tools/salesforce/update_task.ts | 6 +++--- 16 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/sim/tools/salesforce/create_case.ts b/apps/sim/tools/salesforce/create_case.ts index ea7e4626e1e..364605e610c 100644 --- a/apps/sim/tools/salesforce/create_case.ts +++ b/apps/sim/tools/salesforce/create_case.ts @@ -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 }, diff --git a/apps/sim/tools/salesforce/create_contact.ts b/apps/sim/tools/salesforce/create_contact.ts index 0a4921e5a2c..0ba539f2471 100644 --- a/apps/sim/tools/salesforce/create_contact.ts +++ b/apps/sim/tools/salesforce/create_contact.ts @@ -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 diff --git a/apps/sim/tools/salesforce/create_opportunity.ts b/apps/sim/tools/salesforce/create_opportunity.ts index 974e35d179d..f1600a8907b 100644 --- a/apps/sim/tools/salesforce/create_opportunity.ts +++ b/apps/sim/tools/salesforce/create_opportunity.ts @@ -85,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 diff --git a/apps/sim/tools/salesforce/create_task.ts b/apps/sim/tools/salesforce/create_task.ts index 719206531a5..2eef3adc9c2 100644 --- a/apps/sim/tools/salesforce/create_task.ts +++ b/apps/sim/tools/salesforce/create_task.ts @@ -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 }, diff --git a/apps/sim/tools/salesforce/delete_account.ts b/apps/sim/tools/salesforce/delete_account.ts index 7cb07564468..f43a15eb60b 100644 --- a/apps/sim/tools/salesforce/delete_account.ts +++ b/apps/sim/tools/salesforce/delete_account.ts @@ -74,7 +74,7 @@ export const salesforceDeleteAccountTool: ToolConfig< 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 b1fff18f413..2a77f70d609 100644 --- a/apps/sim/tools/salesforce/delete_case.ts +++ b/apps/sim/tools/salesforce/delete_case.ts @@ -63,7 +63,7 @@ export const salesforceDeleteCaseTool: ToolConfig< 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 b0eb12d96c4..22e9574666e 100644 --- a/apps/sim/tools/salesforce/delete_contact.ts +++ b/apps/sim/tools/salesforce/delete_contact.ts @@ -56,7 +56,7 @@ export const salesforceDeleteContactTool: ToolConfig< 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 23c06399364..d3d357b3790 100644 --- a/apps/sim/tools/salesforce/delete_lead.ts +++ b/apps/sim/tools/salesforce/delete_lead.ts @@ -49,7 +49,7 @@ export const salesforceDeleteLeadTool: ToolConfig< 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 efb875eaa8b..4d52d7349a1 100644 --- a/apps/sim/tools/salesforce/delete_opportunity.ts +++ b/apps/sim/tools/salesforce/delete_opportunity.ts @@ -53,7 +53,7 @@ export const salesforceDeleteOpportunityTool: ToolConfig< 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 f058d052a32..372a3dba11b 100644 --- a/apps/sim/tools/salesforce/delete_task.ts +++ b/apps/sim/tools/salesforce/delete_task.ts @@ -63,7 +63,7 @@ export const salesforceDeleteTaskTool: ToolConfig< return { success: true, output: { - id: params?.taskId || '', + id: params?.taskId?.trim() || '', deleted: true, }, } diff --git a/apps/sim/tools/salesforce/update_account.ts b/apps/sim/tools/salesforce/update_account.ts index 0a96ad61bbd..8fc579c8b96 100644 --- a/apps/sim/tools/salesforce/update_account.ts +++ b/apps/sim/tools/salesforce/update_account.ts @@ -173,7 +173,7 @@ export const salesforceUpdateAccountTool: ToolConfig< 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 f95bfa33850..604285f0923 100644 --- a/apps/sim/tools/salesforce/update_case.ts +++ b/apps/sim/tools/salesforce/update_case.ts @@ -102,8 +102,8 @@ export const salesforceUpdateCaseTool: 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 }, @@ -117,7 +117,7 @@ export const salesforceUpdateCaseTool: ToolConfig< 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 aa61c90588d..660d158fb2f 100644 --- a/apps/sim/tools/salesforce/update_contact.ts +++ b/apps/sim/tools/salesforce/update_contact.ts @@ -149,7 +149,7 @@ export const salesforceUpdateContactTool: ToolConfig< 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 0ff3b373136..c0c44f8e2ef 100644 --- a/apps/sim/tools/salesforce/update_lead.ts +++ b/apps/sim/tools/salesforce/update_lead.ts @@ -114,7 +114,7 @@ export const salesforceUpdateLeadTool: ToolConfig< 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 f02a5e2b4cf..bdfa007f801 100644 --- a/apps/sim/tools/salesforce/update_opportunity.ts +++ b/apps/sim/tools/salesforce/update_opportunity.ts @@ -92,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 @@ -109,7 +109,7 @@ export const salesforceUpdateOpportunityTool: ToolConfig< 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 3de99e3dd02..f711cad167f 100644 --- a/apps/sim/tools/salesforce/update_task.ts +++ b/apps/sim/tools/salesforce/update_task.ts @@ -102,8 +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 - 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 }, @@ -117,7 +117,7 @@ export const salesforceUpdateTaskTool: ToolConfig< return { success: true, output: { - id: params?.taskId || '', + id: params?.taskId?.trim() || '', updated: true, }, } From e4cb146772a0ebd918227db957ec82aa74a57729 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 13 Jun 2026 20:07:21 -0700 Subject: [PATCH 5/5] fix(salesforce): trim accountId in update_contact body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last remaining untrimmed relation reference ID — update_contact now trims AccountId like the other create/update tools. --- apps/sim/tools/salesforce/update_contact.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/tools/salesforce/update_contact.ts b/apps/sim/tools/salesforce/update_contact.ts index 660d158fb2f..ff745bb80df 100644 --- a/apps/sim/tools/salesforce/update_contact.ts +++ b/apps/sim/tools/salesforce/update_contact.ts @@ -123,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