Configure Recall bot server variables (#21774)

## What changed

- Added server variables for Recall bot leave behavior
- Added `RECALL_BOT_JOIN_EARLY_MINUTES` so the bot can join slightly
before meeting start
- Defaults stay aligned with Recall where applicable
- Kept descriptions more human-friendly in app config + README


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21774?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
nitin
2026-06-18 18:28:25 +05:30
committed by GitHub
parent e7488deb58
commit 06cefb1dac
25 changed files with 210 additions and 52 deletions
@@ -17,7 +17,9 @@ Run `yarn twenty help` to list all available commands.
## Recall.ai configuration
This app schedules Recall.ai meeting bots and ingests their lifecycle events. A server admin configures it through server variables on the application registration (Settings → Applications → Twenty Meeting Bot):
This app schedules Recall.ai meeting bots and ingests their lifecycle events.
A server admin configures Recall credentials through server variables on the application registration (Settings → Applications → Twenty Meeting Bot):
| Server variable | Required | Purpose |
| --- | --- | --- |
@@ -25,6 +27,16 @@ This app schedules Recall.ai meeting bots and ingests their lifecycle events. A
| `RECALL_REGION` | No | Recall.ai region for API requests. Defaults to `eu-central-1`. |
| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. |
A workspace admin can adjust bot behavior through application variables:
| Application variable | Default | Purpose |
| --- | --- | --- |
| `RECALL_BOT_NAME` | `Twenty Meeting Bot` | Display name used when scheduling Recall.ai meeting bots. |
| `RECALL_BOT_JOIN_EARLY_MINUTES` | `1` | How many minutes before the meeting start time the bot should join. Set to `0` to join at the scheduled start time. |
| `RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS` | `1200` | How many seconds the bot waits in a meeting lobby before giving up and leaving. |
| `RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS` | `1200` | How many seconds the bot stays in an empty meeting when no one else ever joins. |
| `RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS` | `2` | How many seconds the bot keeps recording after everyone else leaves the meeting. |
### Configuring the webhook
The app exposes an unauthenticated route, `POST /webhook/recall`, that verifies the Recall/Svix signature and updates the matching `CallRecording`'s lifecycle status (`JOINING``RECORDING``PROCESSING`, or `FAILED_UNKNOWN`).
@@ -3,11 +3,23 @@ import { defineApplication } from 'twenty-sdk/define';
import { APP_DESCRIPTION } from 'src/constants/app-description';
import { APP_DISPLAY_NAME } from 'src/constants/app-display-name';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/application-universal-identifier';
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-everyone-left-timeout-seconds-app-variable-universal-identifier';
import { RECALL_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-join-early-minutes-app-variable-universal-identifier';
import { RECALL_BOT_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-name-app-variable-universal-identifier';
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-noone-joined-timeout-seconds-app-variable-universal-identifier';
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-bot-waiting-room-timeout-seconds-app-variable-universal-identifier';
import { DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES } from 'src/logic-functions/constants/default-recall-bot-join-early-minutes';
import { DEFAULT_RECALL_BOT_NAME } from 'src/logic-functions/constants/default-recall-bot-name';
import { DEFAULT_RECALL_REGION } from 'src/logic-functions/constants/default-recall-region';
import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name';
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-everyone-left-timeout-seconds';
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-everyone-left-timeout-seconds-env-var-name';
import { RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-join-early-minutes-env-var-name';
import { RECALL_BOT_NAME_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-name-env-var-name';
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds';
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds-env-var-name';
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds';
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds-env-var-name';
import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name';
import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name';
@@ -23,6 +35,38 @@ export default defineApplication({
isSecret: false,
value: DEFAULT_RECALL_BOT_NAME,
},
[RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME]: {
universalIdentifier:
RECALL_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
description:
'How many minutes before the meeting start time the bot should join. Set to 0 to join at the scheduled start time.',
isSecret: false,
value: String(DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES),
},
[RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
universalIdentifier:
RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
description:
'How many seconds the bot waits in a meeting lobby before giving up and leaving.',
isSecret: false,
value: String(RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS),
},
[RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
universalIdentifier:
RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
description:
'How many seconds the bot stays in an empty meeting when no one else ever joins.',
isSecret: false,
value: String(RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS),
},
[RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME]: {
universalIdentifier:
RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER,
description:
'How many seconds the bot keeps recording after everyone else leaves the meeting.',
isSecret: false,
value: String(RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS),
},
},
serverVariables: {
[RECALL_API_KEY_ENV_VAR_NAME]: {
@@ -0,0 +1,2 @@
export const RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
'c866ddd4-fb7b-4cb4-8ad1-5599755e495c';
@@ -0,0 +1,2 @@
export const RECALL_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
'0568ebb2-3f64-47de-8c0d-d367dfbb7462';
@@ -0,0 +1,2 @@
export const RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
'241180e8-d864-4160-ad02-db44a9e8d395';
@@ -0,0 +1,2 @@
export const RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
'12e4e14d-d539-4d07-b477-4773539dd20b';
@@ -0,0 +1 @@
export const DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES = 1;
@@ -0,0 +1 @@
export const MILLISECONDS_PER_MINUTE = 60_000;
@@ -1,7 +1,73 @@
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds';
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds';
import { isUndefined } from '@sniptt/guards';
export const RECALL_BOT_AUTOMATIC_LEAVE = {
waiting_room_timeout: RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS,
noone_joined_timeout: RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS,
import { RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-everyone-left-timeout-seconds-env-var-name';
import { RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-noone-joined-timeout-seconds-env-var-name';
import { RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-waiting-room-timeout-seconds-env-var-name';
import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util';
import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
type RecallBotAutomaticLeave = {
waiting_room_timeout?: number;
noone_joined_timeout?: number;
everyone_left_timeout?: {
timeout: number;
activate_after: number;
};
};
export const getRecallBotAutomaticLeave = ():
| RecallBotAutomaticLeave
| undefined => {
const waitingRoomTimeoutSeconds = getOptionalPositiveIntegerVariable(
RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME,
);
const nooneJoinedTimeoutSeconds = getOptionalPositiveIntegerVariable(
RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME,
);
const everyoneLeftTimeoutSeconds = getOptionalPositiveIntegerVariable(
RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME,
);
const automaticLeave: RecallBotAutomaticLeave = {};
if (!isUndefined(waitingRoomTimeoutSeconds)) {
automaticLeave.waiting_room_timeout = waitingRoomTimeoutSeconds;
}
if (!isUndefined(nooneJoinedTimeoutSeconds)) {
automaticLeave.noone_joined_timeout = nooneJoinedTimeoutSeconds;
}
if (!isUndefined(everyoneLeftTimeoutSeconds)) {
automaticLeave.everyone_left_timeout = {
timeout: everyoneLeftTimeoutSeconds,
activate_after: 0,
};
}
return Object.keys(automaticLeave).length === 0 ? undefined : automaticLeave;
};
const getOptionalPositiveIntegerVariable = (
variableName: string,
): number | undefined => {
const rawValue = normalizeOptionalString(
getApplicationVariableValue(variableName),
);
if (isUndefined(rawValue)) {
return undefined;
}
const timeoutSeconds = Number(rawValue);
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) {
return undefined;
}
return timeoutSeconds;
};
const normalizeOptionalString = (
value: string | undefined,
): string | undefined => (isNonEmptyString(value) ? value.trim() : undefined);
@@ -0,0 +1,2 @@
export const RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME =
'RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS';
@@ -0,0 +1 @@
export const RECALL_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS = 2;
@@ -0,0 +1,2 @@
export const RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME =
'RECALL_BOT_JOIN_EARLY_MINUTES';
@@ -0,0 +1,2 @@
export const RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME =
'RECALL_BOT_NOONE_JOINED_TIMEOUT_SECONDS';
@@ -0,0 +1,2 @@
export const RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME =
'RECALL_BOT_WAITING_ROOM_TIMEOUT_SECONDS';
@@ -1 +1 @@
export const STALE_BOT_STATE_CRON_PATTERN = '*/15 * * * *';
export const STALE_BOT_STATE_CRON_PATTERN = '*/5 * * * *';
@@ -1,9 +1,9 @@
import { isUndefined } from '@sniptt/guards';
import { CALL_RECORDING_MICRO_CREDITS_PER_HOUR } from 'src/logic-functions/constants/call-recording-micro-credits-per-hour';
import { MILLISECONDS_PER_MINUTE } from 'src/logic-functions/constants/milliseconds-per-minute';
const MILLISECONDS_PER_HOUR = 3_600_000;
const MILLISECONDS_PER_MINUTE = 60_000;
export type CallRecordingCharge = {
creditsUsedMicro: number;
@@ -0,0 +1,34 @@
import { DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES } from 'src/logic-functions/constants/default-recall-bot-join-early-minutes';
import { MILLISECONDS_PER_MINUTE } from 'src/logic-functions/constants/milliseconds-per-minute';
import { RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-bot-join-early-minutes-env-var-name';
import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util';
import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
export const computeRecallBotJoinAt = (meetingStartsAt: string): string => {
const meetingStartTimeInMilliseconds = new Date(meetingStartsAt).getTime();
if (Number.isNaN(meetingStartTimeInMilliseconds)) {
return meetingStartsAt;
}
return new Date(
meetingStartTimeInMilliseconds -
getRecallBotJoinEarlyMinutes() * MILLISECONDS_PER_MINUTE,
).toISOString();
};
const getRecallBotJoinEarlyMinutes = (): number => {
const rawValue = getApplicationVariableValue(
RECALL_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME,
);
if (!isNonEmptyString(rawValue)) {
return DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES;
}
const joinEarlyMinutes = Number(rawValue.trim());
return Number.isInteger(joinEarlyMinutes) && joinEarlyMinutes >= 0
? joinEarlyMinutes
: DEFAULT_RECALL_BOT_JOIN_EARLY_MINUTES;
};
@@ -22,6 +22,7 @@ vi.mock('src/logic-functions/recall-api/cancel-recall-bot.util', () => ({
const NOW = new Date('2026-01-01T12:00:00.000Z');
const FUTURE_STARTS_AT = '2026-01-01T13:00:00.000Z';
const FUTURE_RECALL_BOT_JOIN_AT = '2026-01-01T12:59:00.000Z';
const FUTURE_ENDS_AT = '2026-01-01T14:00:00.000Z';
const buildCustomerSyncCallRecordingId = (
@@ -252,7 +253,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
]);
expect(scheduleRecallBotMock).toHaveBeenCalledWith({
meetingUrl: 'https://meet.example.com/customer-sync',
joinAt: FUTURE_STARTS_AT,
joinAt: FUTURE_RECALL_BOT_JOIN_AT,
metadata: {
twentyCallRecordingId: buildCustomerSyncCallRecordingId(),
twentyCalendarEventId: 'calendar-event-1',
@@ -402,7 +403,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
expect(rescheduleRecallBotMock).toHaveBeenCalledWith({
externalBotId: 'recall-bot-1',
meetingUrl: 'https://meet.example.com/customer-sync',
joinAt: FUTURE_STARTS_AT,
joinAt: FUTURE_RECALL_BOT_JOIN_AT,
metadata: {
twentyCallRecordingId: buildCustomerSyncCallRecordingId(),
twentyCalendarEventId: 'calendar-event-1',
@@ -657,6 +658,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
it('cancels the old occurrence and creates a fresh recording when the meeting moves to a new time', async () => {
const NEW_STARTS_AT = '2026-01-02T13:00:00.000Z';
const NEW_RECALL_BOT_JOIN_AT = '2026-01-02T12:59:00.000Z';
const NEW_ENDS_AT = '2026-01-02T14:00:00.000Z';
const client = buildFakeCoreApiClient({
calendarEvents: [
@@ -706,7 +708,7 @@ describe('reconcileMeetingBotForCalendarEventIds', () => {
externalBotId: 'recall-bot-old',
});
expect(scheduleRecallBotMock).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ joinAt: NEW_STARTS_AT }),
expect.objectContaining({ joinAt: NEW_RECALL_BOT_JOIN_AT }),
);
expect(client.callRecordings).toEqual([
expect.objectContaining({
@@ -4,6 +4,7 @@ import { CoreApiClient } from 'twenty-client-sdk/core';
import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util';
import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util';
import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util';
import { scheduleRecallBot } from 'src/logic-functions/recall-api/schedule-recall-bot.util';
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
@@ -14,12 +15,14 @@ export const ensureMeetingBot = async (
{ callRecording, calendarEvent }: MeetingRecording,
): Promise<boolean> => {
const meetingUrl = calendarEvent.conferenceLinkUrl;
const joinAt = calendarEvent.startsAt;
const meetingStartsAt = calendarEvent.startsAt;
if (isUndefined(meetingUrl) || isUndefined(joinAt)) {
if (isUndefined(meetingUrl) || isUndefined(meetingStartsAt)) {
return false;
}
const joinAt = computeRecallBotJoinAt(meetingStartsAt);
const freshCallRecording = (
await findCallRecordingsByIds(client, [callRecording.id])
)[0];
@@ -3,6 +3,7 @@ import { CoreApiClient } from 'twenty-client-sdk/core';
import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util';
import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util';
import { rescheduleRecallBot } from 'src/logic-functions/recall-api/reschedule-recall-bot.util';
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
@@ -19,12 +20,14 @@ export const rescheduleCallRecordingBot = async (
}
const meetingUrl = calendarEvent.conferenceLinkUrl;
const joinAt = calendarEvent.startsAt;
const meetingStartsAt = calendarEvent.startsAt;
if (isUndefined(meetingUrl) || isUndefined(joinAt)) {
if (isUndefined(meetingUrl) || isUndefined(meetingStartsAt)) {
return;
}
const joinAt = computeRecallBotJoinAt(meetingStartsAt);
const rescheduleResult = await rescheduleRecallBot({
externalBotId,
meetingUrl,
@@ -63,10 +63,6 @@ describe('recall bot api', () => {
meeting_url: 'https://meet.google.com/abc-defg-hij',
join_at: '2026-01-01T13:00:00.000Z',
bot_name: 'Twenty Meeting Bot',
automatic_leave: {
waiting_room_timeout: 1200,
noone_joined_timeout: 1200,
},
recording_config: {
video_mixed_mp4: {},
audio_mixed_mp3: {},
@@ -79,33 +75,6 @@ describe('recall bot api', () => {
});
});
it('carries the automatic leave config when rescheduling a bot', async () => {
const result = await rescheduleRecallBot({
externalBotId: 'recall-bot-id',
meetingUrl: 'https://meet.google.com/abc-defg-hij',
joinAt: '2026-01-02T13:00:00.000Z',
metadata: {
twentyCallRecordingId: 'call-recording-id',
twentyCalendarEventId: 'calendar-event-id',
twentyRealMeetingKey: 'meeting-key',
},
});
expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' });
expect(fetchMock).toHaveBeenCalledWith(
'https://ap-northeast-1.recall.ai/api/v1/bot/recall-bot-id/',
expect.objectContaining({ method: 'PATCH' }),
);
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual(
expect.objectContaining({
automatic_leave: {
waiting_room_timeout: 1200,
noone_joined_timeout: 1200,
},
}),
);
});
it('fails when the create response does not include a bot id', async () => {
fetchMock.mockResolvedValue({
ok: true,
@@ -1,4 +1,6 @@
import { RECALL_BOT_AUTOMATIC_LEAVE } from 'src/logic-functions/constants/recall-bot-automatic-leave';
import { isUndefined } from '@sniptt/guards';
import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave';
import { RECALL_BOT_RECORDING_CONFIG } from 'src/logic-functions/constants/recall-bot-recording-config';
import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type';
import {
@@ -25,6 +27,8 @@ export const rescheduleRecallBot = async ({
return { ok: false, status: null, errorMessage: configResult.error };
}
const automaticLeave = getRecallBotAutomaticLeave();
const result = await recallBotApiRequest<RecallBotResponse>({
config: configResult.config,
path: `/bot/${externalBotId}/`,
@@ -33,7 +37,7 @@ export const rescheduleRecallBot = async ({
meeting_url: meetingUrl,
join_at: joinAt,
bot_name: configResult.config.botName,
automatic_leave: RECALL_BOT_AUTOMATIC_LEAVE,
...(isUndefined(automaticLeave) ? {} : { automatic_leave: automaticLeave }),
recording_config: RECALL_BOT_RECORDING_CONFIG,
metadata,
},
@@ -1,6 +1,6 @@
import { isUndefined } from '@sniptt/guards';
import { RECALL_BOT_AUTOMATIC_LEAVE } from 'src/logic-functions/constants/recall-bot-automatic-leave';
import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave';
import { RECALL_BOT_RECORDING_CONFIG } from 'src/logic-functions/constants/recall-bot-recording-config';
import { type RecallBotMetadata } from 'src/logic-functions/types/recall-bot-metadata.type';
import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type';
@@ -28,6 +28,8 @@ export const scheduleRecallBot = async ({
return { ok: false, status: null, errorMessage: configResult.error };
}
const automaticLeave = getRecallBotAutomaticLeave();
const result = await recallBotApiRequest<RecallBotResponse>({
config: configResult.config,
path: '/bot/',
@@ -36,7 +38,7 @@ export const scheduleRecallBot = async ({
meeting_url: meetingUrl,
join_at: joinAt,
bot_name: configResult.config.botName,
automatic_leave: RECALL_BOT_AUTOMATIC_LEAVE,
...(isUndefined(automaticLeave) ? {} : { automatic_leave: automaticLeave }),
recording_config: RECALL_BOT_RECORDING_CONFIG,
metadata,
},
@@ -122,7 +122,7 @@ export default defineLogicFunction({
name: 'reconcile-stale-bot-state',
description:
'Converges call recordings with Recall on a schedule: pulls stale bot statuses and overdue transcripts, finishes failed cancellations, schedules bots for recordings still missing one, and reaps unclaimed bots. Reads calendar events only to heal already-decided recordings, never to discover meetings.',
timeoutSeconds: 300,
timeoutSeconds: 250,
handler: reconcileStaleBotStateHandler,
cronTriggerSettings: {
pattern: STALE_BOT_STATE_CRON_PATTERN,
@@ -1,3 +1,3 @@
// Application variables are injected into process.env on every execution.
// Application and server variables are injected into process.env on every execution.
export const getApplicationVariableValue = (key: string): string | undefined =>
process.env[key];