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,33 @@
{
"extends": [
"../../../../.eslintrc.base.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# pieces-pollybot-ai
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build pieces-pollybot-ai` to build the library.

View File

@@ -0,0 +1,10 @@
{
"name": "@activepieces/piece-pollybot-ai",
"version": "0.0.1",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,65 @@
{
"name": "pieces-pollybot-ai",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/pollybot-ai/src",
"projectType": "library",
"release": {
"version": {
"manifestRootsToUpdate": [
"dist/{projectRoot}"
],
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
},
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/pieces/community/pollybot-ai",
"tsConfig": "packages/pieces/community/pollybot-ai/tsconfig.lib.json",
"packageJson": "packages/pieces/community/pollybot-ai/package.json",
"main": "packages/pieces/community/pollybot-ai/src/index.ts",
"assets": [
"packages/pieces/community/pollybot-ai/*.md",
{
"input": "packages/pieces/community/pollybot-ai/src/i18n",
"output": "./src/i18n",
"glob": "**/!(i18n.json)"
}
],
"buildableProjectDepsInPackageJsonType": "dependencies",
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": [
"prebuild",
"^build"
]
},
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"prebuild": {
"dependsOn": [
"^build"
],
"executor": "nx:run-commands",
"options": {
"cwd": "packages/pieces/community/pollybot-ai",
"command": "bun install --no-save --silent"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
}
}
}

View File

@@ -0,0 +1,21 @@
import { createPiece } from '@activepieces/pieces-framework';
import { pollybotAuth } from './lib/auth';
import { createLead } from './lib/actions/create-lead';
import { getLead } from './lib/actions/get-lead';
import { updateLead } from './lib/actions/update-lead';
import { deleteLead } from './lib/actions/delete-lead';
import { listLeads } from './lib/actions/list-leads';
import { PieceCategory } from '@activepieces/shared';
import { newLead } from './lib/triggers/new-lead';
export const pollybotAi = createPiece({
displayName: 'PollyBot AI',
description: 'Automate lead management with PollyBot AI chatbot integration.',
auth: pollybotAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: 'https://cdn.activepieces.com/pieces/pollybot-ai.png',
categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE],
authors: ['Trayshmhirk', 'sanket-a11y'],
actions: [createLead, getLead, updateLead, deleteLead, listLeads],
triggers: [newLead],
});

View File

@@ -0,0 +1,74 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { pollybotAuth } from '../auth';
import { baseUrl, leadStatusOptions, formatError } from '../common/common';
export const createLead = createAction({
name: 'create_lead',
displayName: 'Create Lead',
description: 'Creates a new lead in your PollyBot chatbot.',
auth: pollybotAuth,
props: {
name: Property.ShortText({
displayName: 'Name',
required: true,
description: "Lead's full name",
}),
email: Property.ShortText({
displayName: 'Email',
required: true,
description: 'Valid email address',
}),
phone: Property.ShortText({
displayName: 'Phone',
required: false,
}),
source: Property.ShortText({
displayName: 'Source',
required: false,
description: 'Lead source (e.g., website, referral)',
}),
status: Property.StaticDropdown({
displayName: 'Status',
required: false,
options: {
options: Object.entries(leadStatusOptions).map(([value, label]) => ({
label,
value,
})),
},
defaultValue: 'new',
}),
metadata: Property.Json({
displayName: 'Metadata',
required: false,
description:
'Custom data as JSON object (e.g., {"company": "Tech Corp"})',
}),
},
async run({ auth, propsValue }) {
const { name, email, phone, source, status, metadata } = propsValue;
// Construct request body with strict typing
const requestBody: Record<string, unknown> = { name, email };
if (phone) requestBody['phone'] = phone;
if (source) requestBody['source'] = source;
if (status) requestBody['status'] = status;
if (metadata) requestBody['metadata'] = metadata;
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseUrl}/chatbots/${auth.props.chatbotId}/leads`,
headers: {
Authorization: `Bearer ${auth.props.apiKey}`,
},
body: requestBody,
});
return response.body.data || response.body;
} catch (e) {
throw new Error(formatError(e));
}
},
});

View File

@@ -0,0 +1,33 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { pollybotAuth } from '../auth';
import { baseUrl, formatError } from '../common/common';
export const deleteLead = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: 'delete_lead',
displayName: 'Delete Lead',
description: 'Permanently deletes a lead from your PollyBot chatbot.',
auth: pollybotAuth,
props: {
id: Property.ShortText({
displayName: 'Lead ID',
required: true,
description: 'The unique identifier of the lead to delete.',
}),
},
async run({ auth, propsValue }) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${baseUrl}/chatbots/${auth.props.chatbotId}/leads/${propsValue.id}`,
headers: {
Authorization: `Bearer ${auth.props.apiKey}`,
},
});
return response.body.data || response.body;
} catch (e) {
throw new Error(formatError(e));
}
},
});

View File

@@ -0,0 +1,33 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { pollybotAuth } from '../auth';
import { baseUrl, formatError } from '../common/common';
export const getLead = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: 'get_Lead',
displayName: 'Get Lead',
description: 'Retrieves a specific lead by ID.',
auth: pollybotAuth,
props: {
id: Property.ShortText({
displayName: 'Lead ID',
required: true,
description: 'The unique identifier of the lead to retrieve.',
}),
},
async run({ auth, propsValue }) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${baseUrl}/chatbots/${auth.props.chatbotId}/leads/${propsValue.id}`,
headers: {
Authorization: `Bearer ${auth.props.apiKey}`,
},
});
return response.body.data || response.body;
} catch (e) {
throw new Error(formatError(e));
}
},
});

View File

@@ -0,0 +1,70 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { pollybotAuth } from '../auth';
import { baseUrl, leadStatusOptions, formatError } from '../common/common';
export const listLeads = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: 'list_leads',
displayName: 'List Leads',
description: 'Retrieves a list of leads with filtering.',
auth: pollybotAuth,
props: {
page: Property.Number({
displayName: 'Page',
required: false,
defaultValue: 1
}),
limit: Property.Number({
displayName: 'Limit',
required: false,
defaultValue: 10,
description: 'Max 100'
}),
status: Property.StaticDropdown({
displayName: 'Status',
required: false,
options: {
options: Object.entries(leadStatusOptions).map(([value, label]) => ({ label, value })),
},
}),
source: Property.ShortText({
displayName: 'Source',
required: false,
}),
search: Property.ShortText({
displayName: 'Search',
required: false,
description: 'Search in name and email fields'
})
},
async run({ auth, propsValue }) {
const { page, limit, status, source, search } = propsValue;
const queryParams: Record<string, string> = {
page: (page ?? 1).toString(),
limit: Math.min((limit ?? 10), 100).toString(),
};
if (status) queryParams['status'] = status;
if (source) queryParams['source'] = source;
if (search) queryParams['search'] = search;
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${baseUrl}/chatbots/${auth.props.chatbotId}/leads`,
headers: {
Authorization: `Bearer ${auth.props.apiKey}`,
},
queryParams: queryParams,
});
// Zapier logic returns just the leads array
const data = response.body.data || response.body;
return Array.isArray(data.leads) ? data.leads : [];
} catch (e) {
throw new Error(formatError(e));
}
},
});

View File

@@ -0,0 +1,66 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { pollybotAuth } from '../auth';
import { baseUrl, leadStatusOptions, formatError } from '../common/common';
export const updateLead = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: 'update_lead',
displayName: 'Update Lead',
description: 'Updates an existing lead. Supports partial updates.',
auth: pollybotAuth,
props: {
id: Property.ShortText({
displayName: 'Lead ID',
required: true,
description: 'The unique identifier of the lead to update.',
}),
name: Property.ShortText({ displayName: 'Name', required: false }),
email: Property.ShortText({ displayName: 'Email', required: false }),
phone: Property.ShortText({ displayName: 'Phone', required: false }),
source: Property.ShortText({ displayName: 'Source', required: false }),
status: Property.StaticDropdown({
displayName: 'Status',
required: false,
options: {
options: Object.entries(leadStatusOptions).map(([value, label]) => ({ label, value })),
},
}),
metadata: Property.Json({
displayName: 'Metadata',
required: false,
description: 'Update or add custom data. Metadata is merged with existing data.',
}),
},
async run({ auth, propsValue }) {
const { id, name, email, phone, source, status, metadata } = propsValue;
// Construct request body - only include provided fields
const requestBody: Record<string, unknown> = {};
if (name) requestBody['name'] = name;
if (email) requestBody['email'] = email;
if (phone) requestBody['phone'] = phone;
if (source) requestBody['source'] = source;
if (status) requestBody['status'] = status;
if (metadata) requestBody['metadata'] = metadata;
if (Object.keys(requestBody).length === 0) {
throw new Error('At least one field must be provided to update.');
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.PUT,
url: `${baseUrl}/chatbots/${auth.props.chatbotId}/leads/${id}`,
headers: {
Authorization: `Bearer ${auth.props.apiKey}`,
},
body: requestBody,
});
return response.body.data || response.body;
} catch (e) {
throw new Error(formatError(e));
}
},
});

View File

@@ -0,0 +1,52 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
export const pollybotAuth = PieceAuth.CustomAuth({
description: 'Connect to your PollyBot instance',
required: true,
props: {
apiKey: Property.LongText({
displayName: 'PollyBot API Key',
description:
'Your API Key starting with `sk-workspace_...`. Find your API Key in your [PollyBot Dashboard Settings](https://pollybot.app/docs/authentication).',
required: true,
}),
chatbotId: Property.ShortText({
displayName: 'Chatbot ID',
description:
'The unique ID for the specific chatbot (e.g., `cmhm6o40a05h8mn12dv3tc456`). Learn more in the [PollyBot Authentication docs](https://pollybot.app/docs/authentication).',
required: true,
}),
},
validate: async ({ auth }) => {
const baseUrl = 'https://pollybot.app/api/v1';
const url = `${baseUrl}/chatbots/${auth.chatbotId}/leads`;
try {
await httpClient.sendRequest({
method: HttpMethod.GET,
url: url,
headers: {
Authorization: `Bearer ${auth.apiKey}`,
'Content-Type': 'application/json',
},
queryParams: {
limit: '1', // Minimal fetch for validation
},
});
return { valid: true };
} catch (e: unknown) {
// Type assertion for the error object
const error = e as {
response?: { body?: { error?: string } };
message?: string;
};
return {
valid: false,
error: `Authentication failed: ${
error?.response?.body?.error || error.message || 'Unknown error'
}`,
};
}
},
});

View File

@@ -0,0 +1,79 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
export const baseUrl = 'https://pollybot.app/api/v1';
export const leadStatusOptions = {
new: 'New',
contacted: 'Contacted',
qualified: 'Qualified',
converted: 'Converted',
lost: 'Lost',
follow_up: 'Follow Up',
};
// Helper to format error messages exactly like your Zapier handleApiError
export function formatError(e: unknown): string {
const error = e as {
response?: {
status?: number;
body?: {
error?: string;
details?: unknown;
};
};
message?: string;
};
const status = error.response?.status;
const errorData = error.response?.body || {};
const message = errorData.error || error.message || 'Unknown Error';
const details = errorData.details ? JSON.stringify(errorData.details) : '';
return `Error (${status}): ${message}. ${details}`;
}
// // Webhook Subscription Helpers
// export const pollybotCommon = {
// // Updated to return both webhookId and secret
// subscribeWebhook: async (
// chatbotId: string,
// apiKey: string,
// webhookUrl: string
// ): Promise<{ webhookId: string; secret: string }> => {
// const response = await httpClient.sendRequest({
// method: HttpMethod.POST,
// url: `${baseUrl}/chatbots/${chatbotId}/webhooks`,
// headers: {
// Authorization: `Bearer ${apiKey}`,
// },
// body: {
// name: `Activepieces - New Lead (${new Date().toISOString()})`,
// url: webhookUrl,
// eventTypes: ['LEAD_CREATED'],
// maxRetries: 3,
// retryDelay: 1000,
// },
// }); // PollyBot returns { webhook: { id: "...", secret: "..." }, ... } or just { id: "...", secret: "..." }
// const body = response.body;
// const webhook = body.webhook || body;
// return {
// webhookId: webhook.id,
// secret: webhook.secret, // Extract and return the secret
// };
// },
// unsubscribeWebhook: async (
// chatbotId: string,
// apiKey: string,
// webhookId: string
// ): Promise<void> => {
// await httpClient.sendRequest({
// method: HttpMethod.DELETE,
// url: `${baseUrl}/chatbots/${chatbotId}/webhooks/${webhookId}`,
// headers: {
// Authorization: `Bearer ${apiKey}`,
// },
// });
// },
// };

View File

@@ -0,0 +1,73 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { pollybotAuth } from '../auth';
export const newLead = createTrigger({
auth: pollybotAuth,
name: 'newLead',
displayName: 'New Lead',
description: 'Triggers when a new lead is created in PollyBot AI chatbot.',
props: {
chatbotid: Property.ShortText({
displayName: 'Chatbot ID',
description: 'The Id of the chatbot to monitor for new leads.',
required: true,
}),
instruction: Property.MarkDown({
value: `## PollyBot AI Webhook Setup
To use this trigger, you need to manually set up a webhook in your PollyBot AI account:
1. Login to your PollyBot AI account.
2. Navigate to the **Chatbots** section from the left navigation menu.
3. Select the desired chatbot for which you want to set up the webhook.
4. Go to the **Settings** tab.
5. Find the **Webhooks** section and **Add Webhook**.
6. Choose the **Lead Created** event and specify the following URL:
\`\`\`text
{{webhookUrl}}
\`\`\`
7. Click Save to register the webhook.
`,
}),
},
sampleData: {
data: {
id: 'cmipr3rf400t3n42y5plvmhd5',
name: 'teswwt',
tags: [],
email: 'teswwwt@gmail.com',
phone: null,
source: 'api',
status: 'NEW',
company: null,
discord: null,
message: null,
urgency: 'low',
priority: 'MEDIUM',
chatbotId: 'cmipnh1je00sxn42y1j34wqnd',
createdAt: '2025-12-03T08:34:31.696Z',
updatedAt: '2025-12-03T08:34:31.696Z',
customFields: null,
preferredMethod: 'email',
},
event: 'LEAD_CREATED',
chatbotId: 'cmipnh1je00sxn42y1j34wqnd',
timestamp: 1764750871,
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
// implement webhook creation logic
},
async onDisable(context) {
// implement webhook deletion logic
},
async run(context) {
const payload = context.payload.body as any;
if (payload.data.chatbotId !== context.propsValue.chatbotid) {
return [];
}
return [context.payload.body];
},
});

View File

@@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}