From c7b24aee5fa6319dd5396d37adf49d308ca0db2c Mon Sep 17 00:00:00 2001 From: Selinali01 Date: Mon, 27 Apr 2026 23:07:58 -0700 Subject: [PATCH] Salesforce schema improvements --- packages/bubble-core/package.json | 2 +- .../salesforce/salesforce.integration.flow.ts | 32 +++++ .../salesforce/salesforce.schema.ts | 79 +++++++++++ .../service-bubble/salesforce/salesforce.ts | 124 +++++++++++++++++- packages/bubble-runtime/package.json | 2 +- packages/bubble-scope-manager/package.json | 2 +- packages/bubble-shared-schemas/package.json | 2 +- packages/create-bubblelab-app/package.json | 2 +- .../templates/basic/package.json | 6 +- .../templates/reddit-scraper/package.json | 4 +- 10 files changed, 239 insertions(+), 16 deletions(-) diff --git a/packages/bubble-core/package.json b/packages/bubble-core/package.json index e49e0b32..d71085fc 100644 --- a/packages/bubble-core/package.json +++ b/packages/bubble-core/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/bubble-core", - "version": "0.1.319", + "version": "0.1.320", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.integration.flow.ts b/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.integration.flow.ts index 6a1136b1..3da3cdeb 100644 --- a/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.integration.flow.ts +++ b/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.integration.flow.ts @@ -125,6 +125,38 @@ export class SalesforceIntegrationTest extends BubbleFlow<'webhook/http'> { : queryResult.error, }); + // 6. Describe Account — verify label + API name come back paired + const describeResult = await new SalesforceBubble({ + operation: 'describe_object', + object_name: 'Account', + }).action(); + + results.push({ + operation: 'describe_object', + success: describeResult.success, + details: describeResult.success + ? `Account has ${describeResult.fields?.length ?? 0} fields; sample: ${describeResult.fields + ?.slice(0, 3) + .map((f) => `${f.label} (${f.apiName})`) + .join(', ')}` + : describeResult.error, + }); + + // 7. List objects — confirm we can enumerate sObjects with labels + const listResult = await new SalesforceBubble({ + operation: 'list_objects', + }).action(); + + results.push({ + operation: 'list_objects', + success: listResult.success, + details: listResult.success + ? `Found ${listResult.objects?.length ?? 0} queryable objects (${ + listResult.objects?.filter((o) => o.custom).length ?? 0 + } custom)` + : listResult.error, + }); + return { testResults: results }; } } diff --git a/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.schema.ts b/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.schema.ts index afe8d947..8512be23 100644 --- a/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.schema.ts +++ b/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.schema.ts @@ -104,8 +104,73 @@ export const SalesforceParamsSchema = z.discriminatedUnion('operation', [ ), credentials: credentialsField, }), + + // Describe an sObject — return its fields with API name + UI label + z.object({ + operation: z + .literal('describe_object') + .describe( + 'Return all fields of a Salesforce object with both API name and user-facing label. Use this to resolve label-vs-API-name mismatches (especially custom fields like "Saving Status" → Treasury_Status__c) before constructing SOQL.' + ), + object_name: z + .string() + .min(1, 'Object name is required') + .describe( + 'API name of the sObject to describe (e.g. "Account", "Contact", "Opportunity", or a custom object like "MyCustomObject__c")' + ), + credentials: credentialsField, + }), + + // List all sObjects available in the org + z.object({ + operation: z + .literal('list_objects') + .describe( + 'List all sObjects in the connected Salesforce org with their API name and label. Use this when the user references an object by its UI label and the API name is not obvious.' + ), + include_custom_only: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, only return custom objects (suffixed __c). Defaults to false (returns all queryable objects).' + ), + credentials: credentialsField, + }), ]); +// Compact field metadata returned by describe_object +const SalesforceFieldMetadataSchema = z.object({ + apiName: z + .string() + .describe('API/backend name of the field (use this in SOQL)'), + label: z.string().describe('User-facing label shown in the Salesforce UI'), + type: z + .string() + .describe('Salesforce field type (e.g. "string", "picklist")'), + custom: z.boolean().describe('Whether this is a custom field (suffixed __c)'), + picklistValues: z + .array(z.string()) + .optional() + .describe('For picklist fields, the allowed values'), + referenceTo: z + .array(z.string()) + .optional() + .describe('For reference/lookup fields, the sObject(s) referenced'), +}); + +// Compact sObject metadata returned by list_objects +const SalesforceObjectMetadataSchema = z.object({ + apiName: z.string().describe('API name of the sObject (use this in SOQL)'), + label: z.string().describe('User-facing label shown in the Salesforce UI'), + custom: z + .boolean() + .describe('Whether this is a custom object (suffixed __c)'), + queryable: z + .boolean() + .describe('Whether this object can be queried via SOQL'), +}); + // Salesforce record — flexible schema since fields vary by query const SalesforceRecordSchema = z .record(z.string(), z.unknown()) @@ -149,6 +214,20 @@ export const SalesforceResultSchema = z.discriminatedUnion('operation', [ done: z.boolean().optional(), error: z.string(), }), + z.object({ + operation: z.literal('describe_object'), + success: z.boolean(), + object_name: z.string().optional(), + object_label: z.string().optional(), + fields: z.array(SalesforceFieldMetadataSchema).optional(), + error: z.string(), + }), + z.object({ + operation: z.literal('list_objects'), + success: z.boolean(), + objects: z.array(SalesforceObjectMetadataSchema).optional(), + error: z.string(), + }), ]); export type SalesforceParams = z.output; diff --git a/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.ts b/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.ts index c8d46308..c89872cf 100644 --- a/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.ts +++ b/packages/bubble-core/src/bubbles/service-bubble/salesforce/salesforce.ts @@ -38,12 +38,34 @@ export class SalesforceBubble< static readonly longDescription = ` Salesforce CRM integration for querying and searching CRM records. - Features: - - Retrieve Account and Contact records by Salesforce ID - - Search Accounts and Contacts with flexible SOQL WHERE conditions - - Run arbitrary SOQL queries for any Salesforce object - - Account-level field lookup by business ID or Salesforce ID - - Support for custom fields and objects via SOQL + Operations: + - get_account / get_contact: retrieve a record by Salesforce ID + - search_accounts / search_contacts: SOQL WHERE-clause search + - query: run arbitrary SOQL against any object + - describe_object: list a sObject's fields with both API name AND user-facing label + - list_objects: list all sObjects in the org with API name AND label + + IMPORTANT — label vs API name: + Salesforce fields and objects have a backend API name (used in SOQL, e.g. + "Treasury_Status__c") that differs from the user-facing label shown in the UI + (e.g. "Saving Status"). Users almost always describe fields by their UI label, + not the API name. Custom fields (suffixed __c) are especially prone to this. + + When the user references a field or object whose API name is NOT obviously a + standard Salesforce field (e.g. Id, Name, Industry, BillingCity, Email, + AnnualRevenue, AccountNumber, Type, OwnerId), you MUST first call + describe_object (or list_objects for object discovery) to fetch the + label→apiName mapping, then construct your SOQL using the apiName. Do not + guess custom field API names from labels — call describe_object first. + + If multiple fields plausibly match the user's term, ask a clarification + question rather than guessing. + + Result shapes: + - describe_object → { object_name, object_label, fields: [{ apiName, label, type, custom, picklistValues?, referenceTo? }] } + - list_objects → { objects: [{ apiName, label, custom, queryable }] } + - get_account / get_contact → { record: {...} } + - search_accounts / search_contacts / query → { records: [...], totalSize, done } Security Features: - OAuth 2.0 authentication with Salesforce @@ -197,6 +219,14 @@ export class SalesforceBubble< return await this.runQuery( p as Extract ); + case 'describe_object': + return await this.describeObject( + p as Extract + ); + case 'list_objects': + return await this.listObjects( + p as Extract + ); default: throw new Error(`Unsupported operation: ${operation}`); } @@ -338,4 +368,86 @@ export class SalesforceBubble< error: '', }; } + + private async describeObject( + params: Extract + ): Promise> { + const { object_name } = params; + + const data = (await this.sfRequest( + `/services/data/${SALESFORCE_API_VERSION}/sobjects/${encodeURIComponent(object_name)}/describe` + )) as { + name: string; + label: string; + fields: Array<{ + name: string; + label: string; + type: string; + custom: boolean; + picklistValues?: Array<{ value: string; active: boolean }>; + referenceTo?: string[]; + }>; + }; + + const fields = data.fields.map((f) => { + const picklistValues = f.picklistValues + ?.filter((p) => p.active) + .map((p) => p.value); + return { + apiName: f.name, + label: f.label, + type: f.type, + custom: f.custom, + ...(picklistValues && picklistValues.length > 0 + ? { picklistValues } + : {}), + ...(f.referenceTo && f.referenceTo.length > 0 + ? { referenceTo: f.referenceTo } + : {}), + }; + }); + + return { + operation: 'describe_object', + success: true, + object_name: data.name, + object_label: data.label, + fields, + error: '', + }; + } + + private async listObjects( + params: Extract + ): Promise> { + const { include_custom_only } = params; + + const data = (await this.sfRequest( + `/services/data/${SALESFORCE_API_VERSION}/sobjects` + )) as { + sobjects: Array<{ + name: string; + label: string; + custom: boolean; + queryable: boolean; + }>; + }; + + const objects = data.sobjects + .filter((o) => o.queryable) + .filter((o) => (include_custom_only ? o.custom : true)) + .map((o) => ({ + apiName: o.name, + label: o.label, + custom: o.custom, + queryable: o.queryable, + })); + + return { + operation: 'list_objects', + success: true, + objects, + error: '', + }; + } } diff --git a/packages/bubble-runtime/package.json b/packages/bubble-runtime/package.json index 4dfc3b98..19912f51 100644 --- a/packages/bubble-runtime/package.json +++ b/packages/bubble-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/bubble-runtime", - "version": "0.1.319", + "version": "0.1.320", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/bubble-scope-manager/package.json b/packages/bubble-scope-manager/package.json index 8ed88688..1dd829ec 100644 --- a/packages/bubble-scope-manager/package.json +++ b/packages/bubble-scope-manager/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/ts-scope-manager", - "version": "0.1.319", + "version": "0.1.320", "private": false, "license": "MIT", "type": "commonjs", diff --git a/packages/bubble-shared-schemas/package.json b/packages/bubble-shared-schemas/package.json index 743e3993..399aafcd 100644 --- a/packages/bubble-shared-schemas/package.json +++ b/packages/bubble-shared-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@bubblelab/shared-schemas", - "version": "0.1.319", + "version": "0.1.320", "type": "module", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/create-bubblelab-app/package.json b/packages/create-bubblelab-app/package.json index f9e370db..4ae0f1ec 100644 --- a/packages/create-bubblelab-app/package.json +++ b/packages/create-bubblelab-app/package.json @@ -1,6 +1,6 @@ { "name": "create-bubblelab-app", - "version": "0.1.319", + "version": "0.1.320", "type": "module", "license": "Apache-2.0", "description": "Create BubbleLab AI agent applications with one command", diff --git a/packages/create-bubblelab-app/templates/basic/package.json b/packages/create-bubblelab-app/templates/basic/package.json index 96210ee0..62961861 100644 --- a/packages/create-bubblelab-app/templates/basic/package.json +++ b/packages/create-bubblelab-app/templates/basic/package.json @@ -11,9 +11,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@bubblelab/bubble-core": "^0.1.319", - "@bubblelab/bubble-runtime": "^0.1.319", - "@bubblelab/shared-schemas": "^0.1.319", + "@bubblelab/bubble-core": "^0.1.320", + "@bubblelab/bubble-runtime": "^0.1.320", + "@bubblelab/shared-schemas": "^0.1.320", "dotenv": "^16.4.5" }, "devDependencies": { diff --git a/packages/create-bubblelab-app/templates/reddit-scraper/package.json b/packages/create-bubblelab-app/templates/reddit-scraper/package.json index eaea3d15..cf37ae39 100644 --- a/packages/create-bubblelab-app/templates/reddit-scraper/package.json +++ b/packages/create-bubblelab-app/templates/reddit-scraper/package.json @@ -11,8 +11,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@bubblelab/bubble-core": "^0.1.319", - "@bubblelab/bubble-runtime": "^0.1.319", + "@bubblelab/bubble-core": "^0.1.320", + "@bubblelab/bubble-runtime": "^0.1.320", "dotenv": "^16.4.5" }, "devDependencies": {