From 4f429565e16b0244412d7498a897d429ccb3b99b Mon Sep 17 00:00:00 2001
From: nitin <142569587+ehconitin@users.noreply.github.com>
Date: Thu, 25 Jun 2026 18:27:43 +0530
Subject: [PATCH] call recorder polishes (#22170)
---
.../public/call-recorder/README.md | 178 ++----------------
.../twenty-apps/public/call-recorder/SETUP.md | 95 ++++++++++
.../public/call-recorder/package.json | 6 +-
.../calendar-event-recording-tab.ts | 2 +-
4 files changed, 114 insertions(+), 167 deletions(-)
create mode 100644 packages/twenty-apps/public/call-recorder/SETUP.md
diff --git a/packages/twenty-apps/public/call-recorder/README.md b/packages/twenty-apps/public/call-recorder/README.md
index 5fc7fa388f..80e5efaeda 100644
--- a/packages/twenty-apps/public/call-recorder/README.md
+++ b/packages/twenty-apps/public/call-recorder/README.md
@@ -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:///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.
diff --git a/packages/twenty-apps/public/call-recorder/SETUP.md b/packages/twenty-apps/public/call-recorder/SETUP.md
new file mode 100644
index 0000000000..7e554fbaee
--- /dev/null
+++ b/packages/twenty-apps/public/call-recorder/SETUP.md
@@ -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:///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 |
diff --git a/packages/twenty-apps/public/call-recorder/package.json b/packages/twenty-apps/public/call-recorder/package.json
index c5ff432b3a..03b42c0034 100644
--- a/packages/twenty-apps/public/call-recorder/package.json
+++ b/packages/twenty-apps/public/call-recorder/package.json
@@ -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",
diff --git a/packages/twenty-apps/public/call-recorder/src/page-layouts/calendar-event-recording-tab.ts b/packages/twenty-apps/public/call-recorder/src/page-layouts/calendar-event-recording-tab.ts
index 90d63b7142..89b5bb8cd8 100644
--- a/packages/twenty-apps/public/call-recorder/src/page-layouts/calendar-event-recording-tab.ts
+++ b/packages/twenty-apps/public/call-recorder/src/page-layouts/calendar-event-recording-tab.ts
@@ -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: