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 reactivelyWorkflow 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
- Auth middleware validates the JWT and resolves the workspace context
- GraphQL Yoga parses the mutation against the workspace-specific schema
- Workspace resolver calls TypeORM to insert into the workspace's
companytable - Entity event is emitted — triggers timeline activity, SSE notification, and any configured webhooks
- 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 fireOther 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 channelWorker fetches messages from Gmail API
The messaging worker:
- Uses the stored refresh token to get a fresh Gmail access token
- Calls the Gmail API to list new messages since last sync
- Downloads message headers, body, and attachments
- Stores raw message data in the
messagetable
Participant matching links messages to records
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 recordMessages 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
- ObjectMetadata record created in the
coreschema - Default FieldMetadata records created (name, createdAt, updatedAt, etc.)
- PostgreSQL table
_vendorcreated in the workspace schema - GraphQL schema regenerated — the
vendortype, queries, and mutations are now available - REST endpoints auto-generated —
/rest/vendorsis now live
User adds custom fields
For each field (e.g., "Contract Value" — currency type):
- FieldMetadata record created
- Column added to the
_vendortable - GraphQL schema updated with the new field
- 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 sequentiallyWorkflow runner executes actions
For each action in the workflow:
- Resolve variables — inject opportunity data (name, amount, owner)
- Execute action — send HTTP POST to Slack webhook URL
- 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:
- Updates the database record
- Creates a timeline activity entry
- Fires SSE event to update all connected browsers
- 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
| From | To | Mechanism | Example |
|---|---|---|---|
| Browser → Server | GraphQL over HTTPS | Creating records, updating stages | |
| Browser → Server | REST over HTTPS | Simple CRUD, batch operations | |
| Server → Browser | SSE (Server-Sent Events) | Real-time record changes | |
| Server → PostgreSQL | TypeORM queries | All data persistence | |
| Server → Redis | Cache + pub/sub | Session data, SSE events | |
| Server → BullMQ (via Redis) | Job queue | Email sync, webhooks, workflows | |
| Gmail API → Worker | REST API (OAuth) | Fetching emails | |
| Worker → PostgreSQL | TypeORM | Storing synced messages | |
| Server → ClickHouse | Analytics client | Usage analytics | |
| Server → External | HTTP POST | Webhook delivery, Slack notifications | |
| Metadata → Schema | Runtime generation | Custom object GraphQL/REST APIs |