Contact Creation in Twenty CRM: How Automated Message Processing Builds Your Contact Database

Twenty CRM automatically creates contacts from incoming messages through a multi-step pipeline that detects participants, enqueues background jobs, filters out internal users, deduplicates email addresses, and creates or restores both companies and persons within the workspace context.

Contact creation in Twenty CRM is fully automated for connected email accounts, transforming incoming messages into structured workspace data without manual data entry. When messages are imported, the system evaluates participants against auto-creation policies to determine which contacts should be added to your database. This process leverages a robust queue-based architecture located in twentyhq/twenty to ensure reliable, transactional contact management at scale.

The Contact Creation Pipeline

The entire workflow is orchestrated through three core modules that handle detection, queuing, and persistence. All operations run inside the workspace’s transactional context using GlobalWorkspaceOrmManager.executeInWorkspaceContext, ensuring atomicity across person, company, and participant tables.

Phase 1: Message Ingestion and Participant Detection

The process begins in MessagingSaveMessagesAndEnqueueContactCreationService.saveMessagesAndEnqueueContactCreation located at packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts. This service parses every message participant and applies the channel’s auto-creation policy to identify candidates.

Key filtering logic includes:

  • Removing the connected account itself from creation candidates
  • Filtering out non-professional email addresses
  • Marking remaining participants with shouldCreateContact = true

Once candidates are identified, the service pushes a job onto the contactCreationQueue containing the workspace ID, connected account, and a list of {handle, displayName} objects for the participants that need contacts (lines 61-78).

Phase 2: Background Job Processing

The CreateCompanyAndContactJob located at packages/twenty-server/src/modules/contact-creation-manager/jobs/create-company-and-contact.job.ts consumes the queued payload and delegates to the creation service. The job handler processes contacts in batches defined by the CONTACTS_CREATION_BATCH_SIZE constant to manage memory and database load efficiently.

The main entry point is CreateCompanyAndPersonService.createCompaniesAndPeopleAndUpdateParticipants in packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts. This method ensures the connected account’s owner workspace member exists and establishes the workspace’s transactional context before processing begins.

Phase 3: Data Sanitization and Deduplication

Before creating records, the pipeline applies two critical utilities:

Filter Internal Users: The filterOutContactsThatBelongToSelfOrWorkspaceMembers utility (filter-out-contacts-that-belong-to-self-or-workspace-members.util.ts) removes contacts belonging to the connected account itself or any workspace member, preventing duplicate creation for internal users.

Deduplicate Handles: The getUniqueContactsAndHandles utility (get-unique-contacts-and-handles.util.ts) reduces the remaining contacts to a unique set of email handles, ensuring the system does not attempt multiple creations for the same email address within a single batch.

Phase 4: Entity Resolution and Persistence

The service determines creation needs through computeContactsThatNeedPersonCreateAndRestoreAndWorkDomainNamesToCreate:

  • Create: Handles not found in the database require new PersonWorkspaceEntity records
  • Restore: Handles found on soft-deleted persons require restoration
  • Company Creation: Work-domain email addresses (isWorkEmail) trigger company creation or restoration based on domain names

Company Resolution: CreateCompanyService.createOrRestoreCompanies (called at line 1110) receives the list of work domain names and returns a map of {domain → companyId} for linking contacts to their organizations.

Person Creation: For new contacts, formatPeopleToCreateFromContacts (line 308) builds Partial<PersonWorkspaceEntity> objects containing the email, derived first/last names from the display name, linked companyId, and audit fields (createdBy). These are persisted via CreatePersonService.createPeople (lines 128-130).

Restoration: For soft-deleted matches, formatPeopleToRestoreFromContacts (line 359) builds restoration DTOs with {personId, companyId} passed to CreatePersonService.restorePeople.

Existing persons are detected via a query builder that searches for PersonWorkspaceEntity records where primary or additional emails match the unique handles (lines 87-96).

Implementing Contact Creation Programmatically

While the pipeline runs automatically during message import, you can trigger or test the process manually.

Triggering Contact Creation from Messages

To manually invoke the contact detection and enqueueing logic:

import { MessagingSaveMessagesAndEnqueueContactCreationService } from
  'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';

// Assume we already have `messages`, `channel`, `connectedAccount`, `workspaceId`
await new MessagingSaveMessagesAndEnqueueContactCreationService(
  messageQueueService,
  messageService,
  messageParticipantService,
  messageFolderAssociationService,
  globalWorkspaceOrmManager,
).saveMessagesAndEnqueueContactCreation(
  messages,
  channel,
  connectedAccount,
  workspaceId,
);

This call automatically persists messages, determines which participants should become contacts, and pushes a CreateCompanyAndContactJob to the queue.

Direct Job Invocation for Testing

For isolated testing or custom workflows, invoke the job handler directly:

import { CreateCompanyAndContactJob } from
  'src/modules/contact-creation-manager/jobs/create-company-and-contact.job';
import { CreateCompanyAndPersonService } from
  'src/modules/contact-creation-manager/services/create-company-and-contact.service';

const job = new CreateCompanyAndContactJob(new CreateCompanyAndPersonService(
  createPersonService,
  createCompanyService,
  globalWorkspaceOrmManager,
  exceptionHandlerService,
));

await job.handle({
  workspaceId: 'workspace-123',
  connectedAccount: myConnectedAccount,
  contactsToCreate: [{ handle: 'john.doe@acme.com', displayName: 'John Doe' }],
  source: 'EMAIL', // FieldActorSource enum value
});

Inspecting Creation Results

The service returns merged data for verification:

const result = await createCompanyAndPersonService.createCompaniesAndPeopleAndUpdateParticipants(...);
console.log('Created / restored persons:', result);

Core Service Architecture and File Structure

The following files implement the end-to-end contact creation workflow in Twenty CRM:

File Role
packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts Detects participants that need contacts and enqueues the job
packages/twenty-server/src/modules/contact-creation-manager/jobs/create-company-and-contact.job.ts Queue consumer that delegates to the creation service
packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts Core logic: batching, filtering, deduplication, company & person creation/restoration
packages/twenty-server/src/modules/contact-creation-manager/utils/get-unique-contacts-and-handles.util.ts Helper that collapses duplicate email handles
packages/twenty-server/src/modules/contact-creation-manager/utils/filter-out-contacts-that-belong-to-self-or-workspace-members.util.ts Prevents creating contacts that already belong to the user or workspace members
packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts Creates or restores companies based on work-domain emails
packages/twenty-server/src/modules/contact-creation-manager/services/create-person.service.ts Persists new persons or restores soft-deleted ones
packages/twenty-server/src/modules/contact-creation-manager/constants/contacts-creation-batch-size.constant.ts Defines how many contacts are processed per batch

Summary

  • Automated Detection: The MessagingSaveMessagesAndEnqueueContactCreationService identifies contact candidates from imported messages while filtering out internal accounts and non-professional emails.
  • Queue-Based Processing: Jobs are enqueued to CreateCompanyAndContactJob for background processing, enabling scalable handling of large message volumes.
  • Transactional Safety: All database operations execute within GlobalWorkspaceOrmManager.executeInWorkspaceContext, ensuring atomic creation of related company and person records.
  • Deduplication: The pipeline filters existing workspace members and deduplicates email handles before database insertion.
  • Smart Restoration: The system detects soft-deleted persons and restores them rather than creating duplicates, maintaining data lineage.
  • Company Linking: Work-domain emails automatically trigger company creation or linking, establishing organizational relationships during contact import.

Frequently Asked Questions

How does Twenty CRM prevent duplicate contact creation?

The system implements a multi-layer deduplication strategy. First, filterOutContactsThatBelongToSelfOrWorkspaceMembers removes the connected account and existing workspace members from the creation list. Then, getUniqueContactsAndHandles collapses duplicate email addresses within the batch. Finally, the service queries existing PersonWorkspaceEntity records (lines 87-96) to detect active or soft-deleted matches, updating or restoring existing records rather than inserting duplicates.

What happens when a contact is created from a work email domain?

When the system encounters a work-domain email (determined by isWorkEmail), it extracts the domain name and passes it to CreateCompanyService.createOrRestoreCompanies. This service either creates a new company record or restores a soft-deleted one, returning a companyId that is linked to the new or restored person record during the formatPeopleToCreateFromContacts phase.

Can contact creation be triggered manually outside of email imports?

Yes. While the standard flow triggers from MessagingSaveMessagesAndEnqueueContactCreationService, you can directly invoke CreateCompanyAndContactJob.handle() with a payload containing workspaceId, connectedAccount, and contactsToCreate array. This is particularly useful for testing, data migration scripts, or integrating custom message sources beyond standard email imports.

How does the system handle soft-deleted contacts?

During the computeContactsThatNeedPersonCreateAndRestoreAndWorkDomainNamesToCreate phase, the service categorizes handles into those requiring creation versus restoration. For contacts matching soft-deleted persons, the system builds restoration DTOs via formatPeopleToRestoreFromContacts and calls CreatePersonService.restorePeople, reactivating the existing record with updated company associations rather than creating a new database entry.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →