call recorder polishes (#22170)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22170?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-25 18:27:43 +05:30
committed by GitHub
parent 864ea452b4
commit 4f429565e1
4 changed files with 114 additions and 167 deletions
@@ -1,173 +1,23 @@
# Call Recorder
Record your meetings automatically and keep every call inside your CRM. Call
Recorder sends a recording bot to your team's calendar meetings, then stores the
video, audio, and a speaker-attributed transcript on the meeting's record —
searchable, in context, and ready for people, AI agents, and workflows to act
on.
**Record, transcribe, and save every meeting — right inside your CRM.**
## What this app does
## What you get
1. A teammate has an upcoming meeting on a synced calendar with a video
conference link (Zoom, Google Meet, Microsoft Teams — anything Recall.ai
supports).
2. Because recording is on by default, the app schedules a call recorder to join
that event shortly before it starts.
3. The bot joins under the configured display name and records the meeting's
audio and video for its duration.
4. When the call ends, Recall.ai processes the recording; the app ingests the
video, the audio, and a speaker-attributed transcript and stores them as a
**Call Recording**.
5. The recording and transcript surface on the meeting's **Calendar Event**,
under a **Call Recording** tab — ready to review, and for AI agents and
workflows to act on.
- **Recordings on every meeting**
- **A Call Recording tab**
- **A per-meeting on/off switch**
- **Built for AI & automation**
## What gets added to your Twenty workspace
## 💳 Billing
- **A "Recording Bot" field on Calendar Events.** A select field (On / Off, On
by default) on every CalendarEvent. Leave it On to record the meeting; switch
it Off to keep the bot out of that specific event.
- **A "Call Recording" tab on the Calendar Event record page.** A viewer with
the meeting's video player and a speaker-attributed, timestamped transcript
that follows along as the recording plays.
- **Call Recording records.** Each recording is stored as a standard
**CallRecording**: the mixed audio (MP3) and video (MP4), the transcript, the
call's actual start and end times, and a lifecycle status (`SCHEDULED`
`JOINING``RECORDING``PROCESSING``COMPLETED`, or `FAILED`), with a
Call Recorder Failure Reason when failure details are available.
- **A default role.** A scoped application role that reads calendar events,
participants, people, and workspace members to decide attendance, and writes
the resulting CallRecording records, uploads recording media, and fills
transcripts. It cannot delete records or change settings.
Metered: **1 credit per recording-hour**, prorated on the bot's actual recording
time. No recording — opted out, canceled, or no-show — means no charge.
## How recording works
## 📌 Heads up
- **On by default.** Once an admin installs the app and configures Recall.ai
credentials, every eligible meeting is recorded automatically — there is
nothing each person has to switch on.
- **A meeting is eligible when** it is not canceled, has a conference link, has
not ended yet, and its **Recording Bot** field is On. If any of those isn't
true — the event was canceled, you turned recording Off, there's no video
link, or the meeting has already ended — no bot is scheduled.
- **Opting out of a single meeting.** Open the event and set **Recording Bot**
to Off (or do it from the calendar events list view). The app cancels any bot
it had scheduled for that meeting.
- **Joining and leaving.** By default the bot joins one minute before the start
time and waits in the lobby up to twenty minutes to be admitted. It leaves on
its own if no one ever joins, or shortly after everyone else has left. These
are all tunable — see [Application variables](#application-variables).
- **It tracks the calendar.** If a meeting's time, link, or recording
preference changes, the app reschedules or cancels the bot to match. A
periodic reconciliation job runs as a safety net, keeping recordings and bots
in sync even when a real-time update is missed.
- **Needs a synced calendar + video link** — ad-hoc calls that were never on
your Google, Outlook, or CalDAV calendar aren't recorded.
- **Your copy is yours** — Twenty stores its own video, audio, and transcript,
so they stay available after the source media expires.
## Billing
Recording is a metered feature. Each recording is charged on its **actual call
duration** — from when the bot starts recording to when it stops — prorated, at
a rate of **1 credit per recording-hour** (1,000,000 micro-credits). A meeting
the bot never recorded (opted out, canceled, or no one showed) is not charged.
## Installing
1. Open **Settings → Applications** in your Twenty workspace.
2. Find **Call Recorder** and click **Install**.
3. A server admin completes the one-time
[Self-hosting setup](#self-hosting-setup-admin-only) below to wire up the
Recall.ai API key and webhook. On Twenty Cloud these may already be
configured.
## Limitations
What this app intentionally does **not** do in v1:
- **Recording is workspace-wide, not per person.** When the app is installed,
every eligible meeting is recorded by default; there is no per-user "record
my meetings" toggle yet. Control is per-meeting via the **Recording Bot**
field. Per-user opt-in/out is planned for a later version.
- **The meeting must be a synced calendar event with a conference link.** The
bot is scheduled from CalendarEvents that Twenty has synced from a connected
calendar (Google / Outlook / CalDAV) and that carry a video-conference link.
Ad-hoc calls that were never on a synced calendar are not recorded.
- **A recording completes only when both its audio and video are ingested.**
Recall produces only the artifacts requested at bot creation (mixed MP3 +
MP4); a recording reaches `COMPLETED` once both have been stored. If
processing fails, it is marked `FAILED`.
- **Recall.ai media is temporary; Twenty's copy is not.** Recall retains the
source media for a limited window (about seven days by default) to stay
inside its free-storage window. Twenty ingests and stores the video, audio,
and transcript in its own storage, so they remain available after Recall's
media expires.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No bot joined a meeting | **Recording Bot** was Off, the event had no conference link, it wasn't synced from a connected calendar, or `RECALL_API_KEY` isn't set | Confirm the event is On, upcoming, has a video link, and came from a synced calendar; admin: confirm `RECALL_API_KEY` is set |
| Recording never reaches `COMPLETED` | A Recall webhook was missed, or only one of audio/video was produced | The reconciliation job pulls the latest status from Recall within a few minutes; if it is marked `FAILED`, inspect the bot in the Recall dashboard |
| Transcript empty, or marked pending/failed | Recall hasn't finished async transcription yet, or transcription failed for that call | Wait for the reconciliation job to ingest the transcript; a persistent failure leaves a marker in the transcript |
| Webhook rejected with `500` (`Invalid webhook signature`, Recall keeps retrying) | `RECALL_WEBHOOK_SECRET` doesn't match the Recall endpoint's signing secret | Re-copy the `whsec_…` secret from the Recall webhook endpoint into the `RECALL_WEBHOOK_SECRET` server variable |
| Webhook rejected with `500` (`RECALL_WEBHOOK_SECRET … not set`) | `RECALL_WEBHOOK_SECRET` is not set | Admin: set it on the application registration |
| Bot left almost immediately | No one was admitted before the lobby / no-one-joined timeout, or everyone left | Adjust `CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS` / `CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS` if too aggressive |
| Bot joined a meeting you didn't want recorded | Recording is on by default | Set the event's **Recording Bot** field to Off; the scheduled bot is canceled |
---
## Self-hosting setup (admin-only)
This section is for Twenty server admins. If you're on Twenty Cloud, the
credentials may already be configured.
### Server variables
Set these on the application registration after installing (Settings →
Applications → Call Recorder):
| Server variable | Required | Purpose |
|---|---|---|
| `RECALL_API_KEY` | Yes | Recall.ai API key for the configured region; used to schedule, update, and cancel bots. |
| `RECALL_REGION` | No | Recall.ai region for API requests. Defaults to `eu-central-1` (Europe / Frankfurt). |
| `CALL_RECORDER_RECORDING_RETENTION_HOURS` | No | How long Recall.ai retains the source media after processing. Defaults to `166` hours (6 days 22 hours), just under Recall's 168-hour free-storage window. Values above `168` may incur Recall storage charges. Twenty's ingested copy is unaffected. |
| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. |
### Application variables
A workspace admin can tune bot behavior through application variables:
| Application variable | Default | Purpose |
|---|---|---|
| `CALL_RECORDER_NAME` | `Twenty.com` | Display name the bot uses when it joins a call. |
| `CALL_RECORDER_JOIN_EARLY_MINUTES` | `1` | Minutes before the start time the bot joins. Set to `0` to join at the scheduled start. |
| `CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS` | `1200` | Seconds the bot waits in the lobby before giving up and leaving. |
| `CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS` | `1200` | Seconds the bot stays in an empty meeting when no one else ever joins. |
| `CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS` | `2` | Seconds the bot keeps recording after everyone else leaves. |
### Configuring the Recall webhook
The app exposes a server webhook route that verifies the Recall/Svix signature,
advances the matching CallRecording's lifecycle status (`JOINING``RECORDING`
`PROCESSING`, or `FAILED`), and — once the recording finishes —
ingests the audio, video, and transcript. It never moves a status backward, so
out-of-order or duplicate deliveries are safe, and it returns a non-2xx response
on signature failures so Recall retries.
Use this URL on your deployment, replacing only the host:
```text
https://<your-twenty-host>/webhooks/server/8da4b8b5-5edf-4880-b51f-ab6e679ec617/9215afe6-1497-4149-a49d-e608e239bbaf
```
The first ID is the **Call Recorder application registration**. The second
ID is the **Recall webhook logic function**.
1. In the Recall.ai dashboard, create a webhook endpoint pointing at your
deployment's webhook URL, subscribed to the **bot status-change**,
**recording**, and **transcript** events (`bot.status_change`,
`recording.done`, `recording.failed`, `transcript.done`,
`transcript.failed`). Status-change drives the lifecycle; the recording and
transcript events trigger media and transcript ingestion. Subscribing to
status changes alone leaves ingestion to the reconciliation backstop.
2. Copy the endpoint's signing secret — it starts with `whsec_`.
3. Set it as the `RECALL_WEBHOOK_SECRET` server variable on the
**Call Recorder** application registration.
4. Set `RECALL_API_KEY` (and optionally `RECALL_REGION`) the same way.
@@ -0,0 +1,95 @@
# Call Recorder — Self-hosting setup
This guide is for Twenty **server admins**. It covers connecting Call Recorder
to the recording service (Recall.ai), the environment variables it reads, and
how to wire up the webhook.
If you're on **Twenty Cloud**, these credentials may already be configured —
check with your workspace before setting them.
## What you need to wire up
Call Recorder talks to [Recall.ai](https://recall.ai) to send bots to meetings
and to receive recordings back. Two things must be configured:
1. A **Recall.ai API key** (and optionally a region), so the app can schedule,
update, and cancel bots.
2. A **webhook** from Recall.ai back to your deployment, so the app learns when
a recording is ready and can ingest it.
## Server variables
Set these on the application registration after installing
(**Settings → Applications → Call Recorder**):
| Server variable | Required | Purpose |
|---|---|---|
| `RECALL_API_KEY` | Yes | Recall.ai API key for the configured region; used to schedule, update, and cancel bots. |
| `RECALL_REGION` | No | Recall.ai region for API requests. Defaults to `eu-central-1` (Europe / Frankfurt). |
| `CALL_RECORDER_RECORDING_RETENTION_HOURS` | No | How long Recall.ai retains the source media after processing. Defaults to `166` hours (6 days 22 hours), just under Recall's 168-hour free-storage window. Values above `168` may incur Recall storage charges. Twenty's ingested copy is unaffected. |
| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. |
> **Bot behavior settings** (display name, join timing, lobby and leave
> timeouts) are **application variables** that a workspace admin tunes inside the
> app — not server variables. See **Customize the bot** in the
> [README](./README.md).
## Configuring the Recall webhook
The app exposes a server webhook route that verifies the Recall/Svix signature,
advances the matching recording's lifecycle status (`JOINING``RECORDING`
`PROCESSING`, or `FAILED`), and — once the recording finishes — ingests the
audio, video, and transcript. It never moves a status backward, so out-of-order
or duplicate deliveries are safe, and it returns a non-2xx response on signature
failures so Recall retries.
Use this URL on your deployment, replacing only the host:
```text
https://<your-twenty-host>/webhooks/server/9215afe6-1497-4149-a49d-e608e239bbaf
```
The ID is the **Recall webhook logic function**.
1. In the Recall.ai dashboard, create a webhook endpoint pointing at your
deployment's webhook URL, subscribed to the **bot status-change**,
**recording**, and **transcript** events (`bot.status_change`,
`recording.done`, `recording.failed`, `transcript.done`,
`transcript.failed`). Status-change drives the lifecycle; the recording and
transcript events trigger media and transcript ingestion. Subscribing to
status changes alone leaves ingestion to the reconciliation backstop.
2. Copy the endpoint's signing secret — it starts with `whsec_`.
3. Set it as the `RECALL_WEBHOOK_SECRET` server variable on the **Call Recorder**
application registration.
4. Set `RECALL_API_KEY` (and optionally `RECALL_REGION`) the same way.
## Recording lifecycle
Each recording is stored as a **CallRecording** record and moves through a
lifecycle status: `SCHEDULED``JOINING``RECORDING``PROCESSING`
`COMPLETED`, or `FAILED` (with a failure reason when one is available).
A recording reaches `COMPLETED` only once **both** its audio and video have been
ingested. Recall produces only the artifacts requested at bot creation (mixed
MP3 + MP4); if processing fails, the recording is marked `FAILED`.
Recall.ai retains the source media for a limited window (about seven days by
default) to stay inside its free-storage window. Twenty ingests and stores the
video, audio, and transcript in its own storage, so they remain available after
Recall's media expires.
A periodic reconciliation job runs as a safety net, pulling the latest status
from Recall and keeping recordings and bots in sync even when a real-time
webhook update is missed.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No bot joined a meeting | **Recording** was off, the event had no conference link, it wasn't synced from a connected calendar, or `RECALL_API_KEY` isn't set | Confirm the event is on, upcoming, has a video link, and came from a synced calendar; confirm `RECALL_API_KEY` is set |
| Recording never reaches `COMPLETED` | A Recall webhook was missed, or only one of audio/video was produced | The reconciliation job pulls the latest status from Recall within a few minutes; if it is marked `FAILED`, inspect the bot in the Recall dashboard |
| Transcript empty, or marked pending/failed | Recall hasn't finished async transcription yet, or transcription failed for that call | Wait for the reconciliation job to ingest the transcript; a persistent failure leaves a marker in the transcript |
| Webhook rejected with `500` (`Invalid webhook signature`, Recall keeps retrying) | `RECALL_WEBHOOK_SECRET` doesn't match the Recall endpoint's signing secret | Re-copy the `whsec_…` secret from the Recall webhook endpoint into the `RECALL_WEBHOOK_SECRET` server variable |
| Webhook rejected with `500` (`RECALL_WEBHOOK_SECRET … not set`) | `RECALL_WEBHOOK_SECRET` is not set | Set it on the application registration |
| Bot left almost immediately | No one was admitted before the lobby / empty-meeting timeout, or everyone left | Adjust the lobby / empty-meeting timeouts in the app settings (see **Customize the bot** in the README) if they're too aggressive |
| Bot joined a meeting you didn't want recorded | Recording is on by default | Set the event's **Recording** field to Off; the scheduled bot is canceled |
@@ -1,13 +1,15 @@
{
"name": "@twentyhq/call-recorder",
"version": "0.1.4",
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [],
"keywords": [
"twenty-app"
],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
@@ -12,7 +12,7 @@ export default definePageLayoutTab({
universalIdentifier:
CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Call Recording',
position: 25,
position: 15,
icon: 'IconVideo',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
pageLayoutUniversalIdentifier: