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,231 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { avomaCommon } from '../common';
export const createCall = createAction({
auth: avomaCommon.avomaAuth,
name: 'create_call',
displayName: 'Create Call',
description: 'Creates a new call in Avoma',
props: {
external_id: Property.ShortText({
displayName: 'External ID',
description: 'Unique id of the call from the dialer system like hubspot, twilio, zoom, etc.',
required: true
}),
user_email: Property.ShortText({
displayName: 'User Email',
description: 'Email of the user who made or received the call. This should be an Avoma user\'s email.',
required: true
}),
source: Property.StaticDropdown({
displayName: 'Source',
description: 'Source of the call',
required: true,
options: {
options: [
{ label: 'Zoom', value: 'zoom' },
{ label: 'Zoom Phone', value: 'zoomphone' },
{ label: 'Twilio', value: 'twilio' },
{ label: 'PhoneBurner', value: 'phoneburner' },
{ label: 'RingCentral', value: 'ringcentral' },
{ label: 'Aircall', value: 'aircall' },
{ label: 'HubSpot', value: 'hubspot' },
{ label: 'Other', value: 'other' }
]
}
}),
direction: Property.StaticDropdown({
displayName: 'Direction',
description: 'Direction of the call',
required: true,
options: {
options: [
{ label: 'Inbound', value: 'Inbound' },
{ label: 'Outbound', value: 'Outbound' }
]
}
}),
start_at: Property.DateTime({
displayName: 'Start Time',
description: 'Start time of the call',
required: true
}),
frm: Property.ShortText({
displayName: 'From Phone Number',
description: 'Phone number from which call was made (e.g., +11234567890)',
required: true
}),
to: Property.ShortText({
displayName: 'To Phone Number',
description: 'Phone number to which call was made (e.g., +12234567890)',
required: true
}),
recording_url: Property.LongText({
displayName: 'Recording URL',
description: 'URL of the recording of the call. This should be a public URL that Avoma can access.',
required: true
}),
participants: Property.Array({
displayName: 'Participants',
description: 'List of participants in the call. First entry should be the prospect/lead.',
required: true,
properties: {
name: Property.ShortText({
displayName: 'Name',
description: 'Name of the participant',
required: true
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Email of the participant',
required: true
}),
crm_id: Property.ShortText({
displayName: 'CRM Record ID',
description: 'CRM record ID for this participant (optional)',
required: false
}),
crm_type: Property.ShortText({
displayName: 'CRM Record Type',
description: 'Type of CRM record (e.g., contact, lead, opportunity)',
required: false
}),
crm_system: Property.ShortText({
displayName: 'CRM System',
description: 'CRM system name (e.g., hubspot, salesforce)',
required: false
})
}
}),
end_at: Property.DateTime({
displayName: 'End Time',
description: 'End time of the call',
required: false
}),
frm_name: Property.ShortText({
displayName: 'From Name',
description: 'Name of the caller who made the call',
required: false
}),
to_name: Property.ShortText({
displayName: 'To Name',
description: 'Name of the person to whom call was made',
required: false
}),
answered: Property.Checkbox({
displayName: 'Answered',
description: 'Whether the call was answered',
required: false
}),
is_voicemail: Property.Checkbox({
displayName: 'Is Voicemail',
description: 'Indicates if the call is a voicemail',
required: false,
defaultValue: false
}),
additional_details: Property.LongText({
displayName: 'Additional Details',
description: 'Additional details of the call (JSON object as string)',
required: false
})
},
async run(context) {
const { auth, propsValue } = context;
const participants = propsValue.participants?.map((participant: any) => {
const participantData: any = {
email: participant.email,
name: participant.name
};
if (participant.crm_id && participant.crm_type && participant.crm_system) {
participantData.associations = [{
id: participant.crm_id,
type: participant.crm_type,
system: participant.crm_system
}];
}
return participantData;
}) || [];
const requestBody: any = {
external_id: propsValue.external_id,
user_email: propsValue.user_email,
source: propsValue.source,
direction: propsValue.direction,
start_at: propsValue.start_at,
frm: propsValue.frm,
to: propsValue.to,
recording_url: propsValue.recording_url,
participants: participants
};
if (propsValue.end_at) {
requestBody.end_at = propsValue.end_at;
}
if (propsValue.frm_name) {
requestBody.frm_name = propsValue.frm_name;
}
if (propsValue.to_name) {
requestBody.to_name = propsValue.to_name;
}
if (propsValue.answered !== undefined) {
requestBody.answered = propsValue.answered;
}
if (propsValue.is_voicemail !== undefined) {
requestBody.is_voicemail = propsValue.is_voicemail;
}
if (propsValue.additional_details) {
try {
requestBody.additional_details = JSON.parse(propsValue.additional_details);
} catch (e) {
requestBody.additional_details = propsValue.additional_details;
}
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: 'https://api.avoma.com/v1/calls/',
headers: {
'Authorization': `Bearer ${auth.secret_text}`,
'Content-Type': 'application/json'
},
body: requestBody
});
if (response.status === 400) {
const errorDetails = response.body;
let errorMessage = 'Bad request: ';
if (typeof errorDetails === 'object') {
const fieldErrors = Object.entries(errorDetails)
.map(([field, errors]) => `${field}: ${Array.isArray(errors) ? errors.join(', ') : errors}`)
.join('; ');
errorMessage += fieldErrors;
} else {
errorMessage += JSON.stringify(errorDetails) || 'Invalid input data';
}
if (errorMessage.includes('external_id') && errorMessage.includes('source')) {
errorMessage += '. This may be due to a duplicate call (same external_id and source already exists)';
} else if (errorMessage.includes('user_email')) {
errorMessage += '. Check that the user exists, is active, belongs to your organization, and has dialer permissions';
}
throw new Error(errorMessage);
}
if (response.status >= 400) {
throw new Error(`API error (${response.status}): ${JSON.stringify(response.body) || 'Unknown error'}`);
}
return response.body;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create call: ${errorMessage}`);
}
}
});

View File

@@ -0,0 +1,73 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { avomaCommon } from '../common';
export const getMeetingRecording = createAction({
auth: avomaCommon.avomaAuth,
name: 'get_meeting_recording',
displayName: 'Get Meeting Recording',
description: 'Returns video and audio recording URLs for a given meeting',
props: {
meeting_uuid: avomaCommon.meetingDropdown
},
async run(context) {
const { auth, propsValue } = context;
const meetingUuid = propsValue.meeting_uuid;
if (!meetingUuid) {
throw new Error('Please select a meeting from the dropdown');
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://api.avoma.com/v1/recordings/?meeting_uuid=${meetingUuid}`,
headers: {
'Authorization': `Bearer ${auth.secret_text}`,
'Content-Type': 'application/json'
}
});
if (response.status === 200) {
return {
success: true,
status: 'available',
meeting_uuid: response.body.meeting_uuid,
recording_uuid: response.body.uuid,
audio_url: response.body.audio_url,
video_url: response.body.video_url,
valid_till: response.body.valid_till,
urls_expire_in_days: 5
};
} else if (response.status === 202) {
return {
success: true,
status: 'processing',
meeting_uuid: response.body.meeting_uuid,
recording_uuid: response.body.uuid,
message: response.body.message || 'Recording is being processed and not yet available',
note: 'Check audio_ready and video_ready parameters in the meeting details to verify recording status'
};
} else if (response.status === 403) {
throw new Error(`Permission denied: ${response.body.detail || 'You do not have permission to access this recording'}`);
} else if (response.status === 404) {
throw new Error(`Recording not found: ${response.body.detail || 'No recording exists for this meeting'}`);
} else if (response.status === 429) {
throw new Error(`Rate limit exceeded: ${response.body.detail || 'Too many requests. Please try again later'}`);
} else if (response.status >= 400) {
throw new Error(`API error (${response.status}): ${JSON.stringify(response.body) || 'Unknown error'}`);
}
return {
success: false,
status: 'unknown',
meeting_uuid: meetingUuid,
error: 'Unexpected response from API'
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to retrieve meeting recording for UUID ${meetingUuid}: ${errorMessage}`);
}
}
});

View File

@@ -0,0 +1,57 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { avomaCommon } from '../common';
export const getMeetingTranscription = createAction({
auth: avomaCommon.avomaAuth,
name: 'get_meeting_transcription',
displayName: 'Get Meeting Transcription',
description: 'Returns transcription with speakers, timestamps, and VTT file URL',
props: {
transcription_uuid: avomaCommon.transcriptionDropdown
},
async run(context) {
const { auth, propsValue } = context;
const transcriptionUuid = propsValue.transcription_uuid;
if (!transcriptionUuid) {
throw new Error('Please select a transcription from the dropdown');
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://api.avoma.com/v1/transcriptions/${transcriptionUuid}`,
headers: {
'Authorization': `Bearer ${auth}`,
'Content-Type': 'application/json'
}
});
if (response.status === 404) {
const errorDetail = response.body?.detail || 'Transcription not found';
throw new Error(`Transcription not found: ${errorDetail}`);
} else if (response.status >= 400) {
const errorDetail = response.body?.detail || JSON.stringify(response.body) || 'Unknown error';
throw new Error(`API error (${response.status}): ${errorDetail}`);
}
const transcription = response.body;
return {
success: true,
meeting_uuid: transcription.meeting_uuid,
transcription_uuid: transcription.uuid,
transcription_vtt_url: transcription.transcription_vtt_url,
speakers: transcription.speakers || [],
transcript: transcription.transcript || [],
transcript_paragraphs_count: transcription.transcript?.length || 0,
speakers_count: transcription.speakers?.length || 0,
note: 'Use transcription_vtt_url to download the VTT subtitle file for the meeting'
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to retrieve transcription: ${errorMessage}`);
}
}
});

View File

@@ -0,0 +1,3 @@
export { createCall } from './create-call';
export { getMeetingRecording } from './get-meeting-recording';
export { getMeetingTranscription } from './get-meeting-transcription';

View File

@@ -0,0 +1,155 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
const avomaAuth = PieceAuth.SecretText({
displayName: 'API Key',
description:
'Your Avoma API Key (Bearer token). Generate it from your Avoma API Integration settings: https://help.avoma.com/api-integration-for-avoma',
required: true
});
export const avomaCommon = {
avomaAuth: avomaAuth,
meetingDropdown: Property.Dropdown({
auth: avomaAuth,
displayName: 'Meeting',
description: 'Select a meeting from your Avoma account',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your Avoma account first',
options: []
};
}
try {
const toDate = new Date().toISOString();
const fromDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://api.avoma.com/v1/meetings/?page_size=100&from_date=${encodeURIComponent(fromDate)}&to_date=${encodeURIComponent(toDate)}`,
headers: {
'Authorization': `Bearer ${auth.secret_text}`,
'Content-Type': 'application/json'
}
});
const meetings = response.body?.results || response.body?.data || response.body || [];
if (!Array.isArray(meetings)) {
return {
disabled: true,
placeholder: 'No meetings found or unexpected response format',
options: []
};
}
return {
disabled: false,
options: meetings.map((meeting: any) => {
const subject = meeting.subject || meeting.title || 'Untitled Meeting';
const startTime = meeting.start_time || meeting.startTime || meeting.created_at;
const dateStr = startTime ? new Date(startTime).toLocaleDateString() : 'Unknown date';
const uuid = meeting.uuid || meeting.id;
return {
label: `${subject} - ${dateStr}`,
value: uuid
};
})
};
} catch (error) {
console.error('Error fetching meetings:', error);
return {
disabled: true,
placeholder: 'Failed to load meetings. Please check your API key.',
options: []
};
}
}
}),
transcriptionDropdown: Property.Dropdown({
auth: avomaAuth,
displayName: 'Transcription',
description: 'Select a transcription from your Avoma meetings',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your Avoma account first',
options: []
};
}
try {
const toDate = new Date().toISOString();
const fromDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://api.avoma.com/v1/transcriptions/?page_size=100&from_date=${encodeURIComponent(fromDate)}&to_date=${encodeURIComponent(toDate)}`,
headers: {
'Authorization': `Bearer ${auth.secret_text}`,
'Content-Type': 'application/json'
}
});
let transcriptions = [];
if (response.body?.results) {
transcriptions = response.body.results;
} else if (Array.isArray(response.body)) {
transcriptions = response.body;
} else if (response.body && typeof response.body === 'object') {
transcriptions = [response.body];
}
if (!Array.isArray(transcriptions)) {
return {
disabled: true,
placeholder: 'Unexpected response format from API',
options: []
};
}
return {
disabled: false,
options: transcriptions.map((transcription: any) => {
const transcriptionUuid = transcription.uuid;
const speakersCount = transcription.speakers?.length || 0;
const transcriptLength = transcription.transcript?.length || 0;
return {
label: `Transcription ${transcriptionUuid.substring(0, 8)}... - ${speakersCount} speakers, ${transcriptLength} paragraphs`,
value: transcriptionUuid
};
})
};
} catch (error) {
console.error('Error fetching transcriptions:', error);
return {
disabled: true,
placeholder: 'Failed to load transcriptions. Please check your API key.',
options: []
};
}
}
}),
meetingUuidProperty: Property.ShortText({
displayName: 'Meeting UUID',
description: 'Enter the meeting UUID directly, or use the dropdown below to select from your meetings',
required: false
}),
transcriptionUuidProperty: Property.ShortText({
displayName: 'Transcription UUID',
description: 'Enter the transcription UUID directly, or use the dropdown below to select from available transcriptions',
required: false
})
};

View File

@@ -0,0 +1,4 @@
export { newNote } from './new-note';
export { newMeetingScheduled } from './new-meeting-scheduled';
export { meetingRescheduled } from './meeting-rescheduled';
export { meetingCancelled } from './meeting-cancelled';

View File

@@ -0,0 +1,67 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
export const meetingCancelled = createTrigger({
name: 'meeting_cancelled',
displayName: 'Meeting Cancelled',
description: 'Triggers when a meeting booked via the scheduling page is cancelled',
type: TriggerStrategy.WEBHOOK,
props: {
setupInstructions: Property.MarkDown({
value: `
**Quick Setup:**
1. In Avoma: **Settings > Integrations > Webhooks**
2. **Webhook URL:** \`{{webhookUrl}}\`
3. **Event Type:** Select **"MEETING_BOOKED_VIA_SCHEDULER_CANCELED"**
4. **HTTP Method:** POST
5. **Content Type:** application/json
**Note:** Requires admin permissions in Avoma.
`,
}),
},
sampleData: {
booker_email: 'client@example.com',
cancel_reason: 'Schedule conflict - need to reschedule for next week',
conference_link: 'https://zoom.us/j/123456789',
created: '2019-08-24T14:15:22Z',
event_end_time: '2019-08-24T15:15:22Z',
event_start_time: '2019-08-24T14:15:22Z',
event_type: 'MEETING_BOOKED_VIA_SCHEDULER_CANCELED',
invitee_details: {
email: 'client@example.com',
locale: 'en-US',
name: 'John Client',
tz: 'America/New_York'
},
invitee_responses: [
{
question: 'What would you like to discuss?',
response: 'Product demo and pricing discussion'
},
{
question: 'Company size?',
response: '50-100 employees'
}
],
meeting_uuid: '65fb768c-30b9-4a9a-999f-5dab85e66635',
modified: '2019-08-24T16:30:22Z',
organizer_email: 'sales@company.com',
organizer_timezone: 'America/New_York',
purpose: 'Sales Demo',
scheduling_page_link: 'https://meet.avoma.com/sales-demo',
subject: 'Product Demo - John Client (Cancelled)',
uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'
},
async onEnable(context) {
// Manual setup - no programmatic registration needed
},
async onDisable(context) {
// Manual setup - users manage webhooks in Avoma UI
},
async run(context) {
return [context.payload.body];
}
});

View File

@@ -0,0 +1,67 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
export const meetingRescheduled = createTrigger({
name: 'meeting_rescheduled',
displayName: 'Meeting Rescheduled',
description: 'Triggers when a scheduled meeting is rescheduled',
type: TriggerStrategy.WEBHOOK,
props: {
setupInstructions: Property.MarkDown({
value: `
**Quick Setup:**
1. In Avoma: **Settings > Integrations > Webhooks**
2. **Webhook URL:** \`{{webhookUrl}}\`
3. **Event Type:** Select **"MEETING_BOOKED_VIA_SCHEDULER_RESCHEDULED"**
4. **HTTP Method:** POST
5. **Content Type:** application/json
**Note:** Requires admin permissions in Avoma.
`,
}),
},
sampleData: {
booker_email: 'client@example.com',
cancel_reason: null,
conference_link: 'https://zoom.us/j/123456789',
created: '2019-08-24T14:15:22Z',
event_end_time: '2019-08-25T15:15:22Z',
event_start_time: '2019-08-25T14:15:22Z',
event_type: 'MEETING_BOOKED_VIA_SCHEDULER_RESCHEDULED',
invitee_details: {
email: 'client@example.com',
locale: 'en-US',
name: 'John Client',
tz: 'America/New_York'
},
invitee_responses: [
{
question: 'What would you like to discuss?',
response: 'Product demo and pricing discussion'
},
{
question: 'Company size?',
response: '50-100 employees'
}
],
meeting_uuid: '65fb768c-30b9-4a9a-999f-5dab85e66635',
modified: '2019-08-24T16:30:22Z',
organizer_email: 'sales@company.com',
organizer_timezone: 'America/New_York',
purpose: 'Sales Demo',
scheduling_page_link: 'https://meet.avoma.com/sales-demo',
subject: 'Product Demo - John Client (Rescheduled)',
uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'
},
async onEnable(context) {
// Manual setup - no programmatic registration needed
},
async onDisable(context) {
// Manual setup - users manage webhooks in Avoma UI
},
async run(context) {
return [context.payload.body];
}
});

View File

@@ -0,0 +1,67 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
export const newMeetingScheduled = createTrigger({
name: 'new_meeting_scheduled',
displayName: 'New Meeting Scheduled',
description: 'Triggers when a meeting is booked via one of your Avoma scheduling pages',
type: TriggerStrategy.WEBHOOK,
props: {
setupInstructions: Property.MarkDown({
value: `
**Quick Setup:**
1. In Avoma: **Settings > Integrations > Webhooks**
2. **Webhook URL:** \`{{webhookUrl}}\`
3. **Event Type:** Select **"MEETING_BOOKED_VIA_SCHEDULER"**
4. **HTTP Method:** POST
5. **Content Type:** application/json
**Note:** Requires admin permissions in Avoma.
`,
}),
},
sampleData: {
booker_email: 'client@example.com',
cancel_reason: null,
conference_link: 'https://zoom.us/j/123456789',
created: '2019-08-24T14:15:22Z',
event_end_time: '2019-08-24T15:15:22Z',
event_start_time: '2019-08-24T14:15:22Z',
event_type: 'MEETING_BOOKED_VIA_SCHEDULER',
invitee_details: {
email: 'client@example.com',
locale: 'en-US',
name: 'John Client',
tz: 'America/New_York'
},
invitee_responses: [
{
question: 'What would you like to discuss?',
response: 'Product demo and pricing discussion'
},
{
question: 'Company size?',
response: '50-100 employees'
}
],
meeting_uuid: '65fb768c-30b9-4a9a-999f-5dab85e66635',
modified: '2019-08-24T14:15:22Z',
organizer_email: 'sales@company.com',
organizer_timezone: 'America/New_York',
purpose: 'Sales Demo',
scheduling_page_link: 'https://meet.avoma.com/sales-demo',
subject: 'Product Demo - John Client',
uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'
},
async onEnable(context) {
// Manual setup - no programmatic registration needed
},
async onDisable(context) {
// Manual setup - users manage webhooks in Avoma UI
},
async run(context) {
return [context.payload.body];
}
});

View File

@@ -0,0 +1,94 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
export const newNote = createTrigger({
name: 'new_note',
displayName: 'New Note',
description:
'Triggers when notes are successfully generated for meetings or calls',
type: TriggerStrategy.WEBHOOK,
props: {
setupInstructions: Property.MarkDown({
value: `
**Quick Setup:**
1. In Avoma: **Settings > Integrations > Webhooks**
2. **Webhook URL:** \`{{webhookUrl}}\`
3. **Event Type:** Select **"AINOTE"**
4. **HTTP Method:** POST
5. **Content Type:** application/json
**Note:** Requires admin permissions in Avoma.
`,
}),
},
sampleData: {
action_items: [
{
action_item: 'Follow up with client on project timeline',
company: 'Avoma',
email: 'john.doe@avoma.com',
name: 'John Doe'
}
],
ai_notes: '<h2>Participants</h2><ul><li><p>Avoma: John Doe</p></li></ul>',
ai_notes_txt: 'Participants: Avoma: John Doe',
attendees: [
{
email: 'user@example.com',
name: 'John Doe',
response_status: 'accepted',
uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'
}
],
audio_ready: true,
audio_url: 'http://example.com',
created: '2019-08-24T14:15:22Z',
duration: 3600,
end_at: '2019-08-24T14:15:22Z',
event_type: 'AINOTE',
is_call: true,
is_internal: false,
meeting_url:
'https://app.avoma.com/meeting/aa380fc2-725d-4ef8-909c-c595c0e62bcd',
modified: '2019-08-24T14:15:22Z',
notes_ready: true,
organizer_email: 'user@example.com',
organizer_name: 'John Doe',
privacy: 'private',
processing_status: 'completed',
insights: {
filler_wpm: [],
longest_monologue: {},
patience: [],
sentiment: {},
speaker_mapping: {},
talk_stats: {},
wpm: []
},
purpose: {
label: 'Sales Call',
uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f'
},
recording_uuid: '3e256a03-2cdd-4fe9-823b-2b61bb25d916',
start_at: '2019-08-24T14:15:22Z',
state: 'completed',
subject: 'Weekly Team Meeting',
transcript_ready: true,
transcription_uuid: '4753c6bf-25a6-45a1-8d86-0af7c8bde615',
transcription_vtt_url: 'http://example.com',
uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f',
video_ready: true,
video_url: 'http://example.com'
},
async onEnable(context) {
// Manual setup - no programmatic registration needed
},
async onDisable(context) {
// Manual setup - users manage webhooks in Avoma UI
},
async run(context) {
return [context.payload.body];
}
});