chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
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'
export const salesforceCreateAccountTool: ToolConfig<
SalesforceCreateAccountParams,
SalesforceCreateAccountResponse
> = {
id: 'salesforce_create_account',
name: 'Create Account in Salesforce',
description: 'Create a new account in Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Account name (required)',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Account type (e.g., Customer, Partner, Prospect)',
},
industry: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Industry (e.g., Technology, Healthcare, Finance)',
},
phone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone number',
},
website: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Website URL',
},
billingStreet: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing street address',
},
billingCity: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing city',
},
billingState: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing state/province',
},
billingPostalCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing postal code',
},
billingCountry: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing country',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Account description',
},
annualRevenue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Annual revenue as a number',
},
numberOfEmployees: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Number of employees as an integer',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/sobjects/Account`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {
Name: params.name,
}
if (params.type) body.Type = params.type
if (params.industry) body.Industry = params.industry
if (params.phone) body.Phone = params.phone
if (params.website) body.Website = params.website
if (params.billingStreet) body.BillingStreet = params.billingStreet
if (params.billingCity) body.BillingCity = params.billingCity
if (params.billingState) body.BillingState = params.billingState
if (params.billingPostalCode) body.BillingPostalCode = params.billingPostalCode
if (params.billingCountry) body.BillingCountry = params.billingCountry
if (params.description) body.Description = params.description
if (params.annualRevenue) body.AnnualRevenue = Number.parseFloat(params.annualRevenue)
if (params.numberOfEmployees)
body.NumberOfEmployees = Number.parseInt(params.numberOfEmployees)
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
extractErrorMessage(data, response.status, 'Failed to create account in Salesforce')
)
}
return {
success: true,
output: {
id: data.id,
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created account data',
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
},
},
}
+125
View File
@@ -0,0 +1,125 @@
import type {
SalesforceCreateCaseParams,
SalesforceCreateCaseResponse,
} 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'
export const salesforceCreateCaseTool: ToolConfig<
SalesforceCreateCaseParams,
SalesforceCreateCaseResponse
> = {
id: 'salesforce_create_case',
name: 'Create Case in Salesforce',
description: 'Create a new case',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
subject: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Case subject (required)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Status (e.g., New, Working, Escalated)',
},
priority: {
type: 'string',
required: false,
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,
visibility: 'user-or-llm',
description: 'Case description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { Subject: params.subject }
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.origin) body.Origin = params.origin
if (params.contactId) body.ContactId = params.contactId.trim()
if (params.accountId) body.AccountId = params.accountId.trim()
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok)
throw new Error(extractErrorMessage(data, response.status, 'Failed to create case'))
return {
success: true,
output: {
id: data.id,
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created case data',
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
},
},
}
+160
View File
@@ -0,0 +1,160 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceCreateContactParams,
SalesforceCreateContactResponse,
} 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('SalesforceContacts')
export const salesforceCreateContactTool: ToolConfig<
SalesforceCreateContactParams,
SalesforceCreateContactResponse
> = {
id: 'salesforce_create_contact',
name: 'Create Contact in Salesforce',
description: 'Create a new contact in Salesforce CRM',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
lastName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Last name (required)',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Email address',
},
phone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone number',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Salesforce Account ID (18-character string starting with 001)',
},
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
department: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Department',
},
mailingStreet: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing street',
},
mailingCity: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing city',
},
mailingState: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing state',
},
mailingPostalCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing postal code',
},
mailingCountry: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing country',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Contact description',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/sobjects/Contact`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { LastName: params.lastName }
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.trim()
if (params.title) body.Title = params.title
if (params.department) body.Department = params.department
if (params.mailingStreet) body.MailingStreet = params.mailingStreet
if (params.mailingCity) body.MailingCity = params.mailingCity
if (params.mailingState) body.MailingState = params.mailingState
if (params.mailingPostalCode) body.MailingPostalCode = params.mailingPostalCode
if (params.mailingCountry) body.MailingCountry = params.mailingCountry
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(
extractErrorMessage(data, response.status, 'Failed to create contact in Salesforce')
)
}
return {
success: true,
output: {
id: data.id,
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created contact data',
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,198 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceCreateCustomFieldParams,
SalesforceCreateCustomFieldResponse,
} from '@/tools/salesforce/types'
import { CUSTOM_FIELD_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import {
buildCustomFieldMetadata,
extractErrorMessage,
getInstanceUrl,
requireId,
toCustomApiName,
} from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceCreateCustomField')
/**
* Create a custom field on a Salesforce object (standard or custom) via the
* Tooling API. This is a schema/metadata change — distinct from record CRUD —
* and requires the user to have the "Customize Application" permission.
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
*/
export const salesforceCreateCustomFieldTool: ToolConfig<
SalesforceCreateCustomFieldParams,
SalesforceCreateCustomFieldResponse
> = {
id: 'salesforce_create_custom_field',
name: 'Create Custom Field in Salesforce',
description: 'Create a custom field on a Salesforce object (e.g., Account) using the Tooling API',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
objectName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'API name of the object to add the field to (e.g., Account, Contact, Lead, MyObject__c)',
},
fieldName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'API name of the new field; the __c suffix is added automatically (e.g., Region)',
},
label: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Display label shown in the UI (defaults to the field name when omitted)',
},
fieldType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Field data type: Text, TextArea, LongTextArea, Html, Number, Currency, Percent, Checkbox, Date, DateTime, Time, Phone, Email, Url, Picklist, or MultiselectPicklist',
},
length: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Maximum length for Text (1-255), LongTextArea, Html, or MultiselectPicklist fields',
},
precision: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Total number of digits for Number, Currency, or Percent fields (1-18)',
},
scale: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of digits to the right of the decimal for numeric fields',
},
visibleLines: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of visible lines for LongTextArea, Html, or MultiselectPicklist fields',
},
required: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the field is required on record create/edit',
},
unique: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the field enforces unique values',
},
externalId: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the field is an external ID (for Text, Number, or Email fields)',
},
defaultValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Default value; for Checkbox fields use true or false',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Internal description of the field',
},
inlineHelpText: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Help text shown next to the field in the UI',
},
picklistValues: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated values for Picklist or MultiselectPicklist fields',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const objectName = requireId(params.objectName, 'Object Name')
const fieldApiName = toCustomApiName(params.fieldName, 'Field Name')
const fallbackLabel = fieldApiName.replace(/__c$/, '').replace(/_/g, ' ')
return {
FullName: `${objectName}.${fieldApiName}`,
Metadata: buildCustomFieldMetadata(params, fallbackLabel),
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok || data?.success === false) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to create custom field in Salesforce'
)
logger.error('Failed to create custom field', { data, status: response.status })
throw new Error(errorMessage)
}
const objectName = params?.objectName?.trim() ?? ''
const fieldApiName = params?.fieldName ? toCustomApiName(params.fieldName, 'Field Name') : ''
return {
success: true,
output: {
id: data.id,
fullName: objectName && fieldApiName ? `${objectName}.${fieldApiName}` : '',
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created custom field metadata',
properties: CUSTOM_FIELD_CREATE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,148 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceCreateCustomObjectParams,
SalesforceCreateCustomObjectResponse,
} from '@/tools/salesforce/types'
import { CUSTOM_OBJECT_CREATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, toCustomApiName } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceCreateCustomObject')
/**
* Create a custom object via the Tooling API. The object is created with a
* Text Name field and deployed immediately. Custom fields can then be added
* with the Create Custom Field tool.
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customobject.htm
*/
export const salesforceCreateCustomObjectTool: ToolConfig<
SalesforceCreateCustomObjectParams,
SalesforceCreateCustomObjectResponse
> = {
id: 'salesforce_create_custom_object',
name: 'Create Custom Object in Salesforce',
description: 'Create a custom object in Salesforce using the Tooling API',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
objectName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'API name of the new object; the __c suffix is added automatically (e.g., Project)',
},
label: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Singular display label for the object (e.g., Project)',
},
pluralLabel: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Plural display label for the object (e.g., Projects)',
},
nameFieldLabel: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Label for the standard Name field (defaults to "<label> Name")',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Internal description of the object',
},
sharingModel: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Org-wide sharing model: ReadWrite, Read, Private, or ControlledByParent (default ReadWrite)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomObject`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const objectApiName = toCustomApiName(params.objectName, 'Object Name')
const label = params.label?.trim()
const pluralLabel = params.pluralLabel?.trim()
if (!label) throw new Error('Label is required to create a custom object.')
if (!pluralLabel) throw new Error('Plural Label is required to create a custom object.')
const metadata: Record<string, any> = {
label,
pluralLabel,
nameField: {
type: 'Text',
label: params.nameFieldLabel?.trim() || `${label} Name`,
},
deploymentStatus: 'Deployed',
sharingModel: params.sharingModel?.trim() || 'ReadWrite',
}
if (params.description?.trim()) metadata.description = params.description.trim()
return {
FullName: objectApiName,
Metadata: metadata,
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok || data?.success === false) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to create custom object in Salesforce'
)
logger.error('Failed to create custom object', { data, status: response.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
id: data.id,
fullName: params?.objectName ? toCustomApiName(params.objectName, 'Object Name') : '',
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created custom object metadata',
properties: CUSTOM_OBJECT_CREATE_OUTPUT_PROPERTIES,
},
},
}
+121
View File
@@ -0,0 +1,121 @@
import type {
SalesforceCreateLeadParams,
SalesforceCreateLeadResponse,
} 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'
export const salesforceCreateLeadTool: ToolConfig<
SalesforceCreateLeadParams,
SalesforceCreateLeadResponse
> = {
id: 'salesforce_create_lead',
name: 'Create Lead in Salesforce',
description: 'Create a new lead',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
lastName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Last name (required)',
},
company: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Company name (required)',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Email address',
},
phone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone number',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead status (e.g., Open, Working, Closed)',
},
leadSource: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead source (e.g., Web, Referral, Campaign)',
},
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { LastName: params.lastName, Company: params.company }
if (params.firstName) body.FirstName = params.firstName
if (params.email) body.Email = params.email
if (params.phone) body.Phone = params.phone
if (params.status) body.Status = params.status
if (params.leadSource) body.LeadSource = params.leadSource
if (params.title) body.Title = params.title
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok)
throw new Error(extractErrorMessage(data, response.status, 'Failed to create lead'))
return {
success: true,
output: {
id: data.id,
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created lead data',
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,120 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceCreateOpportunityParams,
SalesforceCreateOpportunityResponse,
} 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('SalesforceCreateOpportunity')
export const salesforceCreateOpportunityTool: ToolConfig<
SalesforceCreateOpportunityParams,
SalesforceCreateOpportunityResponse
> = {
id: 'salesforce_create_opportunity',
name: 'Create Opportunity in Salesforce',
description: 'Create a new opportunity',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Opportunity name (required)',
},
stageName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Stage name (required, e.g., Prospecting, Qualification, Closed Won)',
},
closeDate: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Close date in YYYY-MM-DD format (required)',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Salesforce Account ID (18-character string starting with 001)',
},
amount: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Deal amount as a number',
},
probability: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Win probability as integer (0-100)',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opportunity description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {
Name: params.name,
StageName: params.stageName,
CloseDate: params.closeDate,
}
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
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Failed to create opportunity', { data, status: response.status })
throw new Error(extractErrorMessage(data, response.status, 'Failed to create opportunity'))
}
return {
success: true,
output: {
id: data.id,
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created opportunity data',
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
},
},
}
+125
View File
@@ -0,0 +1,125 @@
import type {
SalesforceCreateTaskParams,
SalesforceCreateTaskResponse,
} 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'
export const salesforceCreateTaskTool: ToolConfig<
SalesforceCreateTaskParams,
SalesforceCreateTaskResponse
> = {
id: 'salesforce_create_task',
name: 'Create Task in Salesforce',
description: 'Create a new task',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
subject: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Task subject (required)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Status (e.g., Not Started, In Progress, Completed)',
},
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Priority (e.g., Low, Normal, High)',
},
activityDate: {
type: 'string',
required: false,
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,
visibility: 'user-or-llm',
description: 'Task description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { Subject: params.subject }
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.activityDate) body.ActivityDate = params.activityDate
if (params.whoId) body.WhoId = params.whoId.trim()
if (params.whatId) body.WhatId = params.whatId.trim()
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok)
throw new Error(extractErrorMessage(data, response.status, 'Failed to create task'))
return {
success: true,
output: {
id: data.id,
success: data.success === true,
created: data.success === true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created task data',
properties: SOBJECT_CREATE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,91 @@
import type {
SalesforceDeleteAccountParams,
SalesforceDeleteAccountResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceDeleteAccountTool: ToolConfig<
SalesforceDeleteAccountParams,
SalesforceDeleteAccountResponse
> = {
id: 'salesforce_delete_account',
name: 'Delete Account from Salesforce',
description: 'Delete an account from Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Account ID to delete (18-character string starting with 001)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const accountId = requireId(params.accountId, 'Account ID')
return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}`
},
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(
extractErrorMessage(data, response.status, 'Failed to delete account from Salesforce')
)
}
return {
success: true,
output: {
id: params?.accountId?.trim() || '',
deleted: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted account data',
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
},
},
}
+80
View File
@@ -0,0 +1,80 @@
import type {
SalesforceDeleteCaseParams,
SalesforceDeleteCaseResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceDeleteCaseTool: ToolConfig<
SalesforceDeleteCaseParams,
SalesforceDeleteCaseResponse
> = {
id: 'salesforce_delete_case',
name: 'Delete Case from Salesforce',
description: 'Delete a case',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
caseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Case ID to delete (18-character string starting with 500)',
},
},
request: {
url: (params) => {
const caseId = requireId(params.caseId, 'Case ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete case'))
}
return {
success: true,
output: {
id: params?.caseId?.trim() || '',
deleted: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted case data',
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,73 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceDeleteContactParams,
SalesforceDeleteContactResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceContacts')
export const salesforceDeleteContactTool: ToolConfig<
SalesforceDeleteContactParams,
SalesforceDeleteContactResponse
> = {
id: 'salesforce_delete_contact',
name: 'Delete Contact from Salesforce',
description: 'Delete a contact from Salesforce CRM',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
contactId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Contact ID to delete (18-character string starting with 003)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const contactId = requireId(params.contactId, 'Contact ID')
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
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(
extractErrorMessage(data, response.status, 'Failed to delete contact from Salesforce')
)
}
return {
success: true,
output: {
id: params?.contactId?.trim() || '',
deleted: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted contact data',
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,87 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceDeleteCustomFieldParams,
SalesforceDeleteCustomFieldResponse,
} from '@/tools/salesforce/types'
import { CUSTOM_FIELD_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceDeleteCustomField')
/**
* Delete a custom field via the Tooling API. Deleting a field removes its data;
* the field is moved to the org's recycle bin. Retrieve the field Id with the
* Tooling Query tool.
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
*/
export const salesforceDeleteCustomFieldTool: ToolConfig<
SalesforceDeleteCustomFieldParams,
SalesforceDeleteCustomFieldResponse
> = {
id: 'salesforce_delete_custom_field',
name: 'Delete Custom Field in Salesforce',
description: 'Delete a custom field from a Salesforce object using the Tooling API',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
fieldId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Tooling API Id of the custom field to delete (find it via the Tooling Query tool)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const fieldId = requireId(params.fieldId, 'Field ID')
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField/${fieldId}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to delete custom field in Salesforce'
)
logger.error('Failed to delete custom field', { status: response.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
id: params?.fieldId?.trim() ?? '',
deleted: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted custom field metadata',
properties: CUSTOM_FIELD_DELETE_OUTPUT_PROPERTIES,
},
},
}
+66
View File
@@ -0,0 +1,66 @@
import type {
SalesforceDeleteLeadParams,
SalesforceDeleteLeadResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceDeleteLeadTool: ToolConfig<
SalesforceDeleteLeadParams,
SalesforceDeleteLeadResponse
> = {
id: 'salesforce_delete_lead',
name: 'Delete Lead from Salesforce',
description: 'Delete a lead',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
leadId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Lead ID to delete (18-character string starting with 00Q)',
},
},
request: {
url: (params) => {
const leadId = requireId(params.leadId, 'Lead ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}`
},
method: 'DELETE',
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete lead'))
}
return {
success: true,
output: {
id: params?.leadId?.trim() || '',
deleted: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted lead data',
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,70 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceDeleteOpportunityParams,
SalesforceDeleteOpportunityResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceDeleteOpportunity')
export const salesforceDeleteOpportunityTool: ToolConfig<
SalesforceDeleteOpportunityParams,
SalesforceDeleteOpportunityResponse
> = {
id: 'salesforce_delete_opportunity',
name: 'Delete Opportunity from Salesforce',
description: 'Delete an opportunity',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
opportunityId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Opportunity ID to delete (18-character string starting with 006)',
},
},
request: {
url: (params) => {
const opportunityId = requireId(params.opportunityId, 'Opportunity ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}`
},
method: 'DELETE',
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
logger.error('Failed to delete opportunity', { data, status: response.status })
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete opportunity'))
}
return {
success: true,
output: {
id: params?.opportunityId?.trim() || '',
deleted: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted opportunity data',
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
},
},
}
+80
View File
@@ -0,0 +1,80 @@
import type {
SalesforceDeleteTaskParams,
SalesforceDeleteTaskResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceDeleteTaskTool: ToolConfig<
SalesforceDeleteTaskParams,
SalesforceDeleteTaskResponse
> = {
id: 'salesforce_delete_task',
name: 'Delete Task from Salesforce',
description: 'Delete a task',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Task ID to delete (18-character string starting with 00T)',
},
},
request: {
url: (params) => {
const taskId = requireId(params.taskId, 'Task ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(extractErrorMessage(data, response.status, 'Failed to delete task'))
}
return {
success: true,
output: {
id: params?.taskId?.trim() || '',
deleted: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted task data',
properties: SOBJECT_DELETE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,100 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceDescribeObjectParams,
SalesforceDescribeObjectResponse,
} from '@/tools/salesforce/types'
import { DESCRIBE_OBJECT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceQuery')
/**
* Describe a Salesforce object to get its metadata/fields
* Useful for discovering available fields for queries
*/
export const salesforceDescribeObjectTool: ToolConfig<
SalesforceDescribeObjectParams,
SalesforceDescribeObjectResponse
> = {
id: 'salesforce_describe_object',
name: 'Describe Salesforce Object',
description: 'Get metadata and field information for a Salesforce object',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
objectName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce object API name (e.g., Account, Contact, Lead, Custom_Object__c)',
},
},
request: {
url: (params) => {
if (!params.objectName || params.objectName.trim() === '') {
throw new Error(
'Object Name is required. Please provide a valid Salesforce object API name (e.g., Account, Contact, Lead).'
)
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const objectName = params.objectName.trim()
return `${instanceUrl}/services/data/v59.0/sobjects/${objectName}/describe`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
`Failed to describe object: ${params?.objectName}`
)
logger.error('Failed to describe object', { data, status: response.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
objectName: data.name ?? params?.objectName ?? '',
label: data.label,
labelPlural: data.labelPlural,
fields: data.fields,
keyPrefix: data.keyPrefix ?? null,
queryable: data.queryable,
createable: data.createable,
updateable: data.updateable,
deletable: data.deletable,
childRelationships: data.childRelationships,
recordTypeInfos: data.recordTypeInfos,
fieldCount: data.fields?.length || 0,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Object metadata',
properties: DESCRIBE_OBJECT_OUTPUT_PROPERTIES,
},
},
}
+133
View File
@@ -0,0 +1,133 @@
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'
export const salesforceGetAccountsTool: ToolConfig<
SalesforceGetAccountsParams,
SalesforceGetAccountsResponse
> = {
id: 'salesforce_get_accounts',
name: 'Get Accounts from Salesforce',
description: 'Retrieve accounts from Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Salesforce API',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID token from Salesforce OAuth (contains instance URL)',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The Salesforce instance URL',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (default: 100, max: 2000)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated field API names (e.g., "Id,Name,Industry,Phone")',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Field and direction for sorting (e.g., "Name ASC" or "CreatedDate DESC")',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields =
params.fields ||
'Id,Name,Type,Industry,BillingCity,BillingState,BillingCountry,Phone,Website'
const orderBy = params.orderBy || 'Name ASC'
// Build SOQL query
const query = `SELECT ${fields} FROM Account ORDER BY ${orderBy} LIMIT ${limit}`
const encodedQuery = encodeURIComponent(query)
return `${instanceUrl}/services/data/v59.0/query?q=${encodedQuery}`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
throw new Error(
extractErrorMessage(data, response.status, 'Failed to fetch accounts from Salesforce')
)
}
const accounts = data.records || []
return {
success: true,
output: {
accounts,
paging: {
nextRecordsUrl: data.nextRecordsUrl ?? null,
totalSize: data.totalSize || accounts.length,
done: data.done !== false,
},
metadata: {
totalReturned: accounts.length,
hasMore: !data.done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Accounts data',
properties: {
accounts: { type: 'array', description: 'Array of account objects' },
paging: QUERY_PAGING_OUTPUT,
metadata: RESPONSE_METADATA_OUTPUT,
success: { type: 'boolean', description: 'Salesforce operation success' },
},
},
},
}
+117
View File
@@ -0,0 +1,117 @@
import type { SalesforceGetCasesParams, SalesforceGetCasesResponse } from '@/tools/salesforce/types'
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceGetCasesTool: ToolConfig<
SalesforceGetCasesParams,
SalesforceGetCasesResponse
> = {
id: 'salesforce_get_cases',
name: 'Get Cases from Salesforce',
description: 'Get case(s) from Salesforce',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
caseId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Salesforce Case ID (18-character string starting with 500) to get a single case',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of field API names to return',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Field and direction for sorting (e.g., CreatedDate DESC)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.caseId) {
const caseId = requireId(params.caseId, 'Case ID')
const fields =
params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId'
return `${instanceUrl}/services/data/v59.0/sobjects/Case/${caseId}?fields=${encodeURIComponent(fields)}`
}
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields =
params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId'
const orderBy = params.orderBy || 'CreatedDate DESC'
const query = `SELECT ${fields} FROM Case ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok)
throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch cases'))
if (params?.caseId) {
return {
success: true,
output: { case: data, success: true },
}
}
const cases = data.records || []
return {
success: true,
output: {
cases,
paging: {
nextRecordsUrl: data.nextRecordsUrl ?? null,
totalSize: data.totalSize || cases.length,
done: data.done !== false,
},
metadata: {
totalReturned: cases.length,
hasMore: !data.done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Case data',
properties: {
case: { type: 'object', description: 'Single case object (when caseId provided)' },
cases: { type: 'array', description: 'Array of case objects (when listing)' },
paging: QUERY_PAGING_OUTPUT,
metadata: RESPONSE_METADATA_OUTPUT,
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}
+143
View File
@@ -0,0 +1,143 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceGetContactsParams,
SalesforceGetContactsResponse,
} from '@/tools/salesforce/types'
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceContacts')
export const salesforceGetContactsTool: ToolConfig<
SalesforceGetContactsParams,
SalesforceGetContactsResponse
> = {
id: 'salesforce_get_contacts',
name: 'Get Contacts from Salesforce',
description: 'Get contact(s) from Salesforce - single contact if ID provided, or list if not',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
contactId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Salesforce Contact ID (18-character string starting with 003) to get a single contact',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (default: 100, max: 2000). Only for list query.',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated field API names (e.g., "Id,FirstName,LastName,Email,Phone")',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Field and direction for sorting (e.g., "LastName ASC"). Only for list query.',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
// Single contact by ID
if (params.contactId) {
const contactId = requireId(params.contactId, 'Contact ID')
const fields =
params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department'
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}?fields=${encodeURIComponent(fields)}`
}
// List contacts with SOQL query
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields = params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department'
const orderBy = params.orderBy || 'LastName ASC'
const query = `SELECT ${fields} FROM Contact ORDER BY ${orderBy} LIMIT ${limit}`
const encodedQuery = encodeURIComponent(query)
return `${instanceUrl}/services/data/v59.0/query?q=${encodedQuery}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params?) => {
const data = await response.json()
if (!response.ok) {
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(
extractErrorMessage(data, response.status, 'Failed to fetch contacts from Salesforce')
)
}
// Single contact response
if (params?.contactId) {
return {
success: true,
output: {
contact: data,
singleContact: true,
success: true,
},
}
}
// List contacts response
const contacts = data.records || []
return {
success: true,
output: {
contacts,
paging: {
nextRecordsUrl: data.nextRecordsUrl ?? null,
totalSize: data.totalSize || contacts.length,
done: data.done !== false,
},
metadata: {
totalReturned: contacts.length,
hasMore: !data.done,
},
singleContact: false,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Contact(s) data',
properties: {
contacts: { type: 'array', description: 'Array of contacts (list query)' },
contact: { type: 'object', description: 'Single contact (by ID)' },
paging: QUERY_PAGING_OUTPUT,
metadata: RESPONSE_METADATA_OUTPUT,
singleContact: { type: 'boolean', description: 'Whether single contact was returned' },
success: { type: 'boolean', description: 'Salesforce operation success' },
},
},
},
}
@@ -0,0 +1,94 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceGetDashboardParams,
SalesforceGetDashboardResponse,
} from '@/tools/salesforce/types'
import { DASHBOARD_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceDashboards')
/**
* Get details for a specific dashboard
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_dashboard_results.htm
*/
export const salesforceGetDashboardTool: ToolConfig<
SalesforceGetDashboardParams,
SalesforceGetDashboardResponse
> = {
id: 'salesforce_get_dashboard',
name: 'Get Dashboard from Salesforce',
description: 'Get details and results for a specific dashboard',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
dashboardId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Dashboard ID (18-character string starting with 01Z)',
},
},
request: {
url: (params) => {
if (!params.dashboardId || params.dashboardId.trim() === '') {
throw new Error('Dashboard ID is required. Please provide a valid Salesforce Dashboard ID.')
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${params.dashboardId}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
`Failed to get dashboard ID: ${params?.dashboardId}`
)
logger.error('Failed to get dashboard', { data, status: response.status })
throw new Error(errorMessage)
}
const meta = data.dashboardMetadata ?? {}
const attrs = meta.attributes ?? {}
return {
success: true,
output: {
dashboard: data,
dashboardId: attrs.dashboardId ?? params?.dashboardId ?? '',
components: data.componentData || [],
dashboardName: attrs.dashboardName ?? null,
dashboardMetadata: data.dashboardMetadata ?? null,
runningUser: meta.runningUser ?? null,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Dashboard data',
properties: DASHBOARD_OUTPUT_PROPERTIES,
},
},
}
+121
View File
@@ -0,0 +1,121 @@
import type { SalesforceGetLeadsParams, SalesforceGetLeadsResponse } from '@/tools/salesforce/types'
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceGetLeadsTool: ToolConfig<
SalesforceGetLeadsParams,
SalesforceGetLeadsResponse
> = {
id: 'salesforce_get_leads',
name: 'Get Leads from Salesforce',
description: 'Retrieve lead(s) from Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
leadId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Salesforce Lead ID (18-character string starting with 00Q) to get a single lead',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of field API names to return',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Field and direction for sorting (e.g., LastName ASC)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.leadId) {
const leadId = requireId(params.leadId, 'Lead ID')
const fields =
params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource'
return `${instanceUrl}/services/data/v59.0/sobjects/Lead/${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'
const orderBy = params.orderBy || 'LastName ASC'
const query = `SELECT ${fields} FROM Lead ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok)
throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch leads'))
if (params?.leadId) {
return {
success: true,
output: {
lead: data,
singleLead: true,
success: true,
},
}
}
const leads = data.records || []
return {
success: true,
output: {
leads,
paging: {
nextRecordsUrl: data.nextRecordsUrl ?? null,
totalSize: data.totalSize || leads.length,
done: data.done !== false,
},
metadata: {
totalReturned: leads.length,
hasMore: !data.done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Lead data',
properties: {
lead: { type: 'object', description: 'Single lead object (when leadId provided)' },
leads: { type: 'array', description: 'Array of lead objects (when listing)' },
paging: QUERY_PAGING_OUTPUT,
metadata: RESPONSE_METADATA_OUTPUT,
singleLead: { type: 'boolean', description: 'Whether single lead was returned' },
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}
@@ -0,0 +1,129 @@
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 { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceGetOpportunities')
export const salesforceGetOpportunitiesTool: ToolConfig<
SalesforceGetOpportunitiesParams,
SalesforceGetOpportunitiesResponse
> = {
id: 'salesforce_get_opportunities',
name: 'Get Opportunities from Salesforce',
description: 'Get opportunity(ies) from Salesforce',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
opportunityId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Salesforce Opportunity ID (18-character string starting with 006) to get a single opportunity',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of field API names to return',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Field and direction for sorting (e.g., CloseDate DESC)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.opportunityId) {
const opportunityId = requireId(params.opportunityId, 'Opportunity ID')
const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability'
return `${instanceUrl}/services/data/v59.0/sobjects/Opportunity/${opportunityId}?fields=${encodeURIComponent(fields)}`
}
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability'
const orderBy = params.orderBy || 'CloseDate DESC'
const query = `SELECT ${fields} FROM Opportunity ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
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,
output: { opportunity: data, success: true },
}
}
const opportunities = data.records || []
return {
success: true,
output: {
opportunities,
paging: {
nextRecordsUrl: data.nextRecordsUrl ?? null,
totalSize: data.totalSize || opportunities.length,
done: data.done !== false,
},
metadata: {
totalReturned: opportunities.length,
hasMore: !data.done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Opportunity data',
properties: {
opportunity: {
type: 'object',
description: 'Single opportunity object (when opportunityId provided)',
},
opportunities: {
type: 'array',
description: 'Array of opportunity objects (when listing)',
},
paging: QUERY_PAGING_OUTPUT,
metadata: RESPONSE_METADATA_OUTPUT,
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}
+87
View File
@@ -0,0 +1,87 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceGetReportParams,
SalesforceGetReportResponse,
} from '@/tools/salesforce/types'
import { GET_REPORT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceReports')
/**
* 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,
SalesforceGetReportResponse
> = {
id: 'salesforce_get_report',
name: 'Get Report Metadata from Salesforce',
description: 'Get the describe (definition and metadata) for a specific report',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
reportId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Report ID (18-character string starting with 00O)',
},
},
request: {
url: (params) => {
if (!params.reportId || params.reportId.trim() === '') {
throw new Error('Report ID is required. Please provide a valid Salesforce Report ID.')
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/analytics/reports/${params.reportId}/describe`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
`Failed to get report metadata for report ID: ${params?.reportId}`
)
logger.error('Failed to get report metadata', { data, status: response.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
report: data,
reportId: params?.reportId || '',
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Report metadata',
properties: GET_REPORT_OUTPUT_PROPERTIES,
},
},
}
+128
View File
@@ -0,0 +1,128 @@
import type { SalesforceGetTasksParams, SalesforceGetTasksResponse } from '@/tools/salesforce/types'
import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceGetTasksTool: ToolConfig<
SalesforceGetTasksParams,
SalesforceGetTasksResponse
> = {
id: 'salesforce_get_tasks',
name: 'Get Tasks from Salesforce',
description: 'Get task(s) from Salesforce',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
taskId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Salesforce Task ID (18-character string starting with 00T) to get a single task',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of field API names to return',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Field and direction for sorting (e.g., ActivityDate DESC)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.taskId) {
const taskId = requireId(params.taskId, 'Task ID')
const fields =
params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId'
return `${instanceUrl}/services/data/v59.0/sobjects/Task/${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'
const orderBy = params.orderBy || 'ActivityDate DESC'
const query = `SELECT ${fields} FROM Task ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok)
throw new Error(extractErrorMessage(data, response.status, 'Failed to fetch tasks'))
if (params?.taskId) {
return {
success: true,
output: { task: data, success: true },
}
}
const tasks = data.records || []
return {
success: true,
output: {
tasks,
paging: {
nextRecordsUrl: data.nextRecordsUrl ?? null,
totalSize: data.totalSize || tasks.length,
done: data.done !== false,
},
metadata: {
totalReturned: tasks.length,
hasMore: !data.done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Task data',
properties: {
task: { type: 'object', description: 'Single task object (when taskId provided)' },
tasks: { type: 'array', description: 'Array of task objects (when listing)' },
paging: QUERY_PAGING_OUTPUT,
metadata: RESPONSE_METADATA_OUTPUT,
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}
+40
View File
@@ -0,0 +1,40 @@
export { salesforceCreateAccountTool } from './create_account'
export { salesforceCreateCaseTool } from './create_case'
export { salesforceCreateContactTool } from './create_contact'
export { salesforceCreateCustomFieldTool } from './create_custom_field'
export { salesforceCreateCustomObjectTool } from './create_custom_object'
export { salesforceCreateLeadTool } from './create_lead'
export { salesforceCreateOpportunityTool } from './create_opportunity'
export { salesforceCreateTaskTool } from './create_task'
export { salesforceDeleteAccountTool } from './delete_account'
export { salesforceDeleteCaseTool } from './delete_case'
export { salesforceDeleteContactTool } from './delete_contact'
export { salesforceDeleteCustomFieldTool } from './delete_custom_field'
export { salesforceDeleteLeadTool } from './delete_lead'
export { salesforceDeleteOpportunityTool } from './delete_opportunity'
export { salesforceDeleteTaskTool } from './delete_task'
export { salesforceDescribeObjectTool } from './describe_object'
export { salesforceGetAccountsTool } from './get_accounts'
export { salesforceGetCasesTool } from './get_cases'
export { salesforceGetContactsTool } from './get_contacts'
export { salesforceGetDashboardTool } from './get_dashboard'
export { salesforceGetLeadsTool } from './get_leads'
export { salesforceGetOpportunitiesTool } from './get_opportunities'
export { salesforceGetReportTool } from './get_report'
export { salesforceGetTasksTool } from './get_tasks'
export { salesforceListDashboardsTool } from './list_dashboards'
export { salesforceListObjectsTool } from './list_objects'
export { salesforceListReportTypesTool } from './list_report_types'
export { salesforceListReportsTool } from './list_reports'
export { salesforceQueryTool } from './query'
export { salesforceQueryMoreTool } from './query_more'
export { salesforceRefreshDashboardTool } from './refresh_dashboard'
export { salesforceRunReportTool } from './run_report'
export { salesforceToolingQueryTool } from './tooling_query'
export { salesforceUpdateAccountTool } from './update_account'
export { salesforceUpdateCaseTool } from './update_case'
export { salesforceUpdateContactTool } from './update_contact'
export { salesforceUpdateCustomFieldTool } from './update_custom_field'
export { salesforceUpdateLeadTool } from './update_lead'
export { salesforceUpdateOpportunityTool } from './update_opportunity'
export { salesforceUpdateTaskTool } from './update_task'
@@ -0,0 +1,91 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceListDashboardsParams,
SalesforceListDashboardsResponse,
} from '@/tools/salesforce/types'
import { LIST_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceDashboards')
/**
* List the current user's recently used dashboards.
* The Dashboard List resource returns recently used dashboards, not the org's
* full dashboard catalog.
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_getbasic_dashboardlist.htm
*/
export const salesforceListDashboardsTool: ToolConfig<
SalesforceListDashboardsParams,
SalesforceListDashboardsResponse
> = {
id: 'salesforce_list_dashboards',
name: 'List Dashboards from Salesforce',
description: 'Get a list of recently used dashboards for the current user',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/analytics/dashboards`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to list dashboards from Salesforce'
)
logger.error('Failed to list dashboards', { data, status: response.status })
throw new Error(errorMessage)
}
// 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,
output: {
dashboards,
totalReturned: dashboards.length,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Dashboards data',
properties: {
dashboards: { type: 'array', description: 'Array of dashboard objects' },
...LIST_OUTPUT_PROPERTIES,
},
},
},
}
+82
View File
@@ -0,0 +1,82 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceListObjectsParams,
SalesforceListObjectsResponse,
} from '@/tools/salesforce/types'
import { LIST_OBJECTS_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceQuery')
/**
* List all available Salesforce objects
* Useful for discovering what objects are available
*/
export const salesforceListObjectsTool: ToolConfig<
SalesforceListObjectsParams,
SalesforceListObjectsResponse
> = {
id: 'salesforce_list_objects',
name: 'List Salesforce Objects',
description: 'Get a list of all available Salesforce objects',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/sobjects`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to list Salesforce objects'
)
logger.error('Failed to list objects', { data, status: response.status })
throw new Error(errorMessage)
}
const objects = data.sobjects || []
return {
success: true,
output: {
objects,
encoding: data.encoding ?? null,
maxBatchSize: data.maxBatchSize ?? null,
totalReturned: objects.length,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Objects list',
properties: LIST_OBJECTS_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,81 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceListReportTypesParams,
SalesforceListReportTypesResponse,
} from '@/tools/salesforce/types'
import { LIST_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceReports')
/**
* Get list of available report types
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_list_reporttypes.htm
*/
export const salesforceListReportTypesTool: ToolConfig<
SalesforceListReportTypesParams,
SalesforceListReportTypesResponse
> = {
id: 'salesforce_list_report_types',
name: 'List Report Types from Salesforce',
description: 'Get a list of available report types',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/analytics/reportTypes`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to list report types from Salesforce'
)
logger.error('Failed to list report types', { data, status: response.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
reportTypes: data,
totalReturned: Array.isArray(data) ? data.length : 0,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Report types data',
properties: {
reportTypes: { type: 'array', description: 'Array of report type objects' },
...LIST_OUTPUT_PROPERTIES,
},
},
},
}
+101
View File
@@ -0,0 +1,101 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceListReportsParams,
SalesforceListReportsResponse,
} from '@/tools/salesforce/types'
import { LIST_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceReports')
/**
* List up to 200 of the current user's most recently viewed reports.
* The Report List resource returns recently viewed reports, not the org's full
* report catalog — use a SOQL query against the Report object for that.
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportlist.htm
*/
export const salesforceListReportsTool: ToolConfig<
SalesforceListReportsParams,
SalesforceListReportsResponse
> = {
id: 'salesforce_list_reports',
name: 'List Reports from Salesforce',
description: 'Get a list of up to 200 recently viewed reports for the current user',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
searchTerm: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter reports by name (case-insensitive partial match)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/analytics/reports`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to list reports from Salesforce'
)
logger.error('Failed to list reports', { data, status: response.status })
throw new Error(errorMessage)
}
// 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 : []
// 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())
)
}
return {
success: true,
output: {
reports,
totalReturned: reports.length,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Reports data',
properties: {
reports: { type: 'array', description: 'Array of report objects' },
...LIST_OUTPUT_PROPERTIES,
},
},
},
}
+94
View File
@@ -0,0 +1,94 @@
import { createLogger } from '@sim/logger'
import type { SalesforceQueryParams, SalesforceQueryResponse } from '@/tools/salesforce/types'
import { QUERY_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceQuery')
/**
* Execute a custom SOQL query
* @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm
*/
export const salesforceQueryTool: ToolConfig<SalesforceQueryParams, SalesforceQueryResponse> = {
id: 'salesforce_query',
name: 'Run SOQL Query in Salesforce',
description: 'Execute a custom SOQL query to retrieve data from Salesforce',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SOQL query to execute (e.g., SELECT Id, Name FROM Account LIMIT 10)',
},
},
request: {
url: (params) => {
if (!params.query || params.query.trim() === '') {
throw new Error(
'SOQL Query is required. Please provide a valid SOQL query (e.g., SELECT Id, Name FROM Account LIMIT 10).'
)
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const encodedQuery = encodeURIComponent(params.query)
return `${instanceUrl}/services/data/v59.0/query?q=${encodedQuery}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to execute SOQL query'
)
logger.error('Failed to execute SOQL query', { data, status: response.status })
throw new Error(errorMessage)
}
const records = data.records || []
const done = data.done !== false
return {
success: true,
output: {
records,
totalSize: data.totalSize || records.length,
done,
nextRecordsUrl: data.nextRecordsUrl ?? null,
query: params?.query || '',
metadata: {
totalReturned: records.length,
hasMore: !done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Query results',
properties: QUERY_OUTPUT_PROPERTIES,
},
},
}
+103
View File
@@ -0,0 +1,103 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceQueryMoreParams,
SalesforceQueryMoreResponse,
} from '@/tools/salesforce/types'
import { QUERY_MORE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceQuery')
/**
* Retrieve additional query results using the nextRecordsUrl
* @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm
*/
export const salesforceQueryMoreTool: ToolConfig<
SalesforceQueryMoreParams,
SalesforceQueryMoreResponse
> = {
id: 'salesforce_query_more',
name: 'Get More Query Results from Salesforce',
description: 'Retrieve additional query results using the nextRecordsUrl from a previous query',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
nextRecordsUrl: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The nextRecordsUrl value from a previous query response (e.g., /services/data/v59.0/query/01g...)',
},
},
request: {
url: (params) => {
if (!params.nextRecordsUrl || params.nextRecordsUrl.trim() === '') {
throw new Error(
'Next Records URL is required. This should be the nextRecordsUrl value from a previous query response.'
)
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
// nextRecordsUrl is typically a relative path like /services/data/v59.0/query/01g...
const nextUrl = params.nextRecordsUrl.startsWith('/')
? params.nextRecordsUrl
: `/${params.nextRecordsUrl}`
return `${instanceUrl}${nextUrl}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to get more query results'
)
logger.error('Failed to get more query results', { data, status: response.status })
throw new Error(errorMessage)
}
const records = data.records || []
const done = data.done !== false
return {
success: true,
output: {
records,
totalSize: data.totalSize || records.length,
done,
nextRecordsUrl: data.nextRecordsUrl ?? null,
metadata: {
totalReturned: records.length,
hasMore: !done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Query results',
properties: QUERY_MORE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,96 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceRefreshDashboardParams,
SalesforceRefreshDashboardResponse,
} from '@/tools/salesforce/types'
import { REFRESH_DASHBOARD_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceDashboards')
/**
* Refresh a dashboard to get latest data
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_refresh_dashboard.htm
*/
export const salesforceRefreshDashboardTool: ToolConfig<
SalesforceRefreshDashboardParams,
SalesforceRefreshDashboardResponse
> = {
id: 'salesforce_refresh_dashboard',
name: 'Refresh Dashboard in Salesforce',
description: 'Refresh a dashboard to get the latest data',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
dashboardId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Dashboard ID (18-character string starting with 01Z)',
},
},
request: {
url: (params) => {
if (!params.dashboardId || params.dashboardId.trim() === '') {
throw new Error('Dashboard ID is required. Please provide a valid Salesforce Dashboard ID.')
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${params.dashboardId}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: () => ({}),
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
`Failed to refresh dashboard ID: ${params?.dashboardId}`
)
logger.error('Failed to refresh dashboard', { data, status: response.status })
throw new Error(errorMessage)
}
const meta = data.dashboardMetadata ?? {}
const attrs = meta.attributes ?? {}
return {
success: true,
output: {
dashboard: data,
dashboardId: attrs.dashboardId ?? params?.dashboardId ?? '',
components: data.componentData ?? data.componentStatus ?? [],
status: data.dashboardStatus ?? data.status ?? null,
statusUrl: data.statusUrl ?? attrs.statusUrl ?? null,
dashboardName: attrs.dashboardName ?? null,
dashboardMetadata: data.dashboardMetadata ?? null,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Refreshed dashboard data',
properties: REFRESH_DASHBOARD_OUTPUT_PROPERTIES,
},
},
}
+127
View File
@@ -0,0 +1,127 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import type {
SalesforceRunReportParams,
SalesforceRunReportResponse,
} from '@/tools/salesforce/types'
import { RUN_REPORT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceReports')
/**
* Run a report and return the results
* @see https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportdata.htm
*/
export const salesforceRunReportTool: ToolConfig<
SalesforceRunReportParams,
SalesforceRunReportResponse
> = {
id: 'salesforce_run_report',
name: 'Run Report in Salesforce',
description: 'Execute a report and retrieve the results',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
reportId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Report ID (18-character string starting with 00O)',
},
includeDetails: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Include detail rows (true/false, default: true)',
},
filters: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of report filter objects to apply',
},
},
request: {
url: (params) => {
if (!params.reportId || params.reportId.trim() === '') {
throw new Error('Report ID is required. Please provide a valid Salesforce Report ID.')
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
// Default to including detail rows (Salesforce's own API default is false);
// report runs in a workflow almost always want the underlying rows.
const includeDetails = params.includeDetails !== 'false'
return `${instanceUrl}/services/data/v59.0/analytics/reports/${params.reportId}?includeDetails=${includeDetails}`
},
// Use GET for simple report runs, POST only when filters are provided
method: (params) => (params.filters ? 'POST' : 'GET'),
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
// Only send a body when filters are provided (POST request)
if (params.filters) {
try {
const filters = JSON.parse(params.filters)
return { reportMetadata: { reportFilters: filters } }
} catch (e) {
throw new Error(
`Invalid report filters JSON: ${getErrorMessage(e, 'Parse error')}. Please provide a valid JSON array of filter objects.`
)
}
}
// Return undefined for GET requests (no body)
return undefined
},
},
transformResponse: async (response, params?) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
`Failed to run report ID: ${params?.reportId}`
)
logger.error('Failed to run report', { data, status: response.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
reportId: params?.reportId || '',
reportMetadata: data.reportMetadata ?? null,
reportExtendedMetadata: data.reportExtendedMetadata ?? null,
factMap: data.factMap ?? null,
groupingsDown: data.groupingsDown ?? null,
groupingsAcross: data.groupingsAcross ?? null,
hasDetailRows: data.hasDetailRows ?? null,
allData: data.allData ?? null,
reportName: data.reportMetadata?.name ?? data.attributes?.reportName ?? null,
reportFormat: data.reportMetadata?.reportFormat ?? null,
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Report results',
properties: RUN_REPORT_OUTPUT_PROPERTIES,
},
},
}
+104
View File
@@ -0,0 +1,104 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceToolingQueryParams,
SalesforceToolingQueryResponse,
} from '@/tools/salesforce/types'
import { TOOLING_QUERY_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceToolingQuery')
/**
* Execute a SOQL query against the Tooling API. Use this to inspect metadata
* objects such as CustomField and CustomObject — for example to find a field's
* Id before updating or deleting it:
* `SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account'`.
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/intro_rest_resources.htm
*/
export const salesforceToolingQueryTool: ToolConfig<
SalesforceToolingQueryParams,
SalesforceToolingQueryResponse
> = {
id: 'salesforce_tooling_query',
name: 'Run Tooling SOQL Query in Salesforce',
description: 'Execute a SOQL query against the Tooling API to inspect metadata objects',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
"Tooling SOQL query (e.g., SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account')",
},
},
request: {
url: (params) => {
if (!params.query || params.query.trim() === '') {
throw new Error(
"Tooling SOQL Query is required (e.g., SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId = 'Account')."
)
}
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const encodedQuery = encodeURIComponent(params.query)
return `${instanceUrl}/services/data/v59.0/tooling/query?q=${encodedQuery}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to execute Tooling SOQL query'
)
logger.error('Failed to execute Tooling SOQL query', { data, status: response.status })
throw new Error(errorMessage)
}
const records = data.records || []
const done = data.done !== false
return {
success: true,
output: {
records,
totalSize: data.totalSize ?? records.length,
done,
nextRecordsUrl: data.nextRecordsUrl ?? null,
query: params?.query || '',
metadata: {
totalReturned: records.length,
hasMore: !done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Tooling query results',
properties: TOOLING_QUERY_OUTPUT_PROPERTIES,
},
},
}
File diff suppressed because it is too large Load Diff
+190
View File
@@ -0,0 +1,190 @@
import type {
SalesforceUpdateAccountParams,
SalesforceUpdateAccountResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceUpdateAccountTool: ToolConfig<
SalesforceUpdateAccountParams,
SalesforceUpdateAccountResponse
> = {
id: 'salesforce_update_account',
name: 'Update Account in Salesforce',
description: 'Update an existing account in Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Account ID to update (18-character string starting with 001)',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Account name',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Account type (e.g., Customer, Partner, Prospect)',
},
industry: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Industry (e.g., Technology, Healthcare, Finance)',
},
phone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone number',
},
website: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Website URL',
},
billingStreet: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing street address',
},
billingCity: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing city',
},
billingState: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing state/province',
},
billingPostalCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing postal code',
},
billingCountry: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Billing country',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Account description',
},
annualRevenue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Annual revenue as a number',
},
numberOfEmployees: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Number of employees as an integer',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const accountId = requireId(params.accountId, 'Account ID')
return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}`
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {}
if (params.name) body.Name = params.name
if (params.type) body.Type = params.type
if (params.industry) body.Industry = params.industry
if (params.phone) body.Phone = params.phone
if (params.website) body.Website = params.website
if (params.billingStreet) body.BillingStreet = params.billingStreet
if (params.billingCity) body.BillingCity = params.billingCity
if (params.billingState) body.BillingState = params.billingState
if (params.billingPostalCode) body.BillingPostalCode = params.billingPostalCode
if (params.billingCountry) body.BillingCountry = params.billingCountry
if (params.description) body.Description = params.description
if (params.annualRevenue) body.AnnualRevenue = Number.parseFloat(params.annualRevenue)
if (params.numberOfEmployees)
body.NumberOfEmployees = Number.parseInt(params.numberOfEmployees)
return body
},
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json()
throw new Error(
extractErrorMessage(data, response.status, 'Failed to update account in Salesforce')
)
}
return {
success: true,
output: {
id: params?.accountId?.trim() || '',
updated: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated account data',
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
},
},
}
+134
View File
@@ -0,0 +1,134 @@
import type {
SalesforceUpdateCaseParams,
SalesforceUpdateCaseResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceUpdateCaseTool: ToolConfig<
SalesforceUpdateCaseParams,
SalesforceUpdateCaseResponse
> = {
id: 'salesforce_update_case',
name: 'Update Case in Salesforce',
description: 'Update an existing case',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
caseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Case ID to update (18-character string starting with 500)',
},
subject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Case subject',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Status (e.g., New, Working, Escalated, Closed)',
},
priority: {
type: 'string',
required: false,
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,
visibility: 'user-or-llm',
description: 'Case description',
},
},
request: {
url: (params) => {
const caseId = requireId(params.caseId, 'Case ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.subject) body.Subject = params.subject
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.origin) body.Origin = params.origin
if (params.contactId) body.ContactId = params.contactId.trim()
if (params.accountId) body.AccountId = params.accountId.trim()
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json()
throw new Error(extractErrorMessage(data, response.status, 'Failed to update case'))
}
return {
success: true,
output: {
id: params?.caseId?.trim() || '',
updated: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated case data',
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
},
},
}
+166
View File
@@ -0,0 +1,166 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceUpdateContactParams,
SalesforceUpdateContactResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceContacts')
export const salesforceUpdateContactTool: ToolConfig<
SalesforceUpdateContactParams,
SalesforceUpdateContactResponse
> = {
id: 'salesforce_update_contact',
name: 'Update Contact in Salesforce',
description: 'Update an existing contact in Salesforce CRM',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
contactId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Contact ID to update (18-character string starting with 003)',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Last name',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Email address',
},
phone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone number',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Salesforce Account ID (18-character string starting with 001)',
},
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
department: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Department',
},
mailingStreet: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing street',
},
mailingCity: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing city',
},
mailingState: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing state',
},
mailingPostalCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing postal code',
},
mailingCountry: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Mailing country',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Contact description',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const contactId = requireId(params.contactId, 'Contact ID')
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.lastName) body.LastName = params.lastName
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.trim()
if (params.title) body.Title = params.title
if (params.department) body.Department = params.department
if (params.mailingStreet) body.MailingStreet = params.mailingStreet
if (params.mailingCity) body.MailingCity = params.mailingCity
if (params.mailingState) body.MailingState = params.mailingState
if (params.mailingPostalCode) body.MailingPostalCode = params.mailingPostalCode
if (params.mailingCountry) body.MailingCountry = params.mailingCountry
if (params.description) body.Description = params.description
return body
},
},
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(
extractErrorMessage(data, response.status, 'Failed to update contact in Salesforce')
)
}
return {
success: true,
output: {
id: params?.contactId?.trim() || '',
updated: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated contact data',
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,236 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceUpdateCustomFieldParams,
SalesforceUpdateCustomFieldResponse,
} from '@/tools/salesforce/types'
import { CUSTOM_FIELD_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import {
extractErrorMessage,
getInstanceUrl,
mergeCustomFieldMetadata,
requireId,
} from '@/tools/salesforce/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
const logger = createLogger('SalesforceUpdateCustomField')
/**
* Update an existing custom field via the Tooling API.
*
* Updates a field's attributes (label, length, help text, required, picklist
* values, etc.) while keeping its existing data type — changing a field's type
* is a separate, conversion-driven operation in Salesforce and is intentionally
* out of scope here.
*
* The Tooling API PATCH replaces the field's entire `Metadata` compound, so a
* naive partial PATCH would wipe any property the caller omits. To avoid that,
* this tool performs a read-modify-write in `directExecution`: it GETs the
* field's current metadata, overlays only the provided changes, then PATCHes the
* merged result. Unspecified properties (type, length, etc.) are preserved.
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
*/
export const salesforceUpdateCustomFieldTool: ToolConfig<
SalesforceUpdateCustomFieldParams,
SalesforceUpdateCustomFieldResponse
> = {
id: 'salesforce_update_custom_field',
name: 'Update Custom Field in Salesforce',
description: 'Update an existing custom field on a Salesforce object using the Tooling API',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
fieldId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Tooling API Id of the custom field to update (find it via the Tooling Query tool)',
},
label: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Display label shown in the UI',
},
length: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum length for Text, LongTextArea, Html, or MultiselectPicklist fields',
},
precision: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Total number of digits for Number, Currency, or Percent fields',
},
scale: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of digits to the right of the decimal for numeric fields',
},
visibleLines: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of visible lines for LongTextArea, Html, or MultiselectPicklist fields',
},
required: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the field is required on record create/edit',
},
unique: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the field enforces unique values',
},
externalId: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the field is an external ID',
},
defaultValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Default value; for Checkbox fields use true or false',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Internal description of the field',
},
inlineHelpText: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Help text shown next to the field in the UI',
},
picklistValues: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated values to add to a Picklist or MultiselectPicklist field (existing values are kept)',
},
},
/**
* Read-modify-write so omitted properties are preserved rather than reset by
* the Tooling API's full-metadata PATCH semantics.
*/
directExecution: async (params): Promise<ToolResponse> => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const fieldId = requireId(params.fieldId, 'Field ID')
const url = `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField/${fieldId}`
const headers = {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
const readResponse = await fetch(url, { headers })
const existing = await readResponse.json().catch(() => ({}))
if (!readResponse.ok) {
const errorMessage = extractErrorMessage(
existing,
readResponse.status,
'Failed to load custom field for update'
)
logger.error('Failed to read custom field metadata', { status: readResponse.status })
throw new Error(errorMessage)
}
const metadata = mergeCustomFieldMetadata(existing?.Metadata, params)
const patchResponse = await fetch(url, {
method: 'PATCH',
headers,
body: JSON.stringify({ Metadata: metadata }),
})
if (!patchResponse.ok) {
const errorData = await patchResponse.json().catch(() => ({}))
const errorMessage = extractErrorMessage(
errorData,
patchResponse.status,
'Failed to update custom field in Salesforce'
)
logger.error('Failed to update custom field', { status: patchResponse.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
id: fieldId,
updated: true,
},
}
},
/**
* Declarative fallback. `directExecution` is the authoritative path and handles
* the read-modify-write; this is only used if direct execution is bypassed.
*/
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
const fieldId = requireId(params.fieldId, 'Field ID')
return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField/${fieldId}`
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => ({ Metadata: mergeCustomFieldMetadata(undefined, params) }),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
const errorMessage = extractErrorMessage(
data,
response.status,
'Failed to update custom field in Salesforce'
)
logger.error('Failed to update custom field', { status: response.status })
throw new Error(errorMessage)
}
return {
success: true,
output: {
id: params?.fieldId?.trim() ?? '',
updated: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated custom field metadata',
properties: CUSTOM_FIELD_UPDATE_OUTPUT_PROPERTIES,
},
},
}
+131
View File
@@ -0,0 +1,131 @@
import type {
SalesforceUpdateLeadParams,
SalesforceUpdateLeadResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceUpdateLeadTool: ToolConfig<
SalesforceUpdateLeadParams,
SalesforceUpdateLeadResponse
> = {
id: 'salesforce_update_lead',
name: 'Update Lead in Salesforce',
description: 'Update an existing lead',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
leadId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Lead ID to update (18-character string starting with 00Q)',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Last name',
},
company: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Company name',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Email address',
},
phone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone number',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead status (e.g., Open, Working, Closed)',
},
leadSource: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead source (e.g., Web, Referral, Campaign)',
},
title: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Job title' },
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead description',
},
},
request: {
url: (params) => {
const leadId = requireId(params.leadId, 'Lead ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.lastName) body.LastName = params.lastName
if (params.company) body.Company = params.company
if (params.firstName) body.FirstName = params.firstName
if (params.email) body.Email = params.email
if (params.phone) body.Phone = params.phone
if (params.status) body.Status = params.status
if (params.leadSource) body.LeadSource = params.leadSource
if (params.title) body.Title = params.title
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json()
throw new Error(extractErrorMessage(data, response.status, 'Failed to update lead'))
}
return {
success: true,
output: {
id: params?.leadId?.trim() || '',
updated: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated lead data',
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,126 @@
import { createLogger } from '@sim/logger'
import type {
SalesforceUpdateOpportunityParams,
SalesforceUpdateOpportunityResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceUpdateOpportunity')
export const salesforceUpdateOpportunityTool: ToolConfig<
SalesforceUpdateOpportunityParams,
SalesforceUpdateOpportunityResponse
> = {
id: 'salesforce_update_opportunity',
name: 'Update Opportunity in Salesforce',
description: 'Update an existing opportunity',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
opportunityId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Opportunity ID to update (18-character string starting with 006)',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opportunity name',
},
stageName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Stage name (e.g., Prospecting, Qualification, Closed Won)',
},
closeDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Close date in YYYY-MM-DD format',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Salesforce Account ID (18-character string starting with 001)',
},
amount: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Deal amount as a number',
},
probability: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Win probability as integer (0-100)',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opportunity description',
},
},
request: {
url: (params) => {
const opportunityId = requireId(params.opportunityId, 'Opportunity ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
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.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
return body
},
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json()
logger.error('Failed to update opportunity', { data, status: response.status })
throw new Error(extractErrorMessage(data, response.status, 'Failed to update opportunity'))
}
return {
success: true,
output: {
id: params?.opportunityId?.trim() || '',
updated: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated opportunity data',
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
},
},
}
+134
View File
@@ -0,0 +1,134 @@
import type {
SalesforceUpdateTaskParams,
SalesforceUpdateTaskResponse,
} from '@/tools/salesforce/types'
import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types'
import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils'
import type { ToolConfig } from '@/tools/types'
export const salesforceUpdateTaskTool: ToolConfig<
SalesforceUpdateTaskParams,
SalesforceUpdateTaskResponse
> = {
id: 'salesforce_update_task',
name: 'Update Task in Salesforce',
description: 'Update an existing task',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Salesforce Task ID to update (18-character string starting with 00T)',
},
subject: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Task subject',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Status (e.g., Not Started, In Progress, Completed)',
},
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Priority (e.g., Low, Normal, High)',
},
activityDate: {
type: 'string',
required: false,
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,
visibility: 'user-or-llm',
description: 'Task description',
},
},
request: {
url: (params) => {
const taskId = requireId(params.taskId, 'Task ID')
return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.subject) body.Subject = params.subject
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.activityDate) body.ActivityDate = params.activityDate
if (params.whoId) body.WhoId = params.whoId.trim()
if (params.whatId) body.WhatId = params.whatId.trim()
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response, params?) => {
if (!response.ok) {
const data = await response.json()
throw new Error(extractErrorMessage(data, response.status, 'Failed to update task'))
}
return {
success: true,
output: {
id: params?.taskId?.trim() || '',
updated: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated task data',
properties: SOBJECT_UPDATE_OUTPUT_PROPERTIES,
},
},
}
+314
View File
@@ -0,0 +1,314 @@
import { createLogger } from '@sim/logger'
const logger = createLogger('SalesforceUtils')
/**
* Extracts Salesforce instance URL from ID token or uses provided instance URL
* @param idToken - The Salesforce ID token containing instance URL
* @param instanceUrl - Direct instance URL if provided
* @returns The Salesforce instance URL
* @throws Error if instance URL cannot be determined
*/
export function getInstanceUrl(idToken?: string, instanceUrl?: string): string {
if (instanceUrl) return instanceUrl
if (idToken) {
try {
const base64Url = 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) return match[1]
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') return match[1]
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
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
}
/**
* Ensures a custom field/object API name carries the required `__c` suffix.
* Salesforce metadata components created via the Tooling API must end in `__c`;
* users commonly omit it, so we append it when missing.
* @param value - The raw API name from params (e.g. "Region" or "Region__c")
* @param label - Human-readable field name used in the error message
* @returns The trimmed API name guaranteed to end with `__c`
* @throws Error if the name is absent or whitespace-only
*/
export function toCustomApiName(value: string | undefined, label: string): string {
const trimmed = value?.trim()
if (!trimmed) {
throw new Error(`${label} is required. Please provide a valid Salesforce API name.`)
}
return trimmed.endsWith('__c') ? trimmed : `${trimmed}__c`
}
/**
* Normalizes a boolean-ish param value into a real boolean.
* Tool params arrive as actual booleans from the LLM or as strings from block
* inputs; this collapses both forms and treats empty values as "unset".
* @param value - The raw param value
* @returns The boolean value, or undefined when the param was not provided
*/
export function normalizeBoolean(value: unknown): boolean | undefined {
if (value === undefined || value === null || value === '') return undefined
if (typeof value === 'boolean') return value
if (typeof value === 'string') return value.trim().toLowerCase() === 'true'
return Boolean(value)
}
/**
* Parses a comma-separated list into trimmed, non-empty entries.
* Used for picklist value sets supplied as a single delimited string.
* @param value - The raw comma-separated string
* @returns An array of trimmed values (empty when nothing parseable is present)
*/
export function parseDelimitedList(value: string | undefined): string[] {
if (!value) return []
return value
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0)
}
/**
* Shape of the custom field metadata inputs accepted from tool params.
* Numeric dimensions arrive as real numbers from the LLM (param `type: 'number'`)
* or as strings from block inputs, so both forms are accepted.
*/
export interface CustomFieldMetadataInput {
fieldType?: string
label?: string
length?: number | string
precision?: number | string
scale?: number | string
visibleLines?: number | string
required?: boolean | string
unique?: boolean | string
externalId?: boolean | string
defaultValue?: string
description?: string
inlineHelpText?: string
picklistValues?: string
}
/**
* Coerces a numeric-ish metadata value (number or string) into a number.
* @returns The parsed number, or undefined when unset or unparseable
*/
function toFieldNumber(value?: number | string): number | undefined {
if (value === undefined || value === null || String(value).trim() === '') return undefined
const parsed = Number(value)
return Number.isNaN(parsed) ? undefined : parsed
}
/**
* Overlays only the explicitly-provided custom field properties onto `target`,
* leaving any property the caller did not supply untouched. Shared by create
* (onto a fresh object) and update (onto the field's existing metadata), so an
* update never fabricates values for omitted properties.
* @param target - The metadata object to mutate in place
* @param params - The provided custom field metadata inputs
*/
function applyProvidedFieldMetadata(
target: Record<string, any>,
params: CustomFieldMetadataInput
): void {
if (params.fieldType?.trim()) target.type = params.fieldType.trim()
if (params.label?.trim()) target.label = params.label.trim()
const length = toFieldNumber(params.length)
if (length !== undefined) target.length = length
const precision = toFieldNumber(params.precision)
if (precision !== undefined) target.precision = precision
const scale = toFieldNumber(params.scale)
if (scale !== undefined) target.scale = scale
const visibleLines = toFieldNumber(params.visibleLines)
if (visibleLines !== undefined) target.visibleLines = visibleLines
const required = normalizeBoolean(params.required)
if (required !== undefined) target.required = required
const unique = normalizeBoolean(params.unique)
if (unique !== undefined) target.unique = unique
const externalId = normalizeBoolean(params.externalId)
if (externalId !== undefined) target.externalId = externalId
if (params.description?.trim()) target.description = params.description.trim()
if (params.inlineHelpText?.trim()) target.inlineHelpText = params.inlineHelpText.trim()
if (params.defaultValue !== undefined && String(params.defaultValue).trim() !== '') {
target.defaultValue =
target.type === 'Checkbox'
? (normalizeBoolean(params.defaultValue) ?? false)
: params.defaultValue
}
const picklistValues = parseDelimitedList(params.picklistValues)
if (picklistValues.length > 0) {
// Union with any existing values so an update adds new options without
// dropping the field's current values (or their default flags).
const existingValues: Array<Record<string, any>> = Array.isArray(
target.valueSet?.valueSetDefinition?.value
)
? target.valueSet.valueSetDefinition.value
: []
const existingFullNames = new Set(existingValues.map((entry) => entry.fullName))
const additions = picklistValues
.filter((value) => !existingFullNames.has(value))
.map((value) => ({ fullName: value, default: false, label: value }))
target.valueSet = {
...(target.valueSet ?? {}),
valueSetDefinition: {
...(target.valueSet?.valueSetDefinition ?? {}),
sorted: target.valueSet?.valueSetDefinition?.sorted ?? false,
value: [...existingValues, ...additions],
},
}
}
}
/**
* Applies type-specific defaults required by Salesforce when the caller did not
* supply them, so common field types work out of the box on create.
* @param metadata - The metadata object to mutate in place (must have a `type`)
*/
function applyFieldTypeDefaults(metadata: Record<string, any>): void {
const fieldType = metadata.type
if (fieldType === 'Text' && metadata.length === undefined) {
metadata.length = 255
}
if (fieldType === 'LongTextArea' || fieldType === 'Html') {
if (metadata.length === undefined) metadata.length = 32768
if (metadata.visibleLines === undefined) metadata.visibleLines = 3
}
if (fieldType === 'MultiselectPicklist') {
if (metadata.visibleLines === undefined) metadata.visibleLines = 4
// Salesforce requires `length` (total characters across selected values) for
// multi-select picklists in addition to visibleLines.
if (metadata.length === undefined) metadata.length = 255
}
if (fieldType === 'Number' || fieldType === 'Currency' || fieldType === 'Percent') {
if (metadata.precision === undefined) metadata.precision = 18
if (metadata.scale === undefined) metadata.scale = 0
}
// Checkbox fields require a default value; Salesforce rejects them without one.
if (fieldType === 'Checkbox' && metadata.defaultValue === undefined) {
metadata.defaultValue = false
}
}
/**
* Builds the `Metadata` object for a Tooling API CustomField create body.
* Applies type-specific defaults so common field types work without the caller
* supplying every property (e.g. Text defaults to length 255).
* @param params - The custom field metadata params
* @param fallbackLabel - Label to use when none is provided
* @returns The Salesforce CustomField Metadata object
* @throws Error if the field type is missing
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
*/
export function buildCustomFieldMetadata(
params: CustomFieldMetadataInput,
fallbackLabel: string
): Record<string, any> {
const fieldType = params.fieldType?.trim()
if (!fieldType) {
throw new Error('Field Type is required (e.g., Text, Number, Checkbox, Date, Picklist).')
}
const metadata: Record<string, any> = {
type: fieldType,
label: params.label?.trim() || fallbackLabel,
}
applyProvidedFieldMetadata(metadata, params)
applyFieldTypeDefaults(metadata)
return metadata
}
/**
* Merges caller-provided custom field changes onto a field's existing metadata
* for a Tooling API update. The Tooling API PATCH replaces the whole `Metadata`
* compound, so we start from the field's current metadata (read first) and
* overlay only what changed — never fabricating defaults or labels that would
* silently clobber unspecified properties.
* @param existing - The field's current `Metadata` object (from a GET)
* @param params - The provided custom field changes
* @returns The merged Salesforce CustomField Metadata object
* @see https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_customfield.htm
*/
export function mergeCustomFieldMetadata(
existing: Record<string, any> | undefined,
params: CustomFieldMetadataInput
): Record<string, any> {
const metadata: Record<string, any> = { ...(existing ?? {}) }
// An attribute update never changes the field's data type — Salesforce treats
// a type change as a separate, conversion-driven operation. Keep the field's
// existing type and overlay only the other provided properties.
applyProvidedFieldMetadata(metadata, { ...params, fieldType: undefined })
return metadata
}
/**
* Extracts a descriptive error message from Salesforce API responses
* @param data - The response data from Salesforce API
* @param status - HTTP status code
* @param defaultMessage - Default message to use if no specific error found
* @returns Formatted error message
*/
export function extractErrorMessage(data: any, status: number, defaultMessage: string): string {
if (Array.isArray(data) && data[0]?.message) {
return `Salesforce API Error (${status}): ${data[0].message}${data[0].errorCode ? ` [${data[0].errorCode}]` : ''}`
}
// Tooling API metadata writes return { success: false, errors: [{ message, statusCode }] }
if (Array.isArray(data?.errors) && data.errors[0]?.message) {
const first = data.errors[0]
return `Salesforce API Error (${status}): ${first.message}${first.statusCode ? ` [${first.statusCode}]` : ''}`
}
if (data?.message) {
return `Salesforce API Error (${status}): ${data.message}`
}
if (data?.error) {
return `Salesforce API Error (${status}): ${data.error}${data.error_description ? ` - ${data.error_description}` : ''}`
}
switch (status) {
case 400:
return `Salesforce API Error (400): Bad Request - The request was malformed or missing required parameters`
case 401:
return `Salesforce API Error (401): Unauthorized - Invalid or expired access token. Please re-authenticate.`
case 403:
return `Salesforce API Error (403): Forbidden - You do not have permission to access this resource.`
case 404:
return `Salesforce API Error (404): Not Found - The requested resource does not exist or you do not have access to it.`
case 500:
return `Salesforce API Error (500): Internal Server Error - An error occurred on Salesforce's servers.`
default:
return `${defaultMessage} (HTTP ${status})`
}
}