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,32 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
groupIdsDropdown,
makeSenderRequest,
senderAuth,
subscriberDropdownSingle,
} from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const addSubscriberToGroupAction = createAction({
auth: senderAuth,
name: 'add_subscriber_to_group',
displayName: 'Add Subscriber to Group',
description: 'Add an existing or new subscriber into one or more groups',
props: {
subscriber: subscriberDropdownSingle,
groups: groupIdsDropdown,
},
async run(context) {
const subscriberData: any = {
groups: context.propsValue.groups,
};
const response = await makeSenderRequest(
context.auth.secret_text,
`/subscribers/${context.propsValue.subscriber}`,
HttpMethod.PATCH,
subscriberData
);
return response.body;
},
});

View File

@@ -0,0 +1,79 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { groupIdsDropdown, makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
import { group } from 'console';
export const addUpdateSubscriberAction = createAction({
auth: senderAuth,
name: 'add_update_subscriber',
displayName: 'Add / Update Subscriber',
description: 'Add a new subscriber or update existing subscriber\'s data',
props: {
email: Property.ShortText({
displayName: 'Email',
description: 'Subscriber email address',
required: true,
}),
firstname: Property.ShortText({
displayName: 'First Name',
description: 'Subscriber first name',
required: false,
}),
lastname: Property.ShortText({
displayName: 'Last Name',
description: 'Subscriber last name',
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone',
description: 'Subscriber phone number',
required: false,
}),
groups: groupIdsDropdown,
customFields: Property.Json({
displayName: 'Custom Fields',
description: 'JSON object with custom field keys and values',
required: false,
}),
triggerAutomation: Property.Checkbox({
displayName: 'Trigger Automation',
description: 'Whether to trigger automation workflows',
required: false,
defaultValue: false,
}),
},
async run(context) {
const subscriberData: any = {
email: context.propsValue.email,
};
if (context.propsValue.firstname) {
subscriberData.firstname = context.propsValue.firstname;
}
if (context.propsValue.lastname) {
subscriberData.lastname = context.propsValue.lastname;
}
if (context.propsValue.phone) {
subscriberData.phone = context.propsValue.phone;
}
if (context.propsValue.groups) {
subscriberData.groups = context.propsValue.groups;
}
if (context.propsValue.customFields) {
subscriberData.fields = context.propsValue.customFields;
}
if (context.propsValue.triggerAutomation) {
subscriberData.trigger_automation = true;
}
const response = await makeSenderRequest(
context.auth.secret_text,
'/subscribers',
HttpMethod.POST,
subscriberData
);
return response.body;
},
});

View File

@@ -0,0 +1,62 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { groupIdsDropdown, makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
import { group } from 'console';
export const createCampaignAction = createAction({
auth: senderAuth,
name: 'create_campaign',
displayName: 'Create Campaign',
description: 'Creates a draft campaign in Sender',
props: {
title: Property.ShortText({
displayName: 'Campaign Name',
description: 'The name of the campaign',
required: false,
}),
subject: Property.ShortText({
displayName: 'Email Subject',
description: 'The subject line of the email',
required: true,
}),
from: Property.ShortText({
displayName: 'From Name',
description: 'Sender name',
required: true,
}),
reply_to: Property.ShortText({
displayName: 'Reply To',
description: 'Reply-to email address',
required: true,
}),
content_type: Property.LongText({
displayName: 'Content Type',
description: 'The value must be one of "editor", "html", or "text"',
required: true,
}),
groups: groupIdsDropdown,
},
async run(context) {
const campaignData: any = {
title: context.propsValue.title,
subject: context.propsValue.subject,
from: context.propsValue.from,
content_type: context.propsValue.content_type,
groups: context.propsValue.groups,
};
if (context.propsValue.reply_to) {
campaignData.reply_to = context.propsValue.reply_to;
}
const response = await makeSenderRequest(
context.auth.secret_text,
'/campaigns',
HttpMethod.POST,
campaignData
);
return response.body;
},
});

View File

@@ -0,0 +1,42 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
groupIdDropdown,
makeSenderRequest,
senderAuth,
subscribersDropdown,
} from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const removeSubscriberFromGroupAction = createAction({
auth: senderAuth,
name: 'remove_subscriber_from_group',
displayName: 'Remove Subscriber from Group',
description: 'Remove a subscriber from a specific group',
props: {
subscribers: subscribersDropdown,
groupId: groupIdDropdown,
},
async run(context) {
const subscribers = context.propsValue.subscribers;
const groupId = context.propsValue.groupId;
const requestBody = {
subscribers: subscribers,
};
const response = await makeSenderRequest(
context.auth.secret_text,
`/subscribers/groups/${groupId}`,
HttpMethod.DELETE,
requestBody
);
return {
success: true,
subscribers,
groupId,
removedAt: new Date().toISOString(),
response: response.body,
};
},
});

View File

@@ -0,0 +1,28 @@
import { createAction } from '@activepieces/pieces-framework';
import {
campaignDropdown,
makeSenderRequest,
senderAuth,
} from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const sendCampaignAction = createAction({
auth: senderAuth,
name: 'send_campaign',
displayName: 'Send Campaign',
description: 'Trigger sending of a drafted campaign to its recipient list',
props: {
campaignId: campaignDropdown,
},
async run(context) {
const campaignId = context.propsValue.campaignId;
const response = await makeSenderRequest(
context.auth.secret_text,
`/campaigns/${campaignId}/send`,
HttpMethod.POST,
);
return response.body;
},
});

View File

@@ -0,0 +1,34 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
makeSenderRequest,
senderAuth,
subscriberDropdownSingle,
} from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
import { subscribe } from 'diagnostics_channel';
export const unsubscribeSubscriberAction = createAction({
auth: senderAuth,
name: 'unsubscribe_subscriber',
displayName: 'Unsubscribe Subscriber',
description: 'Mark an email address as unsubscribed globally or from a group',
props: {
subscriber: subscriberDropdownSingle,
},
async run(context) {
const subscriber = context.propsValue.subscriber;
const subscriberId = subscriber;
const requestBody = {
subscribers: [subscriberId],
};
const response = await makeSenderRequest(
context.auth.secret_text,
`/subscribers`,
HttpMethod.DELETE,
requestBody
);
return response.body;
},
});

View File

@@ -0,0 +1,70 @@
import { createAction, Property} from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth, subscriberDropdownSingle } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
import { subscribe } from 'diagnostics_channel';
export const updateSubscriberAction = createAction({
auth: senderAuth,
name: 'update_subscriber',
displayName: 'Update Subscriber',
description: 'Update an existing subscriber\'s data',
props: {
subscriber: subscriberDropdownSingle,
email: Property.ShortText({
displayName: 'Email',
description: 'Subscriber email address to update',
required: true,
}),
firstname: Property.ShortText({
displayName: 'First Name',
description: 'New first name',
required: false,
}),
lastname: Property.ShortText({
displayName: 'Last Name',
description: 'New last name',
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone',
description: 'New phone number',
required: false,
}),
customFields: Property.Json({
displayName: 'Custom Fields',
description: 'JSON object with custom field keys and values to update',
required: false,
}),
},
async run(context) {
const email = context.propsValue.email;
const phone = context.propsValue.phone;
const {subscriber}= context.propsValue;
const subscriberId = subscriber;
const updateData: any = {};
if (context.propsValue.firstname) {
updateData.firstname = context.propsValue.firstname;
}
if (context.propsValue.lastname) {
updateData.lastname = context.propsValue.lastname;
}
if (context.propsValue.phone) {
updateData.phone = context.propsValue.phone;
}
if (context.propsValue.customFields) {
updateData.fields = context.propsValue.customFields;
}
const response = await makeSenderRequest(
context.auth.secret_text,
`/subscribers/${subscriberId}`,
HttpMethod.PATCH,
updateData
);
return response.body;
},
});

View File

@@ -0,0 +1,231 @@
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { PieceAuth, Property } from '@activepieces/pieces-framework';
export const senderAuth = PieceAuth.SecretText({
displayName: 'API Token',
description: 'Enter your Sender API Token',
required: true,
});
const SENDER_API_BASE_URL = 'https://api.sender.net/v2';
export async function makeSenderRequest(
auth: string,
endpoint: string,
method: HttpMethod = HttpMethod.GET,
body?: any
) {
return await httpClient.sendRequest({
method,
url: `${SENDER_API_BASE_URL}${endpoint}`,
headers: {
Authorization: `Bearer ${auth}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body,
});
}
export const groupIdDropdown = Property.Dropdown({
auth: senderAuth,
displayName: 'Groups',
description: 'Select one or more groups',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Sender account first',
};
}
try {
const response: any = await makeSenderRequest(
auth.secret_text,
'/groups',
HttpMethod.GET
);
const groups = response.body.data || [];
return {
disabled: false,
options: groups.map((group: any) => ({
label: group.title,
value: group.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading groups',
};
}
},
});
export const groupIdsDropdown = Property.MultiSelectDropdown({
auth: senderAuth,
displayName: 'Groups',
description: 'Select one or more groups',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Sender account first',
};
}
try {
const response: any = await makeSenderRequest(
auth.secret_text,
'/groups',
HttpMethod.GET
);
const groups = response.body.data || [];
return {
disabled: false,
options: groups.map((group: any) => ({
label: group.title,
value: group.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading groups',
};
}
},
});
export const subscribersDropdown = Property.MultiSelectDropdown<string, true, typeof senderAuth>({
auth: senderAuth,
displayName: 'Subscribers',
description: 'Select one or more subscribers to delete',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Sender account first',
};
}
try {
const response: any = await makeSenderRequest(
auth.secret_text,
'/subscribers?limit=50',
HttpMethod.GET
);
const subscribers = response.body.data || [];
return {
disabled: false,
options: subscribers.map((sub: any) => ({
label: sub.email,
value: sub.email,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading subscribers',
};
}
},
});
export const subscriberDropdownSingle = Property.Dropdown<string, true, typeof senderAuth>({
auth: senderAuth,
displayName: 'Subscriber',
description: 'Select a subscriber',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Sender account first',
};
}
try {
const response: any = await makeSenderRequest(
auth.secret_text,
'/subscribers?limit=50',
HttpMethod.GET
);
const subscribers = response.body.data || [];
return {
disabled: false,
options: subscribers.map((sub: any) => ({
label: sub.email,
value: sub.email,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading subscribers',
};
}
},
});
export const campaignDropdown = Property.Dropdown({
auth: senderAuth,
displayName: 'Campaign',
description: 'Select a campaign',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Sender account first',
};
}
try {
const response: any = await makeSenderRequest(
auth.secret_text,
`/campaigns?limit=50&status=DRAFT`,
HttpMethod.GET
);
const campaigns = response.body.data || [];
return {
disabled: false,
options: campaigns.map((c: any) => ({
label: c.title || c.subject || c.id,
value: c.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading campaigns',
};
}
},
});

View File

@@ -0,0 +1,55 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const newCampaignTrigger = createTrigger({
auth: senderAuth,
name: 'new_campaign',
displayName: 'New Campaign',
description: 'Fires when a new campaign is created in Sender',
type: TriggerStrategy.WEBHOOK,
props: {},
async onEnable(context) {
const webhookUrl = context.webhookUrl;
const webhookData = {
url: webhookUrl,
topic : 'campaigns/new',
};
const response = await makeSenderRequest(
context.auth.secret_text,
'/account/webhooks',
HttpMethod.POST,
webhookData
);
await context.store.put('webhookId', response.body.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await makeSenderRequest(
context.auth.secret_text,
`/account/webhooks/${webhookId}`,
HttpMethod.DELETE
);
}
await context.store.delete('webhookId');
},
async run(context) {
return [context.payload.body];
},
async test(context) {
const response = await makeSenderRequest(
context.auth.secret_text,
'/campaigns?limit=1',
HttpMethod.GET
);
return response.body.data || [];
},
sampleData: {},
});

View File

@@ -0,0 +1,55 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const newGroupTrigger = createTrigger({
auth: senderAuth,
name: 'new_group',
displayName: 'New Group',
description: 'Fires when a new group/list is created',
type: TriggerStrategy.WEBHOOK,
props: {},
async onEnable(context) {
const webhookUrl = context.webhookUrl;
const webhookData = {
url: webhookUrl,
topic : 'groups/new',
};
const response = await makeSenderRequest(
context.auth.secret_text,
'/account/webhooks',
HttpMethod.POST,
webhookData
);
await context.store.put('webhookId', response.body.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await makeSenderRequest(
context.auth.secret_text,
`/account/webhooks/${webhookId}`,
HttpMethod.DELETE
);
}
await context.store.delete('webhookId');
},
async run(context) {
return [context.payload.body];
},
async test(context) {
const response = await makeSenderRequest(
context.auth.secret_text,
'/groups?limit=1',
HttpMethod.GET
);
return response.body.data || [];
},
sampleData: {},
});

View File

@@ -0,0 +1,63 @@
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const newSubscriberInGroupTrigger = createTrigger({
auth: senderAuth,
name: 'new_subscriber_in_group',
displayName: 'New Subscriber in Group',
description: 'Fires when a subscriber is added to a specific group/list',
type: TriggerStrategy.WEBHOOK,
props: {
groupId: Property.ShortText({
displayName: 'Group ID',
description: 'The ID of the group to monitor',
required: true,
}),
},
async onEnable(context) {
const webhookUrl = context.webhookUrl;
const groupId = context.propsValue.groupId;
const webhookData = {
url: webhookUrl,
topic : 'groups/new-subscriber',
relation_id : groupId,
};
const response = await makeSenderRequest(
context.auth.secret_text,
'/account/webhooks',
HttpMethod.POST,
webhookData
);
await context.store.put('webhookId', response.body.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await makeSenderRequest(
context.auth.secret_text,
`/account/webhooks/${webhookId}`,
HttpMethod.DELETE
);
}
await context.store.delete('webhookId');
},
async run(context) {
return [context.payload.body];
},
async test(context) {
const groupId = context.propsValue.groupId;
const response = await makeSenderRequest(
context.auth.secret_text,
`/groups/${groupId}/subscribers?limit=1`
);
return response.body.data || [];
},
sampleData: {},
});

View File

@@ -0,0 +1,54 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const newSubscriberTrigger = createTrigger({
auth: senderAuth,
name: 'new_subscriber',
displayName: 'New Subscriber',
description: 'Fires when a subscriber is added to any group or to account',
type: TriggerStrategy.WEBHOOK,
props: {},
async onEnable(context) {
const webhookUrl = context.webhookUrl;
const webhookData = {
url: webhookUrl,
topic: 'subscribers/new',
};
const response = await makeSenderRequest(
context.auth.secret_text,
'/account/webhooks',
HttpMethod.POST,
webhookData
);
await context.store.put('webhookId', response.body.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await makeSenderRequest(
context.auth.secret_text,
`/account/webhooks/${webhookId}`,
HttpMethod.DELETE
);
}
await context.store.delete('webhookId');
},
async run(context) {
return [context.payload.body];
},
async test(context) {
const response = await makeSenderRequest(
context.auth.secret_text,
'/subscribers?limit=1',
HttpMethod.GET
);
return response.body.data;
},
sampleData: {},
});

View File

@@ -0,0 +1,58 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const newUnsubscriberFromGroupTrigger = createTrigger({
auth: senderAuth,
name: 'new_unsubscriber_from_group',
displayName: 'New Unsubscriber From Group',
description: 'Fires when a subscriber is removed/unsubscribed from a specific group',
type: TriggerStrategy.WEBHOOK,
props: {
groupId: Property.ShortText({
displayName: 'Group ID',
description: 'The ID of the group to monitor',
required: true,
}),
},
async onEnable(context) {
const webhookUrl = context.webhookUrl;
const groupId = context.propsValue.groupId;
const webhookData = {
url: webhookUrl,
topic: 'groups/unsubscribed',
relation_id : groupId,
};
const response = await makeSenderRequest(
context.auth.secret_text,
'/account/webhooks',
HttpMethod.POST,
webhookData
);
await context.store.put('webhookId', response.body.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await makeSenderRequest(
context.auth.secret_text,
`/account/webhooks/${webhookId}`,
HttpMethod.DELETE
);
}
await context.store.delete('webhookId');
},
async run(context) {
return [context.payload.body];
},
async test(context) {
return [];
},
sampleData: {},
});

View File

@@ -0,0 +1,50 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const newUnsubscriberTrigger = createTrigger({
auth: senderAuth,
name: 'new_unsubscriber',
displayName: 'New Unsubscriber',
description: 'Fires when someone unsubscribes globally',
type: TriggerStrategy.WEBHOOK,
props: {},
async onEnable(context) {
const webhookUrl = context.webhookUrl;
const webhookData = {
url: webhookUrl,
topic: 'subscribers/unsubscribed',
};
const response = await makeSenderRequest(
context.auth.secret_text,
'/account/webhooks',
HttpMethod.POST,
webhookData
);
await context.store.put('webhookId', response.body.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await makeSenderRequest(
context.auth.secret_text,
`/account/webhooks/${webhookId}`,
HttpMethod.DELETE
);
}
await context.store.delete('webhookId');
},
async run(context) {
return [context.payload.body];
},
async test(context) {
return [];
},
sampleData: {},
});

View File

@@ -0,0 +1,54 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { makeSenderRequest, senderAuth } from '../common/common';
import { HttpMethod } from '@activepieces/pieces-common';
export const updatedSubscriberTrigger = createTrigger({
auth: senderAuth,
name: 'updated_subscriber',
displayName: 'Updated Subscriber',
description: "Fires when a subscriber's data (fields) is updated",
type: TriggerStrategy.WEBHOOK,
props: {},
async onEnable(context) {
const webhookUrl = context.webhookUrl;
const webhookData = {
url: webhookUrl,
topic: 'subscribers/updated',
};
const response = await makeSenderRequest(
context.auth.secret_text,
'/account/webhooks',
HttpMethod.POST,
webhookData
);
await context.store.put('webhookId', response.body.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await makeSenderRequest(
context.auth.secret_text,
`/account/webhooks/${webhookId}`,
HttpMethod.DELETE
);
}
await context.store.delete('webhookId');
},
async run(context) {
return [context.payload.body];
},
async test(context) {
const response = await makeSenderRequest(
context.auth.secret_text,
'/subscribers?limit=1',
HttpMethod.GET
);
return response.body.data || [];
},
sampleData: {},
});