Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,34 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const addContactToCampaign = createAction({
auth: salesforceAuth,
name: 'add_contact_to_campaign',
displayName: 'Add Contact to Campaign',
description: 'Add a contact to a campaign.',
props: {
campaign_id: salesforcesCommon.campaign,
contact_id: salesforcesCommon.contact,
status: salesforcesCommon.status,
},
async run(context) {
const { campaign_id, contact_id, status } = context.propsValue;
const body = {
CampaignId: campaign_id,
ContactId: contact_id,
Status: status,
};
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/CampaignMember',
body
);
return response.body;
},
});

View File

@@ -0,0 +1,44 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const addFileToRecord = createAction({
auth: salesforceAuth,
name: 'add_file_to_record',
displayName: 'Add File to Record',
description: 'Uploads a file and attaches it to an existing record.',
props: {
object: salesforcesCommon.object,
record_id: salesforcesCommon.record,
file: Property.File({
displayName: 'File',
description: 'The file to upload.',
required: true,
}),
file_name: Property.ShortText({
displayName: 'File Name',
description: 'The name of the file, including its extension (e.g., "report.pdf").',
required: true,
})
},
async run(context) {
const { record_id, file, file_name } = context.propsValue;
const body = {
Title: file_name,
PathOnClient: file_name,
VersionData: file.base64,
FirstPublishLocationId: record_id,
};
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/ContentVersion',
body
);
return response.body;
},
});

View File

@@ -0,0 +1,34 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const addLeadToCampaign = createAction({
auth: salesforceAuth,
name: 'add_lead_to_campaign',
displayName: 'Add Lead to Campaign',
description: 'Adds an existing lead to an existing campaign.',
props: {
campaign_id: salesforcesCommon.campaign,
lead_id: salesforcesCommon.lead,
status: salesforcesCommon.status,
},
async run(context) {
const { campaign_id, lead_id, status } = context.propsValue;
const body = {
CampaignId: campaign_id,
LeadId: lead_id,
Status: status,
};
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/CampaignMember',
body
);
return response.body;
},
});

View File

@@ -0,0 +1,43 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createAttachment = createAction({
auth: salesforceAuth,
name: 'create_attachment',
displayName: 'Create Attachment (Legacy)',
description: 'Creates a legacy Attachment record. Salesforce recommends using "Add File to Record" for modern apps.',
props: {
object: salesforcesCommon.object,
parent_id: salesforcesCommon.record,
file: Property.File({
displayName: 'File',
description: 'The file to attach.',
required: true,
}),
file_name: Property.ShortText({
displayName: 'File Name',
description: 'The name of the file, including its extension (e.g., "attachment.pdf").',
required: true,
})
},
async run(context) {
const { parent_id, file, file_name } = context.propsValue;
const body = {
ParentId: parent_id,
Name: file_name,
Body: file.base64,
};
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/Attachment',
body
);
return response.body;
},
});

View File

@@ -0,0 +1,70 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createCase = createAction({
auth: salesforceAuth,
name: 'create_case',
displayName: 'Create Case',
description: 'Creates a Case, which represents a customer issue or problem.',
props: {
Subject: Property.ShortText({
displayName: 'Subject',
required: true,
}),
Description: Property.LongText({
displayName: 'Description',
required: false,
}),
Status: salesforcesCommon.caseStatus,
Priority: salesforcesCommon.casePriority,
Origin: salesforcesCommon.caseOrigin,
AccountId: salesforcesCommon.account,
ContactId: salesforcesCommon.optionalContact,
other_fields: Property.Json({
displayName: 'Other Fields',
description: 'Enter additional fields as a JSON object.',
required: false
})
},
async run(context) {
const {
Subject,
Description,
Status,
Priority,
Origin,
AccountId,
ContactId,
other_fields
} = context.propsValue;
const rawBody = {
Subject,
Description,
Status,
Priority,
Origin,
AccountId,
ContactId,
...(other_fields || {}),
};
const cleanedBody = Object.entries(rawBody).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/Case',
cleanedBody
);
return response.body;
},
});

View File

@@ -0,0 +1,77 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createContact = createAction({
auth: salesforceAuth,
name: 'create_contact',
displayName: 'Create Contact',
description: 'Creates a new contact record.',
props: {
LastName: Property.ShortText({
displayName: 'Last Name',
required: true,
}),
FirstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
AccountId: salesforcesCommon.account,
Email: Property.ShortText({
displayName: 'Email',
required: false,
}),
Phone: Property.ShortText({
displayName: 'Phone',
required: false,
}),
Title: Property.ShortText({
displayName: 'Title',
required: false,
}),
other_fields: Property.Json({
displayName: 'Other Fields',
description: 'Enter additional fields as a JSON object.',
required: false
})
},
async run(context) {
const {
LastName,
FirstName,
AccountId,
Email,
Phone,
Title,
other_fields
} = context.propsValue;
const rawBody = {
LastName,
FirstName,
AccountId,
Email,
Phone,
Title,
...(other_fields || {}),
};
const cleanedBody = Object.entries(rawBody).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/Contact',
cleanedBody
);
return response.body;
},
});

View File

@@ -0,0 +1,78 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createLead = createAction({
auth: salesforceAuth,
name: 'create_lead',
displayName: 'Create Lead',
description: 'Creates a new lead.',
props: {
LastName: Property.ShortText({
displayName: 'Last Name',
required: true,
}),
Company: Property.ShortText({
displayName: 'Company',
required: true,
}),
FirstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
Email: Property.ShortText({
displayName: 'Email',
required: false,
}),
Phone: Property.ShortText({
displayName: 'Phone',
required: false,
}),
LeadSource: salesforcesCommon.leadSource,
other_fields: Property.Json({
displayName: 'Other Fields',
description: 'Enter additional fields as a JSON object (e.g., {"Title": "Manager", "Website": "http://example.com"}).',
required: false
})
},
async run(context) {
const {
LastName,
Company,
FirstName,
Email,
Phone,
LeadSource,
other_fields
} = context.propsValue;
const rawBody = {
LastName,
Company,
FirstName,
Email,
Phone,
LeadSource,
...(other_fields || {}),
};
const cleanedBody = Object.entries(rawBody).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/Lead',
cleanedBody
);
return response.body;
},
});

View File

@@ -0,0 +1,32 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { callSalesforceApi, salesforcesCommon } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
export const createNewObject = createAction({
auth: salesforceAuth,
name: 'create_new_object',
displayName: 'Create Object (Advanced)',
description: 'Create new object',
props: {
object: salesforcesCommon.object,
data: Property.Json({
displayName: 'Data',
description: 'Select mapped object',
required: true,
defaultValue: {},
}),
},
async run(context) {
const { data, object } = context.propsValue;
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
`/services/data/v56.0/sobjects/${object}`,
{
...data,
}
);
return response;
},
});

View File

@@ -0,0 +1,42 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createNote = createAction({
auth: salesforceAuth,
name: 'create_note',
displayName: 'Create Note',
description: 'Creates a note and attaches it to a record.',
props: {
object: salesforcesCommon.object,
parent_id: salesforcesCommon.record,
Title: Property.ShortText({
displayName: 'Title',
required: true,
}),
Body: Property.LongText({
displayName: 'Body',
description: 'The content of the note.',
required: true,
}),
},
async run(context) {
const { parent_id, Title, Body } = context.propsValue;
const body = {
ParentId: parent_id,
Title: Title,
Body: Body,
};
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/Note',
body
);
return response.body;
},
});

View File

@@ -0,0 +1,69 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createOpportunity = createAction({
auth: salesforceAuth,
name: 'create_opportunity',
displayName: 'Create Opportunity',
description: 'Creates a new opportunity.',
props: {
Name: Property.ShortText({
displayName: 'Name',
required: true,
}),
CloseDate: Property.ShortText({
displayName: 'Close Date',
description: 'The expected close date in YYYY-MM-DD format.',
required: true,
}),
StageName: salesforcesCommon.opportunityStage,
AccountId: salesforcesCommon.account,
Amount: Property.Number({
displayName: 'Amount',
required: false,
}),
other_fields: Property.Json({
displayName: 'Other Fields',
description: 'Enter additional fields as a JSON object.',
required: false
})
},
async run(context) {
const {
Name,
CloseDate,
StageName,
AccountId,
Amount,
other_fields
} = context.propsValue;
const rawBody = {
Name,
CloseDate,
StageName,
AccountId,
Amount,
...(other_fields || {}),
};
const cleanedBody = Object.entries(rawBody).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/Opportunity',
cleanedBody
);
return response.body;
},
});

View File

@@ -0,0 +1,36 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createRecord = createAction({
auth: salesforceAuth,
name: 'create_record',
displayName: 'Create Record',
description: 'Create a record of a given object.',
props: {
object: salesforcesCommon.object,
data: Property.Json({
displayName: 'Record Data',
description: 'Enter the fields for the new record as a JSON object. For example: {"Name": "My New Account", "Industry": "Technology"}',
required: true,
defaultValue: {},
}),
},
async run(context) {
const { object, data } = context.propsValue;
if (!object) {
throw new Error('Object is not defined. Please select an object.');
}
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
`/services/data/v56.0/sobjects/${object}`,
data as Record<string, unknown>
);
return response.body;
},
});

View File

@@ -0,0 +1,71 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const createTask = createAction({
auth: salesforceAuth,
name: 'create_task',
displayName: 'Create Task',
description: 'Creates a new task.',
props: {
Subject: Property.ShortText({
displayName: 'Subject',
required: true,
}),
OwnerId: salesforcesCommon.owner,
Status: salesforcesCommon.taskStatus,
Priority: salesforcesCommon.taskPriority,
Description: Property.LongText({
displayName: 'Description',
required: false,
}),
WhoId: Property.ShortText({
displayName: 'Related To (Contact/Lead ID)',
description: 'The ID of a Contact or Lead to relate the task to.',
required: false,
}),
WhatId: Property.ShortText({
displayName: 'Related To (Other Object ID)',
description: 'The ID of an Account, Opportunity, or other object to relate the task to.',
required: false,
}),
},
async run(context) {
const {
Subject,
OwnerId,
Status,
Priority,
Description,
WhoId,
WhatId
} = context.propsValue;
const rawBody = {
Subject,
OwnerId,
Status,
Priority,
Description,
WhoId,
WhatId
};
const cleanedBody = Object.entries(rawBody).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
'/services/data/v56.0/sobjects/Task',
cleanedBody
);
return response.body;
},
});

View File

@@ -0,0 +1,28 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const deleteOpportunity = createAction({
auth: salesforceAuth,
name: 'delete_opportunity',
displayName: 'Delete Opportunity',
description: 'Deletes an opportunity.',
props: {
opportunity_id: salesforcesCommon.opportunity,
},
async run(context) {
const { opportunity_id } = context.propsValue;
await callSalesforceApi(
HttpMethod.DELETE,
context.auth,
`/services/data/v56.0/sobjects/Opportunity/${opportunity_id}`,
undefined
);
return {
success: true,
};
},
});

View File

@@ -0,0 +1,33 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const deleteRecord = createAction({
auth: salesforceAuth,
name: 'delete_record',
displayName: 'Delete Record',
description: 'Deletes an existing record in an object.',
props: {
object: salesforcesCommon.object,
record_id: salesforcesCommon.record,
},
async run(context) {
const { object, record_id } = context.propsValue;
if (!object) {
throw new Error('Object is not defined. Please select an object.');
}
await callSalesforceApi(
HttpMethod.DELETE,
context.auth,
`/services/data/v56.0/sobjects/${object}/${record_id}`,
undefined
);
return {
success: true,
};
},
});

View File

@@ -0,0 +1,28 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const findChildRecords = createAction({
auth: salesforceAuth,
name: 'find_child_records',
displayName: 'Find Child Records',
description: 'Finds child records related to a parent record.',
props: {
parent_object: salesforcesCommon.object,
parent_id: salesforcesCommon.parentRecord,
child_relationship: salesforcesCommon.childRelationship,
},
async run(context) {
const { parent_id, child_relationship } = context.propsValue;
const response = await callSalesforceApi(
HttpMethod.GET,
context.auth,
`/services/data/v56.0/ui-api/records/${parent_id}/child-relationships/${child_relationship}`,
undefined
);
return response.body;
},
});

View File

@@ -0,0 +1,39 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { querySalesforceApi, salesforcesCommon } from '../common';
export const findRecord = createAction({
auth: salesforceAuth,
name: 'find_record',
displayName: 'Find Record',
description: 'Finds a record by a field value.',
props: {
object: salesforcesCommon.object,
field: salesforcesCommon.field,
search_value: Property.ShortText({
displayName: 'Search Value',
description: 'The value to search for in the selected field.',
required: true,
})
},
async run(context) {
const { object, field, search_value } = context.propsValue;
if (!object || !field) {
throw new Error('Object and Field must be selected.');
}
const escapedSearchValue = search_value.replace(/'/g, "\\'");
const query = `SELECT FIELDS(ALL) FROM ${object} WHERE ${field} = '${escapedSearchValue}' LIMIT 2000`;
const response = await querySalesforceApi(
HttpMethod.GET,
context.auth,
query
);
return response.body;
},
});

View File

@@ -0,0 +1,36 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { querySalesforceApi, salesforcesCommon } from '../common';
export const findRecordsByQuery = createAction({
auth: salesforceAuth,
name: 'find_records_by_query',
displayName: 'Find Records by Query (Advanced)',
description: 'Finds records in an object using a SOQL WHERE clause.',
props: {
object: salesforcesCommon.object,
where_clause: Property.ShortText({
displayName: 'WHERE Clause',
description: "Enter the WHERE clause for your SOQL query. For example: `Name = 'Acme' AND Industry = 'Technology'`. Do not include the 'WHERE' keyword.",
required: true,
})
},
async run(context) {
const { object, where_clause } = context.propsValue;
if (!object) {
throw new Error('Object must be selected.');
}
const query = `SELECT FIELDS(ALL) FROM ${object} WHERE ${where_clause} LIMIT 200`;
const response = await querySalesforceApi(
HttpMethod.GET,
context.auth,
query
);
return response.body;
},
});

View File

@@ -0,0 +1,51 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { querySalesforceApi, salesforcesCommon } from '../common';
interface QueryResult {
records: any[];
}
export const getRecordAttachments = createAction({
auth: salesforceAuth,
name: 'get_record_attachments',
displayName: 'Get Record Attachments',
description: 'Get all attachments (both classic and modern Files) for a record.',
props: {
object: salesforcesCommon.object,
record_id: salesforcesCommon.record,
},
async run(context) {
const { record_id } = context.propsValue;
const classicAttachmentsQuery = `SELECT Id, Name, BodyLength, ContentType FROM Attachment WHERE ParentId = '${record_id}'`;
const classicAttachmentsResponse = await querySalesforceApi<QueryResult>(
HttpMethod.GET,
context.auth,
classicAttachmentsQuery
);
const classicAttachments = classicAttachmentsResponse.body?.records || [];
const filesQuery = `SELECT ContentDocument.Id, ContentDocument.Title FROM ContentDocumentLink WHERE LinkedEntityId = '${record_id}'`;
const filesResponse = await querySalesforceApi<QueryResult>(
HttpMethod.GET,
context.auth,
filesQuery
);
const files = filesResponse.body?.records || [];
const allAttachments = {
classic_attachments: classicAttachments,
files: files.map((file: any) => ({
Id: file.ContentDocument.Id,
Title: file.ContentDocument.Title,
})),
};
return allAttachments;
},
});

View File

@@ -0,0 +1,47 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const runReport = createAction({
auth: salesforceAuth,
name: 'run_report',
displayName: 'Run Report',
description: 'Execute a Salesforce analytics report.',
props: {
report_id: salesforcesCommon.report,
filters: Property.Json({
displayName: 'Filters',
description: 'Apply dynamic filters to the report run.',
required: false,
defaultValue: [
{
"column": "ACCOUNT.NAME",
"operator": "equals",
"value": "Acme"
}
]
})
},
async run(context) {
const { report_id, filters } = context.propsValue;
let body = undefined;
if (filters && Array.isArray(filters) && filters.length > 0) {
body = {
reportMetadata: {
reportFilters: filters,
},
};
}
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
`/services/data/v56.0/analytics/reports/${report_id}`,
body
);
return response.body;
},
});

View File

@@ -0,0 +1,26 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { salesforcesCommon } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { querySalesforceApi } from '../common';
import { salesforceAuth } from '../..';
export const runQuery = createAction({
auth: salesforceAuth,
name: 'run_query',
displayName: 'Run Query (Advanced)',
description: 'Run a salesforce query',
props: {
query: Property.ShortText({
displayName: 'Query',
description: 'Enter the query',
required: true,
}),
},
async run(context) {
const { query } = context.propsValue;
const response = await await querySalesforceApi<{
records: { CreatedDate: string }[];
}>(HttpMethod.GET, context.auth, query);
return response;
},
});

View File

@@ -0,0 +1,55 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi ,salesforcesCommon } from '../common';
export const sendEmail = createAction({
auth: salesforceAuth,
name: 'send_email',
displayName: 'Send Email',
description: 'Sends an email to a Contact or Lead by creating an EmailMessage record.',
props: {
recipientId: salesforcesCommon.recipient,
subject: Property.ShortText({
displayName: 'Subject',
required: true,
}),
body: Property.LongText({
displayName: 'Body',
description: 'The content of the email. Can be plain text or HTML.',
required: true,
}),
relatedToId: Property.ShortText({
displayName: 'Related To ID (Optional)',
description: 'The ID of a record to relate the email to (e.g., an Account, Opportunity, or Case ID).',
required: false,
})
},
async run(context) {
const { recipientId, subject, body, relatedToId } = context.propsValue;
const emailMessage = {
ToIds: [recipientId],
Subject: subject,
HtmlBody: body,
Status: '3',
...(relatedToId && { RelatedToId: relatedToId }),
};
const cleanedBody = Object.entries(emailMessage).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
const response = await callSalesforceApi(
HttpMethod.POST,
context.auth,
`/services/data/v56.0/sobjects/EmailMessage`,
cleanedBody
);
return response.body;
},
});

View File

@@ -0,0 +1,85 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const updateContact = createAction({
auth: salesforceAuth,
name: 'update_contact',
displayName: 'Update Contact',
description: 'Update an existing contact.',
props: {
contact_id: salesforcesCommon.contact,
FirstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
LastName: Property.ShortText({
displayName: 'Last Name',
required: false,
}),
AccountId: salesforcesCommon.account,
Email: Property.ShortText({
displayName: 'Email',
required: false,
}),
Phone: Property.ShortText({
displayName: 'Phone',
required: false,
}),
Title: Property.ShortText({
displayName: 'Title',
required: false,
}),
other_fields: Property.Json({
displayName: 'Other Fields (Advanced)',
description: 'Enter any additional fields to update as a JSON object.',
required: false
})
},
async run(context) {
const {
contact_id,
FirstName,
LastName,
AccountId,
Email,
Phone,
Title,
other_fields
} = context.propsValue;
const rawBody = {
FirstName,
LastName,
AccountId,
Email,
Phone,
Title,
...(other_fields || {}),
};
const cleanedBody = Object.entries(rawBody).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
if (Object.keys(cleanedBody).length === 0) {
return { success: true, message: "No fields provided to update." };
}
await callSalesforceApi(
HttpMethod.PATCH,
context.auth,
`/services/data/v56.0/sobjects/Contact/${contact_id}`,
cleanedBody
);
return {
success: true,
};
},
});

View File

@@ -0,0 +1,84 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const updateLead = createAction({
auth: salesforceAuth,
name: 'update_lead',
displayName: 'Update Lead',
description: 'Update an existing lead.',
props: {
lead_id: salesforcesCommon.lead,
FirstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
LastName: Property.ShortText({
displayName: 'Last Name',
required: false,
}),
Company: Property.ShortText({
displayName: 'Company',
required: false,
}),
Email: Property.ShortText({
displayName: 'Email',
required: false,
}),
Phone: Property.ShortText({
displayName: 'Phone',
required: false,
}),
LeadSource: salesforcesCommon.leadSource,
other_fields: Property.Json({
displayName: 'Other Fields (Advanced)',
description: 'Enter any additional fields to update as a JSON object.',
required: false
})
},
async run(context) {
const {
lead_id,
FirstName,
LastName,
Company,
Email,
Phone,
LeadSource,
other_fields
} = context.propsValue;
const rawBody = {
FirstName,
LastName,
Company,
Email,
Phone,
LeadSource,
...(other_fields || {}),
};
const cleanedBody = Object.entries(rawBody).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {} as Record<string, unknown>);
if (Object.keys(cleanedBody).length === 0) {
return { success: true, message: "No fields provided to update." };
}
await callSalesforceApi(
HttpMethod.PATCH,
context.auth,
`/services/data/v56.0/sobjects/Lead/${lead_id}`,
cleanedBody
);
return {
success: true,
};
},
});

View File

@@ -0,0 +1,38 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { callSalesforceApi, salesforcesCommon } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
export const UpdateObjectById = createAction({
auth: salesforceAuth,
name: 'update_object_by_id',
displayName: 'Update Object (Advanced)',
description: 'Update object by Id',
props: {
object: salesforcesCommon.object,
id: Property.ShortText({
displayName: 'Id',
description: 'Select the Id',
required: true,
}),
data: Property.Json({
displayName: 'Data',
description: 'Select mapped object',
required: true,
defaultValue: {},
}),
},
async run(context) {
const { object, id, data } = context.propsValue;
const response = await callSalesforceApi(
HttpMethod.PATCH,
context.auth,
`/services/data/v56.0/sobjects/${object}/${id}`,
{
...data,
}
);
return response;
},
});

View File

@@ -0,0 +1,39 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
import { callSalesforceApi, salesforcesCommon } from '../common';
export const updateRecord = createAction({
auth: salesforceAuth,
name: 'update_record',
displayName: 'Update Record',
description: 'Updates an existing record.',
props: {
object: salesforcesCommon.object,
record_id: salesforcesCommon.record,
data: Property.Json({
displayName: 'Data to Update',
description: 'Enter the fields to update as a JSON object. For example: {"BillingCity": "San Francisco"}',
required: true,
defaultValue: {},
}),
},
async run(context) {
const { object, record_id, data } = context.propsValue;
if (!object) {
throw new Error('Object is not defined. Please select an object.');
}
await callSalesforceApi(
HttpMethod.PATCH,
context.auth,
`/services/data/v56.0/sobjects/${object}/${record_id}`,
data as Record<string, unknown>
);
return {
success: true,
};
},
});

View File

@@ -0,0 +1,78 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import {
createBulkJob,
getBulkJobInfo,
notifyBulkJobComplete,
salesforcesCommon,
uploadToBulkJob,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
export const upsertByExternalIdBulk = createAction({
auth: salesforceAuth,
name: 'upsert_by_external_id_bulk',
displayName: 'Bulk Upsert (Advanced)',
description: 'Bulk upsert a record by external id',
props: {
object: salesforcesCommon.object,
external_field: Property.ShortText({
displayName: 'External Field',
description: 'Select the External Field',
required: true,
}),
records: Property.LongText({
displayName: 'Records',
description: 'Select the Records (CSV format)',
required: true,
}),
},
async run(context) {
const records = context.propsValue.records;
let jobId;
if (!records) {
throw new Error('Expect CSV of records to upsert');
}
const { object, external_field } = context.propsValue;
// create bulk job
const create = await createBulkJob(HttpMethod.POST, context.auth, {
object: object,
externalIdFieldName: external_field,
contentType: 'CSV',
operation: 'upsert',
lineEnding: 'CRLF',
});
if (create.status == 200) {
jobId = create.body.id;
} else {
throw new Error(`job creation failed: ${JSON.stringify(create)}`);
}
// upload records to bulk job
await uploadToBulkJob(HttpMethod.PUT, context.auth, jobId, records).catch(
(e) => {
throw new Error(`job upload failed: ${JSON.stringify(e)}`);
}
);
// notify upload complete
await notifyBulkJobComplete(
HttpMethod.PATCH,
context.auth,
{ state: 'UploadComplete' },
jobId
).catch((e) => {
throw new Error(`job failed: ${JSON.stringify(e)}`);
});
const response = await getBulkJobInfo(
HttpMethod.GET,
context.auth,
jobId
).catch((e) => {
throw new Error(`job failed: ${JSON.stringify(e)}`);
});
return response;
},
});

View File

@@ -0,0 +1,46 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { callSalesforceApi, salesforcesCommon } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { salesforceAuth } from '../..';
export const upsertByExternalId = createAction({
auth: salesforceAuth,
name: 'upsert_by_external_id',
displayName: 'Batch Upsert (Advanced)',
description: 'Batch upsert a record by external id',
props: {
object: salesforcesCommon.object,
external_field: Property.ShortText({
displayName: 'External Field',
description: 'Select the External Field',
required: true,
}),
records: Property.Json({
displayName: 'Records',
description: 'Select the Records',
required: true,
defaultValue: {
records: [],
},
}),
},
async run(context) {
const records = context.propsValue?.records?.records;
if (!records) {
throw new Error(
'Expect records field inside json to be an array with records to upsert'
);
}
const { object, external_field } = context.propsValue;
const response = await callSalesforceApi(
HttpMethod.PATCH,
context.auth,
`/services/data/v55.0/composite/sobjects/${object}/${external_field}`,
{
allOrNone: false,
...context.propsValue.records,
}
);
return response;
},
});

View File

@@ -0,0 +1,712 @@
import {
AuthenticationType,
HttpMethod,
HttpMessageBody,
HttpResponse,
httpClient,
} from '@activepieces/pieces-common';
import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework';
import { salesforceAuth } from '../..';
export const salesforcesCommon = {
account: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Account',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
`SELECT Id, Name FROM Account ORDER BY Name LIMIT 100`
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Name,
value: record.Id,
})),
};
},
}),
object: Property.Dropdown<string,true,typeof salesforceAuth>({
auth: salesforceAuth,
displayName: 'Object',
required: true,
description: 'Select the Object',
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'connect your account first',
options: [],
};
}
const options = await getSalesforceObjects(auth as OAuth2PropertyValue);
return {
disabled: false,
options: options.body['sobjects']
.map((object: any) => {
return {
label: object.label,
value: object.name,
};
})
.sort((a: { label: string }, b: { label: string }) =>
a.label.localeCompare(b.label)
)
.filter((object: { label: string }) => !object.label.startsWith('_')),
};
},
}),
record: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Record',
description: 'The record to select. The list shows the 20 most recently created records.',
required: true,
refreshers: ['object'],
options: async ({ auth, object }) => {
if (!auth || !object) {
return {
disabled: true,
placeholder: 'Select an object first',
options: [],
};
}
try {
const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, object as string);
const fields = describeResponse.body['fields'].map((f: any) => f.name);
let displayField = 'Id';
if (fields.includes('Name')) {
displayField = 'Name';
} else if (fields.includes('Subject')) {
displayField = 'Subject';
} else if (fields.includes('Title')) {
displayField = 'Title';
}
const response = await querySalesforceApi<{ records: { Id: string, [key: string]: any }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
`SELECT Id, ${displayField} FROM ${object} ORDER BY CreatedDate DESC LIMIT 20`
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record[displayField] ?? record.Id,
value: record.Id,
})),
};
} catch (e) {
console.error(e);
const fallbackResponse = await querySalesforceApi<{ records: { Id: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
`SELECT Id FROM ${object} LIMIT 20`
);
return {
disabled: false,
options: fallbackResponse.body.records.map((record) => ({
label: record.Id,
value: record.Id,
})),
}
}
},
}),
recipient: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Recipient',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const contactQuery = `SELECT Id, Name FROM Contact ORDER BY Name LIMIT 50`;
const leadQuery = `SELECT Id, Name FROM Lead ORDER BY Name LIMIT 50`;
const [contactsResponse, leadsResponse] = await Promise.all([
querySalesforceApi<{ records: { Id: string, Name: string }[] }>(HttpMethod.GET, auth as OAuth2PropertyValue, contactQuery),
querySalesforceApi<{ records: { Id: string, Name: string }[] }>(HttpMethod.GET, auth as OAuth2PropertyValue, leadQuery)
]);
const contactOptions = contactsResponse.body.records.map((record) => ({
label: `${record.Name} (Contact)`,
value: record.Id,
}));
const leadOptions = leadsResponse.body.records.map((record) => ({
label: `${record.Name} (Lead)`,
value: record.Id,
}));
return {
disabled: false,
options: [...contactOptions, ...leadOptions].sort((a, b) => a.label.localeCompare(b.label)),
};
},
}),
field: Property.Dropdown<string,true,typeof salesforceAuth>({
auth: salesforceAuth,
displayName: 'Field',
description: 'Select the Field',
required: true,
refreshers: ['object'],
options: async ({ auth, object }) => {
if (auth === undefined || !object) {
return {
disabled: true,
placeholder: 'connect your account first',
options: [],
};
}
const options = await getSalesforceFields(
auth as OAuth2PropertyValue,
object as string
);
return {
disabled: false,
options: options.body['fields'].map((field: any) => {
return {
label: field.label,
value: field.name,
};
}),
};
},
}),
campaign: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Campaign',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
"SELECT Id, Name FROM Campaign ORDER BY Name LIMIT 200"
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Name,
value: record.Id,
})),
};
},
}),
contact: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Contact',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string, Email?: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
"SELECT Id, Name, Email FROM Contact ORDER BY Name LIMIT 200"
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Email ? `${record.Name}${record.Email}` : record.Name,
value: record.Id,
})),
};
},
}),
lead: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Lead',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
"SELECT Id, Name FROM Lead ORDER BY Name LIMIT 200"
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Name,
value: record.Id,
})),
};
},
}),
status: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Status',
description: "The campaign member status (e.g., 'Sent', 'Responded').",
required: true,
refreshers: ['campaign_id'],
options: async ({ auth, campaign_id }) => {
if (!auth || !campaign_id) {
return {
disabled: true,
placeholder: 'Select a campaign first',
options: [],
};
}
// Validate campaign_id to prevent SQL injection (Salesforce IDs are 15-18 alphanumeric characters)
const campaignIdStr = String(campaign_id);
if (!/^[a-zA-Z0-9]{15,18}$/.test(campaignIdStr)) {
return {
disabled: true,
placeholder: 'Invalid campaign ID',
options: [],
};
}
const response = await querySalesforceApi<{ records: { Label: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
`SELECT Label FROM CampaignMemberStatus WHERE CampaignId = '${campaignIdStr}'`
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Label,
value: record.Label,
})),
};
},
}),
leadSource: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Lead Source',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
try {
const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, 'Lead');
const leadSourceField = describeResponse.body['fields'].find((field: any) => field.name === 'LeadSource');
if (!leadSourceField || !leadSourceField.picklistValues) {
return { disabled: true, placeholder: 'Lead Source field not found or not a picklist', options: [] };
}
return {
disabled: false,
options: leadSourceField.picklistValues.map((value: any) => {
return {
label: value.label,
value: value.value,
};
}),
};
} catch (e) {
console.error(e);
return {
disabled: true,
placeholder: "Couldn't fetch lead sources",
options: [],
}
}
},
}),
owner: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Owner',
description: 'The owner of the task.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, placeholder: 'Connect your account first', options: [] };
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
"SELECT Id, Name FROM User WHERE IsActive = true ORDER BY Name"
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Name,
value: record.Id,
})),
};
},
}),
opportunity: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Opportunity',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, placeholder: 'Connect your account first', options: [] };
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
"SELECT Id, Name FROM Opportunity ORDER BY CreatedDate DESC LIMIT 100"
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Name,
value: record.Id,
})),
};
},
}),
report: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Report',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, placeholder: 'Connect your account first', options: [] };
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
"SELECT Id, Name FROM Report ORDER BY Name"
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Name,
value: record.Id,
})),
};
},
}),
parentRecord: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Parent Record',
description: 'The parent record to find child records for. The list shows the 20 most recently created records.',
required: true,
refreshers: ['parent_object'],
options: async ({ auth, parent_object }) => {
if (!auth || !parent_object) {
return { disabled: true, placeholder: 'Select a parent object first', options: [] };
}
const response = await querySalesforceApi<{ records: { Id: string, Name?: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
`SELECT Id, Name FROM ${parent_object} ORDER BY CreatedDate DESC LIMIT 20`
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Name ?? record.Id,
value: record.Id,
})),
};
},
}),
childRelationship: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Child Relationship',
description: 'The child relationship to retrieve records from.',
required: true,
refreshers: ['parent_object'],
options: async ({ auth, parent_object }) => {
if (!auth || !parent_object) {
return { disabled: true, placeholder: 'Select a parent object first', options: [] };
}
try {
const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, parent_object as string);
const relationships = describeResponse.body['childRelationships'];
if (!relationships) {
return { disabled: true, placeholder: 'No child relationships found for this object', options: [] };
}
return {
disabled: false,
options: relationships.map((rel: any) => ({
label: `${rel.relationshipName} (${rel.childSObject})`,
value: rel.relationshipName,
})),
};
} catch (e) {
console.error(e);
return { disabled: true, placeholder: "Couldn't fetch child relationships", options: [] };
}
},
}),
optionalContact: Property.Dropdown({
auth: salesforceAuth,
displayName: 'Contact',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, placeholder: 'Connect your account first', options: [] };
}
const response = await querySalesforceApi<{ records: { Id: string, Name: string, Email?: string }[] }>(
HttpMethod.GET,
auth as OAuth2PropertyValue,
"SELECT Id, Name, Email FROM Contact ORDER BY Name LIMIT 200"
);
return {
disabled: false,
options: response.body.records.map((record) => ({
label: record.Email ? `${record.Name}${record.Email}` : record.Name,
value: record.Id,
})),
};
},
}),
taskStatus: createSalesforcePicklistDropdown({
objectName: 'Task',
fieldName: 'Status',
displayName: 'Status',
required: true
}),
taskPriority: createSalesforcePicklistDropdown({
objectName: 'Task',
fieldName: 'Priority',
displayName: 'Priority',
required: true
}),
caseStatus: createSalesforcePicklistDropdown({
objectName: 'Case',
fieldName: 'Status',
displayName: 'Status',
required: false
}),
casePriority: createSalesforcePicklistDropdown({
objectName: 'Case',
fieldName: 'Priority',
displayName: 'Priority',
required: false
}),
caseOrigin: createSalesforcePicklistDropdown({
objectName: 'Case',
fieldName: 'Origin',
displayName: 'Origin',
required: false
}),
opportunityStage: createSalesforcePicklistDropdown({
objectName: 'Opportunity',
fieldName: 'StageName',
displayName: 'Stage',
required: true
}),
};
function createSalesforcePicklistDropdown(config: {
objectName: string,
fieldName: string,
displayName: string,
required: boolean,
description?: string,
}) {
return Property.Dropdown({
auth: salesforceAuth,
displayName: config.displayName,
description: config.description,
required: config.required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, placeholder: 'Connect your account first', options: [] };
}
try {
const describeResponse = await getSalesforceFields(auth as OAuth2PropertyValue, config.objectName);
const field = describeResponse.body['fields'].find((field: any) => field.name === config.fieldName);
if (!field || !field.picklistValues) {
return { disabled: true, placeholder: `${config.fieldName} field not found or not a picklist`, options: [] };
}
return {
disabled: false,
options: field.picklistValues.map((value: any) => ({
label: value.label,
value: value.value,
})),
};
} catch (e) {
console.error(e);
return { disabled: true, placeholder: `Couldn't fetch ${config.fieldName} values`, options: [] };
}
},
});
}
export async function callSalesforceApi<T extends HttpMessageBody>(
method: HttpMethod,
authentication: OAuth2PropertyValue,
url: string,
body: Record<string, unknown> | undefined
): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method: method,
url: `${authentication.data['instance_url']}${url}`,
body,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}
export async function querySalesforceApi<T extends HttpMessageBody>(
method: HttpMethod,
authentication: OAuth2PropertyValue,
query: string
): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method: method,
url: `${authentication.data['instance_url']}/services/data/v56.0/query`,
queryParams: {
q: query,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}
export async function createBulkJob<T extends HttpMessageBody = any>(
method: HttpMethod,
authentication: OAuth2PropertyValue,
jobDetails: HttpMessageBody
): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method: method,
url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/`,
body: jobDetails,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}
export async function uploadToBulkJob<T extends HttpMessageBody>(
method: HttpMethod,
authentication: OAuth2PropertyValue,
jobId: string,
csv: string
): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method: method,
url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}/batches`,
headers: {
'Content-Type': 'text/csv',
},
body: csv as unknown as HttpMessageBody,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}
export async function notifyBulkJobComplete<T extends HttpMessageBody>(
method: HttpMethod,
authentication: OAuth2PropertyValue,
message: HttpMessageBody,
jobId: string
): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method: method,
url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}`,
body: message,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}
export async function getBulkJobInfo<T extends HttpMessageBody>(
method: HttpMethod,
authentication: OAuth2PropertyValue,
jobId: string
): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method: method,
url: `${authentication.data['instance_url']}/services/data/v58.0/jobs/ingest/${jobId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}
async function getSalesforceObjects(
authentication: OAuth2PropertyValue
): Promise<HttpResponse<HttpMessageBody>> {
return await httpClient.sendRequest<HttpMessageBody>({
method: HttpMethod.GET,
url: `${authentication.data['instance_url']}/services/data/v56.0/sobjects`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}
// Write function to list all fields name inside salesforce object
async function getSalesforceFields(
authentication: OAuth2PropertyValue,
object: string
): Promise<HttpResponse<HttpMessageBody>> {
return await httpClient.sendRequest<HttpMessageBody>({
method: HttpMethod.GET,
url: `${authentication.data['instance_url']}/services/data/v56.0/sobjects/${object}/describe`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authentication['access_token'],
},
});
}

View File

@@ -0,0 +1,88 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
OAuth2PropertyValue,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { querySalesforceApi } from '../common';
import dayjs from 'dayjs';
import { salesforceAuth } from '../..';
export const newCaseAttachment = createTrigger({
auth: salesforceAuth,
name: 'new_case_attachment',
displayName: 'New Case Attachment',
description: 'Fires when a new Attachment or File is added to any Case record.',
props: {},
sampleData: {
"Id": "00P7Q000002XyA4UAK",
"ParentId": "5007Q000006g75iQAA",
"Name": "sample_attachment.txt",
"ContentType": "text/plain",
"CreatedDate": "2025-10-10T12:00:00.000Z",
"attachment_type": "Classic"
},
type: TriggerStrategy.POLLING,
async test(ctx) {
return await pollingHelper.test(polling, ctx);
},
async onEnable(ctx) {
await pollingHelper.onEnable(polling, ctx);
},
async onDisable(ctx) {
await pollingHelper.onDisable(polling, ctx);
},
async run(ctx) {
return await pollingHelper.poll(polling, ctx);
},
});
const polling: Polling<AppConnectionValueForAuthProperty<typeof salesforceAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const isoDate = dayjs(lastFetchEpochMS).toISOString();
const classicQuery = `
SELECT FIELDS(ALL) FROM Attachment
WHERE Parent.Type = 'Case' AND SystemModstamp > ${isoDate}
LIMIT 200
`;
const classicResponse = await querySalesforceApi<{ records: { SystemModstamp: string }[] }>(HttpMethod.GET, auth, classicQuery);
const classicAttachments = classicResponse.body?.['records'] || [];
const recentCasesQuery = `SELECT Id FROM Case WHERE SystemModstamp > ${isoDate} LIMIT 200`;
const recentCasesResponse = await querySalesforceApi<{ records: { Id: string }[] }>(HttpMethod.GET, auth, recentCasesQuery);
const recentCaseIds = recentCasesResponse.body?.['records'].map(c => `'${c.Id}'`);
let fileLinks: any[] = [];
if (recentCaseIds && recentCaseIds.length > 0) {
const filesQuery = `
SELECT FIELDS(ALL) FROM ContentDocumentLink
WHERE LinkedEntityId IN (${recentCaseIds.join(',')}) AND SystemModstamp > ${isoDate}
LIMIT 200
`;
const filesResponse = await querySalesforceApi<{ records: { SystemModstamp: string }[] }>(HttpMethod.GET, auth, filesQuery);
fileLinks = filesResponse.body?.['records'] || [];
}
const classicItems = classicAttachments.map((item: any) => ({
epochMilliSeconds: dayjs(item.SystemModstamp).valueOf(),
data: { ...item, attachment_type: 'Classic' },
}));
const fileItems = fileLinks.map((item: any) => ({
epochMilliSeconds: dayjs(item.SystemModstamp).valueOf(),
data: { ...item, attachment_type: 'File' },
}));
const allItems = [...classicItems, ...fileItems];
return allItems.sort((a, b) => a.epochMilliSeconds - b.epochMilliSeconds);
},
};

View File

@@ -0,0 +1,71 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
OAuth2PropertyValue,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { querySalesforceApi } from '../common';
import dayjs from 'dayjs';
import { salesforceAuth } from '../..';
export const newContact = createTrigger({
auth: salesforceAuth,
name: 'new_contact',
displayName: 'New Contact',
description: 'Fires when a new Contact record is created in Salesforce.',
props: {},
sampleData: {
"Id": "0037Q000005x4aXUAQ",
"AccountId": "0017Q00000qM8c9QAC",
"Name": "Jane Doe",
"CreatedDate": "2025-10-10T12:00:00.000Z",
},
type: TriggerStrategy.POLLING,
async test(ctx) {
return await pollingHelper.test(polling, ctx);
},
async onEnable(ctx) {
await pollingHelper.onEnable(polling, ctx);
},
async onDisable(ctx) {
await pollingHelper.onDisable(polling, ctx);
},
async run(ctx) {
return await pollingHelper.poll(polling, ctx);
},
});
const polling: Polling<AppConnectionValueForAuthProperty<typeof salesforceAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const isoDate = dayjs(lastFetchEpochMS).toISOString();
const query = `
SELECT FIELDS(ALL)
FROM Contact
WHERE CreatedDate > ${isoDate}
ORDER BY CreatedDate ASC
LIMIT 200
`;
const response = await querySalesforceApi<{ records: { CreatedDate: string }[] }>(
HttpMethod.GET,
auth,
query
);
const records = response.body?.['records'] || [];
return records.map((record: { CreatedDate: string }) => ({
epochMilliSeconds: dayjs(record.CreatedDate).valueOf(),
data: record,
}));
},
};

View File

@@ -0,0 +1,93 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { querySalesforceApi, salesforcesCommon } from '../common';
import dayjs from 'dayjs';
import { salesforceAuth } from '../..';
export const newFieldHistoryEvent = createTrigger({
auth: salesforceAuth,
name: 'new_field_history_event',
displayName: 'New Field History Event',
description: 'Fires when a tracked field is updated on a specified object.',
props: {
object: salesforcesCommon.object,
},
sampleData: {
"Id": "0177Q00000s912jQAA",
"IsDeleted": false,
"ParentId": "0017Q00000qM8c9QAC",
"CreatedById": "0057Q000003h3VwQAI",
"CreatedDate": "2025-10-10T12:00:00.000Z",
"Field": "Industry",
"OldValue": "Agriculture",
"NewValue": "Technology"
},
type: TriggerStrategy.POLLING,
async test(ctx) {
return await pollingHelper.test(polling, ctx);
},
async onEnable(ctx) {
await pollingHelper.onEnable(polling, ctx);
},
async onDisable(ctx) {
await pollingHelper.onDisable(polling, ctx);
},
async run(ctx) {
return await pollingHelper.poll(polling, ctx);
},
});
const polling: Polling<AppConnectionValueForAuthProperty<typeof salesforceAuth>, { object: string | undefined }> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const { object } = propsValue;
if (!object) {
return [];
}
const isoDate = dayjs(lastFetchEpochMS).toISOString();
const historyObject = object.endsWith('__c')
? object.replace('__c', '__History')
: `${object}History`;
const query = `
SELECT FIELDS(ALL)
FROM ${historyObject}
WHERE CreatedDate > ${isoDate}
ORDER BY CreatedDate ASC
LIMIT 200
`;
try {
const response = await querySalesforceApi<{ records: { CreatedDate: string }[] }>(
HttpMethod.GET,
auth,
query
);
const records = response.body?.['records'] || [];
return records.map((record: { CreatedDate: string }) => ({
epochMilliSeconds: dayjs(record.CreatedDate).valueOf(),
data: record,
}));
} catch (error) {
console.warn(`Could not poll for history on ${historyObject}. It might not exist or have history tracking enabled.`);
return [];
}
},
};

View File

@@ -0,0 +1,70 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { querySalesforceApi } from '../common';
import dayjs from 'dayjs';
import { salesforceAuth } from '../..';
export const newLead = createTrigger({
auth: salesforceAuth,
name: 'new_lead',
displayName: 'New Lead',
description: 'Fires when a new Lead record is created in Salesforce.',
props: {},
sampleData: {
"Id": "00Q7Q000003x4aXUAQ",
"Company": "ACME Inc.",
"Name": "John Doe",
"CreatedDate": "2025-10-10T12:00:00.000Z",
},
type: TriggerStrategy.POLLING,
async test(ctx) {
return await pollingHelper.test(polling, ctx);
},
async onEnable(ctx) {
await pollingHelper.onEnable(polling, ctx);
},
async onDisable(ctx) {
await pollingHelper.onDisable(polling, ctx);
},
async run(ctx) {
return await pollingHelper.poll(polling, ctx);
},
});
const polling: Polling<AppConnectionValueForAuthProperty<typeof salesforceAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const isoDate = dayjs(lastFetchEpochMS).toISOString();
const query = `
SELECT FIELDS(ALL)
FROM Lead
WHERE CreatedDate > ${isoDate}
ORDER BY CreatedDate ASC
LIMIT 200
`;
const response = await querySalesforceApi<{ records: { CreatedDate: string }[] }>(
HttpMethod.GET,
auth,
query
);
const records = response.body?.['records'] || [];
return records.map((record: { CreatedDate: string }) => ({
epochMilliSeconds: dayjs(record.CreatedDate).valueOf(),
data: record,
}));
},
};

View File

@@ -0,0 +1,70 @@
import {
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { XMLParser } from 'fast-xml-parser';
import { salesforceAuth } from '../..';
export const newOutboundMessage = createTrigger({
auth: salesforceAuth,
name: 'new_outbound_message',
displayName: 'New Outbound Message',
description: 'Fires when a new outbound message is received from Salesforce.',
props: {},
// See https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_om_outboundmessaging_notification.htm
sampleData: {
"soapenv:Envelope": {
"@_xmlns:soapenv": "http://schemas.xmlsoap.org/soap/envelope/",
"@_xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
"@_xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"soapenv:Body": {
"notifications": {
"@_xmlns": "http://soap.sforce.com/2005/09/outbound",
"OrganizationId": "00Dxx0000001gEREAY",
"ActionId": "04kxx0000000007EAA",
"SessionId": "00Dxx0000001gER!AQcAQH0dMHZfz972_fL234234_tL2F0M567657_o.6T4x3O3O2_E_i_j_k_l_m_n_o_p",
"EnterpriseUrl": "https://yourInstance.salesforce.com/services/Soap/c/56.0/00Dxx0000001gER",
"PartnerUrl": "https://yourInstance.salesforce.com/services/Soap/u/56.0/00Dxx0000001gER",
"Notification": {
"Id": "04lxx0000000007EAA",
"sObject": {
"@_xsi:type": "sf:Account",
"@_xmlns:sf": "urn:sobject.enterprise.soap.sforce.com",
"Id": "001xx000003D8i1AAC",
"Name": "New Account From Webhook"
}
}
}
}
}
},
type: TriggerStrategy.WEBHOOK,
async onEnable() {
// Webhook triggers don't require setup
},
async onDisable() {
// Webhook triggers don't require cleanup
},
async run(context) {
const requestBody = context.payload.body as string;
const parser = new XMLParser({
attributeNamePrefix: "@_",
textNodeName: "#text",
ignoreAttributes: false,
parseTagValue: true,
parseAttributeValue: true,
trimValues: true,
isArray: (name) => {
return name === "Notification";
}
});
const parsedData = parser.parse(requestBody);
const notifications = parsedData?.['soapenv:Envelope']?.['soapenv:Body']?.['notifications']?.['Notification'] || [];
return [notifications];
},
});

View File

@@ -0,0 +1,119 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
Property,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { querySalesforceApi, salesforcesCommon } from '../common';
import dayjs from 'dayjs';
import { salesforceAuth } from '../..';
export const newRecord = createTrigger({
auth: salesforceAuth,
name: 'new_record',
displayName: 'New Record',
description: 'Triggers when there is new record',
props: {
object: salesforcesCommon.object,
conditions: Property.LongText({
displayName: 'Conditions (Advanced)',
description: 'Enter a SOQL query where clause i. e. IsDeleted = TRUE',
required: false,
}),
},
sampleData: {},
type: TriggerStrategy.POLLING,
async test(ctx) {
return await pollingHelper.test(polling, {
auth: ctx.auth,
store: ctx.store,
propsValue: ctx.propsValue,
files: ctx.files,
});
},
async onEnable(ctx) {
await pollingHelper.onEnable(polling, {
auth: ctx.auth,
store: ctx.store,
propsValue: ctx.propsValue,
});
},
async onDisable(ctx) {
await pollingHelper.onDisable(polling, {
auth: ctx.auth,
store: ctx.store,
propsValue: ctx.propsValue,
});
},
async run(ctx) {
return await pollingHelper.poll(polling, {
auth: ctx.auth,
store: ctx.store,
propsValue: ctx.propsValue,
files: ctx.files,
});
},
});
const polling: Polling<
AppConnectionValueForAuthProperty<typeof salesforceAuth>,
{ object: string | undefined; conditions: string | undefined }
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const items = await getRecords(
auth,
propsValue.object!,
dayjs(lastFetchEpochMS).toISOString(),
propsValue.conditions
);
return items.map((item) => ({
epochMilliSeconds: dayjs(item.CreatedDate).valueOf(),
data: item,
}));
},
};
const getRecords = async (
authentication: AppConnectionValueForAuthProperty<typeof salesforceAuth>,
object: string,
startDate: string,
conditions: string | undefined
) => {
const response = await querySalesforceApi<{
records: { CreatedDate: string }[];
}>(
HttpMethod.GET,
authentication,
constructQuery(object, 200, 0, startDate, conditions)
);
return response.body['records'];
};
function constructQuery(
object: string,
limit: number,
offset: number,
startDate: string,
conditions: string | undefined
) {
return `
SELECT
FIELDS(ALL)
FROM
${object}
WHERE CreatedDate > ${startDate} ${
conditions != undefined ? `AND ${conditions}` : ''
}
ORDER BY CreatedDate ASC
LIMIT ${limit}
OFFSET ${offset}
`;
}

View File

@@ -0,0 +1,65 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { querySalesforceApi } from '../common';
import dayjs from 'dayjs';
import { salesforceAuth } from '../..';
export const newUpdatedFile = createTrigger({
auth: salesforceAuth,
name: 'new_updated_file',
displayName: 'New or Updated File',
description: 'Fires when a file (ContentDocument) is created or updated. Does not fire for classic Attachments or Notes.',
props: {},
sampleData: {
"Id": "0697Q000002qB9iQAE",
"Title": "My Document.pdf",
"LastModifiedDate": "2025-10-10T12:00:00.000Z",
"Type": "ContentDocument"
},
type: TriggerStrategy.POLLING,
async test(ctx) {
return await pollingHelper.test(polling, ctx);
},
async onEnable(ctx) {
await pollingHelper.onEnable(polling, ctx);
},
async onDisable(ctx) {
await pollingHelper.onDisable(polling, ctx);
},
async run(ctx) {
return await pollingHelper.poll(polling, ctx);
},
});
const polling: Polling<AppConnectionValueForAuthProperty<typeof salesforceAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const isoDate = dayjs(lastFetchEpochMS).toISOString();
const query = `
SELECT FIELDS(ALL)
FROM ContentDocument
WHERE SystemModstamp > ${isoDate}
ORDER BY SystemModstamp ASC
LIMIT 200
`;
const response = await querySalesforceApi<{ records: { SystemModstamp: string }[] }>(HttpMethod.GET, auth, query);
const records = response.body?.['records'] || [];
return records.map((record: any) => ({
epochMilliSeconds: dayjs(record.SystemModstamp).valueOf(),
data: { ...record, Type: 'ContentDocument' },
}));
},
};

View File

@@ -0,0 +1,114 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
Property,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { querySalesforceApi, salesforcesCommon } from '../common';
import dayjs from 'dayjs';
import { salesforceAuth } from '../..';
export const newOrUpdatedRecord = createTrigger({
auth: salesforceAuth,
name: 'new_or_updated_record',
displayName: 'New or Updated Record',
description: 'Triggers when there is new or updated record',
props: {
object: salesforcesCommon.object,
conditions: Property.LongText({
displayName: 'Conditions (Advanced)',
description: 'Enter a SOQL query where clause i. e. IsDeleted = TRUE',
required: false,
}),
},
sampleData: {},
type: TriggerStrategy.POLLING,
async test(ctx) {
return await pollingHelper.test(polling, {
auth: ctx.auth,
store: ctx.store,
propsValue: ctx.propsValue,
files: ctx.files,
});
},
async onEnable(ctx) {
await pollingHelper.onEnable(polling, {
auth: ctx.auth,
store: ctx.store,
propsValue: ctx.propsValue,
});
},
async onDisable(ctx) {
await pollingHelper.onDisable(polling, {
auth: ctx.auth,
store: ctx.store,
propsValue: ctx.propsValue,
});
},
async run(ctx) {
return await pollingHelper.poll(polling, ctx);
},
});
const polling: Polling<
AppConnectionValueForAuthProperty<typeof salesforceAuth>,
{ object: string | undefined; conditions: string | undefined }
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const items = await getRecords(
auth,
propsValue.object!,
dayjs(lastFetchEpochMS).toISOString(),
propsValue.conditions
);
return items.map((item) => ({
epochMilliSeconds: dayjs(item.LastModifiedDate).valueOf(),
data: item,
}));
},
};
const getRecords = async (
authentication: AppConnectionValueForAuthProperty<typeof salesforceAuth>,
object: string,
startDate: string,
conditions: string | undefined
) => {
const response = await querySalesforceApi<{
records: { LastModifiedDate: string }[];
}>(
HttpMethod.GET,
authentication,
constructQuery(object, 200, 0, startDate, conditions)
);
return response.body['records'];
};
function constructQuery(
object: string,
limit: number,
offset: number,
startDate: string,
conditions: string | undefined
) {
return `
SELECT
FIELDS(ALL)
FROM
${object}
WHERE LastModifiedDate > ${startDate} ${
conditions != undefined ? `AND ${conditions}` : ''
}
ORDER BY LastModifiedDate ASC
LIMIT ${limit}
OFFSET ${offset}
`;
}