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,184 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
export const addLead = createAction({
auth:autocallsAuth,
name: 'addLead',
displayName: 'Add lead to a campaign',
description: "Add lead to an outbound campaign, to be called by an assistant from our platform.",
props: {
campaign: Property.Dropdown({
auth: autocallsAuth,
displayName: 'Campaign',
description: 'Select a campaign',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: []
};
}
try {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/campaigns',
headers: {
Authorization: "Bearer " + auth,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
if (res.status !== 200) {
return {
disabled: true,
placeholder: 'Error fetching campaigns',
options: [],
};
} else if (!res.body || res.body.length === 0) {
return {
disabled: true,
placeholder: 'No campaigns found. Create one first.',
options: [],
};
}
return {
options: res.body.map((campaign: any) => ({
value: campaign.id,
label: campaign.name,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to fetch campaigns',
options: [],
};
}
}
}),
phone_number: Property.ShortText({
displayName: 'Customer phone number',
description: 'Enter the phone number of the customer',
required: true,
}),
variables: Property.Object({
displayName: 'Variables',
description: 'Variables to pass to the assistant',
required: true,
defaultValue: {
customer_name: 'John',
}
}),
allow_dupplicate: Property.Checkbox({
displayName: 'Allow duplicates',
description: 'Allow the same phone number to be added to the campaign more than once',
required: true,
defaultValue: false
}),
num_secondary_contacts: Property.Number({
displayName: 'Number of Secondary Contacts',
description: 'How many secondary contacts do you want to add?',
required: false,
defaultValue: 0,
}),
secondary_contacts: Property.DynamicProperties({
auth: autocallsAuth,
displayName: 'Secondary Contacts',
description: 'Add secondary contacts for this lead. Each contact can have its own phone number and variables.',
required: false,
refreshers: ['num_secondary_contacts'],
props: async ({ num_secondary_contacts }) => {
const contacts: any = {};
const numContacts = Number(num_secondary_contacts) || 0;
// Generate fields based on the number specified
for (let i = 1; i <= numContacts && i <= 10; i++) {
contacts[`contact_${i}_phone`] = Property.ShortText({
displayName: `Contact ${i} - Phone Number`,
description: `Phone number for secondary contact ${i}`,
required: true,
});
contacts[`contact_${i}_variables`] = Property.Object({
displayName: `Contact ${i} - Variables`,
description: `Variables for secondary contact ${i} as key-value pairs`,
required: false,
defaultValue: {
customer_name: 'John',
}
});
}
return contacts;
},
})
},
async run(context) {
if (!context.auth) {
throw new Error('Authentication is required');
}
try {
const body: any = {
campaign_id: context.propsValue['campaign'],
phone_number: context.propsValue['phone_number'],
variables: context.propsValue['variables'],
allow_dupplicate: context.propsValue['allow_dupplicate'],
};
// Add secondary contacts if provided
if (context.propsValue['secondary_contacts']) {
const secondaryContactsData = context.propsValue['secondary_contacts'] as Record<string, any>;
const numContacts = Number(context.propsValue['num_secondary_contacts']) || 0;
const secondaryContacts: any[] = [];
// Process the specified number of contacts
for (let i = 1; i <= numContacts && i <= 10; i++) {
const phoneNumber = secondaryContactsData[`contact_${i}_phone`];
const variables = secondaryContactsData[`contact_${i}_variables`];
// Only add contact if phone number is provided
if (phoneNumber && phoneNumber.trim() !== '') {
secondaryContacts.push({
phone_number: phoneNumber,
variables: variables || {
customer_name: '',
}
});
}
}
if (secondaryContacts.length > 0) {
body.secondary_contacts = secondaryContacts;
}
}
const res = await httpClient.sendRequest<string[]>({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/lead',
body: body,
headers: {
Authorization: "Bearer " + context.auth.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
if (res.status !== 200) {
throw new Error(`Failed to add lead: ${res.status}`);
}
return res.body;
} catch (error) {
throw new Error(`Failed to add lead: ${error}`);
}
},
});

View File

@@ -0,0 +1,79 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
export const campaignControl = createAction({
auth:autocallsAuth,
name: 'campaignControl',
displayName: 'Start/Stop Campaign',
description: "Start or stop an outbound campaign from our platform.",
props: {
campaign: Property.Dropdown({
auth: autocallsAuth,
displayName: 'Campaign',
description: 'Select a campaign',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
options: async ({ auth }) => {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/campaigns',
headers: {
Authorization: "Bearer " + auth?.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
if (res.status !== 200) {
return {
disabled: true,
placeholder: 'Error fetching campaigns',
options: [],
};
} else if (res.body.length === 0) {
return {
disabled: true,
placeholder: 'No campaigns found. Create one first.',
options: [],
};
}
return {
options: res.body.map((campaign: any) => ({
value: campaign.id,
label: campaign.name,
})),
};
}
}),
action: Property.StaticDropdown({
displayName: 'Action',
description: 'Select action to perform on the campaign',
required: true,
options: {
options: [
{ label: 'Start Campaign', value: 'start' },
{ label: 'Stop Campaign', value: 'stop' }
]
}
})
},
async run(context) {
const res = await httpClient.sendRequest<string[]>({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/campaigns/update-status',
body: {
campaign_id: context.propsValue['campaign'],
action: context.propsValue['action'],
},
headers: {
Authorization: "Bearer " + context.auth.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
return res.body;
},
});

View File

@@ -0,0 +1,66 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
export const deleteLead = createAction({
auth:autocallsAuth,
name: 'deleteLead',
displayName: 'Delete Lead',
description: "Delete a lead from a campaign.",
props: {
lead: Property.Dropdown({
auth: autocallsAuth,
displayName: 'Lead',
description: 'Select a lead to delete',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
options: async ({ auth }) => {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/leads',
headers: {
Authorization: "Bearer " + auth,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
if (res.status !== 200) {
return {
disabled: true,
placeholder: 'Error fetching leads',
options: [],
};
} else if (res.body.length === 0) {
return {
disabled: true,
placeholder: 'No leads found.',
options: [],
};
}
return {
options: res.body.map((lead: any) => ({
value: lead.id,
label: `${lead.phone_number} - ${lead.campaign.name}`,
})),
};
}
})
},
async run(context) {
const leadId = context.propsValue['lead'];
const res = await httpClient.sendRequest<string[]>({
method: HttpMethod.DELETE,
url: baseApiUrl + 'api/user/leads/' + leadId,
headers: {
Authorization: "Bearer " + context.auth.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
return res.body;
},
});

View File

@@ -0,0 +1,79 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
export const makePhoneCall = createAction({
auth:autocallsAuth,
name: 'makePhoneCall',
displayName: 'Make Phone Call',
description: "Call a customer by it's phone number using an assistant from our platform.",
props: {
assistant: Property.Dropdown({
auth: autocallsAuth,
displayName: 'Assistant',
description: 'Select an assistant',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
options: async ({ auth }) => {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/assistants/outbound',
headers: {
Authorization: "Bearer " + auth?.secret_text,
},
});
if (res.status !== 200) {
return {
disabled: true,
placeholder: 'Error fetching assistants',
options: [],
};
} else if (res.body.length === 0) {
return {
disabled: true,
placeholder: 'No outbound assistants found. Create one first.',
options: [],
};
}
return {
options: res.body.map((assistant: any) => ({
value: assistant.id,
label: assistant.name,
})),
};
}
}),
phone_number: Property.ShortText({
displayName: 'Customer phone number',
description: 'Enter the phone number of the customer',
required: true,
}),
variables: Property.Object({
displayName: 'Variables',
description: 'Variables to pass to the assistant',
required: true,
defaultValue: {
customer_name: 'John',
}
})
},
async run(context) {
const res = await httpClient.sendRequest<string[]>({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/make_call',
body: {
assistant_id: context.propsValue['assistant'],
phone_number: context.propsValue['phone_number'],
variables: context.propsValue['variables'],
},
headers: {
Authorization: "Bearer " + context.auth.secret_text,
},
});
return res.body;
},
});

View File

@@ -0,0 +1,80 @@
import { createAction, Property} from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
export const sendSms = createAction({
auth:autocallsAuth,
name: 'sendSms',
displayName: 'Send SMS to Customer',
description: "Send an SMS to a customer using a phone number from our platform.",
props: {
from: Property.Dropdown({
auth: autocallsAuth,
displayName: 'From phone number',
description: 'Select an SMS capable phone number to send the SMS from',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
options: async ({ auth }) => {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/phone-numbers',
headers: {
Authorization: "Bearer " + auth?.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
if (res.status !== 200) {
return {
disabled: true,
placeholder: 'Error fetching phone numbers',
options: [],
};
} else if (res.body.length === 0) {
return {
disabled: true,
placeholder: 'No phone numbers found. Purchase an SMS capable phone number first.',
options: [],
};
}
return {
options: res.body.map((phoneNumber: any) => ({
value: phoneNumber.id,
label: phoneNumber.phone_number,
})),
};
}
}),
to: Property.ShortText({
displayName: 'Customer phone number',
description: 'Enter the phone number of the customer',
required: true,
}),
body: Property.ShortText({
displayName: 'Text message',
description: 'Enter the text message to send to the customer (max 300 characters)',
required: true,
}),
},
async run(context) {
const res = await httpClient.sendRequest<string[]>({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/sms',
body: {
from: context.propsValue['from'],
to: context.propsValue['to'],
body: context.propsValue['body'],
},
headers: {
Authorization: "Bearer " + context.auth.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
return res.body;
},
});

View File

@@ -0,0 +1,109 @@
import { createTrigger, TriggerStrategy, Property, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
import { DedupeStrategy, Polling, pollingHelper, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
import dayjs from 'dayjs';
const polling: Polling<AppConnectionValueForAuthProperty<typeof autocallsAuth>, { start?: string; end?: string }> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue }) => {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/assistants',
headers: {
Authorization: 'Bearer ' + auth.secret_text,
},
});
if (res.status !== 200) {
throw new Error(`Failed to fetch assistants. Status: ${res.status}`);
}
const assistants =
(res.body as Array<{
id: number;
created_at: string;
updated_at: string;
}>) || [];
const filteredAssistants = assistants.filter((assistant) => {
const assistantDate = assistant.created_at
? dayjs(assistant.created_at)
: dayjs(assistant.updated_at);
// Check start date filter
if (propsValue['start']) {
const startDate = dayjs(propsValue['start']);
if (assistantDate.isBefore(startDate)) {
return false;
}
}
// Check end date filter
if (propsValue['end']) {
const endDate = dayjs(propsValue['end']);
if (assistantDate.isAfter(endDate)) {
return false;
}
}
return true;
});
return filteredAssistants.map((assistant) => {
const assistantDate = assistant.created_at
? dayjs(assistant.created_at)
: dayjs(assistant.updated_at);
return {
epochMilliSeconds: assistantDate.valueOf(),
data: assistant,
};
});
},
};
export const getAssistants = createTrigger({
auth:autocallsAuth,
name: 'getAssistants',
displayName: 'Updated Assistant',
description: 'Triggers when assistants are fetched or updated in your Autocalls account.',
props: {
start: Property.DateTime({
displayName: 'Start Date',
description: 'Filter assistants created after this date. Example: 2024-01-15T10:30:00Z',
required: false,
}),
end: Property.DateTime({
displayName: 'End Date',
description: 'Filter assistants created before this date. Example: 2024-12-31T23:59:59Z',
required: false,
}),
},
sampleData: {
id: "assistant_123",
name: "Customer Support Assistant",
description: "Handles customer inquiries and support requests",
status: "active",
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-01-15T14:20:00Z",
settings: {
voice: "en-US-female",
language: "en-US",
max_duration: 300
}
},
type: TriggerStrategy.POLLING,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, auth, propsValue });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, auth, propsValue });
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});

View File

@@ -0,0 +1,89 @@
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
export const inboundCall = createTrigger({
auth:autocallsAuth,
name: 'inboundCall',
displayName: 'Inbound Call',
description: 'Triggers for variables before connecting an inbound call.',
props: {
assistant: Property.Dropdown({
auth: autocallsAuth,
displayName: 'Assistant',
description: 'Select an assistant',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
options: async ({ auth }) => {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/assistants',
headers: {
Authorization: "Bearer " + auth?.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
if (res.status !== 200) {
return {
disabled: true,
placeholder: 'Error fetching assistants',
options: [],
};
} else if (res.body.length === 0) {
return {
disabled: true,
placeholder: 'No assistants found. Create one first.',
options: [],
};
}
return {
options: res.body.map((assistant: any) => ({
value: assistant.id,
label: assistant.name,
})),
};
}
}),
},
sampleData: {
customer_phone: '+16380991171',
assistant_phone: '+16380991171',
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
await httpClient.sendRequest({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/assistants/enable-inbound-webhook',
body: {
assistant_id: context.propsValue['assistant'],
webhook_url: context.webhookUrl,
},
headers: {
Authorization: "Bearer " + context.auth.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
},
async onDisable(context) {
await httpClient.sendRequest({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/assistants/disable-inbound-webhook',
body: {
assistant_id: context.propsValue['assistant'],
},
headers: {
Authorization: "Bearer " + context.auth.secret_text,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
},
async run(context) {
return [context.payload.body]
}
})

View File

@@ -0,0 +1,114 @@
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { autocallsAuth, baseApiUrl } from '../..';
export const phoneCallEnded = createTrigger({
auth:autocallsAuth,
name: 'phoneCallEnded',
displayName: 'Phone Call Ended',
description: 'Triggers when a phone call ends, with extracted variables.',
props: {
assistant: Property.Dropdown({
auth: autocallsAuth,
displayName: 'Assistant',
description: 'Select an assistant',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
options: async ({ auth }) => {
const res = await httpClient.sendRequest({
method: HttpMethod.GET,
url: baseApiUrl + 'api/user/assistants',
headers: {
Authorization: "Bearer " + auth?.secret_text,
},
});
if (res.status !== 200) {
return {
disabled: true,
placeholder: 'Error fetching assistants',
options: [],
};
} else if (res.body.length === 0) {
return {
disabled: true,
placeholder: 'No assistants found. Create one first.',
options: [],
};
}
return {
options: res.body.map((assistant: {id:number,name:string}) => ({
value: assistant.id,
label: assistant.name,
})),
};
}
}),
},
sampleData: {
customer_phone: '+16380991171',
assistant_phone: '+16380991171',
duration: 120,
status: 'completed',
extracted_variables: {
status: false,
summary: 'Call ended without clear objective being met.'
},
input_variables: {
customer_name: 'John'
},
transcript: [
{
sender: 'bot',
timestamp: 1722347063.574402,
text: 'Hi! How are you, John?'
},
{
sender: 'human',
timestamp: 1722347068.886166,
text: 'Im fine. How about you?'
},
{
sender: 'bot',
timestamp: 1722347069.76683,
text: 'Im doing well, thank you for asking.'
},
{
sender: 'bot',
timestamp: 1722347071.577889,
text: 'How can I assist you today?'
},
]
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
await httpClient.sendRequest({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/assistants/enable-webhook',
body: {
assistant_id: context.propsValue['assistant'],
webhook_url: context.webhookUrl,
},
headers: {
Authorization: "Bearer " + context.auth.secret_text,
},
});
},
async onDisable(context) {
await httpClient.sendRequest({
method: HttpMethod.POST,
url: baseApiUrl + 'api/user/assistants/disable-webhook',
body: {
assistant_id: context.propsValue['assistant'],
},
headers: {
Authorization: "Bearer " + context.auth.secret_text,
},
});
},
async run(context) {
return [context.payload.body]
}
})