## Context The hourly calendar and messaging stale-sync crons currently load every active workspace and enqueue one recovery job per workspace. Each recovery job then enters the workspace context and queries its channels, even when that workspace has no stale sync. As the number of workspaces grows, the cost of this check grows with every workspace rather than with the number of syncs that actually need recovery. Because both crons run on the hour, this also creates a synchronized burst of mostly unnecessary queue, database, and workspace-context work. ## What changes The crons now use TypeORM repository `find` operations on `calendarChannel` and `messageChannel` before enqueueing recovery jobs: - Select only `workspaceId` from matching channels. - Keep only active, non-deleted workspaces. - Keep only the scheduled and ongoing stages handled by the existing recovery jobs. - Treat a sync as stale when `syncStageStartedAt` is null or older than the existing 30-minute timeout. - Deduplicate workspace IDs in memory before enqueueing. - Enqueue the existing recovery job only for matching workspaces. - Share the stage definitions between candidate selection and recovery so they cannot drift independently. ## Before and after Before: 1. Load every active workspace. 2. Enqueue a calendar job and a messaging job for each workspace. 3. Enter every workspace context. 4. Query its channels. 5. Usually find nothing to recover. After: 1. Run one candidate lookup for calendar channels and one for message channels. 2. Return only the workspace ID for each potentially stale channel. 3. Deduplicate those IDs and enqueue one job per affected workspace. 4. Let the existing recovery jobs recheck and recover those channels. Database reads now scale with stale channel candidates, while queue and workspace-context work scale with affected workspaces rather than the total workspace count. ## Why deduplicate in memory TypeORM repository find options do not provide `DISTINCT`, so these lookups can return the same workspace ID once per stale channel. A `Set` removes those duplicates before jobs are created. This is a deliberate tradeoff: - The database returns only UUIDs, not full channel records. - The scans run hourly against channel tables, and stale candidates should remain sparse. - It keeps the query expressed through the typed repository API. - It avoids adding a database sort or hash aggregation for `DISTINCT`. If candidate volume becomes large enough for result transfer or in-process deduplication to matter, database-side deduplication can be moved into a dedicated repository query. A practical signal would be tens of thousands of candidates per run, a high duplicate ratio, or measurable lookup and event-loop latency. ## Safety - The recovery jobs and sync-state transitions are unchanged. - The candidate lookup uses the same stage lists and timeout constants as the recovery jobs. - Recovery jobs still recheck staleness after dequeueing, which protects against a channel recovering between candidate selection and execution. - Jobs are still enqueued sequentially with per-workspace exception handling. - Disabled and group channels are not newly excluded, preserving the previous recovery behavior. ## Scope This only changes hourly stale-sync detection. It does not change normal calendar or messaging scheduling, imports, retry policies, or recovery state transitions. ## Testing Added unit coverage for: - active and non-deleted workspace filtering, - selecting only workspace IDs, - scheduled and ongoing stage filtering, - null or expired `syncStageStartedAt`, - the configured stale timeout, - application-side workspace deduplication, - enqueueing one recovery job per affected workspace. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23321?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. -->
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty app:publish --private
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





