Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/bubble-core/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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<typeof SalesforceParamsSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -197,6 +219,14 @@ export class SalesforceBubble<
return await this.runQuery(
p as Extract<SalesforceParams, { operation: 'query' }>
);
case 'describe_object':
return await this.describeObject(
p as Extract<SalesforceParams, { operation: 'describe_object' }>
);
case 'list_objects':
return await this.listObjects(
p as Extract<SalesforceParams, { operation: 'list_objects' }>
);
default:
throw new Error(`Unsupported operation: ${operation}`);
}
Expand Down Expand Up @@ -338,4 +368,86 @@ export class SalesforceBubble<
error: '',
};
}

private async describeObject(
params: Extract<SalesforceParams, { operation: 'describe_object' }>
): Promise<Extract<SalesforceResult, { operation: 'describe_object' }>> {
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<SalesforceParams, { operation: 'list_objects' }>
): Promise<Extract<SalesforceResult, { operation: 'list_objects' }>> {
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: '',
};
}
}
2 changes: 1 addition & 1 deletion packages/bubble-runtime/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/bubble-scope-manager/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bubblelab/ts-scope-manager",
"version": "0.1.319",
"version": "0.1.320",
"private": false,
"license": "MIT",
"type": "commonjs",
Expand Down
2 changes: 1 addition & 1 deletion packages/bubble-shared-schemas/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/create-bubblelab-app/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions packages/create-bubblelab-app/templates/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading