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,49 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
export const addLead = createAction({
auth: famulorAuth,
name: 'addLead',
displayName: 'Add Lead to Campaign',
description: 'Add a lead to an outbound campaign to be called by an AI assistant.',
props: famulorCommon.addLeadProperties(),
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, famulorCommon.addLeadSchema);
// Process secondary contacts if provided
let secondaryContacts: Array<{ phone_number: string; variables?: Record<string, any> }> | undefined;
if (propsValue.secondary_contacts && propsValue.num_secondary_contacts) {
const secondaryContactsData = propsValue.secondary_contacts as Record<string, any>;
const numContacts = Math.min(Number(propsValue.num_secondary_contacts) || 0, 10);
const contacts: Array<{ phone_number: string; variables?: Record<string, any> }> = [];
for (let i = 1; i <= numContacts; i++) {
const phoneNumber = secondaryContactsData[`contact_${i}_phone`];
const variables = secondaryContactsData[`contact_${i}_variables`];
if (phoneNumber && phoneNumber.trim() !== '') {
contacts.push({
phone_number: phoneNumber,
variables: variables || { customer_name: '' }
});
}
}
if (contacts.length > 0) {
secondaryContacts = contacts;
}
}
return await famulorCommon.addLead({
auth: auth.secret_text,
campaign_id: propsValue.campaign,
phone_number: propsValue.phone_number!,
variable: propsValue.variables,
allow_dupplicate: propsValue.allow_dupplicate,
secondary_contacts: secondaryContacts,
});
},
});

View File

@@ -0,0 +1,21 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
export const campaignControl = createAction({
auth: famulorAuth,
name: 'campaignControl',
displayName: 'Start/Stop Campaign',
description: 'Start or stop an outbound calling campaign. Starting requires sufficient leads; stopping cancels ongoing calls.',
props: famulorCommon.campaignControlProperties(),
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, famulorCommon.campaignControlSchema);
return await famulorCommon.campaignControl({
auth: auth.secret_text,
campaign_id: propsValue.campaign,
action: propsValue.action as 'start' | 'stop',
});
},
});

View File

@@ -0,0 +1,20 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
export const deleteLead = createAction({
auth: famulorAuth,
name: 'deleteLead',
displayName: 'Delete Lead',
description: '⚠️ Permanently delete a lead from the system. This action cannot be undone and will abort any ongoing calls.',
props: famulorCommon.deleteLeadProperties(),
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, famulorCommon.deleteLeadSchema);
return await famulorCommon.deleteLead({
auth: auth.secret_text,
lead_id: propsValue.lead_id,
});
},
});

View File

@@ -0,0 +1,22 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
export const makePhoneCall = createAction({
auth: famulorAuth,
name: 'makePhoneCall',
displayName: 'Make Phone Call',
description: 'Initiate an AI-powered phone call to a customer using a selected assistant.',
props: famulorCommon.makePhoneCallProperties(),
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, famulorCommon.makePhoneCallSchema);
return await famulorCommon.makePhoneCall({
auth: auth.secret_text,
assistant_id: propsValue.assistant_id as number,
phone_number: propsValue.phone_number!,
variable: propsValue.variable,
});
},
});

View File

@@ -0,0 +1,22 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
export const sendSms = createAction({
auth: famulorAuth,
name: 'sendSms',
displayName: 'Send SMS',
description: 'Send an SMS message using your purchased phone numbers. Costs are automatically deducted from your account.',
props: famulorCommon.sendSmsProperties(),
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, famulorCommon.sendSmsSchema);
return await famulorCommon.sendSms({
auth: auth.secret_text,
from: propsValue.from as number,
to: propsValue.to!,
bodysuit: propsValue.bodysuit!,
});
},
});

View File

@@ -0,0 +1,271 @@
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import * as properties from './properties';
import * as schemas from './schemas';
import {
ListCampaignsResponse,
AddLeadParams,
SendSmsParams,
MakePhoneCallParams,
CampaignControlParams,
DeleteLeadParams,
LeadResponse
} from './types';
export const baseApiUrl = 'https://app.famulor.de/';
export const famulorCommon = {
baseHeaders: (auth: string) => ({
'Authorization': `Bearer ${auth}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
}),
// Properties
addLeadProperties: properties.addLead,
sendSmsProperties: properties.sendSms,
makePhoneCallProperties: properties.makePhoneCall,
campaignControlProperties: properties.campaignControl,
deleteLeadProperties: properties.deleteLead,
// Schemas
addLeadSchema: schemas.addLead,
sendSmsSchema: schemas.sendSms,
makePhoneCallSchema: schemas.makePhoneCall,
campaignControlSchema: schemas.campaignControl,
deleteLeadSchema: schemas.deleteLead,
// Methods
listAllAssistants: async ({ auth, per_page = 10, page = 1, type }: { auth: string; per_page?: number; page?: number; type?: string }) => {
const queryParams: Record<string, string> = {
per_page: per_page.toString(),
page: page.toString(),
};
if (type && type !== '') {
queryParams['type'] = type;
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${baseApiUrl}api/user/assistants/get`,
headers: famulorCommon.baseHeaders(auth),
queryParams,
});
if (response.status !== 200) {
throw new Error(`Failed to fetch assistants: ${response.status}`);
}
return response.body;
},
listPhoneNumbers: async ({ auth }: { auth: string }) => {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${baseApiUrl}api/user/assistants/phone-numbers`,
headers: famulorCommon.baseHeaders(auth),
});
if (response.status !== 200) {
throw new Error(`Failed to fetch phone numbers: ${response.status}`);
}
return response.body || [];
},
listAssistants: async ({ auth }: { auth: string }) => {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${baseApiUrl}api/user/assistants/outbound`,
headers: famulorCommon.baseHeaders(auth),
});
if (response.status !== 200) {
throw new Error(`Failed to fetch assistants: ${response.status}`);
}
return response.body || [];
},
listLeads: async ({ auth }: { auth: string }) => {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${baseApiUrl}api/user/leads`,
headers: famulorCommon.baseHeaders(auth),
});
if (response.status !== 200) {
throw new Error(`Failed to fetch leads: ${response.status}`);
}
return response.body.leads || response.body;
},
listCampaigns: async ({ auth }: { auth: string }) => {
const response = await httpClient.sendRequest<ListCampaignsResponse>({
method: HttpMethod.GET,
url: `${baseApiUrl}api/user/campaigns`,
headers: famulorCommon.baseHeaders(auth),
});
if (response.status !== 200) {
throw new Error(`Failed to fetch campaigns: ${response.status}`);
}
return response.body.campaigns || response.body;
},
addLead: async (params: AddLeadParams): Promise<LeadResponse> => {
const { auth, ...body } = params;
const response = await httpClient.sendRequest<LeadResponse>({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/lead`,
headers: famulorCommon.baseHeaders(auth),
body,
});
if (response.status !== 200) {
throw new Error(`Failed to add lead: ${response.status}`);
}
return response.body;
},
sendSms: async (params: SendSmsParams) => {
const { auth, ...body } = params;
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/sms`,
headers: famulorCommon.baseHeaders(auth),
body,
});
if (response.status !== 200) {
throw new Error(`Failed to send SMS: ${response.status}`);
}
return response.body;
},
makePhoneCall: async (params: MakePhoneCallParams) => {
const { auth, ...body } = params;
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/make_call`,
headers: famulorCommon.baseHeaders(auth),
body,
});
if (response.status !== 200) {
throw new Error(`Failed to make phone call: ${response.status}`);
}
return response.body;
},
campaignControl: async (params: CampaignControlParams) => {
const { auth, ...body } = params;
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/campaigns/update-status`,
headers: famulorCommon.baseHeaders(auth),
body,
});
if (response.status !== 200) {
throw new Error(`Failed to control campaign: ${response.status}`);
}
return response.body;
},
deleteLead: async (params: DeleteLeadParams) => {
const { auth, lead_id } = params;
const response = await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${baseApiUrl}api/user/leads/${lead_id}`,
headers: famulorCommon.baseHeaders(auth),
});
if (response.status !== 200) {
throw new Error(`Failed to delete lead: ${response.status}`);
}
return response.body;
},
enableInboundWebhook: async ({ auth, assistant_id, webhook_url }: { auth: string; assistant_id: number; webhook_url: string }) => {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/assistants/enable-inbound-webhook`,
headers: famulorCommon.baseHeaders(auth),
body: {
assistant_id,
webhook_url,
},
});
if (response.status !== 200) {
throw new Error(`Failed to enable inbound webhook: ${response.status}`);
}
return response.body;
},
disableInboundWebhook: async ({ auth, assistant_id }: { auth: string; assistant_id: number }) => {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/assistants/disable-inbound-webhook`,
headers: famulorCommon.baseHeaders(auth),
body: {
assistant_id,
},
});
if (response.status !== 200) {
throw new Error(`Failed to disable inbound webhook: ${response.status}`);
}
return response.body;
},
enablePostCallWebhook: async ({ auth, assistant_id, webhook_url }: { auth: string; assistant_id: number; webhook_url: string }) => {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/assistants/enable-webhook`,
headers: famulorCommon.baseHeaders(auth),
body: {
assistant_id,
webhook_url,
},
});
if (response.status !== 200) {
throw new Error(`Failed to enable post-call webhook: ${response.status}`);
}
return response.body;
},
disablePostCallWebhook: async ({ auth, assistant_id }: { auth: string; assistant_id: number }) => {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseApiUrl}api/user/assistants/disable-webhook`,
headers: famulorCommon.baseHeaders(auth),
body: {
assistant_id,
},
});
if (response.status !== 200) {
throw new Error(`Failed to disable post-call webhook: ${response.status}`);
}
return response.body;
},
};

View File

@@ -0,0 +1,281 @@
import { Property } from '@activepieces/pieces-framework';
import { famulorCommon } from '.';
import { famulorAuth } from '../..';
// Dynamic Properties
const campaignDropdown = () =>
Property.Dropdown({
auth: famulorAuth,
displayName: 'Campaign',
description: 'Select the campaign',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
try {
const campaigns = await famulorCommon.listCampaigns({ auth: auth.secret_text });
if (!campaigns || campaigns.length === 0) {
return {
disabled: true,
placeholder: 'No campaigns found. Create one first.',
options: [],
};
}
return {
options: campaigns.map((campaign: any) => ({
label: campaign.name,
value: campaign.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to fetch campaigns',
options: [],
};
}
},
});
const phoneNumberProperty = (displayName: string, description: string, required = true) =>
Property.ShortText({
displayName,
description: `${description} (E.164 format: +1234567890)`,
required,
});
const variablesProperty = (displayName: string, description: string, required = false) =>
Property.Object({
displayName,
description,
required,
defaultValue: {
customer_name: 'John Doe',
},
});
// Action Properties
export const addLead = () => ({
campaign: campaignDropdown(),
phone_number: phoneNumberProperty('Customer Phone Number', 'Enter the phone number of the customer'),
variables: variablesProperty('Variables', 'Variables to pass to the assistant'),
allow_dupplicate: Property.Checkbox({
displayName: 'Allow Duplicates',
description: 'Allow the same phone number to be added to the campaign more than once',
required: false,
defaultValue: false,
}),
num_secondary_contacts: Property.Number({
displayName: 'Number of Secondary Contacts',
description: 'How many secondary contacts do you want to add? (Max: 10)',
required: false,
defaultValue: 0,
}),
secondary_contacts: Property.DynamicProperties({
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'],
auth: famulorAuth,
props: async ({ num_secondary_contacts }) => {
const contacts: any = {};
const numContacts = Math.min(Number(num_secondary_contacts) || 0, 10);
for (let i = 1; i <= numContacts; i++) {
contacts[`contact_${i}_phone`] = phoneNumberProperty(
`Contact ${i} - Phone Number`,
`Phone number for secondary contact ${i}`
);
contacts[`contact_${i}_variables`] = variablesProperty(
`Contact ${i} - Variables`,
`Variables for secondary contact ${i} as key-value pairs`,
false
);
}
return contacts;
},
}),
});
const phoneNumberDropdown = () =>
Property.Dropdown({
auth: famulorAuth,
displayName: 'From Phone Number',
description: 'Select an SMS-capable phone number to send from',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
try {
const phoneNumbers = await famulorCommon.listPhoneNumbers({ auth: auth.secret_text });
if (!phoneNumbers || phoneNumbers.length === 0) {
return {
disabled: true,
placeholder: 'No phone numbers found. Purchase an SMS-capable phone number first.',
options: [],
};
}
return {
options: phoneNumbers.map((phoneNumber: any) => ({
label: `${phoneNumber.phone_number} (${phoneNumber.country_code})`,
value: phoneNumber.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to fetch phone numbers',
options: [],
};
}
},
});
export const sendSms = () => ({
from: phoneNumberDropdown(),
to: phoneNumberProperty('Recipient Phone Number', 'Enter the recipient\'s phone number'),
bodysuit: Property.LongText({
displayName: 'Message',
description: 'SMS message content (max 300 characters). Long messages may be split into multiple segments.',
required: true,
}),
});
const assistantDropdown = () =>
Property.Dropdown({
auth: famulorAuth,
displayName: 'Assistant',
description: 'Select the AI assistant to use for the call',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
try {
const assistants = await famulorCommon.listAssistants({ auth: auth.secret_text });
if (!assistants || assistants.length === 0) {
return {
disabled: true,
placeholder: 'No outbound assistants found. Create one first.',
options: [],
};
}
return {
options: assistants.map((assistant: any) => ({
label: assistant.name,
value: assistant.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to fetch assistants',
options: [],
};
}
},
});
export const makePhoneCall = () => ({
assistant_id: assistantDropdown(),
phone_number: phoneNumberProperty('Customer Phone Number', 'Enter the phone number to call'),
variable: Property.Object({
displayName: 'Variables',
description: 'Variables to pass to the assistant during the call',
required: false,
defaultValue: {
customer_name: 'John Doe',
email: 'john@example.com',
},
}),
});
export const campaignControl = () => ({
campaign: campaignDropdown(),
action: Property.StaticDropdown({
displayName: 'Action',
description: 'Select the action to perform on the campaign',
required: true,
options: {
options: [
{ label: 'Start Campaign', value: 'start' },
{ label: 'Stop Campaign', value: 'stop' },
],
},
}),
});
const leadDropdown = () =>
Property.Dropdown<number,true,typeof famulorAuth>({
auth: famulorAuth,
displayName: 'Lead',
description: 'Select the lead to delete',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
try {
const leads = await famulorCommon.listLeads({ auth: auth.secret_text });
if (!leads || leads.length === 0) {
return {
disabled: true,
placeholder: 'No leads found.',
options: [],
};
}
return {
options: leads.map((lead: any) => ({
label: `${lead.phone_number} - ${lead.campaign?.name || 'Unknown Campaign'}`,
value: lead.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to fetch leads',
options: [],
};
}
},
});
export const deleteLead = () => ({
lead_id: leadDropdown(),
});

View File

@@ -0,0 +1,36 @@
import z from 'zod';
export const addLead = {
campaign_id: z.number().int().positive(),
phone_number: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format (e.g. +1234567890)'),
variable: z.record(z.string(), z.any()).optional(),
allow_dupplicate: z.boolean().optional(),
secondary_contacts: z.array(z.object({
phone_number: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format'),
variables: z.record(z.string(), z.any()).optional(),
})).optional(),
};
export const sendSms = {
from: z.number().int().positive('Phone number ID is required'),
to: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format (e.g. +1234567890)'),
bodysuit: z.string().max(300, 'Message must be 300 characters or less'),
};
export const makePhoneCall = {
assistant_id: z.number().int().positive(),
phone_number: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format (e.g. +1234567890)'),
variable: z.object({
customer_name: z.string().optional(),
email: z.string().email().optional(),
}).catchall(z.any()).optional(),
};
export const campaignControl = {
campaign_id: z.number().int().positive(),
action: z.enum(['start', 'stop']),
};
export const deleteLead = {
lead_id: z.number().int().positive('Lead ID must be a positive integer'),
};

View File

@@ -0,0 +1,66 @@
export interface Campaign {
id: number;
name: string;
status: string;
max_calls_in_parallel: number;
mark_complete_when_no_leads: boolean;
allowed_hours_start_time: string;
allowed_hours_end_time: string;
allowed_days: string[];
max_retries: number;
retry_interval: number;
created_at: string;
updated_at: string;
}
export interface ListCampaignsResponse {
campaigns: Campaign[];
}
export interface AddLeadParams {
auth: string;
campaign_id: number;
phone_number: string;
variable?: Record<string, any>;
allow_dupplicate?: boolean;
secondary_contacts?: Array<{
phone_number: string;
variables?: Record<string, any>;
}>;
}
export interface SendSmsParams {
auth: string;
from: number;
to: string;
bodysuit: string;
}
export interface MakePhoneCallParams {
auth: string;
assistant_id: number;
phone_number: string;
variable?: {
customer_name?: string;
email?: string;
[key: string]: any;
};
}
export interface CampaignControlParams {
auth: string;
campaign_id: number;
action: 'start' | 'stop';
}
export interface DeleteLeadParams {
auth: string;
lead_id: number;
}
export interface LeadResponse {
message: string;
data: {
id: string;
};
}

View File

@@ -0,0 +1,111 @@
import { createTrigger, TriggerStrategy, PiecePropValueSchema, Property } from '@activepieces/pieces-framework';
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
import dayjs from 'dayjs';
const polling: Polling<PiecePropValueSchema<any>, { type?: string; per_page?: number }> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue }) => {
const perPage = propsValue['per_page'] || 10;
const type = propsValue['type'];
// Get all assistants with pagination
let allAssistants: any[] = [];
let currentPage = 1;
let hasMorePages = true;
while (hasMorePages) {
const assistants = await famulorCommon.listAllAssistants({
auth: auth as string,
per_page: perPage,
page: currentPage,
type
});
if (assistants.data && assistants.data.length > 0) {
allAssistants = allAssistants.concat(assistants.data);
hasMorePages = currentPage < assistants.last_page;
currentPage++;
} else {
hasMorePages = false;
}
}
return allAssistants.map((assistant) => {
const assistantDate = assistant.updated_at
? dayjs(assistant.updated_at)
: dayjs(assistant.created_at);
return {
epochMilliSeconds: assistantDate.valueOf(),
data: assistant,
};
});
},
};
export const getAssistants = createTrigger({
auth: famulorAuth,
name: 'getAssistants',
displayName: 'New or Updated Assistant',
description: 'Triggers when AI assistants are created or updated in your Famulor account.',
props: {
type: Property.StaticDropdown({
displayName: 'Assistant Type',
description: 'Filter assistants by type',
required: false,
options: {
options: [
{ label: 'All Types', value: '' },
{ label: 'Inbound', value: 'inbound' },
{ label: 'Outbound', value: 'outbound' },
],
},
}),
per_page: Property.Number({
displayName: 'Items Per Page',
description: 'Number of assistants to fetch per page (1-100, default: 10)',
required: false,
defaultValue: 10,
}),
},
sampleData: {
id: 123,
user_id: 456,
name: "Customer Support Assistant",
type: "inbound",
status: "active",
phone_number_id: 789,
voice_id: 101,
language_id: 1,
language: "en-US",
timezone: "UTC",
initial_message: "Hello! How can I help you today?",
system_prompt: "You are a helpful customer support assistant.",
max_duration: 1800,
record: true,
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-01-15T14:20:00Z",
variable: {
company_name: "ACME Corp",
support_email: "support@acme.com"
},
is_webhook_active: true,
webhook_url: "https://api.example.com/webhook"
},
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,90 @@
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
const inboundAssistantDropdown = () =>
Property.Dropdown({
auth: famulorAuth,
displayName: 'Inbound Assistant',
description: 'Select an inbound assistant to receive webhook notifications for',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
try {
// Filter for inbound assistants only
const assistants = await famulorCommon.listAllAssistants({
auth: auth.secret_text,
type: 'inbound',
per_page: 100
});
if (!assistants.data || assistants.data.length === 0) {
return {
disabled: true,
placeholder: 'No inbound assistants found. Create one first.',
options: [],
};
}
return {
options: assistants.data.map((assistant: any) => ({
label: `${assistant.name} (${assistant.status})`,
value: assistant.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to fetch assistants',
options: [],
};
}
},
});
export const inboundCall = createTrigger({
auth: famulorAuth,
name: 'inboundCall',
displayName: 'Inbound Call Received',
description: 'Triggers when an inbound call is received by your AI assistant. Webhook must be enabled for the selected assistant.',
props: {
assistant_id: inboundAssistantDropdown(),
},
sampleData: {
assistant_id: 123,
customer_phone: '+16380991171',
assistant_phone: '+16380991171',
call_id: "call_abc123",
timestamp: "2024-01-15T10:30:00Z",
status: "incoming",
variables: {
customer_name: "John Doe",
caller_id: "+16380991171"
}
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
await famulorCommon.enableInboundWebhook({
auth: context.auth.secret_text,
assistant_id: context.propsValue.assistant_id as number,
webhook_url: context.webhookUrl,
});
},
async onDisable(context) {
await famulorCommon.disableInboundWebhook({
auth: context.auth.secret_text,
assistant_id: context.propsValue.assistant_id as number,
});
},
async run(context) {
return [context.payload.body];
}
})

View File

@@ -0,0 +1,117 @@
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { famulorAuth } from '../..';
import { famulorCommon } from '../common';
const assistantDropdownForWebhook = () =>
Property.Dropdown({
auth: famulorAuth,
displayName: 'Assistant',
description: 'Select an assistant to receive post-call webhook notifications for',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
try {
// Get all assistants (both inbound and outbound can make calls)
const assistants = await famulorCommon.listAllAssistants({
auth: auth.secret_text,
per_page: 100
});
if (!assistants.data || assistants.data.length === 0) {
return {
disabled: true,
placeholder: 'No assistants found. Create one first.',
options: [],
};
}
return {
options: assistants.data.map((assistant: any) => ({
label: `${assistant.name} (${assistant.type} - ${assistant.status})`,
value: assistant.id,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to fetch assistants',
options: [],
};
}
},
});
export const phoneCallEnded = createTrigger({
auth: famulorAuth,
name: 'phoneCallEnded',
displayName: 'Phone Call Completed',
description: 'Triggers when a phone call is completed, providing full call transcript, extracted variables, and call metadata.',
props: {
assistant_id: assistantDropdownForWebhook(),
},
sampleData: {
id: 480336,
customer_phone: "+4915123456789",
assistant_phone: "+4912345678",
duration: 180,
status: "completed",
extracted_variables: {
customer_interested: true,
appointment_scheduled: false,
contact_reason: "product_inquiry",
follow_up_needed: true,
customer_budget: "10000-50000",
decision_maker: true,
next_contact_date: "2024-02-15"
},
input_variables: {
customer_name: "Max Mustermann",
company: "Beispiel GmbH"
},
transcript: "Assistent: Guten Tag, Herr Mustermann! Ich bin...",
recording_url: "https://recordings.famulor.de/call-480336.mp3",
created_at: "2024-01-15T10:30:00Z",
finished_at: "2024-01-15T10:33:00Z",
lead: {
id: 12345,
phone_number: "+4915123456789",
variables: {
customer_name: "Max Mustermann",
company: "Beispiel GmbH",
source: "website"
},
status: "contacted",
created_at: "2024-01-14T09:00:00Z",
updated_at: "2024-01-15T10:33:00Z"
},
campaign: {
id: 123,
name: "Q1 Sales Campaign"
}
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
await famulorCommon.enablePostCallWebhook({
auth: context.auth.secret_text,
assistant_id: context.propsValue.assistant_id as number,
webhook_url: context.webhookUrl,
});
},
async onDisable(context) {
await famulorCommon.disablePostCallWebhook({
auth: context.auth.secret_text,
assistant_id: context.propsValue.assistant_id as number,
});
},
async run(context) {
return [context.payload.body];
}
})