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-bookedin
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build pieces-bookedin` to build the library.

View File

@@ -0,0 +1,10 @@
{
"name": "@activepieces/piece-bookedin",
"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-bookedin",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/bookedin/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/bookedin",
"tsConfig": "packages/pieces/community/bookedin/tsconfig.lib.json",
"packageJson": "packages/pieces/community/bookedin/package.json",
"main": "packages/pieces/community/bookedin/src/index.ts",
"assets": [
"packages/pieces/community/bookedin/*.md",
{
"input": "packages/pieces/community/bookedin/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/bookedin",
"command": "bun install --no-save --silent"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
}
}
}

View File

@@ -0,0 +1,88 @@
import { createPiece, PieceAuth } from "@activepieces/pieces-framework";
import { httpClient, HttpMethod, createCustomApiCallAction } from '@activepieces/pieces-common';
import { BASE_URL, extractApiKey } from './lib/common/props';
import { getLeads } from "./lib/actions/get-leads";
import { createLead } from "./lib/actions/create-lead";
import { getLead } from "./lib/actions/get-lead";
import { deleteLead } from "./lib/actions/delete-lead";
import { getLeadStats } from "./lib/actions/get-lead-stats";
import { updateLead } from "./lib/actions/update-lead";
import { PieceCategory } from "@activepieces/shared";
// --- Authentication ---
export const bookedinAuth = PieceAuth.SecretText({
displayName: 'API Key',
required: true,
description: `To connect your Bookedin account, please follow these steps to retrieve your API Key:
1. Log in to your Bookedin Dashboard: dashboard.bookedin.ai
2. From the left menu, go to **Business**
3. Open **Settings**
4. Click on **API Key**
5. Copy your API Key (starts with **sk_…**)
6. Paste the key below to authorize the integration
Your API Key allows Activepieces to securely access your Bookedin leads, agents, and booking data.
`,
validate: async ({ auth }) => {
try {
const apiKey = extractApiKey(auth);
if (!apiKey) {
return { valid: false, error: 'API Key is empty' };
}
await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/agents/`,
headers: {
'X-API-Key': apiKey as string,
'accept': 'application/json'
},
queryParams: {
skip: '0',
limit: '1'
}
});
return { valid: true };
} catch (e: any) {
const errorMessage = e?.response?.body?.detail || e?.message || 'Connection failed';
return {
valid: false,
error: errorMessage
};
}
},
});
// --- Piece Definition ---
export const bookedin = createPiece({
displayName: 'Bookedin',
description: 'AI agents for lead conversion and appointment booking.',
logoUrl: 'https://cdn.activepieces.com/pieces/bookedin.png',
categories: [PieceCategory.SALES_AND_CRM],
auth: bookedinAuth,
minimumSupportedRelease: '0.36.1',
authors: ["drona2938", "onyedikachi-david"],
actions: [
getLeads,
createLead,
getLead,
deleteLead,
getLeadStats,
updateLead,
createCustomApiCallAction({
baseUrl: () => BASE_URL,
auth: bookedinAuth,
authMapping: async (auth) => {
const apiKey = extractApiKey(auth);
return {
'X-API-Key': apiKey,
};
},
}),
],
triggers: [],
});

View File

@@ -0,0 +1,40 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdsMultiSelectDropdown, extractApiKey } from '../common/props';
export const bulkDeleteLeads = createAction({
name: 'bulkDeleteLeads',
displayName: 'Bulk Delete Leads',
description: 'Delete multiple leads (max 500 per request)',
auth: bookedinAuth,
props: {
lead_ids: leadIdsMultiSelectDropdown,
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const leadIds = Array.isArray(propsValue.lead_ids) ? propsValue.lead_ids : [propsValue.lead_ids];
if (leadIds.length === 0) {
throw new Error('At least one lead must be selected');
}
if (leadIds.length > 500) {
throw new Error('Maximum 500 leads can be deleted per request');
}
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${BASE_URL}/leads/bulk-delete`,
headers: {
...getBookedinHeaders(apiKey),
'Content-Type': 'application/json',
},
body: {
lead_ids: leadIds,
},
});
return response.body;
},
});

View File

@@ -0,0 +1,55 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, extractApiKey } from '../common/props';
export const createLead = createAction({
name: 'createLead',
displayName: 'Create Lead',
description: 'Creates a new lead in Bookedin AI',
auth: bookedinAuth,
props: {
firstName: Property.ShortText({
displayName: 'First Name',
required: true,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
required: true,
}),
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
phone: Property.ShortText({
displayName: 'Phone Number',
required: true,
}),
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const payload = {
contact: {
name: {
last: propsValue.lastName,
first: propsValue.firstName,
},
email: propsValue.email,
number: propsValue.phone,
},
};
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${BASE_URL}/leads/`,
headers: {
...getBookedinHeaders(apiKey),
'Content-Type': 'application/json',
},
body: payload,
});
return response.body;
},
});

View File

@@ -0,0 +1,25 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdDropdown, extractApiKey } from '../common/props';
export const deleteLead = createAction({
name: 'deleteLead',
displayName: 'Delete Lead',
description: 'Delete a lead.',
auth: bookedinAuth,
props: {
lead_id: leadIdDropdown,
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const response = await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${BASE_URL}/leads/${propsValue.lead_id}`,
headers: getBookedinHeaders(apiKey),
});
return response.body;
},
});

View File

@@ -0,0 +1,23 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, extractApiKey } from '../common/props';
export const getLeadStats = createAction({
name: 'getLeadStats',
displayName: 'Get Lead Stats',
description: 'Get lead statistics (Hot, Warm, Cold, Objectives Met, Total).',
auth: bookedinAuth,
props: {},
async run({ auth }) {
const apiKey = extractApiKey(auth);
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/stats`,
headers: getBookedinHeaders(apiKey),
});
return response.body;
},
});

View File

@@ -0,0 +1,25 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdDropdown, extractApiKey } from '../common/props';
export const getLead = createAction({
name: 'getLead',
displayName: 'Get Lead',
description: 'Get a specific lead by ID.',
auth: bookedinAuth,
props: {
lead_id: leadIdDropdown,
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/${propsValue.lead_id}`,
headers: getBookedinHeaders(apiKey),
});
return response.body;
},
});

View File

@@ -0,0 +1,67 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, extractApiKey } from '../common/props';
export const getLeads = createAction({
name: 'getLeads',
displayName: 'Get Leads',
description: 'Get all leads for the current business with pagination metadata.',
auth: bookedinAuth,
props: {
search: Property.ShortText({
displayName: 'Search',
description: 'Search text in name, email, or phone number',
required: false,
}),
source: Property.ShortText({
displayName: 'Source',
description: 'Filter by lead source (e.g., "API", "Import")',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Filter by exact email address',
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone',
description: 'Filter by phone number',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Number of leads to return',
required: false,
defaultValue: 100,
}),
skip: Property.Number({
displayName: 'Skip',
description: 'Number of leads to skip (pagination)',
required: false,
defaultValue: 0,
}),
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const queryParams: Record<string, string> = {
limit: (propsValue.limit ?? 100).toString(),
skip: (propsValue.skip ?? 0).toString(),
};
if (propsValue.search) queryParams['search'] = propsValue.search;
if (propsValue.source) queryParams['source'] = propsValue.source;
if (propsValue.email) queryParams['email'] = propsValue.email;
if (propsValue.phone) queryParams['phone'] = propsValue.phone;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/`,
headers: getBookedinHeaders(apiKey),
queryParams,
});
return response.body;
},
});

View File

@@ -0,0 +1,90 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
import { BASE_URL, getBookedinHeaders, leadIdDropdown, extractApiKey } from '../common/props';
import { isNil } from '@activepieces/shared';
export const updateLead = createAction({
name: 'updateLead',
displayName: 'Update Lead',
description: 'Update a lead.',
auth: bookedinAuth,
props: {
lead_id: leadIdDropdown,
firstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone Number',
required: false,
}),
handling_status: Property.ShortText({
displayName: 'Handling Status',
required: false,
}),
update_json: Property.Json({
displayName: 'Update Payload (JSON)',
description: 'Optional JSON body for complex updates. Merges with individual fields above.',
required: false,
defaultValue: {},
}),
},
async run({ auth, propsValue }) {
const apiKey = extractApiKey(auth);
const basePayload: Record<string, unknown> = {};
if (!isNil(propsValue.firstName) || !isNil(propsValue.lastName) || !isNil(propsValue.email) || !isNil(propsValue.phone)) {
const contact: Record<string, unknown> = {};
const nameParts: Record<string, string> = {};
if (!isNil(propsValue.firstName) && propsValue.firstName !== '') {
nameParts['first'] = propsValue.firstName;
}
if (!isNil(propsValue.lastName) && propsValue.lastName !== '') {
nameParts['last'] = propsValue.lastName;
}
if (Object.keys(nameParts).length > 0) {
contact['name'] = nameParts;
}
if (!isNil(propsValue.email) && propsValue.email !== '') {
contact['email'] = propsValue.email;
}
if (!isNil(propsValue.phone) && propsValue.phone !== '') {
contact['number'] = propsValue.phone;
}
basePayload['contact'] = contact;
}
if (!isNil(propsValue.handling_status) && propsValue.handling_status !== '') {
basePayload['handling_status'] = propsValue.handling_status;
}
const finalPayload = {
...basePayload,
...(propsValue.update_json || {}),
};
const response = await httpClient.sendRequest({
method: HttpMethod.PUT,
url: `${BASE_URL}/leads/${propsValue.lead_id}`,
headers: {
...getBookedinHeaders(apiKey),
'Content-Type': 'application/json',
},
body: finalPayload,
});
return response.body;
},
});

View File

@@ -0,0 +1,138 @@
import { Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { bookedinAuth } from '../../index';
export const BASE_URL = 'https://api.bookedin.ai/api/v1';
export const getBookedinHeaders = (apiKey: string) => {
return {
'X-API-Key': apiKey,
'accept': 'application/json',
};
};
export const extractApiKey = (auth: unknown): string => {
if (typeof auth === 'string') {
return auth;
}
const authObj = auth as { secret_text?: string; auth?: string };
return authObj?.secret_text || authObj?.auth || '';
};
const fetchLeadOptions = async (apiKey: string) => {
const response = await httpClient.sendRequest<{
items: Array<{
id: string;
contact: {
name: {
first: string;
last: string;
};
email: string;
};
}>;
}>({
method: HttpMethod.GET,
url: `${BASE_URL}/leads/`,
headers: getBookedinHeaders(apiKey),
queryParams: {
limit: '100',
skip: '0',
},
});
return response.body.items.map((lead) => {
const firstName = lead.contact?.name?.first || '';
const lastName = lead.contact?.name?.last || '';
const email = lead.contact?.email || '';
const name = [firstName, lastName].filter(Boolean).join(' ').trim();
const label = name ? `${name} (${email})` : email || lead.id;
return {
label,
value: lead.id,
};
});
};
export const leadIdDropdown = Property.Dropdown({
auth: bookedinAuth,
displayName: 'Lead',
description: 'Select a lead',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first',
options: [],
};
}
try {
const apiKey = extractApiKey(auth);
if (!apiKey) {
return {
disabled: true,
placeholder: 'API key is missing',
options: [],
};
}
const options = await fetchLeadOptions(apiKey);
return {
disabled: false,
options,
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load leads. Please check your connection.',
options: [],
};
}
},
});
export const leadIdsMultiSelectDropdown = Property.MultiSelectDropdown({
auth: bookedinAuth,
displayName: 'Leads',
description: 'Select leads to delete (max 500)',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first',
options: [],
};
}
try {
const apiKey = extractApiKey(auth);
if (!apiKey) {
return {
disabled: true,
placeholder: 'API key is missing',
options: [],
};
}
const options = await fetchLeadOptions(apiKey);
return {
disabled: false,
options,
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load leads. Please check your connection.',
options: [],
};
}
},
});

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"]
}