Learn from OSS
Twenty

Workflows & Data Flows

Key user journeys in Twenty traced through the frontend, API, workers, and database

Workflows & Data Flows

This page traces the most important user journeys through Twenty's codebase — from a click in the browser through the NestJS API, PostgreSQL database, and background workers. Understanding these flows is the fastest way to build a mental model of how a modern CRM works under the hood.

Reading Guide

Each workflow shows what happens at each layer. The Frontend → GraphQL API → Database pattern repeats for synchronous operations. Asynchronous work (email sync, webhooks, workflows) additionally involves BullMQ workers.

How a Request Flows Through Twenty

The general pattern for every user interaction:

Browser (User Action)


React Component              ← captures user input
    │ calls Apollo mutation/query

Apollo Client                ← GraphQL request with JWT token
    │ HTTP POST

NestJS Server
    ├── Auth Middleware       ← validates JWT, resolves workspace
    ├── GraphQL Yoga          ← parses query, resolves schema
    ├── Workspace Resolver    ← fetches/mutates workspace data
    │     └── TypeORM         ← SQL query against workspace schema
    └── Response (JSON)       ← serialized GraphQL response


Apollo Client                ← updates cache, triggers re-render


React Component              ← UI updates reactively

Workflow 1: Creating a Company Record

The most common CRM action — adding a new company to track.

User clicks "+" in the Companies view

The React UI opens an inline creation row (table view) or modal. The user types a company name.

Apollo Client sends a GraphQL mutation

mutation CreateCompany($input: CompanyCreateInput!) {
  createCompany(data: $input) {
    id
    name
    domainName
    createdAt
  }
}

The JWT access token is attached via Apollo's setContext link.

NestJS processes the request

  1. Auth middleware validates the JWT and resolves the workspace context
  2. GraphQL Yoga parses the mutation against the workspace-specific schema
  3. Workspace resolver calls TypeORM to insert into the workspace's company table
  4. Entity event is emitted — triggers timeline activity, SSE notification, and any configured webhooks
  5. Response is returned with the created company record

Background effects fire asynchronously

Company Created Event

    ├──► entity-events-to-db-queue    → Timeline activity saved
    ├──► SSE pub/sub via Redis        → All connected browsers notified
    ├──► webhook-queue                → Configured webhooks called
    └──► workflow-queue               → Matching workflow triggers fire

Other browsers update in real-time

Any other user viewing the Companies list receives an SSE event. Their Apollo cache invalidates, and the list re-renders with the new company.

Why Real-Time Matters for CRM

In a sales team, multiple people often work the same account list. When one rep adds a company or updates a deal stage, everyone needs to see it immediately — otherwise you get duplicate outreach and conflicting data. SSE ensures the whole team sees the same state.

Workflow 2: Email Sync (Gmail)

Twenty syncs emails from Gmail into the CRM, automatically linking them to Company and Person records.

Admin connects Gmail account

In Settings → Accounts, the user authenticates via Google OAuth. Twenty stores the OAuth refresh token and creates a MessageChannel record.

Cron job triggers sync

A BullMQ cron job (messaging-queue) runs periodically:

cron-queue → enqueue MessagingFullSyncJob for each active channel

Worker fetches messages from Gmail API

The messaging worker:

  1. Uses the stored refresh token to get a fresh Gmail access token
  2. Calls the Gmail API to list new messages since last sync
  3. Downloads message headers, body, and attachments
  4. Stores raw message data in the message table

For each email address in the message (From, To, CC):

contact-creation-queue:
  1. Search People table for matching email → link if found
  2. Search Companies table by email domain → link if found
  3. If no match and auto-creation enabled → create Person record

Messages appear on Company/Person timelines

The timeline view aggregates linked messages, showing the full email history for each contact and company.

For PMs: Why This Is Technically Complex

Email sync seems simple but involves: OAuth token management (tokens expire), incremental sync (only fetch new emails), rate limiting (Gmail limits API calls), participant matching (fuzzy matching across thousands of contacts), thread reconstruction (grouping replies), and handling edge cases (BCC, aliases, forwarded emails). This is why it's done asynchronously via workers — a full sync can process thousands of messages.

Workflow 3: Custom Object Creation

A user creates a new "Vendors" object to track supplier relationships.

User navigates to Settings → Data Model

The data model UI shows all standard and custom objects with their fields and relationships.

User creates "Vendors" object

The frontend calls the /metadata GraphQL endpoint:

mutation CreateObject($input: CreateObjectInput!) {
  createOneObject(input: $input) {
    id
    nameSingular
    namePlural
    labelSingular
    labelPlural
  }
}

Metadata engine processes the creation

  1. ObjectMetadata record created in the core schema
  2. Default FieldMetadata records created (name, createdAt, updatedAt, etc.)
  3. PostgreSQL table _vendor created in the workspace schema
  4. GraphQL schema regenerated — the vendor type, queries, and mutations are now available
  5. REST endpoints auto-generated — /rest/vendors is now live

User adds custom fields

For each field (e.g., "Contract Value" — currency type):

  1. FieldMetadata record created
  2. Column added to the _vendor table
  3. GraphQL schema updated with the new field
  4. UI form builder includes the field automatically

Object is ready to use

The Vendors object appears in the sidebar navigation. Users can create records, add views (table, kanban), set up filters, and link vendors to other objects via relations.

Why It Matters

This entire workflow happens without any code deployment or database migration scripts. The metadata engine dynamically evolves the schema. When Twenty's frontend loads, it reads the metadata to know which objects and fields exist, and renders the UI accordingly. This is the same pattern that made Salesforce the dominant CRM platform.

Workflow 4: Workflow Automation

A user creates an automation: "When a new Opportunity is created, send a Slack notification."

User builds the workflow in the visual editor

The workflow editor (built with React Flow / @xyflow/react) lets users define:

  • Trigger: Record Created → Opportunity
  • Action: Send HTTP Request → Slack webhook URL with opportunity data

The workflow definition is saved as a versioned JSON document.

Opportunity is created (trigger fires)

When any user creates an Opportunity, the entity event system checks for matching workflow triggers:

Entity Event: Opportunity Created


workflow-queue → WorkflowTriggerProcessor


Match trigger conditions (object type, filters)


Create WorkflowRun record (status: running)


Execute actions sequentially

Workflow runner executes actions

For each action in the workflow:

  1. Resolve variables — inject opportunity data (name, amount, owner)
  2. Execute action — send HTTP POST to Slack webhook URL
  3. Record result — store success/failure in the WorkflowRun

Workflow completes

The WorkflowRun status is updated to completed (or failed with error details). The user can view run history and debug failed executions.

Workflow 5: Sales Pipeline (Opportunity Management)

How a sales team tracks deals through their pipeline.

Rep creates an Opportunity

An opportunity is created with:

  • Company and contact (Point of Contact) relationships
  • Stage (e.g., "Qualification", "Proposal", "Negotiation", "Closed Won")
  • Amount and expected close date

Team views the pipeline

The Kanban view groups opportunities by stage:

Qualification │ Proposal  │ Negotiation │ Closed Won │ Closed Lost
─────────────┼───────────┼─────────────┼────────────┼────────────
  Acme Corp  │ BigCo     │ MegaCorp    │ StartupXY  │
  $50k       │ $200k     │ $500k       │ $25k       │
  Jane Doe   │ John Sm.  │ Sarah C.    │ Mike T.    │

Rep drags opportunity to next stage

The drag-and-drop action fires a GraphQL mutation to update the opportunity's stage. The change:

  1. Updates the database record
  2. Creates a timeline activity entry
  3. Fires SSE event to update all connected browsers
  4. Triggers any workflow automations matching "Opportunity Updated + Stage Changed"

Manager reviews analytics

The analytics dashboard queries aggregated pipeline data:

  • Total pipeline value by stage
  • Conversion rates between stages
  • Average deal size and close time
  • Revenue forecasts

Information Flow Summary

FromToMechanismExample
Browser → ServerGraphQL over HTTPSCreating records, updating stages
Browser → ServerREST over HTTPSSimple CRUD, batch operations
Server → BrowserSSE (Server-Sent Events)Real-time record changes
Server → PostgreSQLTypeORM queriesAll data persistence
Server → RedisCache + pub/subSession data, SSE events
Server → BullMQ (via Redis)Job queueEmail sync, webhooks, workflows
Gmail API → WorkerREST API (OAuth)Fetching emails
Worker → PostgreSQLTypeORMStoring synced messages
Server → ClickHouseAnalytics clientUsage analytics
Server → ExternalHTTP POSTWebhook delivery, Slack notifications
Metadata → SchemaRuntime generationCustom object GraphQL/REST APIs

What's Next