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,115 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchContacts, fetchHouseholds, WEALTHBOX_API_BASE, handleApiError } from '../common';
import { wealthboxAuth } from '../..';
export const addHouseholdMember = createAction({
name: 'add_household_member',
displayName: 'Add Member to Household',
description: 'Adds a member to an existing household. Link multiple contacts under one family unit.',
auth: wealthboxAuth,
props: {
household_id: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Household',
description: 'Select the household that will receive the new member',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const households = await fetchHouseholds(auth.secret_text);
return {
options: households.map((household: any) => ({
label: household.first_name || `Household ${household.id}`,
value: household.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load households. Please check your authentication.'
};
}
}
}),
contact_id: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Contact',
description: 'Select the contact to add to the household',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
return {
options: contacts.map((contact: any) => ({
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
value: contact.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load contacts. Please check your authentication.'
};
}
}
}),
title: Property.StaticDropdown({
displayName: 'Household Title',
description: 'The household title to assign to the added contact',
required: true,
options: {
options: [
{ label: 'Head', value: 'Head' },
{ label: 'Spouse', value: 'Spouse' },
{ label: 'Partner', value: 'Partner' },
{ label: 'Child', value: 'Child' },
{ label: 'Grandchild', value: 'Grandchild' },
{ label: 'Parent', value: 'Parent' },
{ label: 'Grandparent', value: 'Grandparent' },
{ label: 'Sibling', value: 'Sibling' },
{ label: 'Other', value: 'Other' },
{ label: 'Dependent', value: 'Dependent' }
]
}
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const requestBody = {
id: propsValue.contact_id,
title: propsValue.title
};
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/households/${propsValue.household_id}/members`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('add household member', response.status, response.body);
}
return response.body;
} catch (error) {
throw new Error(`Failed to add member to household: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,319 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { wealthboxAuth } from '../..';
export const createContact = createAction({
name: 'create_contact',
displayName: 'Create Contact',
description: 'Adds a new contact with rich details (name, address, email, tags, etc.)',
auth: wealthboxAuth,
props: {
first_name: Property.ShortText({
displayName: 'First Name',
description: 'The first name of the contact',
required: true
}),
last_name: Property.ShortText({
displayName: 'Last Name',
description: 'The last name of the contact',
required: true
}),
prefix: Property.ShortText({
displayName: 'Prefix',
description: 'The preferred prefix for the contact (e.g., Mr., Ms., Dr.)',
required: false
}),
middle_name: Property.ShortText({
displayName: 'Middle Name',
description: 'The middle name of the contact',
required: false
}),
suffix: Property.ShortText({
displayName: 'Suffix',
description: 'The suffix associated with the contact (e.g., Jr., Sr., M.D.)',
required: false
}),
nickname: Property.ShortText({
displayName: 'Nickname',
description: 'A preferred shortname for the contact',
required: false
}),
job_title: Property.ShortText({
displayName: 'Job Title',
description: 'The title the contact holds at their present company',
required: false
}),
company_name: Property.ShortText({
displayName: 'Company Name',
description: 'The name of the contact\'s present company',
required: false
}),
type: Property.StaticDropdown({
displayName: 'Contact Type',
description: 'The type of the contact being created',
required: false,
defaultValue: 'Person',
options: {
options: [
{ label: 'Person', value: 'Person' },
{ label: 'Household', value: 'Household' },
{ label: 'Organization', value: 'Organization' },
{ label: 'Trust', value: 'Trust' }
]
}
}),
contact_type: Property.StaticDropdown({
displayName: 'Contact Classification',
description: 'A string further classifying the contact',
required: false,
options: {
options: [
{ label: 'Client', value: 'Client' },
{ label: 'Past Client', value: 'Past Client' },
{ label: 'Prospect', value: 'Prospect' },
{ label: 'Vendor', value: 'Vendor' },
{ label: 'Organization', value: 'Organization' }
]
}
}),
status: Property.StaticDropdown({
displayName: 'Status',
description: 'Whether the contact is currently active',
required: false,
defaultValue: 'Active',
options: {
options: [
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' }
]
}
}),
gender: Property.StaticDropdown({
displayName: 'Gender',
description: 'The gender of the contact',
required: false,
options: {
options: [
{ label: 'Female', value: 'Female' },
{ label: 'Male', value: 'Male' },
{ label: 'Non-binary', value: 'Non-binary' },
{ label: 'Unknown', value: 'Unknown' }
]
}
}),
birth_date: Property.DateTime({
displayName: 'Birth Date',
description: 'The birthdate of the contact (YYYY-MM-DD format)',
required: false
}),
marital_status: Property.StaticDropdown({
displayName: 'Marital Status',
description: 'The marital status of the contact',
required: false,
options: {
options: [
{ label: 'Married', value: 'Married' },
{ label: 'Single', value: 'Single' },
{ label: 'Divorced', value: 'Divorced' },
{ label: 'Widowed', value: 'Widowed' },
{ label: 'Life Partner', value: 'Life Partner' },
{ label: 'Separated', value: 'Separated' },
{ label: 'Unknown', value: 'Unknown' }
]
}
}),
email_address: Property.ShortText({
displayName: 'Email Address',
description: 'Primary email address for the contact',
required: false
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Primary phone number for the contact (e.g., (555) 123-4567)',
required: false
}),
street_line_1: Property.ShortText({
displayName: 'Street Address Line 1',
description: 'First line of street address',
required: false
}),
street_line_2: Property.ShortText({
displayName: 'Street Address Line 2',
description: 'Second line of street address (apt, suite, etc.)',
required: false
}),
city: Property.ShortText({
displayName: 'City',
description: 'City for the address',
required: false
}),
state: Property.ShortText({
displayName: 'State',
description: 'State or province for the address',
required: false
}),
zip_code: Property.ShortText({
displayName: 'ZIP Code',
description: 'ZIP or postal code for the address',
required: false
}),
country: Property.ShortText({
displayName: 'Country',
description: 'Country for the address',
required: false,
defaultValue: 'United States'
}),
twitter_name: Property.ShortText({
displayName: 'Twitter Handle',
description: 'The twitter handle of the contact',
required: false
}),
linkedin_url: Property.LongText({
displayName: 'LinkedIn URL',
description: 'The LinkedIn URL for the contact',
required: false
}),
background_information: Property.LongText({
displayName: 'Background Information',
description: 'A brief description of the contact',
required: false
}),
important_information: Property.LongText({
displayName: 'Important Information',
description: 'Any other important info for the contact',
required: false
}),
personal_interests: Property.LongText({
displayName: 'Personal Interests',
description: 'Personal interests for the contact',
required: false
}),
contact_source: Property.StaticDropdown({
displayName: 'Contact Source',
description: 'The method in which this contact was acquired',
required: false,
options: {
options: [
{ label: 'Referral', value: 'Referral' },
{ label: 'Conference', value: 'Conference' },
{ label: 'Direct Mail', value: 'Direct Mail' },
{ label: 'Cold Call', value: 'Cold Call' },
{ label: 'Other', value: 'Other' }
]
}
}),
tags: Property.Array({
displayName: 'Tags',
description: 'Tags to associate with the contact (e.g., "Client", "VIP", "Referral")',
required: false
}),
external_unique_id: Property.ShortText({
displayName: 'External Unique ID',
description: 'A unique identifier for this contact in an external system',
required: false
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const requestBody: any = {};
if (propsValue.type === 'Household') {
if (propsValue.first_name) {
requestBody.name = propsValue.first_name;
}
} else {
if (propsValue.first_name) requestBody.first_name = propsValue.first_name;
if (propsValue.last_name) requestBody.last_name = propsValue.last_name;
}
if (propsValue.prefix) requestBody.prefix = propsValue.prefix;
if (propsValue.middle_name) requestBody.middle_name = propsValue.middle_name;
if (propsValue.suffix) requestBody.suffix = propsValue.suffix;
if (propsValue.nickname) requestBody.nickname = propsValue.nickname;
if (propsValue.job_title) requestBody.job_title = propsValue.job_title;
if (propsValue.company_name) requestBody.company_name = propsValue.company_name;
if (propsValue.type) requestBody.type = propsValue.type;
if (propsValue.contact_type) requestBody.contact_type = propsValue.contact_type;
if (propsValue.status) requestBody.status = propsValue.status;
if (propsValue.gender) requestBody.gender = propsValue.gender;
if (propsValue.birth_date) requestBody.birth_date = propsValue.birth_date;
if (propsValue.marital_status) requestBody.marital_status = propsValue.marital_status;
if (propsValue.twitter_name) requestBody.twitter_name = propsValue.twitter_name;
if (propsValue.linkedin_url) requestBody.linkedin_url = propsValue.linkedin_url;
if (propsValue.background_information) requestBody.background_information = propsValue.background_information;
if (propsValue.important_information) requestBody.important_information = propsValue.important_information;
if (propsValue.personal_interests) requestBody.personal_interests = propsValue.personal_interests;
if (propsValue.contact_source) requestBody.contact_source = propsValue.contact_source;
if (propsValue.external_unique_id) requestBody.external_unique_id = propsValue.external_unique_id;
if (propsValue.email_address) {
requestBody.email_addresses = [{
address: propsValue.email_address,
principal: true,
kind: 'Work'
}];
}
if (propsValue.phone_number) {
requestBody.phone_numbers = [{
address: propsValue.phone_number,
principal: true,
kind: 'Work'
}];
}
if (propsValue.street_line_1 || propsValue.city || propsValue.state || propsValue.zip_code) {
requestBody.street_addresses = [{
street_line_1: propsValue.street_line_1 || '',
street_line_2: propsValue.street_line_2 || '',
city: propsValue.city || '',
state: propsValue.state || '',
zip_code: propsValue.zip_code || '',
country: propsValue.country || 'United States',
principal: true,
kind: 'Work'
}];
}
if (propsValue.tags && Array.isArray(propsValue.tags)) {
requestBody.tags = propsValue.tags;
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: 'https://api.crmworkspace.com/v1/contacts',
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
throw new Error(`Wealthbox API error: ${response.status} - ${JSON.stringify(response.body)}`);
}
return response.body;
} catch (error) {
throw new Error(`Failed to create contact: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,454 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchContacts, fetchUsers, fetchUserGroups, fetchEventCategories, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES, EVENT_STATES } from '../common';
import { wealthboxAuth } from '../..';
export const createEvent = createAction({
name: 'create_event',
auth: wealthboxAuth,
displayName: 'Create Event',
description: 'Creates a calendar event linked to contact. Schedule advisory meetings on behalf of clients.',
props: {
title: Property.ShortText({
displayName: 'Event Title',
description: 'The name of the event (e.g., "Client Meeting", "Portfolio Review")',
required: true
}),
starts_at: Property.DateTime({
displayName: 'Start Date & Time',
description: 'When the event starts (yyyy-mm-dd hh:mm format)',
required: true
}),
ends_at: Property.DateTime({
displayName: 'End Date & Time',
description: 'When the event ends (yyyy-mm-dd hh:mm format)',
required: true
}),
location: Property.ShortText({
displayName: 'Location',
description: 'Where the event takes place (e.g., "Conference Room", "Client Office", "Zoom Meeting")',
required: false
}),
description: Property.LongText({
displayName: 'Description',
description: 'A detailed explanation of the event purpose and agenda',
required: false
}),
all_day: Property.Checkbox({
displayName: 'All Day Event',
description: 'Check if this is an all-day event',
required: false,
defaultValue: false
}),
repeats: Property.Checkbox({
displayName: 'Repeating Event',
description: 'Check if this event repeats',
required: false,
defaultValue: false
}),
state: Property.StaticDropdown({
displayName: 'Event Status',
description: 'The current state of the event',
required: false,
defaultValue: EVENT_STATES.UNCONFIRMED,
options: {
options: [
{ label: 'Unconfirmed', value: EVENT_STATES.UNCONFIRMED },
{ label: 'Confirmed', value: EVENT_STATES.CONFIRMED },
{ label: 'Tentative', value: EVENT_STATES.TENTATIVE },
{ label: 'Completed', value: EVENT_STATES.COMPLETED },
{ label: 'Cancelled', value: EVENT_STATES.CANCELLED }
]
}
}),
contact_id: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Linked Contact',
description: 'Select the contact to link this event to',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
return {
options: contacts.map((contact: any) => ({
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
value: contact.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load contacts. Please check your authentication.'
};
}
}
}),
invitees: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Invitees',
description: 'Add people to invite to this event',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
invitees_array: Property.Array({
displayName: 'Invitees',
description: 'Add invitees to this event',
required: false,
properties: {
invitee: Property.ShortText({
displayName: 'Invitee',
description: 'Invitee name',
required: true
}),
type: Property.StaticDropdown({
displayName: 'Type',
description: 'Type of invitee',
required: true,
options: {
options: [
{ label: 'Contact', value: 'Contact' },
{ label: 'User', value: 'User' }
]
}
})
}
})
};
}
try {
const [contacts, users] = await Promise.all([
fetchContacts(auth.secret_text, { active: true }),
fetchUsers(auth.secret_text)
]);
const contactOptions = contacts.map((contact: any) => ({
label: `${contact.name || `${contact.first_name} ${contact.last_name}`.trim()} (Contact)`,
value: `contact_${contact.id}`
}));
const userOptions = users.map((user: any) => ({
label: `${user.name} (User)`,
value: `user_${user.id}`
}));
const allInviteeOptions = [...contactOptions, ...userOptions];
return {
invitees_array: Property.Array({
displayName: 'Invitees',
description: 'Add invitees to this event',
required: false,
properties: {
invitee: Property.StaticDropdown({
displayName: 'Invitee',
description: 'Select a contact or user to invite',
required: true,
options: {
options: allInviteeOptions
}
})
}
})
};
} catch (error) {
return {
invitees_array: Property.Array({
displayName: 'Invitees',
description: 'Add invitees to this event (API unavailable)',
required: false,
properties: {
invitee: Property.ShortText({
displayName: 'Invitee Name',
description: 'Enter the invitee name',
required: true
}),
type: Property.StaticDropdown({
displayName: 'Type',
description: 'Type of invitee',
required: true,
options: {
options: [
{ label: 'Contact', value: 'Contact' },
{ label: 'User', value: 'User' }
]
}
})
}
})
};
}
}
}),
event_category: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Event Category',
description: 'Select the category for this event',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const categories = await fetchEventCategories(auth.secret_text);
return {
options: categories.map((category: any) => ({
label: category.name,
value: category.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load event categories. Please check your authentication.'
};
}
}
}),
email_invitees: Property.Checkbox({
displayName: 'Email Invitees',
description: 'Send email invitations to invitees',
required: false,
defaultValue: true
}),
visible_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Visible To',
description: 'Select who can view this event',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const userGroups = await fetchUserGroups(auth.secret_text);
return {
options: userGroups.map((group: any) => ({
label: group.name,
value: group.name
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load user groups. Please check your authentication.'
};
}
}
}),
custom_fields: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Custom Fields',
description: 'Add custom fields to this event',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this event',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field',
description: 'Custom field name',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.EVENT);
const customFieldOptions = customFields.map((field: any) => ({
label: field.name,
value: field.name
}));
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this event',
required: false,
properties: {
custom_field: Property.StaticDropdown({
displayName: 'Custom Field',
description: 'Select a custom field for this event',
required: true,
options: {
options: customFieldOptions
}
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
} catch (error) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this event (API unavailable)',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field Name',
description: 'Enter the custom field name exactly',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
}
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const requestBody: any = {
title: propsValue.title,
starts_at: propsValue.starts_at,
ends_at: propsValue.ends_at
};
if (propsValue.location) requestBody.location = propsValue.location;
if (propsValue.description) requestBody.description = propsValue.description;
if (propsValue.all_day !== undefined) requestBody.all_day = propsValue.all_day;
if (propsValue.repeats !== undefined) requestBody.repeats = propsValue.repeats;
if (propsValue.state) requestBody.state = propsValue.state;
if (propsValue.event_category) requestBody.event_category = propsValue.event_category;
if (propsValue.visible_to) requestBody.visible_to = propsValue.visible_to;
if (propsValue.email_invitees !== undefined) requestBody.email_invitees = propsValue.email_invitees;
if (propsValue.contact_id) {
try {
const contacts = await fetchContacts(auth.secret_text, { active: true });
const selectedContact = contacts.find((contact: any) => contact.id === propsValue.contact_id);
requestBody.linked_to = [{
id: propsValue.contact_id,
type: 'Contact',
name: selectedContact ? (selectedContact.name || `${selectedContact.first_name} ${selectedContact.last_name}`.trim()) : `Contact ${propsValue.contact_id}`
}];
} catch (error) {
requestBody.linked_to = [{
id: propsValue.contact_id,
type: 'Contact',
name: `Contact ${propsValue.contact_id}`
}];
}
}
const inviteesArray = (propsValue as any).invitees_array;
if (inviteesArray && Array.isArray(inviteesArray) && inviteesArray.length > 0) {
const invitees: any[] = [];
for (const inviteeItem of inviteesArray) {
const inviteeValue = inviteeItem.invitee;
if (inviteeValue && typeof inviteeValue === 'string') {
if (inviteeValue.startsWith('contact_')) {
const contactId = inviteeValue.replace('contact_', '');
invitees.push({
id: parseInt(contactId),
type: 'Contact'
});
} else if (inviteeValue.startsWith('user_')) {
const userId = inviteeValue.replace('user_', '');
invitees.push({
id: parseInt(userId),
type: 'User'
});
}
}
}
if (invitees.length > 0) {
requestBody.invitees = invitees;
}
}
const customFieldsArray = (propsValue as any).custom_fields_array;
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.EVENT);
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
requestBody.custom_fields = customFieldsArray.map((field: any) => {
const fieldId = customFieldMap.get(field.custom_field);
if (!fieldId) {
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
}
return {
id: fieldId,
value: field.value
};
});
} catch (error) {
if (error instanceof Error && error.message.includes('Custom field')) {
throw error;
}
console.warn('Could not fetch custom fields for validation:', error);
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
id: field.custom_field,
value: field.value
}));
}
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/events`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('create event', response.status, response.body);
}
return response.body;
} catch (error) {
throw new Error(`Failed to create event: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,391 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchContacts, fetchUserGroups, fetchTags, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES } from '../common';
import { wealthboxAuth } from '../..';
export const createHousehold = createAction({
name: 'create_household',
displayName: 'Create Household',
description: 'Creates a household record with emails, tags. Group family member contacts into one household.',
auth: wealthboxAuth,
props: {
name: Property.ShortText({
displayName: 'Household Name',
description: 'The name of the household (e.g., "The Anderson Family", "Smith Household")',
required: true
}),
head_contact_id: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Head of Household',
description: 'Select the contact who will be the head of this household',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
return {
options: contacts.map((contact: any) => ({
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
value: contact.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load contacts. Please check your authentication.'
};
}
}
}),
spouse_contact_id: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Spouse/Partner (Optional)',
description: 'Select the spouse or partner to automatically add to this household',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
return {
options: contacts.map((contact: any) => ({
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
value: contact.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load contacts. Please check your authentication.'
};
}
}
}),
email_address: Property.ShortText({
displayName: 'Primary Email Address',
description: 'Primary email address for the household',
required: false
}),
street_line_1: Property.ShortText({
displayName: 'Street Address Line 1',
description: 'First line of street address',
required: false
}),
street_line_2: Property.ShortText({
displayName: 'Street Address Line 2',
description: 'Second line of street address (apt, suite, etc.)',
required: false
}),
city: Property.ShortText({
displayName: 'City',
description: 'City for the household address',
required: false
}),
state: Property.ShortText({
displayName: 'State',
description: 'State or province for the household address',
required: false
}),
zip_code: Property.ShortText({
displayName: 'ZIP Code',
description: 'ZIP or postal code for the household address',
required: false
}),
country: Property.ShortText({
displayName: 'Country',
description: 'Country for the household address',
required: false,
defaultValue: 'United States'
}),
phone_number: Property.ShortText({
displayName: 'Primary Phone Number',
description: 'Primary phone number for the household',
required: false
}),
type: Property.StaticDropdown({
displayName: 'Household Type',
description: 'The type of household being created',
required: false,
defaultValue: 'Household',
options: {
options: [
{ label: 'Household', value: 'Household' },
{ label: 'Organization', value: 'Organization' },
{ label: 'Trust', value: 'Trust' }
]
}
}),
status: Property.StaticDropdown({
displayName: 'Status',
description: 'Whether the household is currently active',
required: false,
defaultValue: 'Active',
options: {
options: [
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' }
]
}
}),
background_information: Property.LongText({
displayName: 'Background Information',
description: 'Background information about the household',
required: false
}),
important_information: Property.LongText({
displayName: 'Important Information',
description: 'Any important information about the household',
required: false
}),
tags: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Tags',
description: 'Select tags to associate with this household',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
tags_array: Property.Array({
displayName: 'Tags',
description: 'Add tags to this household',
required: false,
properties: {
tag: Property.ShortText({
displayName: 'Tag',
description: 'Tag name',
required: true
})
}
})
};
}
try {
const tags = await fetchTags(auth.secret_text, DOCUMENT_TYPES.CONTACT);
const tagOptions = tags.map((tag: any) => ({
label: tag.name,
value: tag.name
}));
return {
tags_array: Property.Array({
displayName: 'Tags',
description: 'Add tags to this household',
required: false,
properties: {
tag: Property.StaticDropdown({
displayName: 'Tag',
description: 'Select a tag for this household',
required: true,
options: {
options: tagOptions
}
})
}
})
};
} catch (error) {
return {
tags_array: Property.Array({
displayName: 'Tags',
description: 'Add tags to this household (API unavailable)',
required: false,
properties: {
tag: Property.ShortText({
displayName: 'Tag Name',
description: 'Enter the tag name exactly',
required: true
})
}
})
};
}
}
}),
visible_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Visible To',
description: 'Select who can view this household',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const userGroups = await fetchUserGroups(auth.secret_text);
const filteredGroups = userGroups.filter((group: any) => group.name !== 'Only Me');
return {
options: filteredGroups.map((group: any) => {
const displayName = group.user ? `${group.name} (${group.user.name || group.user.email})` : group.name;
return {
label: displayName,
value: group.name
};
})
};
} catch (error) {
return {
options: [],
error: 'Failed to load user groups. Please check your authentication.'
};
}
}
}),
external_unique_id: Property.ShortText({
displayName: 'External Unique ID',
description: 'A unique identifier for this household in an external system',
required: false
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const requestBody: any = {
name: propsValue.name,
type: 'Household',
status: propsValue.status || 'Active'
};
if (propsValue.background_information) requestBody.background_information = propsValue.background_information;
if (propsValue.important_information) requestBody.important_information = propsValue.important_information;
if (propsValue.visible_to && propsValue.visible_to.trim() !== '') {
requestBody.visible_to = propsValue.visible_to;
}
if (propsValue.external_unique_id) requestBody.external_unique_id = propsValue.external_unique_id;
if (propsValue.email_address) {
requestBody.email_addresses = [{
address: propsValue.email_address,
principal: true,
kind: 'Work'
}];
}
if (propsValue.phone_number) {
requestBody.phone_numbers = [{
address: propsValue.phone_number,
principal: true,
kind: 'Work'
}];
}
if (propsValue.street_line_1 || propsValue.city || propsValue.state || propsValue.zip_code) {
requestBody.street_addresses = [{
street_line_1: propsValue.street_line_1 || '',
street_line_2: propsValue.street_line_2 || '',
city: propsValue.city || '',
state: propsValue.state || '',
zip_code: propsValue.zip_code || '',
country: propsValue.country || 'United States',
principal: true,
kind: 'Work'
}];
}
const tagsArray = (propsValue as any).tags_array;
if (tagsArray && Array.isArray(tagsArray) && tagsArray.length > 0) {
requestBody.tags = tagsArray.map((tagItem: any) => tagItem.tag);
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/contacts`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('create household', response.status, response.body);
}
const householdContact = response.body;
const members: any[] = [];
if (propsValue.head_contact_id && householdContact.id) {
try {
const memberResponse = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/households/${householdContact.id}/members`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: {
id: propsValue.head_contact_id,
title: 'Head'
}
});
if (memberResponse.status < 400) {
members.push(memberResponse.body);
}
} catch (memberError) {
console.warn('Failed to add head of household member:', memberError);
}
}
if (propsValue.spouse_contact_id && householdContact.id) {
try {
const spouseResponse = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/households/${householdContact.id}/members`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: {
id: propsValue.spouse_contact_id,
title: 'Spouse'
}
});
if (spouseResponse.status < 400) {
members.push(spouseResponse.body);
}
} catch (spouseError) {
console.warn('Failed to add spouse/partner member:', spouseError);
}
}
if (members.length > 0) {
return {
household: householdContact,
members: members
};
}
return householdContact;
} catch (error) {
throw new Error(`Failed to create household: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,196 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchUserGroups, fetchContacts, fetchTags, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES } from '../common';
import { wealthboxAuth } from '../..';
export const createNote = createAction({
name: 'create_note',
auth: wealthboxAuth,
displayName: 'Create Note',
description: 'Adds a note linked to a contact. Log call summaries against client records.',
props: {
content: Property.LongText({
displayName: 'Note Content',
description: 'The main body of the note (e.g., call summary, meeting notes, client interaction details)',
required: true
}),
contact_id: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Contact',
description: 'Select the contact to link this note to',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
return {
options: contacts.map((contact: any) => ({
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
value: contact.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load contacts. Please check your authentication.'
};
}
}
}),
visible_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Visible To',
description: 'Select who can view this note',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const userGroups = await fetchUserGroups(auth.secret_text);
return {
options: userGroups.map((group: any) => ({
label: group.name,
value: group.name
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load user groups. Please check your authentication.'
};
}
}
}),
tags: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Tags',
description: 'Select tags to associate with this note',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
tags_array: Property.Array({
displayName: 'Tags',
description: 'Add tags to this note',
required: false,
properties: {
tag: Property.ShortText({
displayName: 'Tag',
description: 'Tag name',
required: true
})
}
})
};
}
try {
const tags = await fetchTags(auth.secret_text, DOCUMENT_TYPES.CONTACT_NOTE);
const tagOptions = tags.map((tag: any) => ({
label: tag.name,
value: tag.name
}));
return {
tags_array: Property.Array({
displayName: 'Tags',
description: 'Add tags to this note',
required: false,
properties: {
tag: Property.StaticDropdown({
displayName: 'Tag',
description: 'Select a tag for this note',
required: true,
options: {
options: tagOptions
}
})
}
})
};
} catch (error) {
return {
tags_array: Property.Array({
displayName: 'Tags',
description: 'Add tags to this note (API unavailable)',
required: false,
properties: {
tag: Property.ShortText({
displayName: 'Tag Name',
description: 'Enter the tag name exactly',
required: true
})
}
})
};
}
}
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const linkedToResource: any = {
id: propsValue.contact_id,
type: 'Contact'
};
try {
const contacts = await fetchContacts(auth.secret_text, { active: true });
const selectedContact = contacts.find((contact: any) => contact.id === propsValue.contact_id);
if (selectedContact) {
linkedToResource.name = selectedContact.name || `${selectedContact.first_name} ${selectedContact.last_name}`.trim();
}
} catch (error) {
console.warn('Could not fetch contact name for reference:', error);
}
const requestBody: any = {
content: propsValue.content,
linked_to: [linkedToResource]
};
if (propsValue.visible_to) {
requestBody.visible_to = propsValue.visible_to;
}
const tagsArray = (propsValue as any).tags_array;
if (tagsArray && Array.isArray(tagsArray) && tagsArray.length > 0) {
requestBody.tags = tagsArray.map((tagItem: any) => tagItem.tag);
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/notes`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('create note', response.status, response.body);
}
return response.body;
} catch (error) {
throw new Error(`Failed to create note: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,361 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchContacts, fetchUsers, fetchUserGroups, fetchOpportunityStages, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES, OPPORTUNITY_AMOUNT_KINDS, CURRENCIES } from '../common';
import { wealthboxAuth } from '../..';
export const createOpportunity = createAction({
name: 'create_opportunity',
auth: wealthboxAuth,
displayName: 'Create Opportunity',
description: 'Logs an opportunity including stage, close date, amount. Automate opportunity tracking after meetings.',
props: {
name: Property.ShortText({
displayName: 'Opportunity Name',
description: 'The name of the opportunity (e.g., "Financial Plan", "Investment Advisory", "Estate Planning")',
required: true
}),
target_close: Property.DateTime({
displayName: 'Target Close Date',
description: 'When the opportunity should close',
required: true
}),
probability: Property.Number({
displayName: 'Probability (%)',
description: 'The chance the opportunity will close, as a percentage (0-100)',
required: true
}),
amount: Property.Number({
displayName: 'Amount',
description: 'The monetary value of the opportunity',
required: true
}),
currency: Property.StaticDropdown({
displayName: 'Currency',
description: 'The currency for the opportunity amount',
required: false,
defaultValue: CURRENCIES.USD,
options: {
options: [
{ label: 'USD ($)', value: CURRENCIES.USD },
{ label: 'EUR (€)', value: CURRENCIES.EUR },
{ label: 'GBP (£)', value: CURRENCIES.GBP },
{ label: 'CAD (C$)', value: CURRENCIES.CAD },
{ label: 'AUD (A$)', value: CURRENCIES.AUD }
]
}
}),
amount_kind: Property.StaticDropdown({
displayName: 'Amount Type',
description: 'The type of amount this represents',
required: false,
defaultValue: OPPORTUNITY_AMOUNT_KINDS.FEE,
options: {
options: [
{ label: 'Fee', value: OPPORTUNITY_AMOUNT_KINDS.FEE },
{ label: 'Commission', value: OPPORTUNITY_AMOUNT_KINDS.COMMISSION },
{ label: 'AUM', value: OPPORTUNITY_AMOUNT_KINDS.AUM },
{ label: 'Other', value: OPPORTUNITY_AMOUNT_KINDS.OTHER }
]
}
}),
stage: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Stage',
description: 'Select the current stage of this opportunity',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const stages = await fetchOpportunityStages(auth.secret_text);
return {
options: stages.map((stage: any) => ({
label: stage.name,
value: stage.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load opportunity stages. Please check your authentication.'
};
}
}
}),
contact_id: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Linked Contact',
description: 'Select the contact linked to this opportunity',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
return {
options: contacts.map((contact: any) => ({
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
value: contact.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load contacts. Please check your authentication.'
};
}
}
}),
description: Property.LongText({
displayName: 'Description',
description: 'A detailed explanation of the opportunity',
required: false
}),
manager: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Opportunity Manager',
description: 'Select the user designated as manager of this opportunity',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const users = await fetchUsers(auth.secret_text);
const assignableUsers = users.filter((user: any) => !user.excluded_from_assignments);
return {
options: assignableUsers.map((user: any) => ({
label: `${user.name} (${user.email})`,
value: user.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load users. Please check your authentication.'
};
}
}
}),
visible_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Visible To',
description: 'Select who can view this opportunity',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const userGroups = await fetchUserGroups(auth.secret_text);
return {
options: userGroups.map((group: any) => ({
label: group.name,
value: group.name
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load user groups. Please check your authentication.'
};
}
}
}),
custom_fields: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Custom Fields',
description: 'Add custom fields to this opportunity',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this opportunity',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field',
description: 'Custom field name',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.OPPORTUNITY);
const customFieldOptions = customFields.map((field: any) => ({
label: field.name,
value: field.name
}));
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this opportunity',
required: false,
properties: {
custom_field: Property.StaticDropdown({
displayName: 'Custom Field',
description: 'Select a custom field for this opportunity',
required: true,
options: {
options: customFieldOptions
}
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
} catch (error) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this opportunity (API unavailable)',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field Name',
description: 'Enter the custom field name exactly',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
}
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
if (propsValue.probability < 0 || propsValue.probability > 100) {
throw new Error('Probability must be between 0 and 100');
}
const requestBody: any = {
name: propsValue.name,
target_close: propsValue.target_close,
probability: propsValue.probability,
stage: propsValue.stage,
amounts: [
{
amount: propsValue.amount,
currency: propsValue.currency || CURRENCIES.USD,
kind: propsValue.amount_kind || OPPORTUNITY_AMOUNT_KINDS.FEE
}
]
};
if (propsValue.contact_id) {
try {
const contacts = await fetchContacts(auth.secret_text, { active: true });
const selectedContact = contacts.find((contact: any) => contact.id === propsValue.contact_id);
requestBody.linked_to = [{
id: propsValue.contact_id,
type: 'Contact',
name: selectedContact ? (selectedContact.name || `${selectedContact.first_name} ${selectedContact.last_name}`.trim()) : `Contact ${propsValue.contact_id}`
}];
} catch (error) {
requestBody.linked_to = [{
id: propsValue.contact_id,
type: 'Contact',
name: `Contact ${propsValue.contact_id}`
}];
}
}
if (propsValue.description) {
requestBody.description = propsValue.description;
}
if (propsValue.manager) {
requestBody.manager = propsValue.manager;
}
if (propsValue.visible_to) {
requestBody.visible_to = propsValue.visible_to;
}
const customFieldsArray = (propsValue as any).custom_fields_array;
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.OPPORTUNITY);
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
requestBody.custom_fields = customFieldsArray.map((field: any) => {
const fieldId = customFieldMap.get(field.custom_field);
if (!fieldId) {
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
}
return {
id: fieldId,
value: field.value
};
});
} catch (error) {
if (error instanceof Error && error.message.includes('Custom field')) {
throw error;
}
console.warn('Could not fetch custom fields for validation:', error);
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
id: field.custom_field,
value: field.value
}));
}
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/opportunities`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('create opportunity', response.status, response.body);
}
return response.body;
} catch (error) {
throw new Error(`Failed to create opportunity: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,230 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchUserGroups, fetchUsers, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES } from '../common';
import { wealthboxAuth } from '../..';
export const createProject = createAction({
auth: wealthboxAuth,
name: 'create_project',
displayName: 'Create Project',
description: 'Starts a new project with description and organizer. Launch project-based onboarding when new clients sign up.',
props: {
name: Property.ShortText({
displayName: 'Project Name',
description: 'The name of the project (e.g., "Client Onboarding", "Q1 Review Process")',
required: true
}),
description: Property.LongText({
displayName: 'Project Description',
description: 'A detailed explanation of the project goals and scope',
required: true
}),
organizer: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Organizer',
description: 'Select the user who will be responsible for organizing this project',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const users = await fetchUsers(auth.secret_text);
const assignableUsers = users.filter((user: any) => !user.excluded_from_assignments);
return {
options: assignableUsers.map((user: any) => ({
label: `${user.name} (${user.email})`,
value: user.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load users. Please check your authentication.'
};
}
}
}),
visible_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Visible To',
description: 'Select who can view this project',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const userGroups = await fetchUserGroups(auth.secret_text);
return {
options: userGroups.map((group: any) => ({
label: group.name,
value: group.name
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load user groups. Please check your authentication.'
};
}
}
}),
custom_fields: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Custom Fields',
description: 'Add custom fields to this project',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this project',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field',
description: 'Custom field name',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.PROJECT);
const customFieldOptions = customFields.map((field: any) => ({
label: field.name,
value: field.name
}));
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this project',
required: false,
properties: {
custom_field: Property.StaticDropdown({
displayName: 'Custom Field',
description: 'Select a custom field for this project',
required: true,
options: {
options: customFieldOptions
}
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
} catch (error) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this project (API unavailable)',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field Name',
description: 'Enter the custom field name exactly',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
}
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const requestBody: any = {
name: propsValue.name,
description: propsValue.description
};
if (propsValue.organizer) {
requestBody.organizer = propsValue.organizer;
}
if (propsValue.visible_to) {
requestBody.visible_to = propsValue.visible_to;
}
const customFieldsArray = (propsValue as any).custom_fields_array;
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.PROJECT);
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
requestBody.custom_fields = customFieldsArray.map((field: any) => {
const fieldId = customFieldMap.get(field.custom_field);
if (!fieldId) {
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
}
return {
id: fieldId,
value: field.value
};
});
} catch (error) {
if (error instanceof Error && error.message.includes('Custom field')) {
throw error;
}
console.warn('Could not fetch custom fields for validation:', error);
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
id: field.custom_field,
value: field.value
}));
}
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/projects`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('create project', response.status, response.body);
}
return response.body;
} catch (error) {
throw new Error(`Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,442 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchContacts, fetchUsers, fetchUserGroups, fetchTaskCategories, fetchProjects, fetchOpportunities, fetchCustomFields, WEALTHBOX_API_BASE, handleApiError, DOCUMENT_TYPES, TASK_PRIORITIES, LINK_TYPES } from '../common';
import { wealthboxAuth } from '../..';
export const createTask = createAction({
auth: wealthboxAuth,
name: 'create_task',
displayName: 'Create Task',
description: 'Creates tasks tied to contacts with due dates and assignment types. Assign follow-up actions when opportunities are created.',
props: {
name: Property.ShortText({
displayName: 'Task Name',
description: 'The name of the task (e.g., "Return Bill\'s call", "Follow up on proposal")',
required: true
}),
due_date: Property.DateTime({
displayName: 'Due Date',
description: 'When the task is due',
required: true
}),
assigned_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Assigned To',
description: 'Select the user who the task is assigned to',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const users = await fetchUsers(auth.secret_text);
const assignableUsers = users.filter((user: any) => !user.excluded_from_assignments);
return {
options: assignableUsers.map((user: any) => ({
label: `${user.name} (${user.email})`,
value: user.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load users. Please check your authentication.'
};
}
}
}),
description: Property.LongText({
displayName: 'Description',
description: 'A detailed explanation of the task',
required: false
}),
priority: Property.StaticDropdown({
displayName: 'Priority',
description: 'The priority level of the task',
required: false,
defaultValue: TASK_PRIORITIES.MEDIUM,
options: {
options: [
{ label: 'Low', value: TASK_PRIORITIES.LOW },
{ label: 'Medium', value: TASK_PRIORITIES.MEDIUM },
{ label: 'High', value: TASK_PRIORITIES.HIGH }
]
}
}),
complete: Property.Checkbox({
displayName: 'Mark as Complete',
description: 'Check if the task should be created as already completed',
required: false,
defaultValue: false
}),
link_type: Property.StaticDropdown({
displayName: 'Link To',
description: 'What type of record to link this task to',
required: false,
options: {
options: [
{ label: 'Contact', value: LINK_TYPES.CONTACT },
{ label: 'Project', value: LINK_TYPES.PROJECT },
{ label: 'Opportunity', value: LINK_TYPES.OPPORTUNITY }
]
}
}),
linked_record: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Linked Record',
description: 'Select the record to link this task to',
required: false,
refreshers: ['link_type'],
props: async ({ auth, link_type }) => {
if (!auth || !link_type) {
return {
linked_id: Property.ShortText({
displayName: 'Linked Record ID',
description: 'Enter the record ID to link to',
required: false
})
};
}
try {
const linkTypeStr = link_type as unknown as string;
if (linkTypeStr === LINK_TYPES.CONTACT) {
const contacts = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
return {
linked_id: Property.StaticDropdown({
displayName: 'Contact',
description: 'Select the contact to link this task to',
required: false,
options: {
options: contacts.map((contact: any) => ({
label: contact.name || `${contact.first_name} ${contact.last_name}`.trim() || `Contact ${contact.id}`,
value: contact.id
}))
}
})
};
} else if (linkTypeStr === LINK_TYPES.PROJECT) {
const projects = await fetchProjects(auth.secret_text);
return {
linked_id: Property.StaticDropdown({
displayName: 'Project',
description: 'Select the project to link this task to',
required: false,
options: {
options: projects.map((project: any) => ({
label: project.name || `Project ${project.id}`,
value: project.id
}))
}
})
};
} else if (linkTypeStr === LINK_TYPES.OPPORTUNITY) {
const opportunities = await fetchOpportunities(auth.secret_text);
return {
linked_id: Property.StaticDropdown({
displayName: 'Opportunity',
description: 'Select the opportunity to link this task to',
required: false,
options: {
options: opportunities.map((opportunity: any) => ({
label: opportunity.name || `Opportunity ${opportunity.id}`,
value: opportunity.id
}))
}
})
};
}
} catch (error) {
return {
linked_id: Property.ShortText({
displayName: 'Linked Record ID',
description: 'Enter the record ID to link to (API unavailable)',
required: false
})
};
}
return {
linked_id: Property.ShortText({
displayName: 'Linked Record ID',
description: 'Enter the record ID to link to',
required: false
})
};
}
}),
category: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Category',
description: 'Select the category this task belongs to',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const categories = await fetchTaskCategories(auth.secret_text);
return {
options: categories.map((category: any) => ({
label: category.name,
value: category.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load task categories. Please check your authentication.'
};
}
}
}),
visible_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Visible To',
description: 'Select who can view this task',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const userGroups = await fetchUserGroups(auth.secret_text);
return {
options: userGroups.map((group: any) => ({
label: group.name,
value: group.name
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load user groups. Please check your authentication.'
};
}
}
}),
due_later: Property.ShortText({
displayName: 'Due Later',
description: 'Interval for when this task is due after start (e.g., "2 days later at 5:00 PM")',
required: false
}),
custom_fields: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Custom Fields',
description: 'Add custom fields to this task',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this task',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field',
description: 'Custom field name',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.TASK);
const customFieldOptions = customFields.map((field: any) => ({
label: field.name,
value: field.name
}));
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this task',
required: false,
properties: {
custom_field: Property.StaticDropdown({
displayName: 'Custom Field',
description: 'Select a custom field for this task',
required: true,
options: {
options: customFieldOptions
}
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
} catch (error) {
return {
custom_fields_array: Property.Array({
displayName: 'Custom Fields',
description: 'Add custom fields to this task (API unavailable)',
required: false,
properties: {
custom_field: Property.ShortText({
displayName: 'Custom Field Name',
description: 'Enter the custom field name exactly',
required: true
}),
value: Property.ShortText({
displayName: 'Value',
description: 'The value for this custom field',
required: true
})
}
})
};
}
}
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('API access token is required');
}
const requestBody: any = {
name: propsValue.name,
due_date: propsValue.due_date
};
if (propsValue.assigned_to) {
requestBody.assigned_to = propsValue.assigned_to;
}
if (propsValue.description) {
requestBody.description = propsValue.description;
}
if (propsValue.priority) {
requestBody.priority = propsValue.priority;
}
if (propsValue.complete !== undefined) {
requestBody.complete = propsValue.complete;
}
if (propsValue.category) {
requestBody.category = propsValue.category;
}
if (propsValue.visible_to) {
requestBody.visible_to = propsValue.visible_to;
}
if (propsValue.due_later) {
requestBody.due_later = propsValue.due_later;
}
const linkedRecord = (propsValue as any).linked_record;
if (propsValue.link_type && linkedRecord?.linked_id) {
try {
let recordName = `${propsValue.link_type} ${linkedRecord.linked_id}`;
const linkTypeStr = propsValue.link_type as string;
if (linkTypeStr === LINK_TYPES.CONTACT) {
const contacts = await fetchContacts(auth.secret_text, { active: true });
const contact = contacts.find((c: any) => c.id === linkedRecord.linked_id);
if (contact) {
recordName = contact.name || `${contact.first_name} ${contact.last_name}`.trim();
}
} else if (linkTypeStr === LINK_TYPES.PROJECT) {
const projects = await fetchProjects(auth.secret_text);
const project = projects.find((p: any) => p.id === linkedRecord.linked_id);
if (project) {
recordName = project.name;
}
} else if (linkTypeStr === LINK_TYPES.OPPORTUNITY) {
const opportunities = await fetchOpportunities(auth.secret_text);
const opportunity = opportunities.find((o: any) => o.id === linkedRecord.linked_id);
if (opportunity) {
recordName = opportunity.name;
}
}
requestBody.linked_to = [{
id: linkedRecord.linked_id,
type: propsValue.link_type,
name: recordName
}];
} catch (error) {
requestBody.linked_to = [{
id: linkedRecord.linked_id,
type: propsValue.link_type,
name: `${propsValue.link_type} ${linkedRecord.linked_id}`
}];
}
}
const customFieldsArray = (propsValue as any).custom_fields_array;
if (customFieldsArray && Array.isArray(customFieldsArray) && customFieldsArray.length > 0) {
try {
const customFields = await fetchCustomFields(auth.secret_text, DOCUMENT_TYPES.TASK);
const customFieldMap = new Map(customFields.map((field: any) => [field.name, field.id]));
requestBody.custom_fields = customFieldsArray.map((field: any) => {
const fieldId = customFieldMap.get(field.custom_field);
if (!fieldId) {
throw new Error(`Custom field "${field.custom_field}" not found. Please check the field name.`);
}
return {
id: fieldId,
value: field.value
};
});
} catch (error) {
if (error instanceof Error && error.message.includes('Custom field')) {
throw error;
}
console.warn('Could not fetch custom fields for validation:', error);
requestBody.custom_fields = customFieldsArray.map((field: any) => ({
id: field.custom_field,
value: field.value
}));
}
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/tasks`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('create task', response.status, response.body);
}
return response.body;
} catch (error) {
throw new Error(`Failed to create task: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,333 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchContacts, fetchTags, WEALTHBOX_API_BASE, handleApiError } from '../common';
import { wealthboxAuth } from '../..';
export const findContact = createAction({
name: 'find_contact',
auth: wealthboxAuth,
displayName: 'Find Contact',
description: 'Locate a contact by name, email, phone, or advanced filters. Comprehensive contact search with dynamic filtering options.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'Search by name (supports partial matches across prefix, first, middle, last, suffix, nickname, and full name for households/companies)',
required: false
}),
email: Property.ShortText({
displayName: 'Email Address',
description: 'Search by email address',
required: false
}),
phone: Property.ShortText({
displayName: 'Phone Number',
description: 'Search by phone number (delimiters like -, (), will be stripped automatically)',
required: false
}),
contact_id: Property.Number({
displayName: 'Contact ID',
description: 'Search by specific contact ID (most precise search)',
required: false
}),
external_unique_id: Property.ShortText({
displayName: 'External Unique ID',
description: 'Search by external unique identifier',
required: false
}),
contact_type: Property.StaticDropdown({
displayName: 'Contact Type',
description: 'Filter by contact type',
required: false,
options: {
options: [
{ label: 'Client', value: 'Client' },
{ label: 'Past Client', value: 'Past Client' },
{ label: 'Prospect', value: 'Prospect' },
{ label: 'Vendor', value: 'Vendor' },
{ label: 'Organization', value: 'Organization' }
]
}
}),
type: Property.StaticDropdown({
displayName: 'Contact Entity Type',
description: 'Filter by entity type',
required: false,
options: {
options: [
{ label: 'Person', value: 'person' },
{ label: 'Household', value: 'household' },
{ label: 'Organization', value: 'organization' },
{ label: 'Trust', value: 'trust' }
]
}
}),
household_title: Property.StaticDropdown({
displayName: 'Household Title',
description: 'Filter by household title (only applies to household members)',
required: false,
options: {
options: [
{ label: 'Head', value: 'Head' },
{ label: 'Spouse', value: 'Spouse' },
{ label: 'Partner', value: 'Partner' },
{ label: 'Child', value: 'Child' },
{ label: 'Grandchild', value: 'Grandchild' },
{ label: 'Parent', value: 'Parent' },
{ label: 'Grandparent', value: 'Grandparent' },
{ label: 'Sibling', value: 'Sibling' },
{ label: 'Other', value: 'Other' },
{ label: 'Dependent', value: 'Dependent' }
]
}
}),
tags_filter: Property.MultiSelectDropdown({
auth: wealthboxAuth,
displayName: 'Tags Filter',
description: 'Filter contacts by tags',
required: false,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Connect your Wealthbox account first'
};
}
try {
const availableTags = await fetchTags(auth.secret_text, 'Contact');
const tagOptions = availableTags.map((tag: any) => ({
label: tag.name,
value: tag.name
}));
return {
disabled: false,
options: tagOptions,
placeholder: tagOptions.length === 0 ? 'No tags available' : 'Select tags to filter by'
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
disabled: true,
options: [],
placeholder: `Error loading tags: ${errorMessage}`
};
}
}
}),
active: Property.StaticDropdown({
displayName: 'Active Status',
description: 'Filter by active status',
required: false,
options: {
options: [
{ label: 'Active Only', value: 'true' },
{ label: 'Inactive Only', value: 'false' },
{ label: 'All Contacts', value: '' }
]
}
}),
include_deleted: Property.Checkbox({
displayName: 'Include Deleted Contacts',
description: 'Include contacts that have been deleted',
required: false,
defaultValue: false
}),
updated_since: Property.DateTime({
displayName: 'Updated Since',
description: 'Only return contacts updated on or after this date/time',
required: false
}),
updated_before: Property.DateTime({
displayName: 'Updated Before',
description: 'Only return contacts updated on or before this date/time',
required: false
}),
order: Property.StaticDropdown({
displayName: 'Sort Order',
description: 'How to order the results',
required: false,
options: {
options: [
{ label: 'Recent (newest first)', value: 'recent' },
{ label: 'Created Date (newest first)', value: 'created' },
{ label: 'Updated Date (newest first)', value: 'updated' },
{ label: 'Ascending', value: 'asc' },
{ label: 'Descending', value: 'desc' }
]
}
}),
limit: Property.Number({
displayName: 'Result Limit',
description: 'Maximum number of contacts to return (default: 50, max: 1000)',
required: false,
defaultValue: 50
}),
return_single_result: Property.Checkbox({
displayName: 'Return Single Result Only',
description: 'If checked, returns only the first matching contact. If unchecked, returns all matching contacts.',
required: false,
defaultValue: false
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const searchParams = new URLSearchParams();
const hasSearchCriteria =
propsValue.contact_id ||
propsValue.name ||
propsValue.email ||
propsValue.phone ||
propsValue.external_unique_id ||
propsValue.contact_type ||
propsValue.type ||
propsValue.household_title ||
(propsValue as any).tags?.tags_filter ||
propsValue.active ||
propsValue.include_deleted ||
propsValue.updated_since ||
propsValue.updated_before;
if (!hasSearchCriteria) {
throw new Error('At least one search criteria must be provided (ID, name, email, phone, external ID, or filters)');
}
if (propsValue.contact_id && !(propsValue.name || propsValue.email || propsValue.phone || propsValue.external_unique_id || propsValue.contact_type || propsValue.type || propsValue.household_title || (propsValue as any).tags?.tags_filter || propsValue.active || propsValue.include_deleted || propsValue.updated_since || propsValue.updated_before)) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${WEALTHBOX_API_BASE}/contacts/${propsValue.contact_id}`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Accept': 'application/json'
}
});
if (response.status >= 400) {
handleApiError('find contact by ID', response.status, response.body);
}
return {
found: true,
contact: response.body,
contacts: [response.body],
total_results: 1,
search_criteria: { contact_id: propsValue.contact_id }
};
} catch (error) {
if (error instanceof Error && error.message.includes('404')) {
return {
found: false,
contact: null,
contacts: [],
total_results: 0,
message: `No contact found with ID: ${propsValue.contact_id}`,
search_criteria: { contact_id: propsValue.contact_id }
};
}
throw error;
}
}
if (propsValue.name) searchParams.append('name', propsValue.name);
if (propsValue.email) searchParams.append('email', propsValue.email);
if (propsValue.phone) searchParams.append('phone', propsValue.phone);
if (propsValue.contact_id) searchParams.append('id', propsValue.contact_id.toString());
if (propsValue.external_unique_id) searchParams.append('external_unique_id', propsValue.external_unique_id);
if (propsValue.contact_type) searchParams.append('contact_type', propsValue.contact_type);
if (propsValue.type) searchParams.append('type', propsValue.type);
if (propsValue.household_title) searchParams.append('household_title', propsValue.household_title);
const tagsFilter = propsValue.tags_filter;
if (tagsFilter && Array.isArray(tagsFilter) && tagsFilter.length > 0) {
tagsFilter.forEach((tag: string) => {
searchParams.append('tags[]', tag);
});
}
if (propsValue.active && propsValue.active !== '') {
searchParams.append('active', propsValue.active);
}
if (propsValue.include_deleted) {
searchParams.append('deleted', 'true');
}
if (propsValue.updated_since) searchParams.append('updated_since', propsValue.updated_since);
if (propsValue.updated_before) searchParams.append('updated_before', propsValue.updated_before);
if (propsValue.order) searchParams.append('order', propsValue.order);
const limit = Math.min(propsValue.limit || 50, 1000);
searchParams.append('limit', limit.toString());
const queryString = searchParams.toString();
const url = queryString ? `${WEALTHBOX_API_BASE}/contacts?${queryString}` : `${WEALTHBOX_API_BASE}/contacts`;
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: url,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Accept': 'application/json'
}
});
if (response.status >= 400) {
handleApiError('find contacts', response.status, response.body);
}
const contacts = response.body.contacts || [];
const totalResults = contacts.length;
if (propsValue.return_single_result || totalResults === 1) {
return {
found: totalResults > 0,
contact: totalResults > 0 ? contacts[0] : null,
contacts: contacts,
total_results: totalResults,
search_criteria: Object.fromEntries(searchParams),
message: totalResults === 0 ? 'No contacts found matching the search criteria' : undefined
};
}
return {
found: totalResults > 0,
contacts: contacts,
total_results: totalResults,
search_criteria: Object.fromEntries(searchParams),
message: totalResults === 0 ? 'No contacts found matching the search criteria' : undefined
};
} catch (error) {
throw new Error(`Failed to find contacts: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,347 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchContacts, fetchProjects, fetchOpportunities, fetchUsers, WEALTHBOX_API_BASE, handleApiError } from '../common';
import { wealthboxAuth } from '../..';
export const findTask = createAction({
auth: wealthboxAuth,
name: 'find_task',
displayName: 'Find Task',
description: 'Finds existing tasks using comprehensive search filters. Search by assignment, resource, completion status, and date ranges.',
props: {
task_id: Property.Number({
displayName: 'Task ID (Optional)',
description: 'Search for a specific task by its unique ID. Leave empty to search using filters.',
required: false
}),
resource_type: Property.StaticDropdown({
displayName: 'Linked Resource Type',
description: 'Filter tasks by the type of resource they are linked to',
required: false,
options: {
options: [
{ label: 'Contact', value: 'Contact' },
{ label: 'Project', value: 'Project' },
{ label: 'Opportunity', value: 'Opportunity' }
]
}
}),
resource_record: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Linked Resource',
description: 'Select the specific resource to filter tasks by',
required: false,
refreshers: ['resource_type'],
props: async ({ auth, resource_type }) => {
if (!auth || !resource_type) {
return {
resource_id: Property.Number({
displayName: 'Resource ID',
description: 'Enter the resource ID manually',
required: false
})
};
}
try {
let records: any[] = [];
let recordType = '';
const resourceTypeValue = resource_type as unknown as string;
switch (resourceTypeValue) {
case 'Contact':
records = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
recordType = 'Contact';
break;
case 'Project':
records = await fetchProjects(auth.secret_text);
recordType = 'Project';
break;
case 'Opportunity':
records = await fetchOpportunities(auth.secret_text);
recordType = 'Opportunity';
break;
default:
return {
resource_id: Property.Number({
displayName: 'Resource ID',
description: 'Enter the resource ID manually',
required: false
})
};
}
const recordOptions = records.map((record: any) => ({
label: record.name || record.title || `${recordType} ${record.id}`,
value: record.id
}));
return {
resource_id: Property.StaticDropdown({
displayName: `${recordType} Record`,
description: `Select the ${recordType.toLowerCase()} to filter tasks by`,
required: false,
options: {
options: recordOptions
}
})
};
} catch (error) {
return {
resource_id: Property.Number({
displayName: 'Resource ID',
description: 'Enter the resource ID manually (API unavailable)',
required: false
})
};
}
}
}),
assigned_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Assigned To',
description: 'Filter tasks by the user they are assigned to',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const users = await fetchUsers(auth.secret_text);
return {
options: users.map((user: any) => ({
label: `${user.name} (${user.email})`,
value: user.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load users. Please check your authentication.'
};
}
}
}),
assigned_to_team: Property.Number({
displayName: 'Assigned to Team ID',
description: 'Filter tasks by the team they are assigned to',
required: false
}),
created_by: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Created By',
description: 'Filter tasks by the user who created them',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const users = await fetchUsers(auth.secret_text);
return {
options: users.map((user: any) => ({
label: `${user.name} (${user.email})`,
value: user.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load users. Please check your authentication.'
};
}
}
}),
completed: Property.StaticDropdown({
displayName: 'Completion Status',
description: 'Filter by task completion status',
required: false,
options: {
options: [
{ label: 'All Tasks', value: '' },
{ label: 'Completed Only', value: 'true' },
{ label: 'Incomplete Only', value: 'false' }
]
}
}),
task_type: Property.StaticDropdown({
displayName: 'Task Type',
description: 'Filter by task type',
required: false,
options: {
options: [
{ label: 'All Tasks', value: 'all' },
{ label: 'Parent Tasks Only', value: 'parents' },
{ label: 'Subtasks Only', value: 'subtasks' }
]
}
}),
updated_since: Property.DateTime({
displayName: 'Updated Since',
description: 'Only return tasks updated on or after this date/time',
required: false
}),
updated_before: Property.DateTime({
displayName: 'Updated Before',
description: 'Only return tasks updated on or before this date/time',
required: false
}),
limit: Property.Number({
displayName: 'Result Limit',
description: 'Maximum number of tasks to return (default: 50, max: 1000)',
required: false,
defaultValue: 50
}),
return_single_result: Property.Checkbox({
displayName: 'Return Single Result Only',
description: 'If checked, returns only the first matching task. If unchecked, returns all matching tasks.',
required: false,
defaultValue: false
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const hasSearchCriteria =
propsValue.task_id ||
propsValue.resource_type ||
(propsValue as any).resource_record?.resource_id ||
propsValue.assigned_to ||
propsValue.assigned_to_team ||
propsValue.created_by ||
propsValue.completed ||
propsValue.task_type ||
propsValue.updated_since ||
propsValue.updated_before;
if (!hasSearchCriteria) {
throw new Error('At least one search criteria must be provided (Task ID, resource, assignment, status, or date filters)');
}
if (propsValue.task_id && !(propsValue.resource_type || (propsValue as any).resource_record?.resource_id || propsValue.assigned_to || propsValue.assigned_to_team || propsValue.created_by || propsValue.completed || propsValue.task_type || propsValue.updated_since || propsValue.updated_before)) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${WEALTHBOX_API_BASE}/tasks/${propsValue.task_id}`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Accept': 'application/json'
}
});
if (response.status >= 400) {
handleApiError('find task by ID', response.status, response.body);
}
return {
found: true,
task: response.body,
tasks: [response.body],
total_results: 1,
search_criteria: { task_id: propsValue.task_id }
};
} catch (error) {
if (error instanceof Error && error.message.includes('404')) {
return {
found: false,
task: null,
tasks: [],
total_results: 0,
message: `No task found with ID: ${propsValue.task_id}`,
search_criteria: { task_id: propsValue.task_id }
};
}
throw error;
}
}
const searchParams = new URLSearchParams();
if (propsValue.task_id) searchParams.append('id', propsValue.task_id.toString());
if (propsValue.resource_type) searchParams.append('resource_type', propsValue.resource_type);
const resourceRecord = (propsValue as any).resource_record;
if (resourceRecord?.resource_id) {
searchParams.append('resource_id', resourceRecord.resource_id.toString());
}
if (propsValue.assigned_to) searchParams.append('assigned_to', propsValue.assigned_to);
if (propsValue.assigned_to_team) searchParams.append('assigned_to_team', propsValue.assigned_to_team.toString());
if (propsValue.created_by) searchParams.append('created_by', propsValue.created_by);
if (propsValue.completed && propsValue.completed !== '') {
searchParams.append('completed', propsValue.completed);
}
if (propsValue.task_type && propsValue.task_type !== 'all') {
searchParams.append('task_type', propsValue.task_type);
}
if (propsValue.updated_since) searchParams.append('updated_since', propsValue.updated_since);
if (propsValue.updated_before) searchParams.append('updated_before', propsValue.updated_before);
const limit = Math.min(propsValue.limit || 50, 1000);
searchParams.append('limit', limit.toString());
const queryString = searchParams.toString();
const url = queryString ? `${WEALTHBOX_API_BASE}/tasks?${queryString}` : `${WEALTHBOX_API_BASE}/tasks`;
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: url,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Accept': 'application/json'
}
});
if (response.status >= 400) {
handleApiError('find tasks', response.status, response.body);
}
const tasks = response.body.tasks || [];
const totalResults = tasks.length;
if (propsValue.return_single_result || totalResults === 1) {
return {
found: totalResults > 0,
task: totalResults > 0 ? tasks[0] : null,
tasks: tasks,
total_results: totalResults,
search_criteria: Object.fromEntries(searchParams),
message: totalResults === 0 ? 'No tasks found matching the search criteria' : undefined
};
}
return {
found: totalResults > 0,
tasks: tasks,
total_results: totalResults,
search_criteria: Object.fromEntries(searchParams),
message: totalResults === 0 ? 'No tasks found matching the search criteria' : undefined
};
} catch (error) {
throw new Error(`Failed to find tasks: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});

View File

@@ -0,0 +1,11 @@
export { createContact } from './create-contact';
export { createNote } from './create-note';
export { createProject } from './create-project';
export { addHouseholdMember } from './add-household-member';
export { createHousehold } from './create-household';
export { createEvent } from './create-event';
export { createOpportunity } from './create-opportunity';
export { createTask } from './create-task';
export { startWorkflow } from './start-workflow';
export { findContact } from './find-contact';
export { findTask } from './find-task';

View File

@@ -0,0 +1,376 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fetchWorkflowTemplates, fetchContacts, fetchProjects, fetchOpportunities, fetchUserGroups, WEALTHBOX_API_BASE, handleApiError } from '../common';
import { wealthboxAuth } from '../..';
export const startWorkflow = createAction({
name: 'start_workflow',
displayName: 'Start Workflow',
auth: wealthboxAuth,
description: 'Triggers a workflow template on a contact/project/opportunity. Automate multi-step sequences based on CRM events.',
props: {
workflow_template: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Workflow Template',
description: 'Select the workflow template to trigger',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const templates = await fetchWorkflowTemplates(auth.secret_text);
return {
options: templates.map((template: any) => ({
label: template.name || template.label || `Template ${template.id}`,
value: template.id
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load workflow templates. Please check your authentication.'
};
}
}
}),
linked_type: Property.StaticDropdown({
displayName: 'Link To',
description: 'What type of record to link this workflow to',
required: true,
options: {
options: [
{ label: 'Contact', value: 'Contact' },
{ label: 'Project', value: 'Project' },
{ label: 'Opportunity', value: 'Opportunity' }
]
}
}),
linked_record: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Linked Record',
description: 'Select the record to link this workflow to',
required: true,
refreshers: ['linked_type'],
props: async ({ auth, linked_type }) => {
if (!auth) {
return {
linked_id: Property.Number({
displayName: 'Record ID',
description: 'Enter the record ID manually',
required: true
}),
linked_name: Property.ShortText({
displayName: 'Record Name',
description: 'Enter the record name for reference',
required: false
})
};
}
try {
let records: any[] = [];
let recordType = '';
const linkedTypeValue = linked_type as unknown as string;
switch (linkedTypeValue) {
case 'Contact':
records = await fetchContacts(auth.secret_text, { active: true, order: 'recent' });
recordType = 'Contact';
break;
case 'Project':
records = await fetchProjects(auth.secret_text);
recordType = 'Project';
break;
case 'Opportunity':
records = await fetchOpportunities(auth.secret_text);
recordType = 'Opportunity';
break;
default:
return {
linked_id: Property.Number({
displayName: 'Record ID',
description: 'Enter the record ID manually',
required: true
}),
linked_name: Property.ShortText({
displayName: 'Record Name',
description: 'Enter the record name for reference',
required: false
})
};
}
const recordOptions = records.map((record: any) => ({
label: record.name || record.title || record.label || `${recordType} ${record.id}`,
value: record.id
}));
return {
linked_id: Property.StaticDropdown({
displayName: `${recordType} Record`,
description: `Select the ${recordType.toLowerCase()} to link this workflow to`,
required: true,
options: {
options: recordOptions
}
}),
linked_name: Property.ShortText({
displayName: 'Record Name',
description: 'The name will be automatically populated from the selected record',
required: false
})
};
} catch (error) {
return {
linked_id: Property.Number({
displayName: 'Record ID',
description: 'Enter the record ID manually (API unavailable)',
required: true
}),
linked_name: Property.ShortText({
displayName: 'Record Name',
description: 'Enter the record name for reference',
required: false
})
};
}
}
}),
label: Property.ShortText({
displayName: 'Workflow Label',
description: 'A short name for the workflow (e.g., "Onboard a new client to the firm")',
required: false
}),
starts_at: Property.DateTime({
displayName: 'Start Date & Time',
description: 'When you want the workflow to start (optional, defaults to now)',
required: false
}),
visible_to: Property.Dropdown({
auth: wealthboxAuth,
displayName: 'Visible To',
description: 'Select who can view this workflow',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { options: [] };
try {
const userGroups = await fetchUserGroups(auth.secret_text);
return {
options: userGroups.map((group: any) => ({
label: group.name,
value: group.name
}))
};
} catch (error) {
return {
options: [],
error: 'Failed to load user groups. Please check your authentication.'
};
}
}
}),
workflow_milestones: Property.DynamicProperties({
auth: wealthboxAuth,
displayName: 'Workflow Milestones',
description: 'Add milestones to this workflow',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) {
return {
milestones_array: Property.Array({
displayName: 'Milestones',
description: 'Add workflow milestones',
required: false,
properties: {
milestone_id: Property.ShortText({
displayName: 'Milestone ID',
description: 'The ID of the milestone',
required: true
}),
milestone_name: Property.ShortText({
displayName: 'Milestone Name',
description: 'The name of the milestone',
required: true
}),
milestone_date: Property.DateTime({
displayName: 'Milestone Date',
description: 'When this milestone should occur',
required: false
})
}
})
};
}
try {
return {
milestones_array: Property.Array({
displayName: 'Milestones',
description: 'Add workflow milestones for this template',
required: false,
properties: {
milestone_id: Property.ShortText({
displayName: 'Milestone ID',
description: 'The ID of the milestone (from template)',
required: true
}),
milestone_name: Property.ShortText({
displayName: 'Milestone Name',
description: 'The name of the milestone',
required: true
}),
milestone_date: Property.DateTime({
displayName: 'Milestone Date',
description: 'When this milestone should occur',
required: false
})
}
})
};
} catch (error) {
console.warn('Could not fetch milestones for validation:', error);
return {
milestones_array: Property.Array({
displayName: 'Milestones',
description: 'Add workflow milestones (API unavailable)',
required: false,
properties: {
milestone_id: Property.ShortText({
displayName: 'Milestone ID',
description: 'The ID of the milestone',
required: true
}),
milestone_name: Property.ShortText({
displayName: 'Milestone Name',
description: 'The name of the milestone',
required: true
}),
milestone_date: Property.DateTime({
displayName: 'Milestone Date',
description: 'When this milestone should occur',
required: false
})
}
})
};
}
}
})
},
async run(context) {
const { auth, propsValue } = context;
if (!auth) {
throw new Error('Authentication is required');
}
const requestBody: any = {
workflow_template: propsValue.workflow_template
};
const linkedRecord = (propsValue as any).linked_record;
if (linkedRecord) {
let linkedId: number;
let linkedName: string;
if (linkedRecord.linked_id) {
linkedId = linkedRecord.linked_id;
linkedName = linkedRecord.linked_name || `${propsValue.linked_type} ${linkedId}`;
} else {
linkedId = linkedRecord.linked_id;
linkedName = linkedRecord.linked_name || `${propsValue.linked_type} ${linkedId}`;
}
try {
let recordName = linkedName;
if (propsValue.linked_type === 'Contact' && linkedId) {
const contacts = await fetchContacts(auth.secret_text, { active: true });
const contact = contacts.find((c: any) => c.id === linkedId);
if (contact) {
recordName = contact.name || `${contact.first_name} ${contact.last_name}`.trim();
}
} else if (propsValue.linked_type === 'Project' && linkedId) {
const projects = await fetchProjects(auth.secret_text);
const project = projects.find((p: any) => p.id === linkedId);
if (project) {
recordName = project.name || project.title || `Project ${linkedId}`;
}
} else if (propsValue.linked_type === 'Opportunity' && linkedId) {
const opportunities = await fetchOpportunities(auth.secret_text);
const opportunity = opportunities.find((o: any) => o.id === linkedId);
if (opportunity) {
recordName = opportunity.name || `Opportunity ${linkedId}`;
}
}
requestBody.linked_to = {
id: linkedId,
type: propsValue.linked_type,
name: recordName
};
} catch (error) {
requestBody.linked_to = {
id: linkedId,
type: propsValue.linked_type,
name: linkedName
};
}
}
if (propsValue.label) {
requestBody.label = propsValue.label;
}
if (propsValue.starts_at) {
requestBody.starts_at = propsValue.starts_at;
}
if (propsValue.visible_to) {
requestBody.visible_to = propsValue.visible_to;
}
const milestonesArray = (propsValue as any).workflow_milestones?.milestones_array;
if (milestonesArray && Array.isArray(milestonesArray) && milestonesArray.length > 0) {
requestBody.workflow_milestones = milestonesArray.map((milestone: any) => ({
id: milestone.milestone_id,
name: milestone.milestone_name,
...(milestone.milestone_date && { milestone_date: milestone.milestone_date })
}));
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${WEALTHBOX_API_BASE}/workflows`,
headers: {
'ACCESS_TOKEN': auth.secret_text,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status >= 400) {
handleApiError('start workflow', response.status, response.body);
}
return response.body;
} catch (error) {
throw new Error(`Failed to start workflow: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});