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:
@@ -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",
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user