# AI Source: https://docs.twenty.com/getting-started/core-concepts/ai How Twenty uses AI to enhance your CRM experience. Twenty integrates AI directly into your CRM — not as a gimmick, but as a tool that works within your data model and permission system. ## AI Chatbot Ask questions about your data in natural language. The AI chatbot can query your CRM records, summarize information, and help you find what you're looking for without building complex filters. ## AI Agents AI agents go beyond chat — they can execute multi-step tasks autonomously: * Enrich records with data from external sources * Draft and send follow-up emails * Analyze pipeline health and flag at-risk deals * Process incoming data and route it to the right team Agents work within your existing workflows, so you can combine AI with manual approvals, conditional logic, and external API calls. ## Permissions & safety AI in Twenty respects your permission model. Agents can only access objects and fields that the user (or role) has permission to view. Sensitive data stays protected even when AI is involved. Detailed reference on the chatbot, agents, and permission controls. # Apps Source: https://docs.twenty.com/getting-started/core-concepts/apps Extend Twenty with code — custom objects, server-side logic, UI components, and AI agents, all as TypeScript packages. Most CRMs give you a config panel. Twenty gives you a platform. Apps are how developers extend Twenty beyond what the UI offers — defining data models, server-side logic, UI components, and AI capabilities as code, then deploying them to one or more workspaces. ## Why apps exist Workflows cover no-code automation. But some things need code: a custom pricing engine, a proprietary enrichment pipeline, a compliance check that runs on every record update, a custom UI panel that pulls data from an internal tool. Apps let you build these as first-class extensions — not brittle scripts talking to an API from outside, but code that runs on the platform with full access to the type system, permission model, and UI. ## What an app can define An app is a TypeScript package that declares **entities** using the `twenty-sdk`: | Entity | What it does | | ---------------------- | --------------------------------------------------------------------------------------------- | | **Objects & Fields** | New data tables and fields on existing objects — same treatment as built-in ones | | **Logic Functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events | | **Front Components** | Sandboxed React components that render inside Twenty's UI (side panel, widgets, command menu) | | **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants | | **Views & Navigation** | Pre-configured list views and sidebar menu items | Everything is detected via AST analysis at build time — no config files, no registration boilerplate. Put a `export default defineObject(...)` in any `.ts` file and the SDK picks it up. ## How they run * **Logic functions** execute in isolated Node.js processes, sandboxed from the host. They access data through a typed API client scoped to the app's role permissions. * **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes). * **Permissions** are enforced at the API level. An app only sees what its role allows. ## The developer experience You write your app as a TypeScript project on your machine. The CLI watches your source files and live-syncs them to a running Twenty server — edit a file, see the change in the UI within a second. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty app:publish --private` pushes to a production server, or `yarn twenty app:publish` lists your app on npm and the Twenty marketplace. Three-phase walkthrough — scaffold, run a local server, sync your changes. # Calendar & Email Source: https://docs.twenty.com/getting-started/core-concepts/calendar-and-email Sync your email and calendar with Twenty. Twenty connects to your existing tools so your CRM stays up to date without manual data entry. ## Email sync Connect your **Google Workspace** or **Microsoft 365** mailbox. Once connected: * Emails are automatically linked to the matching Company and People records * Full email threads are visible on each record's timeline * You can send emails directly from Twenty * Multiple mailboxes per user are supported You control what gets imported — filter by date range or sender to avoid pulling in irrelevant emails. ## Calendar sync Calendar events sync automatically from your connected account. Events appear on the relevant CRM records, giving you a complete picture of your interactions with each contact or company. ## Integrations Beyond email and calendar, Twenty connects to external tools through: | Method | Use case | | ------------------------- | -------------------------------------------------------------------- | | **API** | Build custom integrations with the GraphQL or REST API | | **Webhooks** | Push real-time notifications to external systems when records change | | **Zapier** | Connect to 5,000+ apps without code | | **Workflow HTTP actions** | Call any external API as part of an automated workflow | ## Custom apps Developers can build full-featured apps on top of Twenty — adding custom UI, server-side logic, and deep integrations. Apps can be published for the community or kept private. Set up email sync, calendar sync, and troubleshoot issues. Build custom integrations with the Twenty API. # Dashboards Source: https://docs.twenty.com/getting-started/core-concepts/dashboards Track performance and visualize your CRM data with custom dashboards. Dashboards give you real-time visibility into your business metrics — pipeline health, team performance, revenue trends, and anything else you want to track. ## Widgets Each dashboard is made up of widgets. A widget is a single chart or metric tied to your CRM data. You can configure: * **Chart type** — Bar, line, pie, number, and more * **Data source** — Any object in your data model (standard or custom) * **Filters** — Narrow down to specific records, date ranges, or segments * **Aggregation** — Count, sum, average, min, max on any numeric field * **Grouping** — Break down by select fields, dates, or relations ## What you can track * Pipeline value by stage * Deals closed over time * Average deal size by source * Task completion rates * Custom metrics on any object ## Sharing Dashboards are workspace-level — everyone on your team can see them. Arrange widgets in a grid layout and resize them to build the view that works for your team. Detailed reference on creating dashboards, configuring widgets, and chart settings. # Data Model Source: https://docs.twenty.com/getting-started/core-concepts/data-model Understand how Twenty structures your data with objects, fields, and relations. Everything in Twenty is built around **objects** and **fields** — the building blocks of your data model. ## Objects Objects are the tables that hold your data. Twenty comes with standard objects out of the box: * **Companies** — Organizations you do business with * **People** — Individual contacts * **Opportunities** — Deals in your pipeline * **Tasks** — Action items for your team * **Notes** — Free-form text linked to records You can also create **custom objects** for anything your business needs — projects, support tickets, products, contracts, or anything else. ## Fields Fields are the properties on each object. Twenty supports a wide range of field types: | Category | Types | | ------------- | -------------------------------------------------------------------- | | **Basic** | Text, Number, Boolean, Date, Currency, Rating, Select | | **Composite** | Address (street, city, state, zip), Full Name, Links, Phones, Emails | | **Special** | Relation, File Attachment, JSON, Actor (who created/modified) | Every object also gets automatic system fields: `id`, `createdAt`, `updatedAt`, `createdBy`, and `position`. ## Relations Objects connect to each other through relations. A Company has many People, an Opportunity belongs to a Company, and so on. You can create custom relations between any objects, including many-to-many relationships. ## What makes this powerful Unlike traditional CRMs where you're limited to pre-defined fields on pre-defined objects, Twenty lets you model your data exactly the way your business works. Custom objects get the same first-class treatment as built-in ones — including API endpoints, views, permissions, and workflow triggers. Full reference on objects, fields, relations, and how to configure them. # Glossary Source: https://docs.twenty.com/getting-started/core-concepts/glossary Key terms used throughout Twenty. ## API API (Application Programming Interface) allows you to connect Twenty with other software systems and build custom integrations. ## Apps Apps are custom extensions built as code that can define data models and logic functions. They enable developers to create reusable customizations that can be deployed across multiple workspaces. ## Code Actions Code Actions are workflow steps that let you write custom JavaScript to transform data, make calculations, or perform complex logic that isn't possible with built-in actions. ## Command Menu The Command Menu is a quick-access interface (opened with `Cmd + K` on Mac and `Ctrl + K` on Windows) that lets you perform actions, create records, and navigate your workspace efficiently. ## Company & People The CRM has two fundamental types of records: * A `Company` represents a business or organization. * `People` represent your company's current and prospective customers or clients. ## Custom Fields Custom Fields are data fields you create to capture information specific to your business needs and processes. ## Data Model A Data Model is the structure that defines how information is organized in your CRM, including what objects exist, their properties (fields), and how they relate to each other. ## Favorites Favorites are records you've marked for quick access, appearing in your sidebar for instant navigation to important data. ## Field A field refers to a specific area where particular data is stored for an entity. ## Iterator An Iterator is a workflow action that loops through an array of items, executing subsequent actions for each item in the list. ## Kanban A `Kanban` is a visual way to track your business processes using cards and columns. Each column represents a stage in your process (for example: new, ongoing, won, lost), and you move records through these stages as they progress. ## Object An Object is a data structure that represents a specific type of entity in your CRM (like People, Companies, or Opportunities). Objects can be standard (built-in) or custom (created by you). ## Opportunities Opportunities in Twenty CRM are potential deals or sales with accounts or contacts. ## Record A Record indicates an instance of an object, like a specific account or contact. ## Relation Fields Relation Fields create connections between different objects, allowing you to link records together (like connecting a Person to a Company). ## Standard Fields Standard Fields are pre-built data fields that come with objects by default and provide common functionality across all workspaces. ## Tasks Tasks in Twenty CRM are assigned activities relating to contacts, accounts, or opportunities. ## Triggers Triggers are the starting point of a workflow — the event or condition that initiates the automation. Examples include record creation, record updates, webhooks, or scheduled times. ## Views You can customize the display of your records using views, setting different filters, layouts and sorting options for each view. ## Upsert Upsert is an operation that combines "update" and "insert" — it updates an existing record if a match is found, or creates a new record if no match exists. ## Webhooks Webhooks are automated messages sent from Twenty to other applications when specific events occur, enabling real-time data synchronization. ## Workflows Workflows are automated processes that trigger actions based on specific conditions, helping you automate repetitive tasks and business processes. ## Workspace A `Workspace` typically represents a company using Twenty. It holds all the records and data that you and your team members add to Twenty. It has a single domain name, which is typically the domain name your company uses for employee email addresses. ## Workspace Members Workspace Members are the Twenty users from your team who have access to your workspace. They can be assigned as owners or assignees for records. # Layout Source: https://docs.twenty.com/getting-started/core-concepts/layout How to navigate, browse, and view records in Twenty. ## The main layout The center of the screen is where your records live — people, companies, opportunities, tasks, notes, dashboards, workflows, and any custom objects. You view, edit, and delete records here, and create new views. ## Navigation bar The left sidebar gives you: * **Workspace switcher** — switch between workspaces or create a new one (top dropdown) * **Search** — press `/` to focus instantly, searches across all objects * **Settings** — access from the top left * **Favorites** — pinned views, unique per user * **Object shortcuts** — quick access to People, Companies, Opportunities, etc. * **Workflows** — create automations Drag items to reorder, create folders to group related objects, hide what you don't use. ## Command menu Press `Cmd+K` (Mac) or `Ctrl+K` (Windows) — or click the three dots in the top right. From here you can: * Create new records * Import and export data via CSV * Create new views * Access deleted records (Twenty supports soft and hard deletes) * See keyboard shortcuts for navigating your workspace ## Search Accessible via the Command Menu, the top of the navigation bar, or by pressing `/`. Search works across all objects. ## Side panel Click a record to open the side panel on the right — a quick overview of the record's key information without leaving the current page. Click **Open** to go to the full record page. ## Views Every object supports multiple views — unlimited per object. Use the dropdown at the top left to switch between them. * **Table** — spreadsheet-style rows and columns, with grouping, inline editing, and column customization * **Kanban** — drag-and-drop cards organized by a select field, ideal for pipelines * **Calendar** — records plotted by a date field for time-based planning Each view saves its own filters, sorting, and field visibility. Share views with your workspace or keep them private. Favorite views for fast access from the sidebar. ## Record pages When you open a record, the detail page is built from configurable **tabs** and **widgets**. Add, remove, reorder, and resize widgets on a grid — fields, related records, emails, timeline, tasks, notes, files, charts, iframes, and more. Each object type has its own layout. Navigation, views, record pages — detailed reference and how-tos. # Workflows Source: https://docs.twenty.com/getting-started/core-concepts/workflows Automate your business processes with Twenty's visual workflow builder. Workflows let you automate repetitive tasks and connect Twenty to external tools — without writing code (though you can if you want to). ## How workflows work Every workflow has three parts: 1. **Trigger** — What starts the workflow 2. **Steps** — What happens next (one or more actions in sequence) 3. **Variables** — Data that flows between steps ## Triggers | Trigger | When it fires | | ---------------- | -------------------------------------------------------------------------- | | **Record event** | A record is created, updated, deleted, or upserted | | **Manual** | A user clicks a button (on a single record, multiple records, or globally) | | **Schedule** | On a recurring interval (cron syntax) | | **Webhook** | An external system sends an HTTP POST | ## Actions Workflows can chain any combination of: * **Record operations** — Create, update, find, delete, or upsert records * **Send email** — Send or draft emails from connected accounts * **HTTP request** — Call any external API * **Code** — Run custom JavaScript for complex logic * **Branches** — If/else conditions to split the workflow path * **Iterator** — Loop over arrays of data * **AI Agent** — Let an AI agent process data autonomously * **Delay** — Wait before continuing * **Form** — Collect user input mid-workflow ## What you can build * Send Slack alerts when a deal reaches a certain stage * Auto-enrich new contacts with data from external APIs * Detect stale opportunities and notify the owner * Sync data between Twenty and your billing system * Generate PDFs or invoices from record data * Auto-reply to inbound emails matching certain criteria Detailed reference on triggers, actions, variables, and real-world automation recipes. # Why Twenty Source: https://docs.twenty.com/getting-started/introduction You've been choosing between software that's easy to start but impossible to change, and software that's flexible but takes months to set up. Twenty is the third option: **a production-ready CRM you can reshape as you go.** ## What makes Twenty different Twenty is a platform you can build on, not a product you configure. Built for Agents Agents operate inside your data model with real permissions. Skills, Tools, MCP. Secured Extensibility The flexibility of vibe-coded tools on secured foundations. Modern Stack React, TypeScript. Your team already knows how to extend Twenty. No proprietary languages, no gatekeeping. No Lock-In Open-source core, self-hostable, export your data anytime. ## Who is Twenty for * **Large enterprises replacing Salesforce** — your team spends more time fighting the tool than using it. You feel locked-in and the costs keep rising. * **Startups with technical founders** — you've outgrown spreadsheets and Notion. You have big ambitions and want a CRM that scales with you. * **GTM teams looking for an edge** — you want to build your own lead scoring, your own enrichment, your own outbound workflows. * **Privacy-conscious organizations** — you need to self-host and own your data end to end. Regulatory, contractual, or just principle. * **Salesforce partners** — your developers are tired of presenting license cost increases to your clients. You want a better DX, and to deliver projects faster at a lower license cost for your clients. * **Web development agencies** — your team knows TypeScript, React, and PostgreSQL. Twenty opens the CRM market to you without learning APEX or getting proprietary certifications. ## Who Twenty is not for * **Teams that want a CRM they never have to think about.** Twenty rewards teams that want to stay close to their tools and shape them over time. If you want something fully managed, Pipedrive or HubSpot will serve you well. * **Companies that need hundreds of pre-built integrations today.** Our ecosystem is growing fast, but it's not yet as broad as Salesforce or HubSpot. If you're comfortable building what's missing, you'll love it. If not, give us another year. * **Organizations where tools are chosen in boardrooms, not by the people using them.** We don't do steak dinners and executive briefings. We win with teams that have the authority to pick their own tools. Set up Twenty in under 5 minutes — cloud or self-hosted. # Key Features Source: https://docs.twenty.com/getting-started/key-features A tour of everything Twenty can do — from custom data models to AI-powered automation. Twenty is a full-featured CRM platform. Here's what you can build with it. Custom Data Model Define the exact data structure your business needs.
Create custom objects, add custom fields with 20+ field types, and build relationships between any objects.
Views & Pipelines Switch between table views, kanban boards, and calendar views.
Filter with AND/OR logic, sort by multiple fields, group records, and save custom views.
Workflows & Automation Automate any business process without writing code.
Trigger workflows on record changes, schedules, manual actions, or incoming webhooks.
Calendar & Email Sync Connect your Google Workspace or Microsoft 365 account.
Emails and calendar events automatically appear on the relevant CRM records.
AI AI agents that work autonomously within your CRM — answering questions, enriching records, and executing multi-step tasks within your permission model. Dashboards & Reporting Build custom dashboards with real-time widgets.
Track pipeline metrics, team performance, and business KPIs with configurable charts and filters.
Permissions & Access Control Role-based access control at every level — objects, fields, and individual records.
Configure SSO with SAML or OIDC. Audit logs track who did what.
API & Extensibility A developer-first API that adapts to your custom data model.
Both GraphQL and REST endpoints, with auto-generated documentation per workspace.
Data Import & Export Import data from CSV files or via API.
Field mapping, duplicate detection, and error handling built in. Export your data anytime — no lock-in.
Self-Hosting Run Twenty on your own infrastructure with a single Docker Compose command.
Full control over your data, updates on your schedule.
# Quickstart Source: https://docs.twenty.com/getting-started/quickstart Get Twenty up and running in under 5 minutes — on the cloud or self-hosted. ## Signup Go to [app.twenty.com](https://app.twenty.com) and sign up with Google, Microsoft, or email. Pick **30 days** (with card) or **7 days** (without card). Both include full access — unlimited contacts, email integration, custom objects, API. You can change plan or billing interval anytime. After payment confirmation via Stripe, you'll set up your workspace name and user profile. You can cancel anytime. ## Configure your workspace Once you're in, three steps to make Twenty yours: ### 1. Connect your mailbox Go to **Settings → Accounts** and connect your Google or Microsoft account. Twenty will import your emails and calendar events, and auto-create contacts from interactions. Using another provider? You can add mailboxes via SMTP or calendars via CalDAV from the same page. Start here — connecting a mailbox gives your team immediate value with real data before you customize anything else. ### 2. Shape your data model Go to **Settings → Data Model** to create custom objects and fields. A few things to know: * Custom objects and fields are **unlimited on all plans** — no upsell. * **People, Companies, and Opportunities** are the objects that show synced emails and meetings. Use them as your base and add fields to categorize (e.g., a `Person Type` field) rather than creating separate objects that won't have email history. * Two People can't share the same email. Two Companies can't share the same domain. * You can deactivate standard fields/objects you don't need, and hide fields from views without deleting them. [Data Model reference →](/user-guide/data-model/overview) ### 3. Import your data Use the Command Menu (`Cmd+K` / `Ctrl+K`) to import People, Companies, Opportunities, or any custom object via CSV. Download the sample file first to see the expected format. Limit files to 10k records and deduplicate emails/domains before importing. [Data Migration guide →](/user-guide/data-migration/overview) ## Next steps Navigation, views, command menu, side panel. Automate your business processes. Table, kanban, calendar — filter and sort your data. Schema-per-tenant REST and GraphQL. # AI Agents Source: https://docs.twenty.com/user-guide/ai/capabilities/ai-agents Integrate AI capabilities directly into your automation workflows. ## Overview Integrate AI capabilities directly into your automation workflows for intelligent data processing and decision-making. ## Capabilities | Feature | Description | | ------------------- | ------------------------------------------------ | | **AI actions** | Add AI-powered steps to any workflow | | **Data enrichment** | Automatically enhance records with external data | | **Classification** | Categorize records based on content analysis | | **Summarization** | Generate summaries from text fields | | **Custom prompts** | Define exactly how AI processes your data | ## Use Cases * **Lead scoring**: Automatically score and prioritize inbound leads * **Data cleanup**: Standardize company names and contact information * **Email drafts**: Generate follow-up emails based on meeting notes * **Record routing**: Assign records to the right team member based on content ## Related * [Workflows Overview](/user-guide/workflows/overview) — automation basics * [AI Permissions](/user-guide/ai/capabilities/permissions-access-control) — access control for AI agents # AI Chatbot Source: https://docs.twenty.com/user-guide/ai/capabilities/ai-chatbot An intelligent assistant that helps you interact with your CRM data using natural language. ## Overview Ask questions in natural language and the assistant retrieves and explains your CRM data for you. ## Capabilities | Feature | Description | | ---------------------------- | ------------------------------------------------------------------------- | | **Natural language queries** | Ask questions in plain English instead of building filters | | **Full data access** | Query records, relationships, and metrics across your workspace | | **Page context** | Reference "this company" or "this opportunity" based on your current view | | **Conversational** | Follow-up questions maintain context from previous queries | ## Example Interactions ### Finding Records * "Show me all opportunities over \$50,000" * "Find contacts I haven't emailed in 2 weeks" * "List companies in the healthcare industry" ### Getting Insights * "What's my total pipeline value?" * "How many deals closed last month?" * "Which stage has the most stuck opportunities?" ### Using Page Context * "Summarize my interactions with this person" (on a contact page) * "What opportunities are linked to this company?" (on a company page) * "When was this deal last updated?" (on an opportunity page) # Permissions & Access Control Source: https://docs.twenty.com/user-guide/ai/capabilities/permissions-access-control Control what AI agents can access and modify in your workspace. ## Overview AI agents respect your existing permission structure. This is particularly important for teams who want to control exactly what automated AI processes can access or modify in their workspace. ## Assign a Role to an AI Agent 1. Go to **Settings → Members → Roles** 2. Click on the role you want to assign 3. Open the **Assignment** tab 4. Under **AI Agents**, click **+ Assign to AI agent** 5. Select the AI agent from the list 6. Confirm the assignment ## Why Assign Roles to AI Agents? | Benefit | Description | | ---------------- | ------------------------------------------------- | | **Security** | Limit what data AI agents can access or modify | | **Compliance** | Ensure AI only processes the data it needs | | **Control** | Prevent unintended actions from AI automations | | **Auditability** | Track which actions were performed by which agent | For AI agents running within workflows, role assignment ensures the agent cannot access or modify data outside its intended scope—even if the workflow has broader permissions. ## Related * [Permissions](/user-guide/permissions-access/capabilities/permissions) — detailed information on creating and managing roles * [AI Agents](/user-guide/ai/capabilities/ai-agents) — AI capabilities in workflows # AI FAQ Source: https://docs.twenty.com/user-guide/ai/how-tos/ai-faq Frequently asked questions about AI features in Twenty. Two main capabilities of AI are: 1. **AI Chatbot**: A context-aware assistant that can access your Twenty data and help you with queries 2. **AI Agents in Workflows**: Intelligent automation that can process data, make decisions, and execute tasks within your workflows AI agents operate under the permission system. You can assign specific roles to AI agents under **Settings → Members → Roles**, giving you full control over what data they can access and what actions they can perform. AI actions consume workflow credits based on the complexity of the task and the AI model used. On cloud, you can use only AI models provided by Twenty. On your self-hosted instance, you can use your own AI models after obtaining an Organization license. By default, the Roles list is filtered to roles assignable to workspace members. If a role is assignable only to AI agents, you need to change the filter to include AI-agent roles. # AI Source: https://docs.twenty.com/user-guide/ai/overview AI-powered features in Twenty. AI ## Features Currently, Twenty offers: ### 1. AI Chatbot A conversational assistant that understands your context and has access to all your Twenty data. **Key capabilities:** * **Full data access**: Query any record, relationship, or metric in your workspace * **Page context awareness**: Reference "this company" or "this opportunity" based on where you are in Twenty * **Natural language**: Ask questions and get answers without navigating menus **Example prompts:** * "What opportunities are closing this month?" * "Which deals have been in Negotiation for more than 30 days?" * "Summarize my interactions with this person" ### 2. AI Agents in Workflows Extend your workflows with AI-powered actions and autonomous agents. **Key capabilities:** * **AI actions**: Use AI to enrich data, classify records, generate summaries, and more * **Autonomous agents**: Let agents execute multi-step tasks within a workflow * **Custom prompts**: Define exactly how AI should process your data **Use cases:** * Automatically categorize inbound leads * Enrich company data from public sources * Generate follow-up email drafts based on meeting notes * Score opportunities based on engagement patterns ## Permissions and Access Control AI agents are managed through the existing permissions system: 1. Go to **Settings → Members → Roles** 2. Configure which data each AI agent can access 3. Set read/write permissions per object This ensures AI agents respect your data governance policies and only access what they need. * Follow our [GitHub](https://github.com/twentyhq/twenty) for development updates * Join our [Discord](https://discord.gg/UfGNZJfAG6) to share feedback and feature requests # Calendar Source: https://docs.twenty.com/user-guide/calendar-emails/capabilities/calendar Understanding calendar integration features in Twenty. **Note**: To connect your calendar and configure sync settings, visit [Email & Calendar Setup](/user-guide/calendar-emails/overview). ## How Calendar Integration Works Twenty automatically syncs your calendar events and links them to the relevant CRM records, giving you a complete view of your meeting history with contacts and companies. ## Calendar Tab Next to the Emails tab on records, you'll find a `Calendar` tab that contains the history of meetings scheduled with the record. ### Available For * **People**: View all meetings scheduled with a specific contact * **Companies**: See all meetings related to a company and its employees * **Opportunities**: Access meeting history related to the company linked to the opportunity ### Viewing Meeting History 1. **Navigate to a Record**: Go to any Person, Company, or Opportunity record 2. **Select the Calendar Tab**: Click on the `Calendar` tab next to the Emails tab 3. **Browse Meeting History**: View all scheduled meetings and their details 4. **Access Meeting Context**: See meeting participants, times, and related information ## Visibility Settings Calendar data follows the same visibility settings as emails, ensuring consistent privacy controls across both communication channels. ## What Gets Synced * **External Meetings**: All meetings with contacts outside your organization * **Automatic Linking**: Meetings connect to existing People and Company records based on attendee email addresses * **Meeting Details**: Subject, time, duration, and participants * **Updates**: New calendar events sync automatically ## What Doesn't Get Synced * **Internal Meetings**: Meetings with only colleagues (same domain) remain private * **Private Events**: Events marked as private in your calendar # Mailbox Source: https://docs.twenty.com/user-guide/calendar-emails/capabilities/mailbox Understanding email integration features in Twenty. **Note**: To connect your email accounts and configure sync settings, visit [Email & Calendar Setup](/user-guide/calendar-emails/overview). ## How Email Integration Works Twenty automatically links emails from your connected mailboxes to the relevant CRM records, keeping all communication history in one place. ### Objects Where Emails Can Be Found Email conversations appear in three main objects: * **People**: View all emails exchanged with a specific contact * **Companies**: See all emails related to a company and its employees * **Opportunities**: Access email threads related to the company linked to the opportunity. Email threads from individual people on the opportunity are not shown yet. ### Viewing Email Threads 1. **Navigate to a Record**: Go to any Person, Company, or Opportunity record 2. **Select the Emails Tab**: Click on the `Emails` tab to view synced emails 3. **Open an Email Thread**: Click on any email to open and read the full conversation 4. **Browse History**: Scroll through the complete email history with that contact ## What You'll See ### Email Thread View When you open an email thread, you can: * **Read Full Conversations**: See the complete email exchange * **View Participants**: See all people involved in the email thread * **Check Timestamps**: Know exactly when each email was sent * **Access Context**: Understand the full communication history ### Email Visibility Depending on your mailbox settings, you might see: * **Full Content**: Complete email text and details * **Subject + Metadata**: Subject line, sender, recipient, and timestamp * **Metadata Only**: Basic information without email content ## Email Sync Behavior ### Sync Rate Twenty imports emails at approximately **400 messages per minute**. This rate is currently limited by Gmail API rate limiting (and applies similarly to other providers). For reference: * 1,000 emails → \~2.5 minutes * 10,000 emails → \~25 minutes * 50,000 emails → \~2 hours ### What Gets Synced * **External Emails**: All emails with contacts outside your organization * **Automatic Linking**: Emails connect to existing People and Company records * **Multiple Addresses**: Emails from any address link to the same contact record * **Updates**: New emails appear immediately ### What Doesn't Get Synced * **Internal Emails**: By default, emails between colleagues (same domain) remain private (configured in Settings → General → Security) * **Group Emails**: Distribution lists and group emails are excluded * **Excluded Folders**: Folders you've chosen not to sync (configured under Settings → Accounts → Email) ### Internal Emails Choose if internal emails are synchronized in Twenty: 1. Go to **Settings → General → Security** 2. Toggle **Sync Internal Emails** on/off ### Selective Folder Sync Control which email folders sync with Twenty: 1. Go to **Settings → Accounts** and select your connected email account 2. Configure folders under the Email section 3. Choose specific folders to include or exclude (Inbox, Sent, Archive, custom folders) ## Troubleshooting Email Sync ### Common Sync Issues * **Sync Delays**: Emails appear immediately, but initial imports take longer * **Missing Emails**: Check if: * Folders are excluded in Message Folder settings * Contact auto-creation is disabled (emails need existing Twenty records) * Email is from colleagues (same domain) or group lists * Mailbox is still completing initial sync ### Email Limitations * **System Folders**: Some email folders may not be available for sync * **Aliases**: Only true mailboxes can be connected (not email aliases) # Can I Book Meetings from Twenty? Source: https://docs.twenty.com/user-guide/calendar-emails/how-tos/can-i-book-meetings-from-twenty Information about booking meetings directly from Twenty. ## Current Status Twenty's calendar integration is designed to **sync and display** your existing calendar events. ## Scheduling Meetings via Workflows While it's not possible to schedule meetings manually, you can schedule them in Twenty using workflows. 1. Create a new workflow 2. Select any trigger depending on your needs 3. Add the **Create Calendar Event** node 4. Configure all necessary data 5. Activate the workflow ## What You Can Do * **View meeting history** on People, Companies, and Opportunities records * **See upcoming meetings** with contacts in your CRM * **Track meeting context** alongside email communications * **Auto-create contacts** from meeting participants # Can I Send Emails from Twenty? Source: https://docs.twenty.com/user-guide/calendar-emails/how-tos/can-i-send-emails-from-twenty Information about sending emails directly from Twenty. ## Current Status Twenty's email integration is designed to **sync and display** your email history as well as compose and send emails. When you view an email thread on a record page and click **Reply**, you're able to directly compose and send an email. ## How to send a message from Twenty Prerequisite: a connected mail account with the ability to send emails 1. Open the Command menu with the **Ctrl+K** or **Cmd+K** shortcut 2. Click the "Compose email" option 3. Choose recipients, add subject, body and attachments to your email 4. Click the "Send" button to send the email ## What You Can Do Today * **View email history** on People, Companies, and Opportunities records * **Read full email threads** with contacts in your CRM * **Track communication context** alongside calendar events * **Auto-create contacts** from email interactions * **Reply via redirect** — click Reply to jump to your mailbox ## Sending Emails via Workflows You **can send emails automatically using workflows**. This is useful for: * Automated follow-ups * Notifications to contacts * Triggered communications based on record changes Emails sent via workflows go through your connected mailbox account. → Learn about the [Send Email action](/user-guide/workflows/capabilities/workflow-actions#send-email) ## Email Sequences and Newsletters For email sequences and newsletters, we recommend using workflows to connect Twenty to a dedicated email marketing tool. Mass emails should not be sent directly from your mailbox to protect your domain reputation. Use a dedicated tool for bulk communications. → See [How to send emails from workflows](/user-guide/workflows/capabilities/send-emails-from-workflows) for setup instructions ## Future Plans Native email composition from within Twenty is on our roadmap. Join our [GitHub discussions](https://github.com/twentyhq/twenty/discussions) to share your use case and help prioritize this feature. # Can I Track Email Activity on All Objects? Source: https://docs.twenty.com/user-guide/calendar-emails/how-tos/can-i-track-email-activity-on-all-objects Understanding email activity tracking across different objects. ## Supported Objects Email activity is currently available on **three standard objects**: | Object | What You See | | ----------------- | ---------------------------------------------------------------- | | **People** | All emails exchanged with that specific contact | | **Companies** | All emails with anyone from that company (based on email domain) | | **Opportunities** | Emails related to the company linked to the opportunity | ## Why Only These Objects? People, Companies, and Opportunities are the core relationship objects where email context adds the most value. Email threads are automatically linked based on: * **Email address** → matched to People records * **Email domain** → matched to Company records * **Company relation** → linked to Opportunities ## Custom Objects **Email tracking is not available on custom objects** at this time. If you need email context on a custom object, consider: * Using a relation field to link your custom object to People or Companies * Viewing email history on the linked People/Company record ## Future Plans Extending email visibility to custom objects is being considered. Share your use case on our [GitHub discussions](https://github.com/twentyhq/twenty/discussions) to help prioritize this feature. # Connect Several Mailboxes per User Source: https://docs.twenty.com/user-guide/calendar-emails/how-tos/connect-several-mailboxes-per-user Connect multiple email accounts for a single user. ## Overview Twenty supports **unlimited email accounts per user**. This is useful if you manage multiple inboxes, such as: * Personal work email + shared team inbox * Multiple client-facing email addresses * Different email accounts for different roles ## How to Add Multiple Mailboxes 1. Go to **Settings → Accounts** 2. Click **Add account** 3. Connect an additional Google or Microsoft account, or any other mail provider supporting IMAP/SMTP protocols 4. Configure sync settings for this mailbox 5. Repeat for each mailbox you want to connect ## Managing Multiple Accounts Each connected mailbox has its own settings: * **Email visibility**: Choose what teammates can see * **Contact auto-creation**: Enable/disable per mailbox * **Folder selection**: Choose which folders to sync ## How Emails Appear Emails from all your connected mailboxes are synced to Twenty and appear on: * **People records**: Based on the contact's email address * **Company records**: Based on the email domain * **Opportunities**: Based on the linked company Each email shows which mailbox it was sent from or received in, so you can track which account was used for each communication. ## Important Notes Only true mailboxes can be connected. Email aliases that forward to another mailbox cannot be connected separately — they'll sync through the main mailbox. # I Don't See Emails on Records Source: https://docs.twenty.com/user-guide/calendar-emails/how-tos/i-dont-see-emails-on-records Troubleshooting missing emails on records. ## Common Reasons ### 1. Initial Sync Still in Progress Email sync takes time, especially for large mailboxes. Twenty imports emails at approximately **400 messages per minute** (limited by Gmail API rate limiting). * **Calendar sync**: Completes in minutes * **Email sync**: Depends on mailbox size (e.g., 10,000 emails ≈ 25 minutes, 50,000 emails ≈ 2 hours) **Solution**: Wait for the initial import to complete. You can estimate timing based on your mailbox size. ### 2. Contact Doesn't Exist in Twenty Emails only appear on existing People records. If the contact wasn't created yet: * Enable **Contact Auto-Creation** in your mailbox settings * Or manually create the Person record first **Solution**: Go to **Settings → Accounts**, select your mailbox, and enable contact auto-creation. ### 3. Internal Emails Are Excluded Emails between colleagues (same email domain) are never synced to maintain privacy. **Solution**: This is expected behavior. Only external emails are synced. ### 4. Email Is from a Group or Distribution List Group emails and distribution lists are excluded from sync. **Solution**: This is expected behavior. ### 5. Folder Not Selected for Sync If you're using the Message Folder feature, some folders might be excluded. **Solution**: Go to **Settings → Accounts**, select your mailbox, and check folder sync settings. ### 6. Wrong Email Address on Record The Person record might have a different email address than the one used in the email. **Solution**: Add the correct email address to the Person record. ## Still Not Working? 1. Try disconnecting and reconnecting your mailbox 2. Contact support if issues persist # Limit Emails Imported Source: https://docs.twenty.com/user-guide/calendar-emails/how-tos/limit-emails-imported Control which emails are imported into Twenty. ## Overview By default, Twenty syncs all external emails from your connected mailbox. You can limit what gets imported using **folder selection** and **visibility settings**. ## Method 1: Folder Selection (Recommended) Control which email folders sync with Twenty: 1. Go to **Settings → Accounts** 2. Select your connected email account 3. Choose which folders to sync: | Folder | Description | | ------------------ | ----------------------------- | | **Inbox** | Primary incoming emails | | **Sent** | Outgoing emails you've sent | | **Archive** | Archived messages | | **Custom Folders** | Any specific folders you want | 4. Exclude folders you don't want synced (Spam, Trash, personal folders) This gives you precise control over which emails appear in your CRM without syncing everything. ## Method 2: Contact Auto-Creation Settings Control when contacts are created from emails: 1. Go to **Settings → Accounts** 2. Select your connected mailbox 3. Choose an option: * **Deactivated**: No contacts created, but emails still sync to existing contacts * **Sent & Received**: Create contacts from all external emails * **Sent Only**: Only create contacts from emails you send ## Internal Emails Messages between colleagues (same domain) can be synchronized in Twenty: 1. Go to **Settings → General → Security** 2. Toggle **Sync Internal Emails** on/off By default, this option is turned off. ## What's Always Excluded These emails are never synced, regardless of settings: * **Group emails**: Distribution lists and group messages * **Spam/Trash**: System folders are typically excluded ## Important Note We don't provide a CC email address for selective syncing yet. Use the folder selection feature above to achieve the same level of control. # Calendar & Emails Source: https://docs.twenty.com/user-guide/calendar-emails/overview Connect your email and calendar accounts to Twenty. ## Connection Options ### Google Account (Gmail & Google Calendar) 1. Go to **Settings → Accounts** 2. Click **Add account** 3. Select **Continue with Google** 4. Authorize Twenty to access your Gmail and Google Calendar 5. Configure email sync settings (visibility, auto-creation) → click **Next** 6. Configure calendar sync settings (visibility, auto-creation) → click **Add Account** 7. Your emails and calendar events will start syncing automatically ### Microsoft Account (Outlook & Microsoft Calendar) 1. Go to **Settings → Accounts** 2. Click **Add account** 3. Select **Continue with Microsoft** 4. Authorize Twenty to access your Outlook and Microsoft Calendar 5. Configure email sync settings (visibility, auto-creation) → click **Next** 6. Configure calendar sync settings (visibility, auto-creation) → click **Add Account** 7. Your emails and calendar events will start syncing automatically ### IMAP/SMTP/CalDAV Setup (Other Providers) For other email and calendar providers: 1. Go to **Settings → Accounts** 2. Configure IMAP settings to sync incoming email and SMTP settings to send email 3. Configure CalDAV settings for calendar 4. Test the connection **Self-hosting on an air-gapped or internal network**: By default, Twenty refuses outbound connections to private/internal IP addresses (SSRF protection). If your mail or calendar server runs on a local/private IP (e.g. an on-premise server on a LAN), connections to it will be blocked. To allow them, set the following environment variable on the server: ``` OUTBOUND_HTTP_SAFE_MODE_ENABLED=false ``` This disables safe mode for **all** outbound requests (HTTP workflow actions, webhooks, and IMAP/SMTP/CalDAV connections), so only set it on trusted, isolated networks where the SSRF protection is not needed. ### Multiple Mailboxes * **Unlimited Accounts**: Connect multiple email accounts per user * **Account Management**: Switch between different mailboxes * **Sync Settings**: Configure different settings per mailbox Only true mailboxes can be connected (e.g., [support@domain.com](mailto:support@domain.com) with its own inbox). Email aliases that forward to another mailbox cannot be connected to Twenty. ## Email Configuration ### Message Visibility Choose different levels of visibility for your emails: * **Metadata Only**: Share only basic information (sender, recipient, date, time) * **Subject and Metadata**: Share subject line along with metadata * **All Email Content**: Share entire email content including attachments ### Contact Auto-Creation * **Deactivated**: No automatic contact creation * **For messages sent and received**: Create contacts for all external email interactions * **For messages sent only**: Create contacts only for emails you send * **Note**: By default, internal emails (where all participants share your domain) are not synced to protect privacy When enabled, contacts are automatically linked to their Company records based on their email domain. If the company doesn't exist yet, Twenty creates it for you. **Syncing internal emails**: The "internal emails not synced" behavior is the default, but it can be turned off. The toggle lives in advanced settings: 1. Open **Settings** and enable the **Advanced** toggle at the bottom of the settings page 2. Go to **General → Security** 3. Enable the **Sync Internal Emails** toggle to include emails where all participants share the same domain This is a workspace-wide setting (useful for universities or shared-domain organizations). ### Control which emails get synced with Message Folder Selection Control which email folders sync with Twenty: 1. Go to **Settings → Accounts** and select your connected email account 2. Choose which folders to sync: * **Inbox**: Primary incoming emails * **Sent**: Outgoing emails you've sent * **Custom Folders**: Any specific folders you want to include * **Exclude Folders**: Skip folders like Spam, Trash, or personal folders This gives you precise control over which emails appear in your CRM without syncing everything. **What Gets Synced:** * **External Emails**: All emails with external contacts from selected folders * **Internal Emails**: Not synced by default (same domain emails remain private). Enable the **Advanced** toggle at the bottom of **Settings**, then turn on **Sync Internal Emails** under **General → Security** to include them workspace-wide. * **Attachments**: Coming in H1 2026 **Note**: We don't provide a CC email address for selective syncing. Instead, use the Message Folder feature above to achieve the same level of control over which emails sync with Twenty. ## Calendar Configuration ### Event Visibility Choose what will be visible to other users in your workspace: * **Everything**: The whole event's details will be shared with your team * **Metadata**: Only date and participants will be shared with your team ### Contact Auto-Creation for Meetings * **Yes**: Automatically create contacts for meeting participants not in your CRM * **No**: Only link meetings to existing contacts When enabled, contacts are automatically linked to their Company records based on their email domain. If the company doesn't exist yet, Twenty creates it for you. ### Control which events get synced * **Meeting Import**: Automatically import calendar events * **Contact Linking**: Link meetings to People and Company records **What Gets Synced:** * **Meetings**: Calendar events with external participants * **Contact Linking**: Events automatically linked to CRM records * **Team Events**: Shared calendar visibility ## Sync Frequency **Updates every 5 minutes**: Both email and calendar data sync automatically every 5 minutes after the initial import. **Initial sync timing**: Calendar sync completes quickly (usually within minutes), while email sync takes longer for large mailboxes. Twenty imports emails at approximately **400 messages per minute** (limited by Gmail API rate limiting), so a mailbox with 10,000 emails takes about 25 minutes. Don't worry if you see contacts from calendar events appearing before your email contacts; this is normal behavior. ## Next Steps * [Mailbox capabilities](/user-guide/calendar-emails/capabilities/mailbox) * [Troubleshoot missing emails](/user-guide/calendar-emails/how-tos/i-dont-see-emails-on-records) # Chart Settings Source: https://docs.twenty.com/user-guide/dashboards/capabilities/chart-settings Configure your charts to display data exactly how you need it. ## Accessing Chart Settings 1. Click the **Edit** button in the top-right corner of your dashboard Edit mode 2. The dashboard enters edit mode 3. Click on any widget to select it 4. The settings panel opens on the right Settings are organized into sections: * **Data**: What data to pull and how to filter it * **X axis**: How to group and sort categories (bar/line charts) * **Y axis**: What to measure and how to display it (bar/line charts) * **Style**: Colors, labels, and visual options Some settings only appear when other settings are configured. For example, **Date granularity** only appears when you select a date field. *** ## Data Settings ### Source Choose which object to pull data from. 1. Click **Source** 2. Select an object (Opportunities, People, Companies, etc.) All your standard and custom objects are available. Changing the source resets other settings since field options depend on the selected object. Source ### Filter Limit which records are included in the chart. 1. Click **Filter** 2. The filter builder opens 3. Click **+ Add filter** 4. Select a field, condition, and value 5. Add more filters as needed Filters work the same as in table views. Use them to focus on specific segments of your data. **Example**: Filter opportunities where Stage equals "Won" to show only closed deals. Filter For date fields, try **Is relative** to create dynamic filters like "Past 7 days" or "This month" that automatically update. Advanced filters are supported—add multiple filter rules and combine them with **And** or **Or** logic. *** ## X Axis Settings These settings control the horizontal axis of bar and line charts—typically how your data is grouped into categories. ### Data on display Select the field to group your data by. This determines the categories shown on the X axis. **Example**: Select "Stage" to see data grouped by pipeline stage, or "Created date" to see data over time. Data on display ### Date granularity When grouping by a date field, choose the time period for grouping. | Option | Groups data by | Example output | | ------------------- | -------------------- | --------------------------------------------- | | **Day** | Each calendar day | Jan 1, Jan 2, Jan 3... | | **Week** | Calendar weeks | Week 1, Week 2, Week 3... | | **Month** | Calendar months | January, February, March... | | **Quarter** | Fiscal quarters | Q1, Q2, Q3, Q4 | | **Year** | Calendar years | 2023, 2024, 2025 | | **Day of week** | Weekday (aggregated) | Monday, Tuesday... (all Mondays combined) | | **Month of year** | Month (aggregated) | January, February... (all Januaries combined) | | **Quarter of year** | Quarter (aggregated) | Q1, Q2, Q3, Q4 (all Q1s combined) | The "of week/year" options aggregate across all time. Use **Day of week** to see which weekday typically gets the most deals. Use **Month** to see trends month by month. ### Sort by Control the order of categories on the X axis. * **Position** – Uses the order defined in your Select field settings * **Alphabetical** – A to Z (or chronological for dates) * **By value** – Ordered by the metric (highest or lowest first) * **Manual** – Drag to set your own order Sort by **Tip**: For pipeline stages, use **Position** to show stages in your sales process order (Lead → Qualified → Proposal → Won). The options shown depend on your field type. Select fields have all options; date and text fields show fewer. With **Manual** sorting, drag categories to create a custom order—perfect for highlighting specific segments or telling a story with your data. ### Omit zero values Hide categories that have zero or no data. * **On**: Categories with no records are hidden * **Off**: All categories are shown, even if empty Use this to clean up charts with sparse data. *** ## Y Axis Settings These settings control what you're measuring and how it's displayed. ### Data on display Select the field and operation to calculate your metric. * **Counts** – Count all, Count empty, Count not empty, Count unique values * **Math** – Sum, Average, Min, Max (number fields) * **Percentages** – Percent empty, Percent not empty Boolean fields add Count true/false. Aggregate charts with date fields show Earliest/Latest. **Example**: Select "Amount" with Sum to see total deal value per category, or Count all to see number of deals. Data on display The options shown depend on your field and chart type. If you don't see an expected operation, check your field type. ### Ratio For Select, Multi-select, or Boolean fields, you can calculate the percentage of a specific value. 1. Select a Select, Multi-select, or Boolean field 2. Enable **Ratio** 3. Choose the option to measure **Example**: Show "Won" deals as a percentage of all opportunities. If you have 10 Won out of 50 total, the chart shows 20%. ### Group by Add a second dimension to create multiple series (multiple bars or lines). 1. Set your X axis field first 2. Click **Group by** 3. Select a second field **Example**: Group deals by Stage (X axis) AND by Owner (Group by) to see each person's pipeline breakdown side by side. Group by When Group by is set, you'll see multiple colors in your chart and a legend appears. ### Date granularity When Group by is a date field, choose the time period (same options as X axis). ### Sort by When **Group by** is set, control the order of grouped series (e.g., which owner's bar appears first within each category). Same options as X axis sorting—Position, Alphabetical, and Manual—except **value-based sorting is not available** for secondary grouping. ### Cumulative Show running totals instead of individual values. * **Off**: Each bar/point shows its own value * **On**: Each bar/point shows the sum of all previous values plus its own **Example**: January = 10, February = 15 * Cumulative off: Shows 10 and 15 * Cumulative on: Shows 10 and 25 (10 + 15) Use cumulative to track progress toward a goal over time. Cumulative ### Min range / Max range Set fixed minimum and maximum values for the Y axis. * Leave blank for automatic scaling * Enter a number to fix the boundary **When to use:** * Set Min to 0 for accurate visual comparisons * Set both for consistent scales across multiple charts * Focus on a specific value range by setting both *** ## Style Settings ### Colors Set the color scheme for your chart. Choose from a variety of color palettes. When grouping by a Select field, the chart automatically uses the colors defined for each option in your field settings. ### Axis name Choose which axis labels to show: **None**, **X axis**, **Y axis**, or **Both** (bar and line charts only). ### Stacked bars / Stacked lines When Group by is set, choose how multiple series are displayed. * **Off (Grouped)**: Series appear side by side. Use when comparing exact values between groups. * **On (Stacked)**: Series stack on top of each other. Use when showing composition—how parts make up a whole. **Example**: Deals by stage, grouped by owner * Grouped: See each owner's exact numbers side by side * Stacked: See total deals per stage, with each owner's contribution stacked ### Data labels Show values directly on chart elements (bars, points, slices). * **On**: Values appear on the chart * **Off**: No values shown (cleaner look) ### Legend Show or hide the legend (only appears when Group by is set). * **On**: Legend shows what each color represents * **Off**: No legend displayed **Hover** over a legend item to highlight that group. **Click** to temporarily hide it from the chart. *** ## Chart-Specific Notes ### Pie charts * **Each slice represents** – Define how slices are grouped * **Hide empty category** – Remove zero-value slices * **Show value in center** – Display the aggregate value (sum, average, count, etc.) ### Aggregate charts * **Prefix** – Add text before the number (e.g., `$`) * **Suffix** – Add text after the number (e.g., `%`) # Dashboards Source: https://docs.twenty.com/user-guide/dashboards/capabilities/dashboards Create and organize dashboards with tabs to visualize your CRM data. ## Overview Dashboards in Twenty are organized in a hierarchy: **Dashboards → Tabs → Widgets**. Each dashboard can contain multiple tabs, and each tab contains widgets (charts, numbers, iFrames). ## Creating a Dashboard 1. Go to **Dashboards** in the navigation 2. Click **+ New Record** Create a new dashboard 3. Give your dashboard a name 4. Start adding tabs and widgets ## Working with Tabs Tabs help you organize your dashboard into logical sections. ### Creating Tabs 1. In edit mode, click **+** in the tab bar, and a side panel will open on the right 2. Name your tab (e.g., "Pipeline Overview", "Team Performance") 3. Add widgets to the tab ### Reordering Tabs 1. In edit mode, click and hold on the tab you want to move 2. Drag and drop to the new position 3. You can also click on the tab and use the command menu to move the tab to the left or right ### Deleting Tabs 1. In edit mode, click on the tab you want to delete 2. Click the **Delete** button in the command menu ### Duplicating Tabs 1. Click on the tab you want to duplicate 2. Click the **Duplicate** button in the side panel Dashboard tab ## Dashboard Layout ### Arranging Widgets * Drag and drop to position * Resize for emphasis * Group related charts together ### Duplicating a Dashboard 1. Select the dashboard you want to duplicate 2. Open the command bar with **Cmd + K** (or **Ctrl + K** on Windows) 3. Select **Duplicate dashboard** Duplicate a dashboard ### Best Practices * **Logical flow**: Arrange from overview to detail * **Visual hierarchy**: Larger charts for key metrics * **Consistent styling**: Use matching colors and fonts ## Visibility & Access ### Dashboard Visibility Dashboards are visible to everyone who has access to your Twenty workspace. There is no private dashboard option at the moment. ### Favorites You can add dashboards to your favorites for quick access. This is a personal setting—your favorites are not visible to other users. To add a dashboard to favorites, open the dashboard and click the star icon. ### Timezone Behavior Dashboards currently display data based on the timezone of the user viewing them. This means the same dashboard may show different metrics for team members in different regions (e.g., APAC vs. US). **Coming soon**: We will add the ability to set a specific timezone for a dashboard, so all users see consistent data regardless of their location. **Coming soon**: Dashboard-level filters will allow you to apply filters across all widgets at once, making it faster to explore your data. # Widgets Source: https://docs.twenty.com/user-guide/dashboards/capabilities/widgets Explore the widget types and visualization options in Twenty. ## Available Widgets Twenty provides various widget types to visualize your CRM data. ### Bar Charts Display data as horizontal or vertical bars. Bar chart **Best for:** * Comparing values across categories * Showing rankings * Tracking metrics by time period **Example uses:** * Deals by stage * Revenue by sales rep * Contacts added per month **Display limits**: Bar charts can show a maximum of 100 bars, and 50 groups per bar when using Group By. If you see the warning "Undisplayed data: max X bars per chart", add filters to narrow down your data or change the grouping (e.g., group by week instead of days). ### Pie Charts Show proportions of a whole. Pie chart **Best for:** * Showing composition or distribution * Comparing parts to whole * Highlighting major segments **Example uses:** * Deal distribution by source * Contact breakdown by industry * Pipeline composition by owner ### Line Charts Display trends over time. Line chart **Best for:** * Tracking changes over time * Identifying trends * Comparing multiple metrics **Example uses:** * Monthly deal count trend * Revenue growth over quarters * Activity levels over time ### Aggregate Charts Display single key values prominently. Aggregate chart **Best for:** * Highlighting KPIs * Showing totals or averages * Quick status checks **Example uses:** * Total pipeline value * Number of open opportunities * Conversion rate **Advanced options:** * **Ratio**: For Select fields, calculate ratios between values. Go to **Data on display** → select your field → enable the **Ratio** option. * **Prefix & Suffix**: Add custom text before or after the number (e.g., "\$" prefix or "%" suffix) for better readability. ### iFrames Embed external tools and content directly in your dashboard. Table chart **Best for:** * Searching individual items * Checking exact values * Auditing or reference **Example uses:** * Deals closing this month * New leads this week * Overdue invoices ### Table See filtered records directly in your dashboard. iFrame **Best for:** * Displaying external reports or dashboards * Integrating third-party sales tools * Showing live content from other systems **Example uses:** * Metrics from your Support tool * Metrics from your dialer * Live content from your Sales sequence tool ### Rich Text Add formatted text and content directly to your dashboard. Rich text **Best for:** * Adding context or instructions * Dashboard descriptions * Notes and annotations **Features:** * Rich text formatting (bold, italic, lists, etc.) * Image and file uploads * Markdown-style editing **Coming soon**: Gauge charts are not yet available but are on our roadmap. ### Duplicating Widgets 1. Click on the widget 2. Open **Options** 3. Click **Duplicate widget** # Dashboards FAQ Source: https://docs.twenty.com/user-guide/dashboards/how-tos/dashboards-faq Frequently asked questions about dashboards in Twenty. No, dashboards are currently visible to everyone with access to your Twenty workspace. Private dashboards are not yet available. Dashboards currently display data based on the viewer's timezone. If you're in different regions (e.g., APAC vs. US), you may see slightly different numbers for the same dashboard. We're working on adding a timezone setting per dashboard to ensure consistent data across teams. Exporting dashboards is not available at the moment. This feature is on our roadmap. No, sharing dashboards with users outside your Twenty workspace (non-Twenty users) is not currently supported. Open the dashboard you want to favorite, then click the star icon. Favorites are personal—they won't affect other users. * **Tabs** organize your dashboard into sections (like pages within the dashboard) * **Widgets** are the individual visualizations (charts, numbers, iFrames) within each tab Structure: Dashboard → Tabs → Widgets Dashboard-level filters are not available yet, but this feature is on our roadmap. Currently, you need to apply filters to each widget individually. Not yet. Gauge charts are on our roadmap and will be added in a future release. 1. Make sure you're in view mode (not editing) 2. Open the command bar with **Cmd + K** (or **Ctrl + K** on Windows) 3. Select **Duplicate dashboard** Widgets update automatically as your CRM data changes: * Real-time updates for most metrics * Use the refresh button for a manual update if needed * Historical data is preserved for trend analysis # Dashboards Source: https://docs.twenty.com/user-guide/dashboards/overview Learn the basics of reporting and dashboards in Twenty. ## Understanding Dashboards Dashboards in Twenty provide a visual way to track your key performance metrics and gain insights from your CRM data. Dashboards ## Key Concepts ### Dashboards A dashboard is a collection of tabs that display your CRM data at a glance. You can create multiple dashboards for different purposes: * Sales performance * Team activity * Pipeline health * Custom metrics ### Tabs Tabs allow you to organize your dashboard into sections. Each tab contains one or more widgets. ### Widgets Widgets are individual visualizations that display specific data. Types include: * Bar charts * Pie charts * Line charts * Aggregate charts * iFrames * Rich text **Current limitations**: * Exporting dashboards and sharing with external users (non-Twenty users) are not available at the moment. * Gauge charts are not yet available. ## Getting Started ### Creating Your First Dashboard 1. Navigate to the **Dashboards** section 2. Click **+ New Dashboard** 3. Give your dashboard a name 4. Add tabs to organize your content 5. Add widgets to display your data 6. Save ### Adding Widgets 1. Open a tab on your dashboard 2. Click **+ Add Widget** 3. Select the widget type 4. Choose the data source (object) 5. Configure the widget settings 6. Save and view your widget ## Best Practices * **Start simple**: Begin with a few key metrics and add more over time * **Focus on actionable data**: Display metrics that drive decisions * **Regular review**: Check your dashboards regularly to spot trends * **Share with team**: Make dashboards visible to relevant team members ## Next Steps * [Widgets and visualizations](/user-guide/dashboards/capabilities/widgets) * [Chart settings reference](/user-guide/dashboards/capabilities/chart-settings) * [Dashboards FAQ](/user-guide/dashboards/how-tos/dashboards-faq) # Error Handling & Validation Source: https://docs.twenty.com/user-guide/data-migration/capabilities/error-handling Review and fix import errors directly in the UI before confirming. ## Pre-Import Validation After uploading your file and mapping fields, Twenty validates your data **before** importing. This allows you to catch and fix errors without affecting your existing data. ## How It Works 1. **Upload** your CSV file 2. **Map** your columns to Twenty fields 3. **Review** the potential errors highlighted in yellow 4. **Fix errors** directly in the UI 5. **Confirm** the import ## Error Display Rows with issues are highlighted in **yellow**. You can: * **Edit the cell directly** to fix the error * **Remove the row** to skip it entirely This inline editing saves time — no need to go back to your spreadsheet, fix errors, and re-upload. ## Common Error Types ### Duplicate Values **Cause**: A unique field (email, domain) already exists in Twenty or appears twice in your file. **Fix**: * Edit the duplicate value in the import UI * Remove one of the duplicate rows See [Uniqueness Constraints](/user-guide/data-migration/capabilities/uniqueness-constraints) for more details on how uniqueness is enforced. ### Invalid Format **Cause**: Data doesn't match the expected format (e.g., invalid email, wrong date format). **Fix**: Edit the cell to use the correct format. See [Field Mapping](/user-guide/data-migration/capabilities/field-mapping) for the expected format of each field type. ### Missing Required Fields **Cause**: A required field is empty. **Fix**: Enter a value in the required field or remove the row. ### Relation Not Found **Cause**: The referenced record doesn't exist (e.g., a Company domain that wasn't imported). **Fix**: * Import the parent records first * Or correct the reference value See [Import Relations](/user-guide/data-migration/capabilities/import-relations) for the correct import order and how to link records. ## Tips for Fewer Errors 1. **Download the template** to see the expected format prior to importing your file 2. **Clean your data** in the spreadsheet first 3. **Import files in correct order** to import relations (Companies → People → Opportunities) 4. **Test with small batches** before full import 5. **Check for duplicates** before uploading 6. **Limit each file to 10,000 records** # Field Mapping Source: https://docs.twenty.com/user-guide/data-migration/capabilities/field-mapping How field mapping works during data import. ## How Field Mapping Works When you upload a file, Twenty analyzes your columns and attempts to match them to existing fields. ### Automatic Mapping Twenty tries to match columns based on: * Column header names (exact or similar matches) * Data type detection (dates, numbers, emails) * Common field patterns **Quick tip:** Export a few rows from the object you want to import. The exported file will have the exact column names Twenty expects, making automatic mapping seamless during import. ### Manual Mapping Options For each column, you can: * **Map to a field**: Select the matching Twenty field from a dropdown * **Do not map**: Skip the column entirely (data won't be imported) **Fields must exist before import.** The import creates records, not fields. Create custom fields under **Settings → Data Model** before importing. ## Field Type Compatibility All field types available in the Data Model are supported for import. You can also import `id` values to either assign a specific ID to new records or update existing ones. ## Data Format Requirements **Some fields have special syntax.** We recommend downloading the sample file before preparing your import to see the expected syntax for each field type. ### Address Fields Address is a nested field with multiple columns. Some can be left empty. * **Address / Address 1**: Street address line 1 * **Address / Address 2**: Street address line 2 * **Address / City**: City name * **Address / State**: State or province * **Address / Country**: Country name * **Address / Post Code**: Postal/ZIP code ### Array Fields Use the following format: ``` ["value1","value2"] ``` ### Boolean Fields Use `TRUE` or `FALSE` (uppercase) - not `true` or `false` ### Currency Fields Currency is a nested field with two columns that **both must be filled**: * **Amount / Amount**: The numeric value (e.g., `1234.56`) * **Amount / Currency**: The currency code (e.g., `USD`, `EUR`) ### Date Fields Supported formats: * `YYYY-MM-DD` (recommended) * `MM/DD/YYYY` * `DD/MM/YYYY` * ISO 8601 format ### Domain Fields * It is recommended to use the format `https://domain.com` to avoid creating duplicates, as this is the format used for Companies created by the mailbox and calendar synchronizations * A `Domain Label` and `Domain URL` can be filled: best practice is to fill `domain.com` in the label and `https://domain.com` in the url * Domains must be unique within the Companies object * **Domains must be unique within the file to import** ### Email Fields * Must be valid email format * Emails must be unique within the People object * **Emails must be unique within the file to import** * For additional emails: use **Emails / Primary Email** for the main email, and **Emails / Additional Emails** with this format: ``` ["jane@twenty.com","jane.doe@twenty.com"] ``` ### ID Fields Specifying an `id` during import is optional. Twenty auto-generates one if not provided. Use cases for mapping an `id` column: * **Set a specific ID**: Choose the UUID for newly created records * **Update existing records**: Match against existing records to update them instead of creating duplicates. In that case, it is recommended to not map the other unique fields: mapping only one unique field ensures a smoother import. If you provide an `id`, it must be in UUID format (e.g., `c776ee49-f608-4a77-8cc8-6fe96ae1e43f`). ### JSON Fields Use valid JSON format: ``` {"key":"value","key2":"value2"} ``` ### Links Fields Similar to Domain fields: * Fill both the label and URL columns: **Links / Link URL** and **Links / Link Label** * Use full URL format: `https://example.com` * For secondary links, use **Links / Secondary Links** column with this format: ``` [{"url":"https://twenty.com","label":"Twenty"}] ``` ### Multi-Select Fields Use the **API names** (not the display labels) in the following format: ``` ["VALUE1","VALUE2"] ``` See [here](#finding-api-names-for-select-fields) where to find the API names. New select options will not be created automatically by the import. They must be added under **Settings → Data Model** before importing. **Import overwrites; it does not add.** If a record already has `VALUE2` and `VALUE3` selected, and you import `["VALUE1"]`, the record will only have `VALUE1` after import. The previous selections are replaced, not merged. ### Number Fields * Numbers only * Decimals use period: `1234.56` * No thousands separators ### Phone Fields Phone is a nested field with multiple columns that **must be filled** * **Phones / Primary Phone Number**: The phone number (e.g., `4159095555`) * **Phones / Primary Phone Country Code**: Country code (e.g., `US`) * **Phones / Primary Phone Calling Code**: Dialing code (e.g., `+1`) ### Rating Fields Use the API name format: `RATING_1`, `RATING_2`, `RATING_3`, `RATING_4`, `RATING_5` ### Relation Fields Please see our dedicated article: [Import Relations Between Objects](/user-guide/data-migration/capabilities/import-relations) ### Select Fields Use the **API name** of the option (not the display label): ``` VALUE1 ``` See [here](#finding-api-names-for-select-fields) where to find the API names. New select options will not be created automatically by the import. They must be added under **Settings → Data Model** before importing. ### Text Fields * No special formatting required * Leading/trailing spaces are trimmed ## Finding API Names For Select, Multi-Select, and Array fields with predefined options, you must use the **API names**, not the display labels. ### How to Find API Names 1. Go to **Settings → Data Model** 2. Select the object and field 3. Enable **Advanced mode** (toggle at the bottom right of the settings page) 4. View the API name for each option # Supported File Formats Source: https://docs.twenty.com/user-guide/data-migration/capabilities/file-formats File formats supported for data import in Twenty. ## Supported Formats Twenty supports three file formats for import: | Format | Extension | Notes | | ------------------ | --------- | ---------------------------- | | **CSV** | .csv | Recommended, most compatible | | **Excel** | .xlsx | Modern Excel format | | **Excel (Legacy)** | .xls | Older Excel format | ## File Requirements | Requirement | Value | | ---------------- | ------------------------------------- | | **Encoding** | UTF-8 recommended | | **Record limit** | 10,000 records per file | | **Structure** | First row must contain column headers | | **Content** | One object type per file | ## CSV Best Practices * **Delimiter**: Use comma (`,`) or semicolon (`;`) * **Text qualifier**: Use double quotes (`"`) for text containing commas * **Line endings**: Windows (CRLF) or Unix (LF) both supported * **Empty values**: Leave cells empty, don't use "NULL" or "N/A" ## Excel Best Practices When exporting from Excel: * Remove formulas (export values only) * Delete empty rows at the end * Ensure no merged cells * Use the first sheet only ## Large Datasets For datasets larger than 10,000 records: * Split into multiple files * Or use the [API import](/user-guide/data-migration/how-tos/import-data-via-api) for unlimited records For very large migrations (100,000+ records), the API is significantly faster and more reliable than CSV imports. # Import Relations Between Objects Source: https://docs.twenty.com/user-guide/data-migration/capabilities/import-relations Import relationships between records via CSV. ## Overview Twenty supports importing relationships between objects during CSV import. This allows you to link records (e.g., attach People to Companies) as part of your data migration. **Currently supported for import**: One-to-many relations pointing to a single object type on each side (e.g., People → Companies). Relations pointing to multiple object types are not yet supported in import/export. ## How Relations Work in Twenty ### One to Many / Many to One Twenty supports standard relations where one record links to many others: * **One Company → Many People**: A company can have multiple employees, but each person belongs to one company * **One Company → Many Opportunities**: A company can have multiple deals, but each opportunity belongs to one company ### Relations That Can Point to Multiple Object Types Some relations can connect to different types of objects. This works in two ways: **Pattern 1: Many records linking to one record each from different object types** Several Notes or Tasks can each be attached to multiple object types at once: * **Notes** can be linked to one Person, one Company, and one Opportunity simultaneously * **Tasks** can be linked to one Person, one Company, and one Opportunity simultaneously Here, the Notes/Tasks are on the "many" side. Each links to one record per object type. **Pattern 2: One record receiving links from many records of different object types** A Project can receive links from multiple records across different object types: * **A Project** can have many People linked to it, many Companies linked to it, and many Notes attached to it Here, the Project is on the "one" side. Multiple records from different objects can all link to the same Project. **Import/Export limitation**: Relations that point to multiple object types (like Notes → People/Companies/Opportunities) are **not yet supported** in CSV import or export. * **Import**: Only one-to-many relations pointing to a single object type on each side can be imported * **Export**: Columns for relations pointing to multiple object types are currently left empty This is on our roadmap. ### What's Not Supported Today **Many to Many relations** are not yet available. For example, you cannot currently create a relation where: * Many People are linked to many Projects Many to Many relations are planned for H2 2026. ## Linking Records During Import **Reminder**: Only one-to-many relations pointing to a single object type can be imported (e.g., People → Companies). Relations pointing to multiple object types (e.g., Notes → People/Companies/Opportunities) are not yet supported. ### Step 1: Identify the "One" and "Many" Sides First, determine which object is on the "one" side and which is on the "many" side of the relationship. **Example**: * **Company** is the "one" side (one company has many employees) * **People** is the "many" side (each person belongs to one company) ### Step 2: Ensure the "One" Side Records Exist Before importing the "many" side, the "one" side records must already exist in Twenty. * Import or create the "one" side records first (e.g., Companies) * Validate their unique identifier. This can be: * The `id` (Twenty's UUID) * A field set as unique (e.g., `domain` for Companies, or an external ID from your previous system) The import will fail if a reference is made to a record that does not exist. ### Step 3: Prepare Your CSV File Add a column in your "many" side CSV file that references the "one" side record. **Example**: For a People CSV file linking to Companies: ``` firstName,lastName,email,companyDomain John,Smith,john@acme.com,https://acme.com Jane,Doe,jane@widgets.co,https://widgets.co ``` **Important**: * The value must **exactly match** the unique field on the Company record * For domains, use the **Domain URL** (e.g., `https://acme.com`), not the Domain Label * Map only **one** unique identifier per relation: this leads to a smoother import ### Step 4: Ensure the Relation Field Exists Before uploading your file, make sure the relation field exists between your objects. If it doesn't exist: 1. Go to **Settings → Data Model** 2. Select your object (e.g., People) 3. Create a relation field pointing to the target object (e.g., Company) ### Step 5: Upload and Map the Relation 1. Upload your CSV file via the import UI 2. In the field mapping step, find your relation column (e.g., `companyDomain`) 3. Map it to the relation field (e.g., Company) 4. Twenty will automatically link each record to the matching parent ### Available Unique Fields for Relations | Object | Unique Fields Available | | ------------------------------------- | --------------------------------------- | | **Companies** | `id`, `domain`, any custom unique field | | **People** | `id`, `email`, any custom unique field | | **Workspace Members** | `id`, `email` (not name) | | **Other standard and custom objects** | `id`, any field marked as unique | **Linking to Workspace Members**: When the relation points to Workspace Members (your team logging into Twenty), reference them by their **email address**, not their name. We recommend using `domain` for Companies and `email` for People, as these are human-readable and easy to maintain in spreadsheets. **Reminder**: Soft-deleted records (visible under Command Menu → See deleted records) count toward uniqueness criteria. If you import a record with the same unique value as a deleted record, the deleted record will be restored. See [Uniqueness Constraints](/user-guide/data-migration/capabilities/uniqueness-constraints) for more details. ## Import Order Rule **Always import the "one" side first!** 1. **Companies** first (no dependencies) 2. **People** second (linked to Companies) 3. **Opportunities** third (linked to Companies/People) 4. **Custom objects** following their dependencies The parent record must exist before you can reference it. # Uniqueness Constraints Source: https://docs.twenty.com/user-guide/data-migration/capabilities/uniqueness-constraints How Twenty enforces data uniqueness during import. ## Overview Twenty enforces uniqueness on certain fields to prevent duplicate records and ensure data integrity. Understanding these constraints is essential for successful imports. ## Default Unique Fields | Object | Unique Fields | | ------------------ | ---------------------- | | **People** | `id`, `email` | | **Companies** | `id`, `domain` | | **Custom objects** | `id` only (by default) | The `id` field is Twenty's internal identifier, auto-generated for each record. It uses UUID format (e.g., `c776ee49-f608-4a77-8cc8-6fe96ae1e43f`). ## Custom Unique Fields You can define additional unique fields under **Settings → Data Model**: 1. Go to **Settings → Data Model** 2. Select the object 3. Click on a field 4. Enable **Unique** in field settings ### Use Cases for Custom Unique Fields * **External IDs**: Store IDs from other systems (Salesforce ID, HubSpot ID) * **Business identifiers**: Employee numbers, customer codes * **Alternative contact info**: LinkedIn profile, phone number The field name `id` is reserved for Twenty's internal ID. Use a different name like `externalId` or `legacyId` for external identifiers. ## Import Behavior ### Creating New Records If a unique field value doesn't exist, a new record is created. ### Updating Existing Records If a unique field value matches an existing record, that record is **updated** with the new data. To **update existing records**, it is recommended to **only match one unique field**. ### Soft-Deleted Records **Deleted records count toward uniqueness.** Soft-deleted records (visible under Command Menu → See deleted records) are included in uniqueness checks. If you import a record with the same unique value as a deleted record, the deleted record will be **restored** with the new data. ## Duplicate Detection During Import During the validation phase: * Duplicates within your file are highlighted in yellow * You can edit or remove duplicate rows from the UI before starting the import ## Best Practices 1. **Remove duplicates** from your file before importing 2. **Check for existing records** in Twenty before importing 3. **Use external IDs** when migrating from other systems 4. **Include unique fields** if you want to update existing records # Export Your Data Source: https://docs.twenty.com/user-guide/data-migration/how-tos/export-your-data Complete step-by-step guide to exporting data from Twenty. ## Overview Export your workspace data to CSV for backups, reporting, or migration. **Use cases:** * **Regular backups** — keep copies of your data * **External reporting** — analyze data in Excel, Google Sheets, or BI tools * **Migration** — move data to another system * **Bulk updates** — export, edit, and re-import to update records ## What You Need to Know ### Export Limits * **Maximum 20,000 records** per export * Only **visible columns** are exported * Only **filtered records** are exported (based on your current view) For larger exports (20,000+ records), use filters to export in batches or use the [API](/developers/extend/api). ### Permissions You need the **"Export CSV"** permission to export data. Contact your workspace admin if you don't have this option. ## Step 1: Navigate to the Object Go to the object you want to export: * **People** — for contacts * **Companies** — for organizations * **Opportunities** — for deals * **Custom objects** — any object you've created ## Step 2: Configure Your View **Important:** The export includes only what's visible in your current view. ### Add/Remove Columns 1. Click **Options → Fields** (or the **+** at the end of columns) 2. Check the fields you want to export 3. Uncheck fields you don't need ### Filter Records (Optional) If you only need a subset of data: 1. Click **Filter** 2. Add filter conditions (e.g., "Created date > January 1, 2024") 3. Only matching records will be exported ### Sort Records (Optional) 1. Click a column header to sort 2. The export will follow your sort order **Create a dedicated export view.** Save a view specifically configured for exports so you don't need to reconfigure each time. ## Step 3: Export the Data 1. Click the **⋮** icon on the top right of the table 2. Select **Export view** 3. Choose where to save the CSV file 4. Wait for the download to complete ## What Gets Exported | Included | Not Included | | -------------------------------- | ---------------------- | | All visible columns | Hidden columns | | Records matching current filters | Filtered-out records | | Custom field values | Fields not in the view | | Record IDs | File attachments | | Relation IDs | Images | ### Relation Fields Relation IDs are only exported on the **"many" side** of a relationship: * **People export** includes a `companyId` column (People → Company relation) * **Companies export** does NOT include `peopleIds` (Companies is the "one" side) This means you can use the People export to re-import and maintain the Company link, but you'll need to re-import People after Companies to recreate the relationships. ## Exporting for Specific Purposes ### For Backups 1. Create a view with **all fields** visible 2. Remove all filters to include all records 3. Export each object type separately 4. Store exports in a secure location 5. Set a recurring reminder (weekly/monthly) ### For External Reporting 1. Include only the fields you need for analysis 2. Apply filters to focus on relevant data 3. Consider sorting by the field you'll analyze ### For Bulk Updates 1. Export the records you want to update 2. Include the unique identifier (`email`, `domain`, or `id`) 3. Edit the exported file 4. Re-import to update records See: [How to Update Existing Records](/user-guide/data-migration/how-tos/update-existing-records-via-import) ### For Migration If you're exporting to migrate to another system: 1. **Export each object separately** — People, Companies, Opportunities, etc. 2. **Include ID fields** — these help maintain relationships 3. **Document field mappings** — note how Twenty fields map to your target system ## Handling Large Datasets (20,000+ Records) The export limit is 20,000 records. For larger datasets: ### Option 1: Export in Batches 1. Add a filter (e.g., "Created date" ranges) 2. Export the first batch 3. Change the filter 4. Export the next batch 5. Combine files in your spreadsheet **Example filters for batching:** * By date range (January, February, March...) * By owner (Team member A, Team member B...) * By status (Active, Inactive...) ### Option 2: Use the API The API has no record limit: 1. Get your API key from **Settings → Developers** 2. Use the GraphQL API to query records 3. Process results in your application See: [API Documentation](/developers/extend/api) ## Tips and Best Practices ### Create Export Views Save views configured specifically for exports: 1. Configure columns and filters 2. Click **View options** → **Save as new view** 3. Name it "Export - \[Purpose]" ### Secure Your Exports Exported files may contain sensitive data: * Store in secure locations * Delete old exports when no longer needed * Be careful sharing export files ### Check Before Exporting Correct columns are visible Filters are set correctly (or removed for full export) You have Export permission ## FAQ Only visible columns are exported. Add the columns you need via **Options → Fields** before exporting. Check your filters. The export only includes records matching your current view filters. Remove filters to export all records. Not in a single export. Use filters to export in batches, or use the API for larger datasets. CSV (Comma Separated Values). Opens in Excel, Google Sheets, or any spreadsheet application. Yes, but only on the "many" side of relationships. For example, a People export includes `companyId`, but a Companies export does not include people IDs. Not directly through the UI. Use the API to build automated export workflows. ## Next Steps * [How to Update Existing Records](/user-guide/data-migration/how-tos/update-existing-records-via-import) — edit and re-import your export * [How to Import Data via API](/user-guide/data-migration/how-tos/import-data-via-api) — for large datasets * [API Documentation](/developers/extend/api) — build custom export workflows # Fix Import Errors Source: https://docs.twenty.com/user-guide/data-migration/how-tos/fix-import-errors Complete troubleshooting guide for resolving CSV import errors. ## Overview Import not working? This guide helps you identify and fix common import errors step by step. ## How Import Validation Works After uploading your file and mapping columns, Twenty validates your data: 1. **Validation runs** — Twenty checks each row for errors 2. **Errors are highlighted** — problematic rows appear in **yellow** 3. **You can fix in-place** — edit cells directly in the import UI 4. **Or remove rows** — skip problematic records entirely **Fix errors in the UI.** You don't need to go back to your spreadsheet. Edit cells directly during import to save time. ## Step-by-Step Troubleshooting ### Step 1: Identify the Error Type Click on a highlighted row to see the specific error message. Common error types: | Error Message | What It Means | | --------------------------------------------------------------------- | ------------------------------------------------------------ | | Duplicate values highlighted in yellow | Value already exists in Twenty or appears twice in your file | | `{field} is not a valid {type}` (hover on yellow cell) | Data doesn't match expected format | | Required field highlighted | A required field is empty | | `Can't connect to {object}. No unique record found...` (import fails) | Referenced record doesn't exist | | `Too many records. Up to 10000 allowed` (upload blocked) | File has more than 10,000 records | ### Step 2: Fix the Error Follow the specific instructions below for each error type. *** ## Error: Duplicate Value ### What You'll See Rows with duplicate values are **highlighted in yellow** in the import UI before the import starts. ### What It Means A unique field (email, domain) either: * Already exists in Twenty * Appears twice in your file ### How to Fix **Option 1: Edit the duplicate value** 1. Click the cell with the error 2. Change to a unique value 3. Continue with import **Option 2: Remove the duplicate row** 1. Click the X next to the row 2. The row will be skipped during import **Option 3: Let Twenty update the existing record** 1. Ensure your file includes a unique identifier (`email`, `domain`, or `id`) 2. Map the unique identifier field 3. Twenty will update the existing record instead of creating a duplicate **You can update unique fields too.** * If you keep the `id` but change the `email` → the email will be updated * If you keep the `email` but change the `id` → the id will be updated As long as one unique identifier matches, Twenty updates the record. ### How to Prevent This Error Before importing: 1. Sort your spreadsheet by the unique field 2. Remove duplicate rows 3. Check if records already exist in Twenty **Soft-deleted records count toward uniqueness.** Check Command Menu → See deleted records. Records there still enforce uniqueness. Permanently delete them or restore and update. For more details: [Uniqueness Constraints](/user-guide/data-migration/capabilities/uniqueness-constraints) *** ## Error: Invalid Format ### What You'll See The cell value is highlighted in yellow. Hover over it to see the error message: ``` {field name} is not a valid {field type} ``` ### What It Means The data doesn't match the expected format for that field type. ### How to Fix — By Field Type #### Email **Problem:** Invalid email format **Solution:** Use format `name@domain.com` ``` ❌ john.smith@ ❌ john smith@acme.com ✓ john.smith@acme.com ``` #### Domain **Problem:** Inconsistent format may cause duplicates **Solution:** Use `https://domain.com` format (recommended) ``` ⚠️ acme.com (valid, but not recommended) ⚠️ www.acme.com (valid, but not recommended) ✅ https://acme.com (recommended) ``` All formats are valid, but `https://domain.com` is recommended because it matches the format used by email/calendar sync. Using other formats may create duplicate companies. #### Date **Problem:** Unrecognized date format **Solution:** Use `YYYY-MM-DD` format consistently throughout file ``` ✓ 2024-03-15 (YYYY-MM-DD) ❌ 03/15/2024 (MM/DD/YYYY) ❌ 15/03/2024 (DD/MM/YYYY) ``` #### Phone **Problem:** Missing required columns **Solution:** Include all phone columns | Column | Example | | --------------------------------------- | ------------ | | **Phones / Primary Phone Number** | `4159095555` | | **Phones / Primary Phone Country Code** | `US` | | **Phones / Primary Phone Calling Code** | `+1` | #### Boolean **Problem:** Wrong boolean value **Solution:** Use uppercase `TRUE` or `FALSE` ``` ❌ true ❌ yes ❌ 1 ✓ TRUE ✓ FALSE ``` #### Select / Multi-Select **Problem:** Value doesn't match existing options **Solution:** Use **API names**, not display labels How to find API names: 1. Go to **Settings → Data Model** 2. Select the object and field 3. Enable **Advanced mode** (toggle at bottom right) 4. Use the API name (e.g., `OPTION_1`, not "Option 1") ``` ❌ High Priority ✓ HIGH_PRIORITY ``` #### Currency **Problem:** Missing amount or currency code **Solution:** Fill both columns | Column | Example | | --------------------- | --------- | | **Amount / Amount** | `1234.56` | | **Amount / Currency** | `USD` | #### Number **Problem:** Non-numeric characters **Solution:** Numbers only, period for decimals ``` ❌ $1,234.56 ❌ 1,234.56 ✓ 1234.56 ``` For complete format reference: [Field Mapping](/user-guide/data-migration/capabilities/field-mapping) *** ## Error: Required Field Missing ### What You'll See The row is highlighted in yellow with the required field cell marked. ### What It Means A required field is empty for this row. ### How to Fix **Option 1: Enter a value** 1. Click the empty cell 2. Enter a value 3. Continue with import **Option 2: Remove the row** 1. If you don't have the data, click X to skip the row ### How to Prevent This Error Before importing, identify required fields: 1. Go to **Settings → Data Model** 2. Select your object 3. Check which fields are marked as required *** ## Error: Relation Not Found ### What You'll See This error appears **after the import starts** — the import fails with a message like: ``` Can't connect to company. No unique record found with condition: id = 7776ee49-f608-4a77-8cc8-6fe96ae1e43f ``` This means there is no Company in Twenty with that specific identifier. Unlike other errors, this one is not caught during the data review step. The import will start and then fail when it encounters the missing relation. ### What It Means You're trying to link to a record that doesn't exist in Twenty. ### How to Fix **Option 1: Import parent records first** 1. Cancel the current import 2. Import the parent records (e.g., Companies) 3. Then import the child records (e.g., People) **Option 2: Fix the reference value** 1. Check the reference value in your file 2. Ensure it exactly matches an existing record 3. Verify format: domains should be `https://domain.com` **Option 3: Remove the relation** 1. Clear the cell to import without the relation 2. Add the relation manually later ### How to Prevent This Error 1. **Import in the correct order:** * Companies first * People second (with company references) * Opportunities third 2. **Verify reference values:** * Export parent records to get exact identifiers * Use domain format `https://domain.com` * Check for typos and case sensitivity **Import will fail if a reference is made to a non-existent record.** Always import parent objects before child objects. For more details: [Import Relations](/user-guide/data-migration/capabilities/import-relations) *** ## Error: File Too Large ### What You'll See This error appears **when uploading your file** — the upload is blocked entirely: ``` Too many records. Up to 10000 allowed ``` You won't be able to proceed to the data review step until you reduce the file size. ### What It Means Your file has more than 10,000 records. ### How to Fix **Option 1: Split into multiple files** 1. Divide your data into files of 10,000 records or fewer 2. Import each file separately 3. Maintain import order (Companies before People) **Option 2: Use API import** For very large datasets, use the API which has no record limit. See: [How to Import Data via API](/user-guide/data-migration/how-tos/import-data-via-api) *** ## Error: Field Not Recognized ### What It Means A column in your file can't be mapped because the field doesn't exist in Twenty. ### How to Fix 1. Go to **Settings → Data Model** 2. Select the object you're importing 3. Click **+ Add field** 4. Create the custom field with the appropriate type 5. Re-upload your file The CSV import creates records, not fields. All fields must exist before importing. *** ## Error: User Relation Empty ### What It Means You're trying to assign a record to a user (Owner, Assignee) but the relation isn't being mapped. ### Common Causes 1. **User hasn't accepted their invitation** — the user doesn't exist in Twenty yet 2. **Using user ID from old system** — Twenty can't match IDs from another system 3. **Wrong email format** — the email doesn't match the user's Twenty account ### How to Fix 1. Ensure all users have **accepted their invitation** to your Twenty workspace 2. Use the user's **email address** (not their name or old system ID) 3. Use the same email they used to join Twenty **Users must accept invitations before importing.** If a user hasn't accepted their invitation, records referencing them will have empty user relations. *** ## Pre-Import Checklist Avoid errors by checking these before importing: ### File Requirements File is CSV, XLSX, or XLS format File has fewer than 10,000 records File uses UTF-8 encoding ### Data Quality No duplicate emails (for People) No duplicate domains (for Companies) All dates use consistent format All domains use `https://domain.com` format ### Field Formats Boolean fields use `TRUE` or `FALSE` (uppercase) Select fields use API names, not display labels Phone fields have all required columns Currency fields have both Amount and Currency Code ### Relations Parent records imported before child records Relation columns reference existing records Domain format matches Twenty's format exactly ### Data Model All custom fields exist in Settings → Data Model Select options exist before importing *** ## Still Having Issues? If you've tried the above solutions: 1. **Download the sample file** — see the exact format Twenty expects 2. **Export existing records** — compare your file to working data 3. **Test with a small batch** — try 5-10 rows first 4. **Check the reference articles:** * [Field Mapping](/user-guide/data-migration/capabilities/field-mapping) * [Uniqueness Constraints](/user-guide/data-migration/capabilities/uniqueness-constraints) * [Import Relations](/user-guide/data-migration/capabilities/import-relations) * [Error Handling](/user-guide/data-migration/capabilities/error-handling) # Import Companies via CSV Source: https://docs.twenty.com/user-guide/data-migration/how-tos/import-companies-via-csv Complete step-by-step guide to importing companies into Twenty. ## Overview This guide walks you through importing your companies into Twenty. **Companies should be imported first** because People and Opportunities link to Companies. ## Before You Start ### Prerequisites Checklist Your file is CSV, XLSX, or XLS format File has fewer than 10,000 records No duplicate domains in your file All custom fields exist in **Settings → Data Model** Need to import more than 10,000 companies? Split into multiple files or use the [API import](/user-guide/data-migration/how-tos/import-data-via-api). ## Step 1: Prepare Your Company Data ### Required and Recommended Fields | Field | Required? | Format | Notes | | ----------------- | ----------- | -------------------- | ------------------------ | | **Name** | Recommended | Text | Company display name | | **Domain** | Recommended | `https://domain.com` | Unique identifier | | **Address** | Optional | Multiple columns | See below | | **Employees** | Optional | Number | Employee count | | **Custom fields** | Optional | Varies | Must exist in Data Model | ### Domain Format **Use the format `https://domain.com` for domains.** This matches the format used when Companies are auto-created from email/calendar sync, preventing duplicates later. **Domain columns:** * **Domain / Domain Label**: `acme.com` * **Domain / Domain URL**: `https://acme.com` ### Address Format Address is a nested field with multiple columns: ``` Address / Address 1,Address / City,Address / State,Address / Country,Address / Post Code 123 Main Street,San Francisco,CA,USA,94105 ``` ### Sample CSV Structure ```csv theme={null} name,Domain / Domain URL,Domain / Domain Label,Address / City,Address / Country,employees Acme Corp,https://acme.com,acme.com,San Francisco,USA,250 Widget Co,https://widgets.co,widgets.co,New York,USA,50 ``` **Pro tip:** Click **Download sample file** during import to see the exact column names Twenty expects. ## Step 2: Access the Import Feature **Option 1: From the Companies View** 1. Navigate to **Companies** in the left sidebar 2. Click the **⋮** icon on the top right 3. Select **Import records** **Option 2: Using Command Menu** 1. Press `Cmd + K` (Mac) or `Ctrl + K` (Windows) 2. Type "import" 3. Select **Import records** 4. Choose **Companies** ## Step 3: Upload Your File 1. Click **Select file** 2. Choose your CSV, XLSX, or XLS file 3. Wait for Twenty to analyze your file ## Step 4: Map Your Columns Twenty automatically tries to match your columns to fields. Review and adjust: 1. **Check automatic mappings** — verify they're correct 2. **Fix incorrect mappings** — click the dropdown to select the right field 3. **Skip columns** — select **Do not map** for columns you don't want to import ### Important Mapping Rules * **Domain**: Map to **Domain / Domain URL** (not Domain Label) * **Address**: Map each part to its specific column (City, State, etc.) * **Select fields**: Values must match existing options (or you'll map them in the next step) ## Step 5: Map Select Field Values If you have Select or Multi-Select fields: 1. Twenty shows your values alongside existing options 2. Match each value in your file to a Twenty option 3. Or create new options if needed Select options use **API names**, not display labels. Check **Settings → Data Model** → Enable **Advanced mode** to see API names. ## Step 6: Review and Fix Errors Before completing the import, Twenty validates your data: 1. Click **Next Steps** 2. Rows with errors are highlighted in **yellow** 3. **Fix errors directly** — click a cell and edit the value 4. **Remove problematic rows** — click the X to skip that row ### Common Company Import Errors | Error | Cause | Solution | | -------------------------- | ------------------------------- | ------------------------------------------ | | **Duplicate domain** | Domain already exists in Twenty | Remove from file or update existing record | | **Invalid domain format** | Wrong format | Use `https://domain.com` | | **Missing required field** | Required field is empty | Fill in the value or remove the row | ## Step 7: Complete the Import 1. Review the import summary 2. Click **Confirm** to import 3. Wait for the import to complete 4. Verify by checking a few records ## After Importing Companies Now you can import records that link to Companies: 1. **[Import People](/user-guide/data-migration/how-tos/import-contacts-via-csv)** — link them to Companies using the domain 2. **Import Opportunities** — link them to Companies 3. **Verify the import** — spot-check a few records to ensure data is correct ## Updating Existing Companies To update companies instead of creating new ones: 1. Include the `domain` or `id` column in your file 2. Twenty matches records by this unique identifier 3. Existing companies are updated; new ones are created See [How to Update Existing Records](/user-guide/data-migration/how-tos/update-existing-records-via-import) for details. ## FAQ Domain is a unique identifier in Twenty. This prevents duplicate companies and ensures email sync correctly links emails to the right company. You can leave the domain empty. However, we recommend adding domains when possible for better data quality and automatic email linking. Yes! You can import companies first, then import People later and link them using the company domain. If you include a unique identifier (domain or id) that matches an existing company, Twenty updates that company instead of creating a duplicate. Either remove the duplicate from your file, or include the company's `id` to update the existing record instead. ## Troubleshooting Having issues? Check: * [How to Fix Import Errors](/user-guide/data-migration/how-tos/fix-import-errors) * [Field Mapping Reference](/user-guide/data-migration/capabilities/field-mapping) * [Uniqueness Constraints](/user-guide/data-migration/capabilities/uniqueness-constraints) # Import Contacts via CSV Source: https://docs.twenty.com/user-guide/data-migration/how-tos/import-contacts-via-csv Complete step-by-step guide to importing people/contacts into Twenty. ## Overview This guide walks you through importing your contacts (People) into Twenty. ## Before You Start ### Prerequisites Checklist Your file is CSV, XLSX, or XLS format File has fewer than 10,000 records No duplicate email addresses in your file **Companies imported first** (if linking People to Companies) All custom fields exist in **Settings → Data Model** **Import Companies Before People** If you want to link People to Companies, import Companies first. The Company must exist before you can reference it. ## Step 1: Prepare Your Contact Data ### Required and Recommended Fields | Field | Required? | Format | Notes | | ----------------- | ----------- | ----------------- | ------------------------- | | **Email** | Recommended | `name@domain.com` | Must be unique | | **First Name** | Recommended | Text | | | **Last Name** | Recommended | Text | | | **Company** | Optional | Domain or ID | Links to existing Company | | **Phone** | Optional | Multiple columns | See below | | **Job Title** | Optional | Text | | | **Custom fields** | Optional | Varies | Must exist in Data Model | ### Email Format * Must be valid email format: `name@domain.com` * **Must be unique** — no duplicates in your file or in Twenty * For additional emails, use the **Emails / Additional Emails** column: ``` ["jane@twenty.com","jane.doe@twenty.com"] ``` ### Phone Format Phone is a **nested field** requiring multiple columns: | Column | Example | | --------------------------------------- | ------------ | | **Phones / Primary Phone Number** | `4159095555` | | **Phones / Primary Phone Country Code** | `US` | | **Phones / Primary Phone Calling Code** | `+1` | ### Linking to Companies Add a column with the Company's unique identifier: | Column Name | Format | Example | | --------------- | ---------- | -------------------------------------- | | `companyDomain` | URL format | `https://acme.com` | | `companyId` | UUID | `c776ee49-f608-4a77-8cc8-6fe96ae1e43f` | **Use Domain URL format** (`https://acme.com`), not the label. This matches how Companies are stored in Twenty. ### Sample CSV Structure ```csv theme={null} firstName,lastName,email,jobTitle,companyDomain,Phones / Primary Phone Number,Phones / Primary Phone Country Code John,Smith,john@acme.com,CEO,https://acme.com,4159095555,US Jane,Doe,jane@widgets.co,CTO,https://widgets.co,2125551234,US ``` **Pro tip:** Click **Download sample file** during import or export a few existing People to see the exact column names Twenty expects. ## Step 2: Access the Import Feature **Option 1: From the People View** 1. Navigate to **People** in the left sidebar 2. Click the **⋮** icon on the top right 3. Select **Import records** **Option 2: Using Command Menu** 1. Press `Cmd + K` (Mac) or `Ctrl + K` (Windows) 2. Type "import" 3. Select **Import records** 4. Choose **People** ## Step 3: Upload Your File 1. Click **Select file** 2. Choose your CSV, XLSX, or XLS file 3. Wait for Twenty to analyze your file ## Step 4: Map Your Columns Twenty automatically tries to match your columns to fields. Review and adjust: 1. **Check automatic mappings** — verify they're correct 2. **Fix incorrect mappings** — click the dropdown to select the right field 3. **Skip columns** — select **Do not map** for columns you don't want to import ### Important Mapping Rules | Column Type | Map To | Notes | | ----------------- | ------------------------------ | ---------------------------------- | | Company reference | **Company** relation field | Use domain OR id, not both | | Email | **Email** | Primary email address | | Additional emails | **Emails / Additional Emails** | Array format | | Phone | Separate columns | Number, Country Code, Calling Code | ### Mapping the Company Relation When mapping the company column: 1. Find your company reference column (e.g., `companyDomain`) 2. Map it to the **Company** relation field 3. Twenty will link each Person to the matching Company **Map only ONE unique identifier for relations.** Don't map both `companyId` AND `companyDomain`. Choose one—preferably domain since it's human-readable. ## Step 5: Map Select Field Values If you have Select or Multi-Select fields (like Lead Source): 1. Twenty shows your values alongside existing options 2. Match each value in your file to a Twenty option 3. Or create new options if needed Select options use **API names**, not display labels. Check **Settings → Data Model** → Enable **Advanced mode** to see API names. ## Step 6: Review and Fix Errors Before completing the import, Twenty validates your data: 1. Click **Next Steps** 2. Rows with errors are highlighted in **yellow** 3. **Fix errors directly** — click a cell and edit the value 4. **Remove problematic rows** — click the X to skip that row ### Common Contact Import Errors | Error | Cause | Solution | | -------------------------- | -------------------------------------- | ------------------------------------------- | | **Duplicate email** | Email already exists in Twenty or file | Remove duplicate or update existing record | | **Invalid email format** | Email format incorrect | Fix to `name@domain.com` | | **Relation not found** | Company doesn't exist | Import Companies first or fix the reference | | **Missing required field** | Required field is empty | Fill in the value or remove the row | ## Step 7: Complete the Import 1. Review the import summary 2. Click **Confirm** to import 3. Wait for the import to complete 4. Verify by checking a few records and their Company links ## After Importing Contacts Your contacts are now in Twenty! Next steps: 1. **Verify Company links** — open a few People records to confirm they're linked to the right Company 2. **Import Opportunities** — if needed, link them to People and Companies 3. **Set up email sync** — connect your mailbox to see email history on contact records ## Updating Existing Contacts To update contacts instead of creating new ones: 1. Include the `email` or `id` column in your file 2. Twenty matches records by this unique identifier 3. Existing contacts are updated; new ones are created See [How to Update Existing Records](/user-guide/data-migration/how-tos/update-existing-records-via-import) for details. ## FAQ Email is a unique identifier in Twenty. This prevents duplicate contacts and ensures email sync correctly links emails to the right person. You can leave the email empty. However, we recommend adding emails when possible for better data quality and email sync functionality. Add a column with the Company's domain (e.g., `https://acme.com`) or ID. During mapping, connect this column to the Company relation field. Import Companies first, then import People. The Company must exist before you can reference it. Yes! Create a custom field marked as "unique" in your data model to store the external ID. Note: the field name `id` is reserved for Twenty's internal ID. The Company you're referencing doesn't exist. Either import the Company first, or check that the domain/ID exactly matches an existing Company. ## Troubleshooting Having issues? Check: * [How to Fix Import Errors](/user-guide/data-migration/how-tos/fix-import-errors) * [How to Import Relations](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) * [Field Mapping Reference](/user-guide/data-migration/capabilities/field-mapping) # Import Data via API Source: https://docs.twenty.com/user-guide/data-migration/how-tos/import-data-via-api When and how to use Twenty's APIs for large-scale data imports. ## Overview Twenty provides both **GraphQL** and **REST APIs** for programmatic data import. Use the API when CSV import isn't practical for your data volume or when you need automated, recurring imports. ## When to Use API Import | Scenario | Recommended Method | | ---------------------------------- | ----------------------------- | | Under 10,000 records | CSV Import | | 10,000 - 50,000 records | CSV Import (split into files) | | **50,000+ records** | **API Import** | | One-time migration | Either (based on volume) | | **Recurring imports** | **API Import** | | **Real-time sync** | **API Import** | | **Integration with other systems** | **API Import** | For datasets in the hundreds of thousands, the API is significantly faster and more reliable than multiple CSV imports. ## API Rate Limits Twenty enforces rate limits to ensure system stability: | Limit | Value | | -------------------------- | ---------------------- | | **Requests per minute** | 100 | | **Records per batch call** | 60 | | **Maximum throughput** | \~6,000 records/minute | **Plan your import around these limits.** For 100,000 records at maximum throughput, expect approximately 17 minutes of import time. Add buffer time for error handling and retries. ## Getting Started ### Step 1: Get Your API Key 1. Go to **Settings → Developers** 2. Click **+ Create API key** 3. Give your key a descriptive name 4. Copy the API key immediately (it won't be shown again) 5. Store it securely **Keep your API key secret.** Anyone with your API key can access and modify your workspace data. Never commit it to code repositories or share it publicly. ### Step 2: Choose Your API Twenty supports two API types: | API | Best For | Documentation | | ----------- | ----------------------------------------------------------- | ---------------------------------- | | **GraphQL** | Flexible queries, fetching related data, complex operations | [API Docs](/developers/extend/api) | | **REST** | Simple CRUD operations, familiar REST patterns | [API Docs](/developers/extend/api) | Both APIs support: * Creating, reading, updating, and deleting records * **Batch operations** — create or update up to 60 records per call **For imports, use batch operations** to maximize throughput within rate limits. ### Step 3: Plan Your Import Order Just like CSV imports, **order matters** for relations: 1. **Companies** first (no dependencies) 2. **People** second (can link to Companies) 3. **Opportunities** third (can link to Companies and People) 4. **Tasks/Notes** (can link to any of the above) 5. **Custom objects** (following their dependencies) ## Best Practices ### Batch Your Requests * Don't send records one at a time * Group up to **60 records per API call** * This maximizes throughput within rate limits ### Handle Rate Limits * Implement delays between requests (600ms minimum for sustained imports) * Use exponential backoff when you hit limits * Monitor for 429 (Too Many Requests) responses ### Validate Data First * Clean and validate your data before importing * Check required fields are populated * Verify formats match Twenty's requirements (see [Field Mapping](/user-guide/data-migration/capabilities/field-mapping)) ### Log Everything * Log every record imported (including IDs) * Log errors with full context * This helps debug issues and verify completion ### Test First * Test with a small batch (10-20 records) * Verify data appears correctly in Twenty * Then run the full import ### Upsert to Avoid Duplicates The GraphQL API supports **batch upsert** — update if the record exists, create if not. This prevents duplicates when re-running imports. ## Finding Object and Field Names To see available objects and fields: 1. Go to **Settings → API and Webhooks** 2. Browse the **Metadata API** 3. View all standard and custom objects with their fields The documentation shows all standard and custom objects, their fields, and the expected data types. ## Professional Services For complex API migrations, our partners can help: | Service | What's Included | | ----------------------- | ---------------------------------- | | **Data Model Design** | design your optimal data structure | | **Migration Scripts** | write and run the import scripts | | **Data Transformation** | handle complex mapping and cleanup | | **Validation & QA** | verify the migration is complete | **Best for:** * Migrations of 100,000+ records * Complex data transformations * Tight timelines * Teams without developer resources Contact us at [contact@twenty.com](mailto:contact@twenty.com) or explore our [Implementation Services](/user-guide/getting-started/capabilities/implementation-services). ## FAQ GraphQL lets you request exactly the data you need in a single query and is better for complex operations. REST uses standard HTTP methods (GET, POST, PUT, DELETE) and may be more familiar if you've worked with traditional APIs. Yes! Use update mutations (GraphQL) or PUT/PATCH requests (REST) with the record's `id`. Query for existing records first using unique identifiers (email, domain). Update if exists, create if not. Yes, use delete mutations (GraphQL) or DELETE requests (REST). Not currently, but both APIs work with any HTTP client in any language. ## API Documentation For full implementation details, code examples, and schema reference: * [API Documentation](/developers/extend/api) # Import Relations Between Objects via CSV Source: https://docs.twenty.com/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv Complete step-by-step guide to linking records during CSV import. ## Overview This guide walks you through importing relations between objects—for example, linking People to Companies, or Opportunities to People. **What can be imported:** Only one-to-many relations pointing to a single object type. Relations pointing to multiple object types (like Notes linking to People AND Companies) are not yet supported for import. ## Understanding Relations ### What is a "One-to-Many" Relation? In a one-to-many relation: * **One** Company has **many** People (employees) * **One** Company has **many** Opportunities * **One** Person has **many** Tasks The "one" side is the **parent**. The "many" side is the **child**. ### Common Relations in Twenty | Relation | "One" Side (Parent) | "Many" Side (Child) | | ------------------------- | ------------------- | ------------------- | | Companies → People | Company | People | | Companies → Opportunities | Company | Opportunities | | People → Tasks | Person | Tasks | | People → Notes | Person | Notes | ## Step 1: Identify the "One" and "Many" Sides Before importing, determine which object is the parent and which is the child. **Ask yourself:** "Does ONE \[Object A] have MANY \[Object B]?" * One Company → Many People ✓ (Company is parent) * One Person → Many Companies ✗ (This is wrong—a person belongs to one company) ## Step 2: Import the Parent Records First The parent ("one" side) must exist in Twenty before you can reference it. **Import order:** 1. **Companies** first (no dependencies) 2. **People** second (link to Companies) 3. **Opportunities** third (link to Companies and/or People) 4. **Tasks/Notes** (link to any of the above) **If the parent record doesn't exist, the import will fail.** Always verify that Companies are imported before importing People with company references. ## Step 3: Note the Parent's Unique Identifier You need to reference the parent record using a **unique identifier**. Available options: | Parent Object | Available Unique Identifiers | | --------------------- | --------------------------------------------------------------- | | **Companies** | `id` (UUID), `domain` (recommended), or any custom unique field | | **People** | `id` (UUID), `email`, or any custom unique field | | **Workspace Members** | `id` (UUID), `email` (not name) | | **Custom Objects** | `id` (UUID), or any field marked as unique | **Recommended:** Use `domain` for Companies and `email` for People. These are human-readable and easy to verify in your spreadsheet. ### Finding the Identifier If you need the `id`: 1. Export the parent records from Twenty 2. The export includes the `id` column 3. Use these IDs in your child records file ## Step 4: Verify the Relation Field Exists Before importing, ensure the relation field exists between your objects. **To check or create:** 1. Go to **Settings → Data Model** 2. Select your child object (e.g., People) 3. Look for a relation field pointing to the parent (e.g., Company) 4. If it doesn't exist, create it: * Click **+ Add field** * Select **Relation** type * Choose the parent object ## Step 5: Prepare Your CSV File Add a column to your child CSV that references the parent using its unique identifier. ### Example: People Linking to Companies **Your People CSV:** ```csv theme={null} firstName,lastName,email,jobTitle,companyDomain John,Smith,john@acme.com,CEO,https://acme.com Jane,Doe,jane@widgets.co,CTO,https://widgets.co Bob,Johnson,bob@techstart.io,Developer,https://techstart.io ``` The `companyDomain` column references the Company's domain. ### Format Requirements | Identifier | Format | Example | | ---------- | -------------- | -------------------------------------- | | Domain | URL format | `https://acme.com` | | Email | Standard email | `john@acme.com` | | ID | UUID | `c776ee49-f608-4a77-8cc8-6fe96ae1e43f` | **Domain format matters!** Use `https://domain.com` (not just `domain.com`). This matches how Twenty stores Company domains and prevents matching errors. ### Important Rules 1. **Exact match required** — the value must exactly match the parent record 2. **Map only ONE unique identifier** — don't include both `companyId` AND `companyDomain` 3. **Case sensitive** — `Acme.com` ≠ `acme.com` ## Step 6: Upload and Map the Relation 1. Navigate to the child object (e.g., People) 2. Click **⋮** → **Import records** 3. Upload your CSV file 4. In the field mapping step: * Find your relation column (e.g., `companyDomain`) * Map it to the **Company** relation field 5. Complete the remaining mapping 6. Review errors and confirm Twenty will automatically link each child record to the matching parent. ## Step 7: Verify the Import After importing: 1. Open a few child records (e.g., People) 2. Verify the relation field shows the correct parent (e.g., Company) 3. Open a parent record and check the related records section ## Common Mistakes to Avoid | Mistake | Problem | Solution | | -------------------------- | -------------------------------------------------- | ------------------------------------------------------- | | **Wrong import order** | Importing People before Companies | Always import parents first, then children | | **Wrong domain format** | Using `acme.com` instead of `https://acme.com` | Use full URL format with `https://` | | **Multiple unique fields** | Mapping both `companyId` AND `companyDomain` | Map only ONE unique identifier | | **Missing relation field** | The relation field doesn't exist in the data model | Create it in **Settings → Data Model** before importing | | **Non-existent records** | The parent record doesn't exist in Twenty | Import parent records first, or check for typos | | **Case mismatch** | `Acme.com` in file but `acme.com` in Twenty | Ensure exact case matching | ## Linking to Workspace Members When linking to Workspace Members (your team): * Use their **email address**, not their name * Example: `owner@yourcompany.com`, not "John Smith" ```csv theme={null} taskName,assignedTo Follow up with client,john@yourcompany.com Review proposal,jane@yourcompany.com ``` ## FAQ You have two options: 1. Use the Twenty `id` (export parent records to get their IDs) 2. Create a custom unique field in your data model to store an external ID from your previous system Yes! Include the child record's unique identifier (e.g., `email` for People) and the new relation value. The import will update the relation. Many-to-Many relations are not yet supported for import. This is planned for H1 2026. Relations pointing to multiple object types are not yet supported for import/export. This is on our roadmap. The import will show an error for that row. You can either: * Import the parent record first, then re-import * Fix the reference value * Remove the row from import Common causes: * Wrong format (use `https://domain.com` for domains) * Case mismatch (check exact spelling) * Parent doesn't exist (import parents first) * Mapping multiple identifiers (use only one) **Remember: Soft-deleted records count toward uniqueness.** If you're getting "not found" errors but the record seems to exist, check Command Menu → See deleted records. The parent may have been soft-deleted. ## Troubleshooting Having issues? Check: * [How to Fix Import Errors](/user-guide/data-migration/how-tos/fix-import-errors) * [Import Relations Capabilities](/user-guide/data-migration/capabilities/import-relations) * [Uniqueness Constraints](/user-guide/data-migration/capabilities/uniqueness-constraints) # Migrating from Other CRMs Source: https://docs.twenty.com/user-guide/data-migration/how-tos/migrating-from-other-crms Step-by-step guide to migrate your data from any CRM to Twenty. ## Overview This guide walks you through migrating your data from any CRM to Twenty. The process involves auditing your data, preparing your Twenty workspace, exporting from your current system, and importing into Twenty. Views, workflows, and permissions must be recreated manually after migration. Plan time for this configuration work. ## Step 1: Audit Your Current Data Migration is an opportunity for a fresh start. Don't bring over clutter. **What to keep:** * Active contacts and companies * Open opportunities and deals * Important notes and activities * Custom fields you actually use **What to leave behind:** * Outdated contacts (no activity in 2+ years) * Duplicate records * Test data * Unused custom fields ## Step 2: Map Your Data Model Create a mapping document between your current CRM and Twenty: | Your CRM | Twenty | | ---------------------- | -------------------- | | Account / Organization | **Company** | | Contact / Person | **People** | | Deal / Opportunity | **Opportunity** | | Activity | **Task** or **Note** | | Custom Object | **Custom Object** | **For each field, document:** * The source field name * The target Twenty field * Any format transformations needed (dates, phone numbers, etc.) Keep this mapping document handy during import—you'll reference it when mapping columns. ## Step 3: Set Up Your Twenty Workspace Before importing data, prepare your Twenty workspace: ### Create Custom Objects and Fields 1. Go to **Settings → Data Model** 2. Create any custom objects you need 3. Add custom fields to standard and custom objects 4. Configure field settings (unique, required, select options, etc.) **Fields must exist before import.** The CSV import creates records, not fields. Create all custom fields in Settings → Data Model before importing. ### Invite Your Team **Invite users BEFORE importing data.** If your data includes user references (Account Owner, Assignee, etc.), those users must exist in Twenty before import. Otherwise, those relations cannot be mapped. 1. Go to **Settings → Members** 2. Invite all team members 3. **Wait for everyone to accept** their invitation 4. Verify all users appear in your Members list ## Step 4: Export from Your Current CRM Export your data from your current CRM: 1. Look for an **Export** function (usually under Settings, Data Management, or Admin) 2. Export to **CSV format** when possible 3. Export each object type separately (Companies, Contacts, Deals, etc.) 4. Include all fields you want to migrate **Export these objects (in this order for reference):** 1. Companies / Accounts / Organizations 2. Contacts / People 3. Deals / Opportunities 4. Notes and Activities 5. Custom objects ## Step 5: Clean and Format Your Data Open each exported CSV in a spreadsheet application and prepare it for Twenty. ### Remove Duplicates 1. Sort by the unique field (email for People, domain for Companies) 2. Remove or merge duplicate rows 3. Verify no duplicates exist in Twenty already ### Format Fields Correctly | Field Type | Required Format | | ----------------- | ------------------------------------------------- | | **Domain** | `https://domain.com` | | **Email** | `name@domain.com` (must be unique) | | **Date** | `YYYY-MM-DD` | | **Phone** | Three columns: Number, Country Code, Calling Code | | **Boolean** | `TRUE` or `FALSE` (uppercase) | | **Select fields** | Use API names, not display labels | **Domain format is critical.** Use `https://domain.com` (not `domain.com` or `www.domain.com`). This matches Twenty's format and prevents duplicates when you connect email/calendar sync. See [How to Prepare Your CSV Files](/user-guide/data-migration/how-tos/prepare-your-csv-files) for complete formatting requirements for all field types. ### Add Relation Columns To link records (e.g., People to Companies), add a column with the parent's unique identifier. **Example: People CSV with Company link** ```csv theme={null} firstName,lastName,email,companyDomain John,Smith,john@acme.com,https://acme.com Jane,Doe,jane@widgets.co,https://widgets.co ``` See [How to Import Relations](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) for detailed instructions on linking records. ### Update User References If your data includes user assignments (Owner, Assignee): 1. Add a column with the **user's email** (not just their ID from the old system) 2. Use the same email addresses that users used to join your Twenty workspace See [How to Prepare Your CSV Files](/user-guide/data-migration/how-tos/prepare-your-csv-files) for complete formatting guide. ## Step 6: Import to Twenty **Import Order Matters!** Always import in this order: 1. **Companies** first (no dependencies) 2. **People** second (link to Companies) 3. **Opportunities** third (link to Companies/People) 4. **Notes and Tasks** (link to records) 5. **Custom objects** following their dependencies The parent record must exist before you can reference it. ### Import Each Object For each CSV file, in order: 1. Navigate to the object in Twenty 2. Click **⋮ → Import records** 3. Upload the CSV file 4. Map columns to fields: * Map user email columns to the appropriate relation fields * Map relation columns (like `companyDomain`) to relation fields 5. Review and fix any errors in the UI 6. Confirm the import 7. Verify a few records before proceeding to the next file **Detailed guides:** * [How to Import Companies](/user-guide/data-migration/how-tos/import-companies-via-csv) * [How to Import Contacts](/user-guide/data-migration/how-tos/import-contacts-via-csv) * [How to Import Relations](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) ## Step 7: Large Migrations (50,000+ Records) For large migrations: | Volume | Recommended Approach | | ----------------------- | ----------------------------- | | Under 10,000 records | Single CSV import | | 10,000 - 50,000 records | Split into multiple CSV files | | 50,000+ records | Use the API | **For API imports:** * Faster and more reliable for large datasets * Supports batch operations (up to 60 records per call) * See [How to Import Data via API](/user-guide/data-migration/how-tos/import-data-via-api) ## Step 8: Post-Migration Setup After importing data, complete your workspace configuration: ### Recreate Views * Set up saved views with filters, sorts, and column configurations * Create any kanban or calendar views you need ### Recreate Workflows * Rebuild your automations in **Settings → Workflows** * Start with the most critical workflows * Test each one before relying on it ### Configure Roles and Permissions * Set up roles in **Settings → Members → Roles** * Assign users to appropriate roles ### Connect Email and Calendar * Each user connects their own account in **Settings → Accounts** * Twenty will start syncing emails to contact records * See [Email & Calendar](/user-guide/calendar-emails/overview) ### Train Your Team * Walk through the new interface together * Document any team-specific processes ## Common Issues and Solutions | Issue | Cause | Solution | | ----------------------- | --------------------------- | ------------------------------------------------------------------------------------ | | **Duplicate errors** | Email/domain already exists | Remove duplicates from file, or include unique identifier to update existing records | | **Relation not found** | Parent record doesn't exist | Import parent objects first (Companies before People) | | **Missing fields** | Custom field doesn't exist | Create field in Settings → Data Model before importing | | **Select field errors** | Using display labels | Use API names (enable Advanced mode in Settings to find them) | | **User relation empty** | User hasn't accepted invite | Ensure all users accept invitations before importing | See [How to Fix Import Errors](/user-guide/data-migration/how-tos/fix-import-errors) for detailed troubleshooting steps. ## Post-Migration Checklist ### Data Integrity All records imported (compare counts with source system) Relations working correctly (People linked to Companies) User assignments mapped correctly (Owner, Assignee) Custom fields populated No unexpected duplicates ### Configuration Views recreated Workflows recreated and tested Roles and permissions configured Email/calendar sync connected ### Team Readiness Team trained on new system Old CRM access plan decided (keep for reference? When to disable?) ## FAQ Not currently. Workflows must be recreated manually in Twenty. File attachments are not included in CSV exports. You'll need to re-upload them manually, migrate via API, or contact our team for assistance. Yes, we recommend keeping your old CRM running until you've verified the migration is complete. Just be careful not to create new data in both places. Depends on data volume and complexity. Small migrations (under 10,000 records) can be done in a few hours. Large migrations may take several days including data cleanup and testing. ## Need Help? Want help with your migration? [Find a certified Twenty partner](https://twenty.com/partners/list?categories=SOLUTIONING\&ref=docs-migrate-crm) to move your data, pipelines, and users end-to-end. # Migrating from Self-Hosted to Cloud Source: https://docs.twenty.com/user-guide/data-migration/how-tos/migrating-from-self-hosted-to-cloud Step-by-step guide to migrate your Twenty self-hosted instance to Twenty Cloud. ## Overview This guide walks you through migrating your data from a Twenty self-hosted instance to Twenty Cloud. The process involves setting up your cloud workspace, exporting your data, and re-importing it. Views, workflows, and roles must be recreated manually after migration. Plan time for this configuration work. ## Step 1: Create Your Cloud Workspace 1. Go to [app.twenty.com](https://app.twenty.com) and create a new workspace 2. Complete the initial setup wizard 3. Note your new workspace URL ## Step 2: Recreate Your Data Model Before importing data, recreate your custom objects and fields: 1. Go to **Settings → Data Model** in your cloud instance 2. Create custom objects that match your self-hosted setup 3. Add custom fields to standard and custom objects 4. Configure field settings (unique, required, etc.) Take screenshots of your self-hosted data model for reference, or keep both instances open side by side. ## Step 3: Invite All Users **Critical: Invite users BEFORE importing data.** Users must accept their invitations before you import any records that reference them (like Account Owner fields). If users don't exist yet, those relations cannot be mapped. 1. Go to **Settings → Members** in your cloud instance 2. Invite all team members who had accounts on self-hosted 3. **Wait for everyone to accept** their invitation 4. Verify all users appear in your Members list ## Step 4: Export Data from Self-Hosted Export each object from your self-hosted instance: 1. Navigate to each object (Companies, People, Opportunities, etc.) 2. Configure the view to show **all columns** you want to migrate 3. Click **⋮ → Export view** 4. Save each CSV file with a clear name (e.g., `companies-export.csv`) **Export in this order** (for reference when importing): 1. Companies 2. People 3. Opportunities 4. Custom objects (following their dependencies) 5. Tasks, Notes ## Step 5: Update Workspace Member References The exported CSVs contain user IDs from your self-hosted instance. These IDs won't match your cloud instance, so you need to replace them with emails. **For each CSV file with user references (Owner, Assignee, etc.):** 1. Open the CSV in a spreadsheet application 2. Add a new column next to each user ID column (e.g., `accountOwnerEmail` next to `accountOwnerId`) 3. Fill in the **email address** of each user 4. You can delete the old ID column or leave it (it will be skipped during import) **Example:** Before: ```csv theme={null} name,domain,accountOwnerId Acme Corp,https://acme.com,old-uuid-123 ``` After: ```csv theme={null} name,domain,accountOwnerEmail Acme Corp,https://acme.com,john@yourcompany.com ``` Use the same email addresses that users used to accept their cloud workspace invitation. ## Step 6: Plan Your Import Order Import files in the correct order to maintain relationships: 1. **Companies** first (no dependencies) 2. **People** second (link to Companies) 3. **Opportunities** third (link to Companies and People) 4. **Custom objects** (following their dependencies) 5. **Tasks and Notes** last (link to other records) See [How to Import Relations](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) for details on maintaining relationships. ## Step 7: Import to Cloud For each CSV file, in order: 1. Navigate to the object in your cloud instance 2. Click **⋮ → Import records** 3. Upload the CSV file 4. Map columns to fields: * Map user email columns to the appropriate relation fields * Map other columns as usual 5. Review and fix any errors 6. Confirm the import 7. Verify a few records before proceeding to the next file ## Step 8: Recreate Configuration After importing data, manually recreate: ### Views * Recreate saved views with filters, sorts, and column configurations * Set up any kanban or calendar views ### Workflows * Recreate automations in **Settings → Workflows** * Test each workflow before relying on it ### Roles and Permissions * Configure roles in **Settings → Members → Roles** * Assign users to appropriate roles ### Integrations * Reconnect email and calendar sync for each user * Reconfigure any API integrations with new API keys ## Post-Migration Checklist All data imported successfully Relations between objects working correctly User assignments (Owner, Assignee) mapped correctly Views recreated Workflows recreated and tested Roles and permissions configured Email/calendar sync reconnected API integrations updated with new keys ## FAQ Not currently. Workflows must be recreated manually in your cloud instance. File attachments are not included in CSV exports. You'll need to re-upload any attachments manually, migrate them via API or contact our team for assistance with large migrations. Yes, we recommend keeping your self-hosted instance running until you've verified the cloud migration is complete. Just be careful not to create new data in both places. Records referencing that user will fail to import or the relation will be empty. Ensure all users accept invitations before importing data. ## Need Help? Migrating a large or complex workspace? [Get a certified Twenty partner](https://twenty.com/partners/list?categories=HOSTING\&ref=docs-migrate-cloud) to handle the move end-to-end. *(Or loop in our team: [contact@twenty.com](mailto:contact@twenty.com).)* # Prepare Your CSV Files Source: https://docs.twenty.com/user-guide/data-migration/how-tos/prepare-your-csv-files Complete step-by-step guide to format your data for import into Twenty. ## Overview This guide walks you through preparing your CSV file for a successful import. Follow these steps in order to avoid errors. ## Step 1: Check File Requirements Before you start, ensure your file meets these requirements: | Requirement | Details | | -------------- | ------------------------ | | **Format** | CSV, XLSX, or XLS | | **Size limit** | 10,000 records per file | | **Encoding** | UTF-8 recommended | | **Structure** | One object type per file | For datasets larger than 10,000 records, split into multiple files or use the [API import](/user-guide/data-migration/how-tos/import-data-via-api). ## Step 2: Download the Sample File **This is the most important step.** The sample file shows you the exact column names and format Twenty expects. 1. Go to the object view (People, Companies, etc.) 2. Click **⋮** → **Import records** 3. Click **Download sample file** 4. Use this file as your template **Pro tip:** Export a few existing records instead. This gives you real examples of how data should be formatted, and the column names will map automatically during import. ## Step 3: Remove Duplicate Values Twenty enforces uniqueness on certain fields. Duplicates will cause import errors. | Object | Unique Fields | | ------------------ | ----------------------------------------- | | **People** | `id`, `email` | | **Companies** | `id`, `domain` | | **Custom objects** | `id`, plus any field you marked as unique | **Before importing:** 1. Sort your spreadsheet by the unique field (email or domain) 2. Remove or merge duplicate rows 3. Check for duplicates that already exist in Twenty **Soft-deleted records count toward uniqueness.** Records in Command Menu → See deleted records will cause duplicate errors. Delete them permanently or restore and update them. ## Step 4: Format Each Field Type Correctly Different field types require specific formats. Here's the complete reference: ### Text Fields * No special formatting required * Leading/trailing spaces are automatically trimmed ### Email Fields * Must be valid email format: `name@domain.com` * Must be unique (no duplicates in file or in Twenty) * For additional emails, use this format in the **Emails / Additional Emails** column: ``` ["jane@twenty.com","jane.doe@twenty.com"] ``` ### Domain Fields * **Recommended format**: `https://domain.com` * This matches the format used by mailbox/calendar sync (prevents duplicates) * Fill both columns: * **Domain / Domain Label**: `domain.com` * **Domain / Domain URL**: `https://domain.com` * Must be unique within your file and in Twenty ### Phone Fields Phone is a **nested field** requiring multiple columns: | Column | Example | | --------------------------------------- | ------------ | | **Phones / Primary Phone Number** | `4159095555` | | **Phones / Primary Phone Country Code** | `US` | | **Phones / Primary Phone Calling Code** | `+1` | ### Address Fields Address is a **nested field** with multiple columns (some can be left empty): * **Address / Address 1**: Street address line 1 * **Address / Address 2**: Street address line 2 (optional) * **Address / City**: City name * **Address / State**: State or province * **Address / Country**: Country name * **Address / Post Code**: Postal/ZIP code ### Date Fields Use consistent formatting throughout your file: * `YYYY-MM-DD` (recommended): `2024-03-15` * ISO 8601: `2024-03-15T10:30:00Z` ### Number Fields * Numbers only (no text) * Use period for decimals: `1234.56` * No thousands separators (not `1,234.56`) ### Currency Fields Currency is a **nested field** requiring two columns that **both must be filled**: | Column | Example | | --------------------- | --------- | | **Amount / Amount** | `1234.56` | | **Amount / Currency** | `USD` | ### Boolean Fields Use uppercase: `TRUE` or `FALSE` Lowercase `true` or `false` will not work. ### Select Fields Use the **API name** of the option, not the display label. **How to find API names:** 1. Go to **Settings → Data Model** 2. Select the object and field 3. Enable **Advanced mode** (toggle at bottom right) 4. Copy the API name (e.g., `OPTION_1`, not "Option 1") New select options are not created automatically. Add them in **Settings → Data Model** before importing. ### Multi-Select Fields Use API names in array format: ``` ["VALUE1","VALUE2"] ``` ### Array Fields Use JSON array format: ``` ["value1","value2"] ``` ### Rating Fields Use the format: `RATING_1`, `RATING_2`, `RATING_3`, `RATING_4`, or `RATING_5` ### Links/URL Fields Fill both columns: * **Links / Link Label**: `Twenty` * **Links / Link URL**: `https://twenty.com` For secondary links, use the **Links / Secondary Links** column: ``` [{"url":"https://twenty.com","label":"Twenty"}] ``` ### JSON Fields Use valid JSON format: ``` {"key":"value","key2":"value2"} ``` ### ID Fields * **Optional**: Twenty auto-generates IDs if not provided * **Format**: UUID (e.g., `c776ee49-f608-4a77-8cc8-6fe96ae1e43f`) * **Use case**: Include ID to update existing records instead of creating new ones ## Step 5: Add Relation Columns (If Linking Records) To link records to other objects (e.g., People to Companies), add a column with the unique identifier of the related record. **Example**: Linking People to Companies Add a column to your People CSV: ``` firstName,lastName,email,companyDomain John,Smith,john@acme.com,https://acme.com Jane,Doe,jane@widgets.co,https://widgets.co ``` **Important rules for relations:** * The parent record must already exist in Twenty * Use the **Domain URL** format (`https://domain.com`), not the label * Map only ONE unique identifier (don't include both `companyId` AND `companyDomain`) * For Workspace Members, use their **email** (not name) **Import Order Matters!** Import the "one" side before the "many" side: 1. **Companies** first 2. **People** second (with company reference) 3. **Opportunities** third The parent record must exist before you can reference it. See [How to Import Relations](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) for detailed instructions. ## Step 6: Ensure Fields Exist in Twenty The import creates **records**, not **fields**. All fields you want to import must already exist in your data model. **Before importing:** 1. Go to **Settings → Data Model** 2. Select your object 3. Create any custom fields you need 4. Note the exact field names (they must match your column headers) ## Step 7: Final Checklist Before uploading your file, verify: File is CSV, XLSX, or XLS format File has fewer than 10,000 records Encoding is UTF-8 No duplicate emails (for People) or domains (for Companies) Dates use consistent format throughout Domains use `https://domain.com` format Boolean fields use `TRUE` or `FALSE` (uppercase) Select fields use API names, not display labels All custom fields exist in Settings → Data Model Parent records imported before child records Relation columns reference existing records ## Common Mistakes to Avoid | Mistake | Solution | | -------------------------------------------- | ------------------------------------- | | Using `true` instead of `TRUE` | Boolean values must be uppercase | | Using display labels for Select fields | Find and use API names in Settings | | Importing People before Companies | Always import parent objects first | | Missing currency code for Currency fields | Fill both Amount and Currency columns | | Wrong domain format | Use `https://domain.com` consistently | | Mapping multiple unique fields for relations | Map only ONE (domain OR id, not both) | ## Next Steps Your file is ready! Now: * [Import Companies](/user-guide/data-migration/how-tos/import-companies-via-csv) (import these first) * [Import Contacts](/user-guide/data-migration/how-tos/import-contacts-via-csv) * [Fix any import errors](/user-guide/data-migration/how-tos/fix-import-errors) # Update Existing Records via Import Source: https://docs.twenty.com/user-guide/data-migration/how-tos/update-existing-records-via-import Complete step-by-step guide to bulk updating records using CSV import. ## Overview Need to update many records at once? Instead of editing them one by one, use the CSV import to bulk update existing records. **Use cases:** * Update job titles for multiple people * Change company information in bulk * Add data to new custom fields * Correct data errors across many records ## How It Works When you import a file containing a **unique identifier** that matches an existing record, Twenty updates that record instead of creating a duplicate. | If unique identifier... | Twenty will... | | -------------------------- | ------------------------------------------------ | | Matches an existing record | **Update** the existing record | | Doesn't match any record | **Create** a new record | | Is missing from your file | **Create** a new record (with auto-generated ID) | **Multi-Select fields are overwritten, not merged.** If a record has `Option A` and `Option B` selected, and you import `["Option C"]`, the record will only have `Option C` after import. The import replaces all previous selections—it does not add to them. To keep existing values, include them all in your import: `["Option A","Option B","Option C"]` ## Step 1: Export Your Current Data First, export the records you want to update: 1. Navigate to the object (People, Companies, etc.) 2. **Add the columns you need** — click **Options → Fields** to show the fields you want to update 3. **Filter if needed** — narrow down to only the records you want to update 4. Click **⋮** → **Export view** 5. Save the CSV file **Why export first?** The exported file has the correct format, includes unique identifiers, and maps automatically during import. ### What Gets Exported * All visible columns in your current view * The record's unique identifiers (`id`, `email`, `domain`) * Current field values you can modify ## Step 2: Edit the CSV File Open the exported file in your spreadsheet application (Excel, Google Sheets, etc.): 1. **Keep the unique identifier column** — don't delete `id`, `email`, or `domain` 2. **Update the values** in the columns you want to change 3. **Remove columns you don't need to update** (optional, but cleaner) 4. **Don't change unique identifier values** — or Twenty will create new records ### Example: Updating Job Titles **Exported file:** ```csv theme={null} id,email,firstName,lastName,jobTitle 550e8400-e29b-41d4-a716-446655440001,john@acme.com,John,Smith,Sales Rep 550e8400-e29b-41d4-a716-446655440002,jane@acme.com,Jane,Doe,Sales Rep 550e8400-e29b-41d4-a716-446655440003,bob@acme.com,Bob,Johnson,Sales Rep ``` **After your edits:** ```csv theme={null} id,email,firstName,lastName,jobTitle 550e8400-e29b-41d4-a716-446655440001,john@acme.com,John,Smith,Account Executive 550e8400-e29b-41d4-a716-446655440002,jane@acme.com,Jane,Doe,Senior Account Executive 550e8400-e29b-41d4-a716-446655440003,bob@acme.com,Bob,Johnson,Account Executive ``` **Don't change the unique identifier values.** If you change `john@acme.com` to `john.smith@acme.com`, Twenty will create a new record instead of updating the existing one. ## Step 3: Import the Updated File 1. Navigate to the object 2. Click **⋮** → **Import records** 3. Upload your edited CSV file 4. **Ensure the unique identifier is mapped** — verify `email`, `domain`, or `id` is mapped correctly 5. Review the field mappings 6. Check for errors 7. Click **Confirm** Twenty matches records by the unique identifier and updates them with new values. ## Choosing the Right Unique Identifier | Object | Recommended | Alternative | Notes | | ------------------ | ---------------- | ----------- | ---------------------------- | | **People** | `email` | `id` | Email is human-readable | | **Companies** | `domain` | `id` | Domain is human-readable | | **Custom objects** | Any unique field | `id` | Use your custom unique field | **Use only ONE unique identifier.** Don't map both `email` AND `id`. This can cause confusion and errors. ### Using Custom Unique Fields If you have a custom field marked as unique (like an external ID from another system): 1. Include that field in your export and import 2. Map it during import 3. Twenty will match on that field ## Step 4: Verify the Updates After importing: 1. Open a few updated records 2. Verify the changes were applied 3. Check that no duplicate records were created ## What About Fields Not in Your File? **Fields not included in your import file remain unchanged.** | Your file includes... | Result | | ---------------------------- | ------------------------------------------------------ | | `email`, `jobTitle` | Only `jobTitle` is updated; other fields stay the same | | `email`, `jobTitle`, `phone` | `jobTitle` and `phone` are updated | This means you only need to include the fields you want to change (plus the unique identifier). ## Combining Updates and New Records You can update existing records AND create new ones in the same import: ```csv theme={null} email,firstName,lastName,jobTitle john@acme.com,John,Smith,Senior Manager ← Updates existing (email matches) newperson@acme.com,New,Person,Analyst ← Creates new (email doesn't match) ``` ## Common Mistakes to Avoid | Mistake | Problem | Result | Solution | | ------------------------------ | ------------------------------------------------------- | -------------------------------------- | ----------------------------------------- | | **Changing unique identifier** | Changed `john@acme.com` to `john.smith@acme.com` | Creates new record instead of updating | Keep unique identifiers unchanged | | **Multiple unique fields** | Mapping both `email` AND `id` | Potential matching conflicts | Map only ONE unique identifier | | **No unique identifier** | File only has `firstName`, `lastName`, `jobTitle` | All rows create new records | Always include `email`, `domain`, or `id` | | **Case mismatch** | File has `John@acme.com` but Twenty has `john@acme.com` | Creates new record | Export from Twenty to get exact values | ## FAQ Records with unique identifiers that don't match existing records will be created as new records. This lets you update and create in the same import. Yes, leave the cell empty in your CSV. The import will clear that field's value on the existing record. Fields not in your import file remain unchanged on existing records. Only fields you include are updated. Yes! Include the relation's unique identifier (e.g., `companyDomain`) and map it to the relation field. The relation will be updated. During the import review step, Twenty shows you how many records will be updated vs. created based on unique identifier matches. There's no automatic undo. We recommend exporting your data as a backup before making bulk updates. ## Best Practices 1. **Export first** — always start from an export to ensure correct format 2. **Backup before updating** — export your data before making bulk changes 3. **Test with a few records** — try updating 5-10 records first before doing a large batch 4. **Use human-readable identifiers** — `email` and `domain` are easier to verify than `id` 5. **Only include necessary columns** — fewer columns means less chance for errors ## Troubleshooting Having issues? Check: * [How to Fix Import Errors](/user-guide/data-migration/how-tos/fix-import-errors) * [Uniqueness Constraints](/user-guide/data-migration/capabilities/uniqueness-constraints) * [Field Mapping Reference](/user-guide/data-migration/capabilities/field-mapping) # Data Migration Source: https://docs.twenty.com/user-guide/data-migration/overview Import and export your CRM data via CSV files or API. ## Import Methods Twenty supports two main methods for importing data: | Method | Best For | Volume Limit | | -------------- | ------------------------------------ | ----------------------- | | **CSV Import** | Standard migrations, regular updates | 10,000 records per file | | **API Import** | Large-scale migrations, automation | Unlimited | For very large datasets (hundreds of thousands of records), use the API. Our [implementation partners](/user-guide/getting-started/capabilities/implementation-services) can help run these scripts if needed. ## CSV Import Basics You can import data for any object using CSV, XLSX, or XLS files. Each file should contain **only one type of object** (e.g., only People records). **Fields must exist before import.** Uploading a CSV creates records but does not create fields. If you need custom fields, create them first under **Settings → Data Model**. ### Steps 1. Navigate to the object where you want to import data 2. Click the **⋮** icon on the top right (this is the Command Menu) and click on **Import records** 3. Download the template file to ensure your data is in the expected format 4. Upload your formatted CSV file 5. Map your columns to Twenty fields 6. Review errors (highlighted in yellow) and fix them, directly editing in the UI 7. Confirm the import ### Importing Relations Between Objects You can import relations between objects using the CSV import function. You need to reference the related object using a unique field from this object: the `id`, the `email` for People and Workspace Members, the `domain` for companies, any other field set as unique in the data model for any other object. **Deleted records count toward uniqueness.** Soft-deleted records (visible under Command Menu → See deleted records) are included in uniqueness checks. If you import a record with the same unique value as a deleted record, the deleted record will be restored. **Import Order Matters!** When importing related objects, upload files in this order: 1. **Companies** first (the "one" side of relationships) 2. **People** second (linked to Companies via companyId) 3. **Opportunities** third (linked to Companies/People) 4. **Custom objects** with relations last Why? The "one" side of a one-to-many relationship must exist before you can reference it. For example, the Company record must exist before you import a Person with that company's ID. Please refer to [this article](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) for a step-by-step guide on how to proceed. ## Export Data Export your workspace data for backups, reporting, or migration. ### Steps 1. Navigate to the object you want to export 2. Configure the view with the columns you need 3. Click **⋮** → **Export view** 4. Save the CSV file **Only visible columns are exported.** The CSV file will only contain the columns displayed in your current view. Add or hide columns before exporting to control what data is included. **Export limits**: Up to 20,000 records per export. ## Permissions Data import and export require specific permissions: * **Import**: Requires "Import CSV" permission * **Export**: Requires "Export CSV" permission Contact your workspace admin if you don't have these permissions. ## Next Steps * [Prepare your CSV files](/user-guide/data-migration/how-tos/prepare-your-csv-files) * [Import relations between objects](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) * [Import via API for large datasets](/user-guide/data-migration/how-tos/import-data-via-api) # Fields Source: https://docs.twenty.com/user-guide/data-model/capabilities/fields Understand the role of fields and how to manage them. ## About Fields Fields are like columns in a spreadsheet. They store different types of data like text, numbers, or dates. Fields can be standard (built-in) or custom (the ones you create). ### Standard Fields Standard fields come built-in with Twenty to handle common business needs. For example, `First Name` and `Last Name` are standard fields in the `People` object. They store text data for individual names. You cannot delete standard fields, but you can deactivate them if you don't need them. You can also customize the options of the standard `SELECT` type fields, for example the options for the `Stage` on Opportunities. ### Custom Fields Custom fields can be added to any object. You can store text, numbers, dates, dropdown selections, and more. Use custom fields to track information that's specific to your business. For instance, a custom field for SpaceX could be `Rocket Active Status`, indicating if a rocket is operational. ## Field Types Twenty supports various field types: | Type | Description | Example | | ------------ | ----------------------------------------------------------------- | ------------------ | | Address | Structured address with street, city, state, country, postal code | Office Address | | Array | List of text values | Tags | | Boolean | True/false checkbox | Is Active | | Currency | Monetary value with currency code | Deal Amount (USD) | | Date | Date values | Close Date | | Date & Time | Date with time | Meeting Time | | Domain | Website domain (used for Companies) | acme.com | | Email | Email addresses (with primary + additional) | Contact Email | | JSON | Structured JSON data | Custom metadata | | Links | URLs with labels (primary + secondary) | Website, LinkedIn | | Long Text | Multi-line text | Description, Notes | | Multi-Select | Multiple choices from a predefined list | Tags, Categories | | Number | Numeric values (integers or decimals) | Quantity, Score | | Phone | Phone numbers with country code | Work Phone | | Rating | Star rating (1-5) | Priority, Score | | Relation | Links to records in other objects | Company → People | | Select | Single choice from a predefined list | Stage, Status | | Text | Single line of text | Name, Title | ## Create a Custom Field To add a custom field to any object, follow these steps: 1. Go to `Settings` in the left sidebar. 2. Go to `Data Model`, then select the object you wish to customize. 3. Proceed by clicking on `+ New Field`. 4. Choose a field name and type that suits your requirements. Consider adding a field description for better understanding. Your newly created field is now available within the application's fields. To display it on a specific view, click on the options menu, then select `Fields`. **Quick way:** Click the **+** button at the top right of any object table, then select `Customize fields`. This takes you directly to the Data Model settings. ## Deactivate a Field You can deactivate a field to hide it from the app without losing your data. Think of it as hiding the field rather than deleting it. Here's how you can do it: 1. Find the field you want to deactivate in your object settings. 2. Click the three dots `⋮` next to the field to open the menu. 3. Select `Deactivate` from the dropdown. What happens when you deactivate a field? 1. **In the app:** The field disappears and you can't add new values to it. 2. **Existing relationships:** If it's a relation field, existing connections stay, but you can't create new ones. 3. **API access:** You can still access the field and its data through the API. You can reactivate Standard and Custom Fields or have the option to permanently delete them. ## Make Fields Unique Make a field unique to ensure distinct records cannot have the same value. For example, email addresses are unique for each person. If you get an error when setting uniqueness, check for duplicate values in your data (including deleted records). ## Indexes (Advanced) Database indexes are managed automatically — adding your own is rarely necessary and easy to get wrong. With Advanced mode on, each object has an **Indexes** section under `Settings → Data Model → ` for the cases where you know you need one. ## Field Configuration Best Practices ### Naming Conventions and Limitations * **Singular and plural names must be distinct**: Our GraphQL API needs distinct names for mutations * **Protected field names**: some names are reserved for system usage (e.g., `Type`, `Application`) ### Currency and Phone Fields * **Default currency**: can be configured via the data model * **Default country codes**: can be configured for phone fields via the data model ### Select Fields * **A default option can be selected** for each Select field ### Record Text Fields * **Each object has one main display field**: This field appears in the leftmost column and represents the record when linked to other objects. It must be a text field. For example, People uses `Name` as the main field, so when you link a person to a company, you'll see their name in the company's view. # Objects Source: https://docs.twenty.com/user-guide/data-model/capabilities/objects Learn about standard and custom objects in Twenty. ## Standard Objects Standard objects are predefined entities in your workspace to help you get started. They're part of a shared data model accessible to all users of Twenty. You can use them as-is, customize them or deactivate them. ### People The `People` object stores your contacts. It includes contact details and interaction history, so you can see all your customer interactions in one place. ### Companies The `Companies` object stores your business accounts. It includes details like industry, size and location. Companies connect to both `People` and `Opportunities` objects. ### Opportunities The `Opportunities` object stores deal-related data. It tracks the progression of potential sales, from prospecting to closure, recording stages, deal sizes, associated account, and expected close date. You can view your sales pipeline in a kanban layout. ### Notes The `Notes` object stores free-form notes that can be attached to People, Companies, Opportunities, and other records. Use notes to capture meeting summaries, important details, or any contextual information. ### Tasks The `Tasks` object stores to-dos and action items. Tasks can be linked to People, Companies, Opportunities, and other records. Track due dates, assignees, and completion status to stay on top of your follow-ups. ## Custom Objects Custom objects let you store information that's unique to your organization and that standard objects can't handle. For example, if you're SpaceX, you may want to create a custom object for Rockets and Launches. ### Creating a New Custom Object To create a new custom object: 1. Go to Settings in the sidebar on the left. 2. Under Workspace, go to Data Model. Here you'll be able to see an overview of all your existing Standard and Custom objects (both active and deactivated). 3. Click on `+ New object` at the top. Enter the name (both singular and plural), choose an icon, and add a description for your custom object and hit Save (at the top right). Using Listing as an example of custom object, the singular would be "listing" and the plural would be "listings" along with a description like "Listings that hosts created to showcase their property." 4. Your custom object is now created and will appear in your sidebar. You can start adding records to it right away. ## Managing Objects ### Deactivating Objects If you don't need a standard or custom object: 1. Go to Settings → Data Model 2. Find the object you want to deactivate and click it 3. Go to object's **Settings** tab 4. Click `Deactivate` button 5. The object will be hidden from your workspace but data is preserved ### Reactivating Objects To bring back a deactivated object: 1. Go to Settings → Data Model 2. Enable `Deactivated` filter 3. Look for deactivated objects (they have `Deactivated` next to their name) 4. Click `⋮` and then `Activate` to reactivate it 5. The object and all its data will be restored ## Best Practices ### When to Create Custom Objects * **Unique business entities**: Things specific to your industry or process * **Complex relationships**: When you need to track connections between multiple entities * **Scalable data**: When you might have many instances of something ### When to Use Fields Instead * **Simple attributes**: Properties that describe existing objects * **Categories or labels**: Ways to classify existing records * **Single values**: Information that doesn't need its own lifecycle ### Object Naming * **Use clear, descriptive names**: Make it obvious what the object represents * **Follow conventions**: Use singular for the object name, plural for the collection * **Consider your team**: Choose names everyone will understand # Relation Fields Source: https://docs.twenty.com/user-guide/data-model/capabilities/relation-fields Connect records across different objects using relation fields. ## Types of Relations ### One-to-Many One record in Object A can be linked to many records in Object B. **Example:** One Company can have many People (employees). ### Many-to-One Many records in Object A can be linked to one record in Object B. **Example:** Many People can belong to one Company. ### Relations to Multiple Object Types Some objects can link to multiple object types on one side of the relation. **Example:** A Note can be attached to one Person AND one Company AND one Opportunity simultaneously. The Note is on the "many" side, connecting to multiple "one" sides. Similarly, a Project (on the "one" side) could receive links from multiple People, multiple Companies, and multiple Notes. **Import/Export limitation**: Relations pointing to multiple object types are not yet supported for CSV import/export. This is on our roadmap. ### Many-to-Many Many records in Object A can be linked to many records in Object B. **Example:** Many People can be linked to many Projects, and vice versa. Many-to-many relations use a **junction object** pattern: an intermediate object that connects both sides. With the junction relation feature, Twenty displays the final linked records directly, hiding the intermediate object from the UI. **Beta Feature**: Junction relations must be enabled at **Settings → Community → Features** before use. See [How to Create Many-to-Many Relations](/user-guide/data-model/how-tos/create-many-to-many-relations) for a complete step-by-step guide. ## Creating a Relation Field 1. Go to **Settings → Data Model** 2. Select the object where you want to add the relation 3. Click **+ New field** 4. Select **Relation** as the field type 5. Choose the target object(s) to relate to 6. Configure the relation settings: * **Field name on source object**: The name of the relation field on the object you're editing * **Field name on destination object**: The name of the relation field that will appear on the target object * Relation type (one-to-many, many-to-one) 7. Click **Save** ## Standard Relations Twenty comes with pre-built relations between standard objects: | From Object | To Object | Relation Type | | ------------- | --------- | ------------- | | People | Companies | Many-to-One | | Opportunities | Companies | Many-to-One | | Opportunities | People | Many-to-One | ## Best Practices ### Planning Relations * **Map your data model**: Plan relations before creating them * **Consider direction**: Think about which object "owns" the relationship * **Avoid circular dependencies**: Keep your data model clean ### Naming Relations * **Use clear names**: Make it obvious what the relation represents * **Be consistent**: Use similar naming patterns across relations * **Consider both sides**: Name both sides of the relation appropriately ### Performance * **Don't over-relate**: Too many relations can slow down your workspace ## Limitations * **Deleting relations** removes the link but not the related records * **Circular relations** should be avoided for data integrity # Create Custom Fields Source: https://docs.twenty.com/user-guide/data-model/how-tos/create-custom-fields Step-by-step guide to adding custom fields to any object. Custom fields let you capture information specific to your business. Add them to any object—standard or custom. ## Steps 1. Go to **Settings → Data Model** 2. Select the object you want to add a field to 3. Click **+ New Field** 4. Choose a **field type** (see [Fields](/user-guide/data-model/capabilities/fields) for all types) 5. Enter the **field name** and optional description 6. Configure field-specific settings (see below) 7. Click **Save** **Quick method:** Click the **+** at the end of column headers in any table view → **Customize fields**. ## Show the Field in Views New fields aren't automatically visible. To display: 1. Open the object's table view 2. Click **Options → Fields** 3. Click the **eye icon** next to your field to show it 4. Drag to reorder ## Configuration Options ### For Select / Multi-Select 1. Click **+ Add option** to create choices 2. Set a **default option** if desired 3. Drag to reorder options **Use API names for imports.** Enable **Advanced mode** in Settings to see API names. See [Field Mapping](/user-guide/data-migration/capabilities/field-mapping). ### For Currency Fields Set the **default currency** (USD, EUR, etc.) for new records. ### For Phone Fields Set the **default country code** to pre-fill for new phone numbers. ### Making a Field Unique Toggle **Unique** to prevent duplicate values across records. If duplicates exist (including in deleted records), you'll get an error. Clean up duplicates first. ### Setting Default Values For Select fields, you can choose which option is pre-selected for new records. For Checkbox fields, set whether it's checked or unchecked by default. ## Deactivating a Field 1. Go to **Settings → Data Model** 2. Find the field 3. Click **⋮ → Deactivate** Data is preserved. You can reactivate or permanently delete later. ## Related * [Fields](/user-guide/data-model/capabilities/fields) — all field types explained * [Data Model FAQ](/user-guide/data-model/how-tos/data-model-faq) — common questions # Create Custom Objects Source: https://docs.twenty.com/user-guide/data-model/how-tos/create-custom-objects Step-by-step guide to creating custom objects in Twenty. Custom objects let you store information unique to your business that standard objects don't cover. For example: Projects, Products, Tickets, or Listings. **Not sure if you need an object or a field?** See [Understanding Your Data Model](/user-guide/data-model/overview) for guidance. ## Steps 1. Go to **Settings → Data Model** 2. Click **+ Add object** 3. Fill in: * **Singular name** (e.g., "Listing") * **Plural name** (e.g., "Listings") * **Icon** * **Description** (optional) 4. Click **Save** Your object appears in the sidebar immediately. ## Next: Add Fields New objects start with basic fields. Add custom fields to capture the data you need: 1. In **Settings → Data Model**, select your object 2. Click **+ New Field** 3. Choose a field type, configure, and save See [How to Create Custom Fields](/user-guide/data-model/how-tos/create-custom-fields) for details on field types and configuration. ## Connecting to Other Objects To link your object to People, Companies, or other objects, create a relation field. See [How to Create Relation Fields](/user-guide/data-model/how-tos/create-relation-fields). ## Deactivating an Object If you no longer need an object: 1. Go to **Settings → Data Model → object of your choice → Settings** 2. Click the **Deactivate** button The object is hidden but data is preserved. You can reactivate or permanently delete later. ## Related * [Objects](/user-guide/data-model/capabilities/objects) — standard vs custom objects * [Data Model FAQ](/user-guide/data-model/how-tos/data-model-faq) — common questions # Create Many-to-Many Relations Source: https://docs.twenty.com/user-guide/data-model/how-tos/create-many-to-many-relations Connect records where many items on both sides can be linked together using junction objects. Many-to-many relations let you connect multiple records on both sides. For example: many People can work on many Projects, and each Project can have many People. **Beta Feature**: Junction relations are currently in beta. Enable them at **Settings → Community → Features** before following this guide. This feature also requires **Advanced mode** to be enabled (toggle at the bottom left of Settings). ## When to Use Many-to-Many Use many-to-many when both sides of a relationship can have multiple connections: | Relationship | Example | | ----------------- | ------------------------------------------------------------------------ | | People ↔ Projects | A person works on multiple projects; a project has multiple team members | | Companies ↔ Tags | A company can have multiple tags; a tag can apply to multiple companies | | Products ↔ Orders | A product can be in multiple orders; an order contains multiple products | ## How It Works Twenty uses a **junction object** pattern for many-to-many relations. A junction object sits between two objects and holds the connections: ``` People ←→ Project Assignments ←→ Projects ``` The **Project Assignments** object (junction) has: * A relation to People (many-to-one) * A relation to Projects (many-to-one) When you enable the junction relation toggle, Twenty displays linked records directly instead of showing the intermediate junction records. ## Prerequisites 1. **Enable Junction Relations in Beta Features**: Go to **Settings → Community → Features** and enable **Junction Relations** 2. **Enable Advanced mode**: Toggle on **Advanced mode** at the bottom left of the Settings sidebar 3. Plan your data model: * Which two objects are you connecting? * What should the junction object be called? ## Step 1: Create the Junction Object First, create the intermediate object that will hold the connections. 1. Go to **Settings → Data Model** 2. Click **+ Add object** 3. Name it descriptively (e.g., "Project Assignment", "Team Member", "Product Order") 4. Toggle "Skip creating a Name field" on New pivot object 5. Click **Save** **Naming convention**: Use a name that describes the relationship, like "Project Assignment" or "Team Membership". This makes the data model easier to understand. ## Step 2: Create Relations Between Objects and the Junction Add relation fields from each of your two objects to the junction object. ### First Relation (Object A → Junction) 1. Select your first object in **Settings → Data Model** 2. Click **+ Add Relation** 3. Select the junction object (e.g., "Project Assignments") 4. Set the relation type to **One-To-Many** (one person can link to many assignments) 5. Name the fields: * Field on People: e.g., "Project Assignments" * Field on junction: e.g., "Person" 6. Click **Save** ### Second Relation (Object B → Junction) 1. Select your second object in **Settings → Data Model** 2. Click **+ Add Relation** 3. Select the junction object (e.g., "Project Assignments") 4. Set the relation type to **One-To-Many** (one project can link to many assignments) 5. Enable **"This is a relation to a Junction Object"** 6. Name the fields: * Field on junction: e.g., "Project" * Field on Projects: e.g., "Team Members" 7. Click **Save** ## Step 3: Configure the Junction Relation Display Now configure the source objects to display linked records directly, skipping the intermediate junction object. 1. Go to **Settings → Data Model** 2. Select the first object (e.g., "People") 3. Find the relation field pointing to the junction object (e.g., "Project Assignments") 4. Click to edit the field 5. Enable **"This is a relation to a Junction Object"** 6. Select the **Target relation** (e.g., "Project" — the field on the junction that points to the other side) 7. Click **Save** ## Result After configuration: * On a **Person** record, the "Project Assignments" field displays **Projects** directly (not assignment records) * On a **Project** record, the "Team Members" field displays **People** directly The junction object still exists and stores the connections, but the UI presents a cleaner many-to-many view. ## Example: People ↔ Projects Here's a complete walkthrough: ### Create the Junction Object * Name: **Project Assignment** * Description: "Links people to projects they work on" ### Add Relations 1. **People → Project Assignment** * Type: One-to-Many * Field on People: "Project Assignments" * Field on Assignment: "Person" 2. **Projects → Project Assignment** * Type: One-to-Many * Field on Projects: "Team Members" * Field on Assignment: "Project" ### Configure Junction Display 1. On **People** object: * Edit "Project Assignments" field * Enable junction toggle * Target: "Project" 2. On **Projects** object: * Edit "Team Members" field * Enable junction toggle * Target: "Person" ### Use It * Open a Person record → See their Projects directly * Open a Project record → See team members directly * Create new connections from either side ## Adding Extra Data to Connections Since the junction object is a real object, you can add custom fields to store information about the relationship: * **Role**: "Developer", "Designer", "Manager" * **Start Date**: When the person joined the project * **Hours Allocated**: Weekly hours on this project To access this data, navigate to the junction object directly or query it via the API. ## Limitations * **CSV Import/Export**: Importing many-to-many relations directly is not supported. Import records to the junction object instead. * **Filters**: Filtering by many-to-many relations may have limited options. ## Related * [Relation Fields](/user-guide/data-model/capabilities/relation-fields) — relation types explained * [Create Custom Objects](/user-guide/data-model/how-tos/create-custom-objects) — how to create objects * [Create Relation Fields](/user-guide/data-model/how-tos/create-relation-fields) — basic relation setup # Create Relation Fields Source: https://docs.twenty.com/user-guide/data-model/how-tos/create-relation-fields Step-by-step guide to connecting objects with relation fields. Relation fields connect records from different objects—for example, linking People to Companies. **Relation names cannot be changed after creation** (they affect the API). Plan your names carefully. ## Before You Start Decide: * Which objects are you connecting? (e.g., People → Companies) * Which is the "one" side? (e.g., Company) * Which is the "many" side? (e.g., People — many people work at one company) * What should the field be named on each side? See [Relation Fields](/user-guide/data-model/capabilities/relation-fields) for relation types explained. ## Steps 1. Go to **Settings → Data Model** 2. Select the object where you want the relation (typically the "many" side) 3. Click **+ New Field** 4. Select **Relation** as the field type 5. Choose the **target object** 6. Select **One-to-Many** or **Many-to-One** 7. Enter field names for **both sides** of the relation 8. Click **Save** ## Example: People → Companies * Go to **Settings → Data Model → People** * Add a Relation field * Target: **Companies** * Type: **Many-to-One** * Field on People: **Company** * Field on Companies: **Employees** Now each Person can be linked to a Company, and each Company shows its People. ## Deactivating a Relation 1. Go to **Settings → Data Model → object of your choice** 2. Find the relation field and click it 3. Click **Deactivate** button Links are preserved but hidden. Reactivate to restore. **Deactivating a relation doesn't delete records.** Only the link between them is removed. ## Deleting a Relation 1. Go to **Settings → Data Model → object of your choice** 2. Find the relation field and click it 3. Click **Delete** button Applies only to custom relations. Standard relations can only be deactivated. ## Related * [Relation Fields](/user-guide/data-model/capabilities/relation-fields) — types and limitations * [How to Import Relations](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) — bulk import linked records # Customize Your Data Model Source: https://docs.twenty.com/user-guide/data-model/how-tos/customize-your-data-model Overview of data model customization options. Twenty's data model is fully customizable. Create objects, fields, and relations to match your business. ## Quick Links | I want to... | Guide | | ------------------------ | -------------------------------------------------------------------------------------- | | Create a new object | [How to Create Custom Objects](/user-guide/data-model/how-tos/create-custom-objects) | | Add fields to an object | [How to Create Custom Fields](/user-guide/data-model/how-tos/create-custom-fields) | | Connect objects together | [How to Create Relation Fields](/user-guide/data-model/how-tos/create-relation-fields) | ## Learn More * [Understanding Your Data Model](/user-guide/data-model/overview) — key concepts and planning tips * [Objects](/user-guide/data-model/capabilities/objects) — standard vs custom objects * [Fields](/user-guide/data-model/capabilities/fields) — all field types * [Relation Fields](/user-guide/data-model/capabilities/relation-fields) — connecting objects * [Data Model FAQ](/user-guide/data-model/how-tos/data-model-faq) — common questions # Data Model FAQ Source: https://docs.twenty.com/user-guide/data-model/how-tos/data-model-faq Frequently asked questions about Twenty's data model. ## Object Management Yes, custom objects can be deleted. You can also deactivate them first, which hides the object and its data from the interface while preserving the data. No, standard objects cannot be deleted. You can only deactivate them, which hides them from the interface but preserves the data. You can create as many custom objects and fields as you need — the price doesn't change. You can rename the label of standard objects (People, Companies, Opportunities), but not their API names. The API names are fixed for consistency across all Twenty workspaces. Yes, you can change the icon for both standard and custom objects in **Settings → Data Model**. Not yet. Object ordering in the navigation is currently fixed, but this feature is planned for a future release. All active objects appear in the navigation. You can deactivate objects you don't need under **Settings → Data Model**. ## Field Capabilities No, field types cannot be changed after creation. If you need a different type, create a new field with the correct type, migrate your data, then deactivate the old field. Our GraphQL API uses both forms for different operations: * `createPerson` (singular) for single record actions * `createPeople` (plural) for bulk operations This creates limitations when singular and plural forms are the same, but it improves the developer experience. Certain field names like `Type` or `Application` are reserved for system use. Choose alternative names like `Category` or `Classification` instead. * The field is hidden from the interface * Existing data is preserved * You can still access the field via API * Existing relations remain but you can't create new ones * You can reactivate the field later Currently, you cannot make custom fields required. All fields accept empty values. You can use workflows to enforce required fields by sending alerts or blocking actions when fields are empty. * **Unique**: No two records can have the same value in this field * **Required**: The field must have a value (not currently supported for custom fields) Formula fields are coming in **H2 2026**. In the meantime, you can use workflows to calculate and update field values automatically. Nested fields are coming in **H2 2026**. Currently, you can use workflows to bring field values from related objects. For example, to display a company's industry on a Person record, create a custom field on People and use a workflow to synchronize the value. Yes, go to Settings → Data model → object of your choice → Layout and click **Customize record page**. If this button is disabled, you must create a record. ## Relations Yes! Self-referencing relations are supported and recommended for use cases like account hierarchies. For example, create a relation from Companies to Companies to track parent/child accounts. Many-to-many relationships are coming in **H2 2026**. Currently, create an intermediate object with two one-to-many relationships as a workaround. For example, to link People and Projects (many-to-many), create a "Project Assignments" object with: * A relation to People (many assignments → one person) * A relation to Projects (many assignments → one project) These allow one object to relate to multiple different object types through a single field. For example, Notes can be attached to People AND Companies AND Opportunities simultaneously. Each Note links to one Person, one Company, and one Opportunity at the same time. Learn more in [Relation Fields](/user-guide/data-model/capabilities/relation-fields). Yes, you can create multiple relations between the same two objects. For example, a Company could have both a "Primary Contact" and "Billing Contact" relation to People. When you delete a record, the relation link is removed from the related records. The related records themselves are not deleted. While technically possible, circular relations (A → B → C → A) should be avoided as they can cause confusion and potential performance issues. ## Access and Permissions Go to **Settings → Data Model** to view and edit all your objects and fields. Reach out to your workspace administrator. Data model access is usually restricted to administrators only. ## Data Management There's no hard limit on record counts. However, very large datasets may impact performance in some views. Use filters and views to manage large datasets effectively. Yes, you can import CSV data into any object, including custom objects. The import process supports field mapping for custom fields. See [How to Prepare Your CSV Files](/user-guide/data-migration/how-tos/prepare-your-csv-files). Currently, there's no built-in export for data model configuration. Contact support if you need to migrate your data model between workspaces. The maximum number of fields a single object can have is 1600 fields as per Postgres limits, including both existing and deleted fields. Be aware that Twenty won't fix your workspace if the limit of 1600 fields is reached as it falls under our Fair Usage policy. ## Need More Help? Check our [Implementation Services](/user-guide/getting-started/capabilities/implementation-services) for help with complex data model design. # Data Model Source: https://docs.twenty.com/user-guide/data-model/overview Learn what a data model is and how to design one that fits your business. ## What is a Data Model? A data model is the structure that defines how information is organized in your CRM. Think of it as the **blueprint** of your customer data — you design it once, then fill it with your actual data. ## Key Concepts ### Objects **Objects** are the main categories of data in your CRM. Each object represents a type of thing you want to track. Twenty comes with standard objects: * **People** — individuals (contacts, leads, partners) * **Companies** — organizations * **Opportunities** — deals or sales * **Notes** — notes attached to records * **Tasks** — to-dos linked to records You can also create **custom objects** for anything specific to your business (e.g., Projects, Subscriptions, Events). ### Fields **Fields** are the properties or attributes that describe each object. They store the actual information. For example, the **People** object has fields like: * Name * Email * Phone * Job Title * Company (a relation to the Companies object) Fields have different **types**: text, number, date, select, multi-select, relation, and more. You can add custom fields to any object. ### Records **Records** are the individual entries within an object — the actual data you create and manage. For example: * "John Smith" is a **record** in the People object * "Acme Corp" is a **record** in the Companies object **An analogy:** | Data Model Concept | Real-World Analogy | | ------------------ | ------------------------------------------ | | **Objects** | Sections in a book (the categories) | | **Fields** | Columns in a spreadsheet (the properties) | | **Records** | Rows in a spreadsheet (the actual entries) | You design the data model (objects + fields) once, then create many records within that structure. ## Why Customize Your Data Model? Every business works differently. Customizing your data model means you can shape Twenty around **your** processes instead of forcing yours into a rigid system. Twenty offers full flexibility: * Create as many custom objects as you need * Add unlimited custom fields * The price doesn't change based on customization ## Tips to Design Your Data Model ### 1. Start with Your Core Objects Identify the main concepts you work with. Twenty already provides: * **People** — your contacts * **Companies** — your accounts * **Opportunities** — your deals Think about what else you might need: * Stripe would need a `Subscriptions` object * Airbnb would need a `Trips` object * An accelerator would need a `Batches` object ### 2. Use Fields for Variations, Not New Objects If something is just a characteristic of an existing object, make it a **field**. **Use fields for:** * Categories and labels (e.g., `Industry` for Companies) * Status values (e.g., `Stage` for Opportunities) * Attributes and properties ### 3. Create an Object When It Stands on Its Own If the concept has its own lifecycle, properties, or relationships, it deserves an object. **Create an object for:** * **Projects** — have deadlines, owners, and tasks * **Subscriptions** — connect companies, products, and invoices * **Events** — involve attendees and follow-up actions These go beyond a single field because they carry their own data and relationships. ### 4. Create an Object When Records Are Open-Ended If something can be linked multiple times and you don't know how many, use an object. **Bad approach:** Creating fields like `Product 1`, `Product 2`, `Product 3`... **Good approach:** Create a `Products` object and relate it to records. This supports one, two, or a hundred products without changing your model. ### 5. Keep It Simple First Start with fields. Move to new objects only when you feel the limits: * Too many fields on one object * Repeated records that should be separate * Relationships that don't fit neatly ## Special Note on People, Companies, and Opportunities **Email and calendar sync only works with People, Companies, and Opportunities.** These are the only objects where you can access synchronized emails and meetings from your mailbox/calendar. We recommend using them as much as possible. **Best practices:** * If you need categories of People, use fields (not new objects) * Example: Use a `Person Type` field with values "Prospect" and "Partner" instead of creating separate objects * Create different **views** to filter: one showing partners, another showing prospects **It's okay to have fields that don't apply to every record.** For example, a `Referral Link` field on People that only applies when `Person Type = Partner`. Hide this field from views where it's not relevant. ## Questions to Guide Your Choice Ask yourself: Is this just a property of something I already have, or does it need its own properties? Will I ever need to track multiple of these per record, without knowing how many? Does this concept connect to several different objects, not just one? Will it have its own lifecycle (stages, start/end dates)? If the answer is "yes" to one or more, it's probably time for a new object. ## Accessing Your Data Model 1. Go to **Settings** in the left sidebar 2. Click **Data Model** 3. View all your objects (standard and custom) 4. Click any object to see and edit its fields **Don't see Data Model in Settings?** Access to the data model is usually restricted to administrators. Contact your workspace admin if you need access. ## Next Steps Once you've planned your data model: * [How to Create Custom Objects](/user-guide/data-model/how-tos/create-custom-objects) * [How to Create Custom Fields](/user-guide/data-model/how-tos/create-custom-fields) * [How to Create Relation Fields](/user-guide/data-model/how-tos/create-relation-fields) ## Need Help? Want help designing your data model? [Find a certified Twenty partner](https://twenty.com/partners/list?categories=SOLUTIONING\&ref=docs-data-model) to design and build your objects, fields, and relationships. # User Guide Source: https://docs.twenty.com/user-guide/introduction Welcome to Twenty User Guide, your resources for advanced configurations and best practices. Data Model Customize your data model to fit your business processes. Data Migration Import and export your data via CSV or API. Calendar & Emails Centralize your team's meetings and emails. Workflows Automate processes and integrate with external tools. AI Enhance your team with AI agents. Layout Navigation, views, and record page customization. Dashboards Real-time insights to track performance. Permissions & Access Manage roles and access to Twenty. Billing Understand how Twenty pricing and billing works. Settings Configure your workspace preferences. # Navigation Source: https://docs.twenty.com/user-guide/layout/capabilities/navigation Customize the left sidebar to match how your team works. The left sidebar is your primary way to move around Twenty. It's fully customizable — you can reorganize it to match your workflow without touching any settings page. ## Reordering items Drag and drop any item in the sidebar to change its position. The order is saved per user, so each team member can arrange their own sidebar. ## Folders Group related items into folders. For example, you might create a "Sales" folder containing your pipeline views, a "Support" folder for tickets, or an "Operations" folder for internal objects. To create a folder, right-click in the sidebar or use the `+` button. ## Hiding objects Objects you don't use can be hidden from the sidebar. They're not deleted — they're just out of the way. You can show them again anytime from Settings → Data Model. ## Favorites Pin views, records, or searches to the Favorites section at the top of the sidebar for one-click access. Favorites are personal — each user manages their own. ## Custom links Add links to external tools directly in the sidebar. Useful for linking to your wiki, dashboards in other tools, or any URL your team uses regularly. ## Command menu Press `Cmd+K` (or `Ctrl+K`) to open the command menu — a quick-access search bar for jumping to any record, view, or action without navigating the sidebar. ## Customizing the sidebar To customize the sidebar, hover over the "Workspace" section in the sidebar and click the wrench icon. Navigation edit icon # Record Pages Source: https://docs.twenty.com/user-guide/layout/capabilities/record-pages Customize the layout of individual record detail pages with tabs and widgets. ## Overview When you open a record in Twenty, the detail page is composed of **tabs** and **widgets**. Both are fully customizable per object type. ## Tabs Each record page can have multiple tabs — similar to tabs in a browser. Use them to organize different aspects of a record. For example, a Company record might have tabs for Overview, Communication, Tasks, and Files. You can: * Add and remove tabs * Rename tabs * Reorder tabs by dragging * Set which tab shows by default ## Widgets Widgets are the building blocks inside each tab. Available widget types include: | Widget | What it shows | | ------------------- | ------------------------------------------ | | **Fields** | Record fields, grouped or individually | | **Related records** | Table of records linked via a relation | | **Emails** | Email history from connected accounts | | **Calendar** | Calendar events associated with the record | | **Timeline** | Activity and event history | | **Tasks** | Associated tasks | | **Notes** | Rich text notes | | **Files** | File attachments | | **Charts** | Visual data from related records | | **iFrame** | Embedded external content | | **Rich text** | Static content or descriptions | ## Customizing a record page 1. Open any record 2. Press `Cmd+K` and search for "Edit record page layout" or 1. Go to Settings → Data Model → object of your choice → Layout 2. Click the "Customize record page" button for that object 3. You're now in customization mode: * **Add widgets** from the widget picker * **Drag widgets** to reposition them on the grid * **Resize widgets** by dragging their edges * **Configure fields** shown within each widget * **Manage tabs** — add, remove, rename, reorder 4. Save your changes — they apply to all records of that object type ## Field visibility Within a Fields widget, you can control which fields are visible and in what order. This lets you create focused layouts — for example, showing only the most important fields on the Overview tab and putting detailed fields in a separate tab. # Layout Source: https://docs.twenty.com/user-guide/layout/overview Customize how you navigate, browse, and view records in Twenty. Twenty's layout is customizable at three levels: how you navigate the app, how you browse lists of records, and what you see when you open an individual record. ## Navigation The left sidebar is fully customizable. You can: * **Reorder items** by dragging and dropping * **Create folders** to group related objects and views * **Hide objects** you don't use * **Add custom links** to external tools * **Pin favorites** for quick access to views, records, or searches [Navigation reference →](/user-guide/layout/capabilities/navigation) ## Views Views control how lists of records are displayed. Twenty supports three view types: | View | Best for | | ------------ | ---------------------------------------------------------------------- | | **Table** | Working with many records at once — spreadsheet-style rows and columns | | **Kanban** | Pipeline tracking — drag-and-drop cards organized by stage | | **Calendar** | Time-based planning — records plotted by a date field | Each view saves its own filters, sorting, field visibility, and grouping configuration. Views can be shared with the workspace or kept private. [Table views →](/user-guide/views-pipelines/capabilities/table-views) · [Kanban views →](/user-guide/views-pipelines/capabilities/kanban-views) · [Calendar view →](/user-guide/views-pipelines/capabilities/calendar-view) ## Record pages When you open a record, the detail page is built from configurable tabs and widgets. You can: * **Add, remove, and reorder tabs** on any record type * **Configure widgets** — fields, related records, emails, timeline, calendar, tasks, notes, files, charts, iFrames, and more * **Drag and resize widgets** on a grid layout * **Control field visibility** per widget Enter layout customization mode from the command menu (`Cmd+K` → "Edit record page layout"). [Record pages reference →](/user-guide/layout/capabilities/record-pages) # Calendar View Source: https://docs.twenty.com/user-guide/views-pipelines/capabilities/calendar-view Display records with date fields on a calendar. ## About Calendar View Calendar view displays your records on a calendar based on a date field. Each record appears as an event on the corresponding date. ## Creating a Calendar View 1. Navigate to an object with date fields 2. Click the view dropdown → **+ Add view** 3. Name your view and click **Create** 4. Open the **Options** on the right 5. Select **Calendar** as the layout 6. Choose the **date field** to use for positioning records 7. Click **Update view** ## Configuring the Calendar ### Choose the Date Field Under **Options**, select which date field determines where records appear on the calendar. ### Display Fields Configure which fields show on each calendar event: 1. Click **Options → Fields** 2. Toggle fields on/off 3. Drag to reorder ## Use Cases * **Meetings and calls**: View upcoming appointments * **Deadlines**: Track due dates and close dates * **Events**: Plan and visualize scheduled activities * **Follow-ups**: See when tasks are due ## Related * [Views Overview](/user-guide/views-pipelines/overview) — creating and managing views * [Filters and Sorting](/user-guide/views-pipelines/capabilities/filters-and-sorting) — filtering calendar data # Fields & Columns Source: https://docs.twenty.com/user-guide/views-pipelines/capabilities/fields-and-columns Choose which fields to display and how to organize them. ## Selecting Fields to Display Each view can show a different set of fields. Customize what's visible to focus on the information that matters. ### Show or Hide Fields 1. Click **Options** in the top right 2. Click **Fields** 3. Click the **eye icon** next to each field to show/hide it ### Reorder Fields Change the order fields appear in your view: 1. Click **Options → Fields** 2. Drag fields up or down 3. Changes save automatically ## Field Display by View Type ### Table Views * Fields appear as columns * Resize columns by dragging borders ### Kanban Views * Fields appear on cards * Reorder via Options → Fields * Use Compact view to hide all fields ### Calendar Views * Selected fields show on calendar events * Configure via Options → Fields ## Best Practices * **Show only what's needed** — too many fields clutter the view * **Put important fields first** — most-used columns on the left * **Create multiple views** — different field sets for different purposes * **Use field visibility per view** — same object, different focus ## Related * [Table Views](/user-guide/views-pipelines/capabilities/table-views) — list view features * [Kanban Views](/user-guide/views-pipelines/capabilities/kanban-views) — card-based views # Filters & Sorting Source: https://docs.twenty.com/user-guide/views-pipelines/capabilities/filters-and-sorting Filter and sort records to find exactly what you need. ## Filtering Data Filters help you focus on specific records by showing only those that match your criteria. ### Adding a Filter 1. Click the **Filter** button in the toolbar 2. Select the field to filter by 3. Choose the operator (equals, contains, etc.) 4. Enter the filter value 5. Click **Apply** ### Filter Operators | Field Type | Available Operators | | ---------- | -------------------------------------------------- | | Text | Equals, Contains, Starts with, Ends with, Is empty | | Number | Equals, Greater than, Less than, Is empty | | Date | Equals, Before, After, Is empty | | Select | Equals, Is any of, Is empty | | Checkbox | Is true, Is false | | Relation | Equals, Is empty | ### Multiple Filters Combine multiple filters to narrow down results: * All filters are applied with AND logic * Each additional filter further restricts results ### Removing Filters * Click the **X** on individual filter chips * Click **Clear all** to remove all filters ## Sorting Data Sorting determines the order records appear. ### Adding a Sort 1. Click the **Sort** button in the toolbar 2. Select the field to sort by 3. Choose ascending (A-Z, 0-9) or descending (Z-A, 9-0) 4. Click **Apply** ### Multiple Sorts Add multiple sort levels: * First sort is primary * Subsequent sorts apply within groups of equal values ### Quick Column Sorting Click any column header to sort: * First click: Ascending * Second click: Descending * Third click: Remove sort ## Saving Filter and Sort Settings Filters and sorts are saved with the view: 1. Configure your filters and sorts 2. Click **Save** to update the current view 3. Or click **Save as new view** to create a variant ## Related * [Table Views](/user-guide/views-pipelines/capabilities/table-views) — group by feature * [Views Overview](/user-guide/views-pipelines/overview) — building and managing views # Kanban Board Views Source: https://docs.twenty.com/user-guide/views-pipelines/capabilities/kanban-views Learn how to use Kanban views to visualize and manage your workflows. ## About Kanban Views Kanban views visually map out process flows, where each column stands for a distinct stage and each card represents a record. ## Move Cards between Stages You can move each card between stages as it goes through your workflow by clicking and holding a card, then dragging it to the next stage. ## Add and Delete Stages You can tailor your workflow to suit your needs using stages, each of which represents a value in a Select field: ### Add Stages To add a stage, access the Select field settings by navigating to Settings > Data Model, selecting your object, and then selecting the field your Kanban board depends on. ### Remove Stages To remove a stage, hover over the stage name or the `⋮` icon, click `Edit from settings` in the Select field settings, and then click **Delete** next to the relevant stage. ## Display Fields You can configure your Kanban board to display some fields and hide others. To show or hide a field, click on **Options** on the top right, then on **Fields** to bring up the list of options, and click the eye icon next to the field to show or hide it. You can also rearrange the order of fields by holding down the field name and dragging it to where you want it. ## Compact view You can hide all the fields and get an overview of all records at a glance. To enable: 1. Click **Options** on the top right 2. Turn on the toggle for **Compact view** ## Column Aggregations Each column in a Kanban view can display aggregated values at the top, helping you understand your data at a glance. ### Available Aggregations | Aggregation | Description | | ----------- | --------------------------------------------- | | **Count** | Number of records in the column | | **Sum** | Total of a numeric field (e.g., deal amounts) | | **Average** | Average value of a numeric field | | **Min** | Lowest value | | **Max** | Highest value | ### Configuring Aggregations 1. Click on the number displayed next to the Stage value, at the top of a column 2. Select the aggregation type 3. Choose the field to aggregate **Example:** Show total deal value per stage by aggregating the Amount field with Sum. ## When to Use Kanban Views Kanban views are ideal for: * **Sales pipelines**: Track deals through stages from lead to close * **Project management**: Monitor tasks through workflow states * **Recruitment**: Track candidates through hiring stages * **Any staged process**: Visualize any workflow with defined stages ## Best Practices ### Organize Your Stages * **Limit stages**: 5-7 stages is ideal for visibility * **Clear naming**: Use descriptive stage names * **Logical order**: Arrange stages in process order ### Optimize Card Display * **Show key fields**: Display only the most important information * **Use Compact view**: For high-level overviews * **Color coding**: Use stage colors to quickly identify status ### Maintain Data Quality * **Update regularly**: Keep cards moving through stages * **Archive completed**: Move closed items out of active view * **Review stale cards**: Follow up on cards stuck in stages # Table Views Source: https://docs.twenty.com/user-guide/views-pipelines/capabilities/table-views Display your data in a spreadsheet-like list format. ## About Table Views Table views display records in rows with customizable columns—like a spreadsheet. This is the default view type for most objects. ## Features ### Column Configuration * Show or hide columns (fields) * Resize column widths * Reorder columns by dragging ### Group By a Select Field Organize records into collapsible groups based on a field of select type. 1. Click **Options** 2. Select **Group** 3. Choose a Select field 4. Configure group order under **Options → Group → Sort**: * **Alphabetical** or **Reverse alphabetical** * **Manual order**: Drag groups under "Visible groups" to reorder * Click the **eye icon** next to a group to hide it **Use cases:** * Group Companies by Type * Group Opportunities by Stage * Group Tasks by Status **For best performance, limit to 10-15 visible groups per view.** If you need more groups, consider using a Dashboard instead. ### Column Widths Resize columns to show more or less content: 1. Hover between two column headers 2. Click and drag the column border 3. Release to set the new width ## When to Use Table Views Table views work best for: * **Browsing large datasets** — scan many records quickly * **Data entry** — edit multiple records efficiently * **Detailed analysis** — see many fields at once * **Sorting and filtering** — find specific records ## Related * [Fields and Columns](/user-guide/views-pipelines/capabilities/fields-and-columns) — configuring which fields to display * [Filters and Sorting](/user-guide/views-pipelines/capabilities/filters-and-sorting) — narrowing down records # View Settings Source: https://docs.twenty.com/user-guide/views-pipelines/capabilities/view-settings Manage view visibility, naming, icons, and organization. ## View Visibility Control who can see your custom views. ### Visibility Options | Setting | Who Can See | | ------------- | --------------------- | | **Workspace** | All workspace members | | **Unlisted** | Only you | ### Changing Visibility 1. Open the view 2. Click **Options → Visibility** 3. Select **Workspace** or **Unlisted** The default "All \[Object Name]" views cannot have their visibility changed. ## Rename a View 1. Open the view dropdown 2. Click the **⋮** menu next to the view 3. Select **Edit** 4. Enter the new name ## Change View Icon 1. Open the view dropdown 2. Click the **⋮** menu next to the view 3. Select **Edit** 4. Click the icon to change it ## Reorder Views Change the order views appear in the dropdown: 1. Open the view dropdown 2. Drag views by their handle 3. Drop in the desired position 4. Order saves automatically ## Favorite a View Pin frequently used views for quick access: 1. Open the view dropdown 2. Click the **⋮** menu next to a view 3. Select **Add to favorites** Favorited views appear in a dedicated section for easy access. ## Delete a View 1. Open the view dropdown 2. Click the **⋮** menu next to the view 3. Select **Delete** 4. Confirm deletion Deleted views cannot be recovered. ## Related * [Views Overview](/user-guide/views-pipelines/overview) — creating views * [How to Restrict Access](/user-guide/views-pipelines/how-tos/restrict-access-to-your-view) — step-by-step guide # Create a Calendar View for Tasks Due Source: https://docs.twenty.com/user-guide/views-pipelines/how-tos/create-a-calendar-view-for-tasks-due Visualize your tasks and deadlines on a calendar. ## Prerequisites Your Tasks object needs a **Due Date** field (Date or Date & Time type). ## Steps 1. Navigate to **Tasks** 2. Click the view dropdown → **+ Add view** 3. Name your view (e.g., "Tasks Calendar") 4. Click **Create** 5. Click **Options** and select **Calendar** as the layout 6. Choose **Due Date** as the date field 7. Click **Save** ## Configure Your Calendar ### Display Fields on Events 1. Click **Options → Fields** 2. Click the **eye icon** to show/hide fields 3. Drag to reorder Recommended fields to display: * **Title** — task name * **Assignee** — who's responsible * **Status** — current progress ### Filter Your Calendar Create focused views: * **My Tasks**: Filter by Assignee = Me * **This Week**: Filter by Due Date = This week * **Overdue**: Filter by Due Date \< Today, Status ≠ Done ## Other Calendar Use Cases | Object | Date Field | Purpose | | ------------- | ---------- | ------------------------- | | Opportunities | Close Date | Track expected closes | | Custom Events | Event Date | Plan activities | | Projects | Deadline | Monitor project timelines | ## Tips * **Review weekly**: Start each week by checking your calendar view * **Combine with table view**: Use calendar for overview, table for details * **Set visibility**: Keep personal task calendars as Unlisted ## Related * [Calendar View](/user-guide/views-pipelines/capabilities/calendar-view) — all calendar features * [Filters and Sorting](/user-guide/views-pipelines/capabilities/filters-and-sorting) — filter your calendar # Create a Kanban View for Projects Source: https://docs.twenty.com/user-guide/views-pipelines/how-tos/create-a-kanban-view-for-projects Track projects through stages using a visual board. Use a Kanban view to visualize your projects (or any object with stages) as cards moving through columns. ## Prerequisites Your object needs a **Select field** to use as columns (e.g., Status, Stage, Phase). If you don't have one: 1. Go to **Settings → Data Model** 2. Select your object 3. Add a Select field with your stage options ## Steps 1. Navigate to your object (e.g., Projects, Tasks) 2. Click the view dropdown → **+ Add view** 3. Name your view (e.g., "Project Board") 4. Click **Create** 5. Click **Options** and select **Kanban** as the layout 6. The view uses your Select field for columns automatically 7. Click **Save** ## Configure Your Board ### Show Key Fields on Cards 1. Click **Options → Fields** 2. Find fields in the "Hidden Fields" section 3. Click the **eye icon** to display them on cards 4. Drag to reorder ### Enable compact view For a high-level overview: 1. Click **Options** 2. Turn on **Compact view** Cards show only the record name. ### Add Aggregations Show counts or totals at the top of each column: 1. Click the number next to a column name 2. Select an aggregation (Count, Sum, etc.) 3. Choose a field if needed ## Moving Cards Drag and drop cards between columns to update their status. ## Example: Task Board | Column (Status) | Cards | | --------------- | ----------------- | | **To Do** | New tasks | | **In Progress** | Active work | | **Review** | Awaiting approval | | **Done** | Completed | ## Related * [Kanban Views](/user-guide/views-pipelines/capabilities/kanban-views) — aggregations, compact view, stages * [How to Set Up a Sales Pipeline](/user-guide/views-pipelines/how-tos/set-up-a-sales-pipeline) — Kanban for Opportunities # Create a Table View with Grouping Source: https://docs.twenty.com/user-guide/views-pipelines/how-tos/create-a-table-view-with-grouping Organize your records into collapsible groups by field value. Group your table view by a Select field to organize records into collapsible sections. ## Steps 1. Navigate to the object (People, Companies, etc.) 2. Click the view dropdown → **+ Add view** 3. Name your view (e.g., "Companies by Type") 4. Click **Create** 5. Click **Options → Group** 6. Choose a Select field to group by 7. Click **Save** ## Configure Group Order Under **Options → Group → Sort**, choose how groups are ordered: | Option | Description | | ------------------------ | --------------------------------------------- | | **Alphabetical** | A to Z | | **Reverse alphabetical** | Z to A | | **Manual order** | Drag groups to reorder under "Visible groups" | Click the **eye icon** next to a group to hide it from the view. **For best performance, limit to 10-15 visible groups.** If you need more, consider using a Dashboard instead. ## Example: Companies by Industry 1. Go to **Companies** 2. Create a new view named "By Industry" 3. Click **Options → Group** 4. Select the **Industry** field 5. Save Now your companies are organized by industry, making it easy to focus on one segment at a time. ## Related * [Table Views](/user-guide/views-pipelines/capabilities/table-views) — all table view features * [Filters and Sorting](/user-guide/views-pipelines/capabilities/filters-and-sorting) — combine grouping with filters # Restrict Access to Your View Source: https://docs.twenty.com/user-guide/views-pipelines/how-tos/restrict-access-to-your-view Control who can see your custom views. Each view (except the default "All \[Object Name]" views) has its own visibility setting. ## Steps 1. Open the view you want to restrict 2. Click **Options** in the top right 3. Click **Visibility** 4. Select **Unlisted** Your view is now visible only to you. ## Visibility Options | Setting | Who Can See | | ------------- | --------------------- | | **Workspace** | All workspace members | | **Unlisted** | Only you | ## Notes * The default "All \[Object Name]" views cannot be made unlisted * Unlisted views don't appear in other users' view dropdowns * You can change visibility back to Workspace at any time ## Related * [View Settings](/user-guide/views-pipelines/capabilities/view-settings) — all view configuration options # Set Up a Sales Pipeline Source: https://docs.twenty.com/user-guide/views-pipelines/how-tos/set-up-a-sales-pipeline Configure your sales pipeline to track opportunities through stages. A sales pipeline in Twenty is a Kanban view of your Opportunities object, where each column represents a stage in your sales process. ## Step 1: Configure Your Stages Stages are defined in the Opportunities object's **Stage** field. 1. Go to **Settings → Data Model** 2. Select **Opportunities** 3. Find and click the **Stage** field 4. Add, remove, or rename stages to match your process ### Recommended Stages | Stage | Purpose | | --------------- | ----------------------------------- | | **New** | Fresh opportunities just identified | | **Qualified** | Confirmed as a good fit | | **Meeting** | Engaged in discussions | | **Proposal** | Proposal sent | | **Negotiation** | Working on terms | | **Closed Won** | Deal successful | | **Closed Lost** | Deal unsuccessful | **5-7 stages is optimal.** Too many stages make the pipeline hard to scan; too few lose visibility into deal progress. ## Step 2: Create a Pipeline View 1. Go to **Opportunities** 2. Click the view dropdown → **+ Add view** 3. Name it "Sales Pipeline" 4. Click **Create** 5. Open **Options** and select **Kanban** as the layout The view automatically uses the Stage field for columns. ## Step 3: Configure Your View ### Show Key Fields 1. Click **Options → Fields** 2. Look for fields in the "Hidden Fields" section 3. Click the **eye icon** to display: Company, Amount, Close Date, Owner ### Enable Aggregations Show totals at the top of each column: 1. Click the number displayed next to a Stage name at the top of a column 2. Select the aggregation type (Count, Sum, Average, etc.) 3. Choose the field to aggregate (e.g., Amount) **Example:** Show total deal value per stage by aggregating the Amount field with Sum. ### Use compact view (optional) For a high-level overview with minimal card content: 1. Click **Options** 2. Turn on the toggle for **Compact view** ## Step 4: Create Personal and Team Views ### "My Pipeline" * **Filter**: Owner = Me * **Visibility**: Unlisted (personal view) ### "Team Pipeline" * **Filter**: None (show all) * **Visibility**: Workspace (shared view) ### "Closing This Month" * **Type**: Table * **Filter**: Close Date = This month, Stage ≠ Closed Won, Stage ≠ Closed Lost * **Sort**: Close Date ascending ## Working with Opportunities ### Creating Opportunities * Click **+ New** in the Opportunities view * Or click **+** in a specific stage column ### Moving Through Stages Drag and drop opportunity cards between columns to update their stage. ## Best Practices ### Pipeline Hygiene * Update deals daily as they progress * Move or close stale deals promptly * Keep close dates realistic ### Stage Discipline * Define clear criteria for each stage * Move deals promptly when criteria are met * Don't let deals sit in stages too long ## Related * [Kanban Views](/user-guide/views-pipelines/capabilities/kanban-views) — aggregations and compact view * [Filters and Sorting](/user-guide/views-pipelines/capabilities/filters-and-sorting) — creating filtered views # Show Expected Amount in Your Pipeline Source: https://docs.twenty.com/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline Calculate and display weighted deal values based on stage probability. The Expected Amount is a calculated value: **Amount × Probability**. This helps you forecast revenue by weighting deals based on how likely they are to close. This is an example of creating [Formula Fields](/user-guide/workflows/how-tos/crm-automations/formula-fields) using workflows. This guide walks you through setting up the custom fields and workflows needed to calculate and display expected amounts in your pipeline. ## Step 1: Create Custom Fields You need two custom fields on the Opportunities object. ### Create the Probability Field 1. Go to **Settings → Data Model → Opportunities** 2. Click **+ Add Field** 3. Configure: * **Name**: Probability * **Type**: Number * **Description**: Stage-based probability (0-100%) 4. Click **Save** ### Create the Expected Amount Field 1. Click **+ Add Field** 2. Configure: * **Name**: Expected Amount * **Type**: Currency * **Description**: Calculated: Amount × Probability 3. Click **Save** ### Optional: Make Fields Read-Only for Users If you don't want users manually editing these calculated fields: 1. Go to **Settings → Members → Roles** 2. Select the role to configure 3. Find the Opportunities object 4. Set **Probability** and **Expected Amount** fields to read-only This ensures only the workflows can update these values. ## Step 2: Create Workflow #1 — Update Probability on Stage Change This workflow automatically sets the Probability when an opportunity moves to a new stage. ### Create the Workflow 1. Go to **Workflows** 2. Click **+ New Workflow** 3. Name it "Update Probability on Stage Change" ### Configure the Trigger 1. Add a **Record Created or Updated** trigger 2. Select **Opportunities** as the object 3. Filter on: **Stage** field is updated ### Add Branches for Each Stage Create a branch for each stage with its probability: | Stage | Probability | | ----------- | ----------- | | New | 10% | | Qualified | 25% | | Meeting | 40% | | Proposal | 60% | | Negotiation | 80% | | Closed Won | 100% | | Closed Lost | 0% | To create a new branch, right click on the workflow canvas and click **New action**. Then, link this action to the previous node by dragging the arrow from the previous node to this new action. For each stage: 1. Add a **Filter** node: Stage = \[stage name] 2. Add an **Update Record** action: * Record: The triggering Opportunity * Field: Probability * Value: \[probability for that stage] ### Calculate Expected Amount After the branches rejoin: 1. Add a **Filter** node: Amount is not empty 2. Add an **Update Record** action: * Record: The triggering Opportunity * Field: Expected Amount * Value: Amount × Probability ## Step 3: Create Workflow #2 — Recalculate on Amount Change This workflow updates the Expected Amount when the deal Amount changes. ### Create the Workflow 1. Go to **Workflows** 2. Click **+ New Workflow** 3. Name it "Recalculate Expected Amount on Amount Change" ### Configure the Trigger 1. Add a **Record Created or Updated** trigger 2. Select **Opportunities** as the object 3. Filter on: **Amount** field is updated ### Add the Logic 1. Add a **Filter** node: Amount is not empty 2. Add an **Update Record** action: * Record: The triggering Opportunity * Field: Expected Amount * Value: Amount × Probability ## Step 4: Display in Your Pipeline Now show the Expected Amount totals in your Kanban view: 1. Open your **Sales Pipeline** Kanban view 2. Click the **number** next to any Stage name at the top of a column 3. Select **Sum** 4. Choose **Expected Amount** Each column now shows the total weighted pipeline value for that stage. ## Summary | Component | Purpose | | ------------------------- | ------------------------------------------------------------------------- | | **Probability field** | Stores the stage-based win probability | | **Expected Amount field** | Stores Amount × Probability | | **Workflow #1** | Updates Probability when Stage changes, then recalculates Expected Amount | | **Workflow #2** | Recalculates Expected Amount when Amount changes | | **Aggregation** | Displays Sum of Expected Amount per stage | ## Related * [Formula Fields](/user-guide/workflows/how-tos/crm-automations/formula-fields) — create calculated fields using workflows * [Kanban Views](/user-guide/views-pipelines/capabilities/kanban-views) — column aggregations * [How to Create Custom Fields](/user-guide/data-model/how-tos/create-custom-fields) — field configuration # Track How Long Opportunities Stay in Each Stage Source: https://docs.twenty.com/user-guide/views-pipelines/how-tos/track-time-in-stage Monitor deal velocity by tracking when opportunities enter each stage. This is an example of creating [Formula Fields](/user-guide/workflows/how-tos/crm-automations/formula-fields) using workflows — specifically date calculations. Tracking when opportunities enter each stage helps you identify bottlenecks and measure deal velocity. This guide walks you through setting up custom fields and a workflow to automatically record when an opportunity moves to each stage, and calculate how many days it spent in the previous stage. ## Step 1: Create Custom Fields You need two types of fields for each stage: * **Date & Time fields**: Record when the opportunity entered each stage * **Number fields**: Store how many days the opportunity spent in each stage ### Create the "Last Entered" Fields 1. Go to **Settings → Data Model → Opportunities** 2. For each stage, click **+ Add Field** and configure: * **Name**: Last Entered \[Stage Name] (e.g., "Last Entered New", "Last Entered Qualified") * **Type**: Date & Time * **Description**: Timestamp when opportunity entered this stage 3. Click **Save** Create these fields: * Last Entered New * Last Entered Qualified * Last Entered Meeting * Last Entered Proposal * Last Entered Negotiation * Last Entered Closed Won * Last Entered Closed Lost ### Create the "Days in Stage" Fields 1. For each stage, click **+ Add Field** and configure: * **Name**: Days in \[Stage Name] (e.g., "Days in New", "Days in Qualified") * **Type**: Number * **Description**: Number of days spent in this stage 2. Click **Save** Create these fields: * Days in New * Days in Qualified * Days in Meeting * Days in Proposal * Days in Negotiation You don't need "Days in" fields for Closed Won and Closed Lost since those are final stages. ### Optional: Make Fields Read-Only If you don't want users manually editing these calculated fields: 1. Go to **Settings → Members → Roles** 2. Select the role to configure 3. Find the Opportunities object 4. Set the "Last Entered" and "Days in" fields to read-only ## Step 2: Create the Workflow This single workflow handles both tasks: * Records the timestamp when entering a new stage * Calculates days spent in the previous stage ### Create the Workflow 1. Go to **Workflows** 2. Click **+ New Workflow** 3. Name it "Track Stage Time" ### Configure the Trigger 1. Add a **Record Updated** trigger 2. Select **Opportunities** as the object 3. Filter on: **Stage** field is updated ### Add Branches for Each Stage To create a new branch, right click on the workflow canvas and click **New action**. Then, link this action to the previous node by dragging the arrow from the previous node to this new action. *** **Branch 1: Stage = New (first stage)** Since this is the first stage, only the entry timestamp is recorded — there's no previous stage to calculate from. 1. Add a **Filter** node: Stage = New 2. Add a **Code** action: ```javascript theme={null} export const main = async (): Promise => { return { now: new Date().toISOString() }; }; ``` 3. Add an **Update Record** action: * Record: The triggering Opportunity * Field: Last Entered New * Value: `now` from Code node *** **Branch 2: Stage = Qualified** When moving to Qualified, record entry time AND calculate days spent in New. 1. Add a **Filter** node: Stage = Qualified 2. Add a **Code** action: ```javascript theme={null} export const main = async (params: { lastEnteredPreviousStage: Date; }): Promise => { const { lastEnteredPreviousStage } = params; const now = new Date(); const entryDate = new Date(lastEnteredPreviousStage); const diffTime = Math.abs(now.getTime() - entryDate.getTime()); const daysInPreviousStage = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return { now: now.toISOString(), daysInPreviousStage: daysInPreviousStage }; }; ``` 3. Configure the Code node input: map `lastEnteredPreviousStage` to the **Last Entered New** field 4. Add an **Update Record** action: * Record: The triggering Opportunity * Fields to update: * Last Entered Qualified = `now` * Days in New = `daysInPreviousStage` *** **Branch 3: Stage = Meeting** When moving to Meeting, record entry time AND calculate days spent in Qualified. 1. Add a **Filter** node: Stage = Meeting 2. Add a **Code** action: ```javascript theme={null} export const main = async (params: { lastEnteredPreviousStage: Date; }): Promise => { const { lastEnteredPreviousStage } = params; const now = new Date(); const entryDate = new Date(lastEnteredPreviousStage); const diffTime = Math.abs(now.getTime() - entryDate.getTime()); const daysInPreviousStage = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return { now: now.toISOString(), daysInPreviousStage: daysInPreviousStage }; }; ``` 3. Configure the Code node input: map `lastEnteredPreviousStage` to the **Last Entered Qualified** field 4. Add an **Update Record** action: * Record: The triggering Opportunity * Fields to update: * Last Entered Meeting = `now` * Days in Qualified = `daysInPreviousStage` *** **Continue for remaining stages:** | Stage | Records | Calculates | | ----------- | ------------------------ | ------------------- | | Proposal | Last Entered Proposal | Days in Meeting | | Negotiation | Last Entered Negotiation | Days in Proposal | | Closed Won | Last Entered Closed Won | Days in Negotiation | | Closed Lost | Last Entered Closed Lost | Days in Negotiation | The branches do not need to rejoin—each runs independently when its stage condition is met. ## Step 3: Analyze Time in Stage With timestamps and day counts recorded, you can now analyze deal velocity. ### Create a "Slow Deals" View 1. Create a Table view of Opportunities 2. Add columns: Name, Stage, Days in \[previous stage], Amount 3. Sort by "Days in" field (descending) 4. Filter by Stage to focus on one stage at a time Deals at the top spent the most time in the previous stage. ### Use Aggregations In your pipeline Kanban view: 1. Click the number next to a Stage name 2. Select **Average** 3. Choose a "Days in" field This shows the average time deals spend in each stage. ## Summary | Component | Purpose | | ------------------------ | ------------------------------------------------- | | **Last Entered fields** | Store when opportunity entered each stage | | **Days in fields** | Store how many days spent in each stage | | **Workflow** | Records timestamp AND calculates days in one pass | | **Views & Aggregations** | Analyze deal velocity and identify bottlenecks | ## Related * [Workflows](/user-guide/workflows/overview) — automation basics * [How to Create Custom Fields](/user-guide/data-model/how-tos/create-custom-fields) — field configuration * [Kanban Views](/user-guide/views-pipelines/capabilities/kanban-views) — aggregations # Send Emails from Workflows Source: https://docs.twenty.com/user-guide/workflows/capabilities/send-emails-from-workflows Send personalized emails automatically using workflow actions. Automatically send emails when specific events occur in your CRM—welcome new contacts, follow up on opportunities, or notify team members. ## Prerequisites Before you can send emails from workflows: 1. Connect an email account under **Settings → Accounts** 2. Ensure the account has sending permissions enabled ## Basic Email Workflow ### Example: Welcome Email for New Contacts **Goal**: Send a welcome email when a new person is added to the CRM. **Setup**: 1. **Create workflow**: Go to **Settings → Workflows** and click **+ New Workflow** 2. **Add trigger**: Select **Record is Created** → **People** 3. **Add Send Email action**: * Click **+** to add an action * Select **Send Email** * Configure the email: | Field | Value | | ----------- | -------------------------------------- | | **To** | `{{trigger.object.email}}` | | **Subject** | `Welcome to {{Your Company Name}}` | | **Body** | `Hi {{trigger.object.firstName}}, ...` | 4. **Test and activate**: Test with a sample record, then activate ## Using Variables in Emails Reference data from previous steps using `{{variable}}` syntax: ```text theme={null} Hi {{trigger.object.firstName}}, Thank you for connecting with us! Your company, {{trigger.object.company.name}}, is now in our system. Best regards, The Team ``` ### Available Variables from Triggers | Trigger Type | Common Variables | | -------------------------- | -------------------------------------- | | **Record Created/Updated** | `{{trigger.object.fieldName}}` | | **Manual** | `{{trigger.selectedRecord.fieldName}}` | | **Webhook** | `{{trigger.body.fieldName}}` | ## Advanced: Conditional Emails ### Example: Different Emails Based on Lead Source **Goal**: Send different welcome emails based on where the lead came from. **Setup**: 1. **Trigger**: Record is Created (People) 2. **Add Filter action**: * Condition: `{{trigger.object.source}}` equals `"Website"` * If true → continue to website welcome email 3. **Branch for other sources**: * Create parallel branches for different sources * Each branch has its own Send Email action ## Sending Emails to Multiple Recipients ### Example: Notify Team When Deal Closes **Goal**: Email the sales rep and their manager when an opportunity is won. **Setup**: 1. **Trigger**: Record is Updated (Opportunities, Stage = "Closed Won") 2. **Search Records**: Find the opportunity owner's manager 3. **Send Email #1**: To opportunity owner * To: `{{trigger.object.owner.email}}` * Subject: `Congratulations on closing {{trigger.object.name}}!` 4. **Send Email #2**: To manager * To: `{{searchRecords.manager.email}}` * Subject: `Deal Won: {{trigger.object.name}}` ## Scheduled Follow-up Emails ### Example: Follow Up 3 Days After Meeting **Goal**: Send a follow-up email 3 days after a meeting is logged. **Setup**: 1. **Trigger**: Record is Created (Activities, Type = "Meeting") 2. **Delay action**: Wait 3 days 3. **Send Email**: * To: Meeting attendee * Subject: Following up on our conversation * Body: Reference meeting details from trigger ## Email Attachments You can attach files to emails sent from workflows. The attachment is a static file that you upload when configuring the workflow. ### How to Add an Attachment 1. In the **Send Email** action, click on the **Attachments** button 2. Click to upload a file from your computer 3. The file will be attached to all emails sent by this action ### Common Use Cases | Use Case | Example File | | ------------------------ | ----------------------------- | | **Welcome emails** | Company brochure (PDF) | | **Onboarding sequences** | Getting started guide | | **Sales follow-ups** | Product catalog or price list | | **Event confirmations** | Event details or agenda | Attachments are static—the same file is sent to all recipients. For dynamic documents (like personalized quotes), generate and attach files using a [Logic Function](/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty). ## Best Practices ### Email Content * Keep subject lines concise and relevant * Personalize with recipient's name * Include a clear call to action * Test emails before activating ### Deliverability * Don't send too many emails too quickly * Use professional email signatures * Avoid spam trigger words * Ensure unsubscribe options for marketing emails ### Troubleshooting * Verify email account is connected and active * Check recipient email address is valid * Review workflow runs for error messages * Test with your own email address first ## Related * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) # Use Branches in Workflows Source: https://docs.twenty.com/user-guide/workflows/capabilities/use-branches-in-workflows Understand how branches work and how to control which path is executed. ## How Branches Work In the workflow editor, you can create multiple paths (branches) going out from a single node. This allows you to build complex automations with different outcomes. **Important**: When a workflow runs, **all branches execute in parallel by default**. There is no built-in "if/else" logic to choose one branch over another—every path will run simultaneously. ## Controlling Which Branch Runs To execute only one branch based on specific conditions, **add a Filter node at the beginning of each branch**. ### Example Setup 1. Create your workflow with multiple branches from a single node 2. Add a **Filter** node as the first step in each branch 3. Set conditions on each Filter to determine when that branch should continue 4. Only the branch(es) whose Filter conditions are met will proceed ### How Filters Work * If the Filter condition is **met**: The branch continues executing * If the Filter condition is **not met**: The branch stops at the Filter node This effectively creates conditional logic where only the appropriate branch runs based on your data. ## Example: Route by Deal Size **Scenario**: When a deal is closed, send different notifications based on deal size. 1. **Trigger**: Opportunity updated (Stage = Closed Won) 2. **Branch 1**: Filter for Amount > \$10,000 → Send Slack message to #big-deals 3. **Branch 2**: Filter for Amount ≤ \$10,000 → Send email to sales manager Both branches start, but only the one matching the deal amount will continue past its Filter. ## Creating Branches To create a new branch from an existing step, click the **+** button on the step and add your action. You can add multiple branches by clicking **+** multiple times. 1. In the workflow editor, select the step you want to branch from 2. Click the **+** button to add an action 3. This creates one branch 4. Click **+** again on the same step to create additional branches 5. Each branch can have its own sequence of actions ## Merging Branches Back Together After parallel branches complete their work, you can merge them back into a single path: 1. Complete your branched actions 2. Add a new step that should run after all branches 3. Drag a connection from the last step of each branch to this new step 4. The merged step waits for all connected branches to complete before executing ### Example: Process Then Notify ``` Trigger │ ├── Branch A: Update Customer Record │ └── Branch B: Create Support Ticket ↘ ↙ Merged Step: Send Confirmation Email ``` The confirmation email is sent only after both the customer update and ticket creation are done. ## Best Practices * Always use **Filter nodes** at the start of branches when you want conditional execution * Keep branch conditions **mutually exclusive** to avoid duplicate actions * Test your workflows with different data to ensure the correct branches run * **Rename branch steps** descriptively so it's clear what each path does * **Merge branches** when you need a final action after parallel processing ## Related * [Workflows FAQ](/user-guide/workflows/how-tos/need-more-help/workflows-faq) — answers about parallel execution * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) — available actions for branches # Use Iterator Source: https://docs.twenty.com/user-guide/workflows/capabilities/use-iterator Loop through arrays of records to perform actions on each item. Iterator lets you loop through an array of records and perform actions on each one. It's essential for workflows that need to process multiple records returned by Search Records or received via webhooks. ## When to Use Iterator | Scenario | Example | | -------------------------- | ---------------------------------------------- | | **Process search results** | Send email to each person found | | **Handle webhook arrays** | Create records for each item in order | | **Bulk updates** | Update multiple records with calculated values | | **Notifications** | Alert multiple people about an event | ## Understanding Iterator Iterator expects an **array** as input. It then: 1. Takes the first item from the array 2. Runs all actions inside the Iterator with that item 3. Moves to the next item 4. Repeats until all items are processed ### Selecting the array to loop over Most array sources (Search Records, a webhook array field, a Bulk manual trigger) can be selected directly in the variable picker. When a **Code** or **Logic Function** step returns a *top-level array*, its output appears in the variable picker as indexed entries (`0`, `1`, `2`, …). To loop over the array as a whole, select the **Whole list** option for that step — the Iterator then infers the shape of each item from the list automatically. ## Basic Setup ### Example: Email Everyone in Search Results **Goal**: Find all contacts in a specific company and send each one a personalized email. ### Step 1: Search for Records 1. Add **Search Records** action 2. Object: **People** 3. Filter: Company equals "Acme Inc" 4. This returns an array of people ### Step 2: Check Results Exist 1. Add **Filter** action 2. Condition: `{{searchRecords.length}}` is greater than 0 3. This prevents Iterator errors on empty results ### Step 3: Add Iterator 1. Add **Iterator** action 2. Array input: Select `{{searchRecords}}` 3. This creates a loop ### Step 4: Add Actions Inside Iterator Actions placed after Iterator run for each item: 1. Add **Send Email** action (inside Iterator) 2. To: `{{iterator.currentItem.email}}` 3. Subject: Hello `{{iterator.currentItem.firstName}}`! 4. Body: Personalized message using current item fields ### Result If Search Records returns 5 people, the Iterator: * Sends email to person 1 * Sends email to person 2 * ... continues for all 5 ## Accessing Current Item Data Inside Iterator, use `{{iterator.currentItem}}` to access the current record: | Variable | Description | | --------------------------------------- | ----------------------------------- | | `{{iterator.currentItem}}` | The entire current record object | | `{{iterator.currentItem.id}}` | Record ID | | `{{iterator.currentItem.email}}` | Email field | | `{{iterator.currentItem.company.name}}` | Related company name | | `{{iterator.index}}` | Current position in array (0-based) | In the variable picker, you can either drill into a specific field of the current item, or pick **Use the whole item** to reference the entire current item (`{{iterator.currentItem}}`). Selecting the whole item is handy when you want to pass a full record straight into a downstream step rather than rebuilding it field by field. ## Common Patterns ### Update Multiple Records **Goal**: Mark all overdue tasks as "Late" ``` 1. Search Records (Tasks, Due Date < Today, Status ≠ Completed) 2. Filter (length > 0) 3. Iterator (searchRecords) └── Update Record - Object: Tasks - Record: {{iterator.currentItem.id}} - Status: Late ``` ### Create Records from Array **Goal**: Webhook receives order with multiple items, create a record for each ``` 1. Webhook Trigger (receives items array) 2. Filter (items.length > 0) 3. Iterator (trigger.body.items) └── Create Record - Object: Order Items - Name: {{iterator.currentItem.name}} - Quantity: {{iterator.currentItem.qty}} - Related Order: {{trigger.body.orderId}} ``` ### Conditional Processing Inside Loop **Goal**: Only send email to contacts with valid emails ``` 1. Search Records (People) 2. Iterator (searchRecords) └── Filter (currentItem.email is not empty) └── Send Email - To: {{iterator.currentItem.email}} ``` ## Troubleshooting ### "Iterator expects an array" **Cause**: You passed a single record instead of an array. **Fix**: Make sure you're passing the result of Search Records or an array field, not a single record. ``` ✅ Correct: {{searchRecords}} ❌ Wrong: {{searchRecords[0]}} ``` ### Iterator Doesn't Run **Cause**: The array is empty. **Fix**: Add a Filter before Iterator to check array length: ``` Filter: {{searchRecords.length}} > 0 ``` ### Actions Run Too Many Times **Cause**: Search Records returned more records than expected. **Fix**: * Add more specific filters to Search Records * Set a limit on Search Records (max 200) * Add Filter inside Iterator for additional conditions ## Performance Considerations * **Credit usage**: Each iteration consumes credits for its actions * **Time**: Large arrays take longer to process * **Limits**: Consider batching very large operations * **Rate limits**: External API calls may hit rate limits with many iterations ## Best Practices 1. **Always check array length** before Iterator to avoid errors 2. **Add filters inside loops** when not all items need processing 3. **Rename your Iterator step** to describe what it's looping through 4. **Test with small arrays** before processing large datasets 5. **Monitor workflow runs** to ensure iterations complete as expected ## Related * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [How to Use Branches](/user-guide/workflows/capabilities/use-branches-in-workflows) * [Workflows FAQ](/user-guide/workflows/how-tos/need-more-help/workflows-faq) # Workflow Actions Source: https://docs.twenty.com/user-guide/workflows/capabilities/workflow-actions Learn about the actions available in Twenty workflows. ## About Actions Actions define what happens after a trigger fires. You can chain multiple actions together to build complex automations. * Use the variable picker (click the `(x+)` icon) to browse available data from previous steps * Hover over any input field to see which step a variable comes from — helpful when the same field (e.g., ID) exists in multiple previous steps * Give each action a descriptive name for easier maintenance ## Record Actions ### Create Record Adds a new record to a selected object. **Configuration**: * Select the target object * Fill out the required and optional fields * Use data from previous steps or input values manually to populate fields **Output**: The newly created record data is available for use in subsequent steps. ### Update Record Modifies an existing record in a selected object. **Configuration**: * Select the target object * Choose the specific record to update. * You can either choose a fixed record, using the drop down menu displaying all available records. * Or you can have the record dynamically selected, by designating a record found in a previous step, using the `(x+)`. You cannot search for the record based on different criteria at this stage. If you've not yet identified the record, add a `Search Record` step before this `Update Record` step. * Select fields to modify and enter new values **Output**: The updated record data is available for use in subsequent steps. ### Delete Record Removes a record from a selected object. **Configuration**: * Select the target object * Choose the specific record to delete **Output**: The deleted record data remains available for use in subsequent steps. ### Search Records Finds records within a selected object using filter conditions. **Configuration**: * Select the object to search * Set filter criteria to narrow results * Configure sorting and limits **Output**: Returns matching records that can be used in subsequent steps. **Limit**: Search Records returns a maximum of **200 records**. If you need to process more, add specific filters to reduce results or use scheduled workflows to process in batches. **Best Practice**: Use [branches](/user-guide/workflows/capabilities/workflow-branches) after Search Records to handle "found" vs "not found" scenarios. ### Upsert Record Creates a new record or updates an existing one based on matching criteria. This is useful when you're not sure if a record already exists. **Configuration**: * Select the target object * Note which fields can be used for matching: email for People, domain for Companies, ID for any object, or any field marked as Unique. You'll need to populate at least one of these below. * Fill out the field values. Do not forget to populate at least one of the unique identifiers. **Matching usually works even better when adding only one unique identifier.** For example, the screenshot below will match companies based on their domain. The ID is not necessarily needed. * Use data from previous steps to populate fields **How it works**: 1. Searches for a record matching your criteria 2. If found → updates the existing record 3. If not found → creates a new record **Output**: The created or updated record data is available for use in subsequent steps. ## Flow Actions ### Iterator **Loops through an array of records** returned from a previous step, allowing you to perform actions on each record individually. **Configuration**: * Select the array of records from a previous step (e.g., results from Search Records, from a Manual trigger with Bulk availability, from a Code node). When a Code or Logic Function step returns a top-level array, select its **Whole list** option to loop over the entire output. * Define the actions to perform on each record in the loop. - You can add several actions within an Iterator. - When using branches inside an Iterator, make sure the last step of each branch connects back to the Iterator to close the loop. * Access `Current Item` Fields: to use fields from the record currently being processed, click on the **Iterator** step, then select **Current item**. The list of available fields from that record will be displayed and can be selected for use in subsequent actions. You can also select **Use the whole item** to pass the entire current item into a downstream step. ### Filter Filters records based on specified conditions, allowing only records that meet the criteria to pass through. **Configuration**: * Select the record to filter * Define filter conditions and criteria * Configure which records should pass through to subsequent steps 1. **Output**: Filter nodes don't return data—they act as gates. If the conditions are met, the workflow continues. If not, the workflow stops at that branch. 2. The `IS` operator can be used with numeric fields. It performs as an `EQUAL`. ### Delay Pauses workflow execution for a specified duration or until a specific date/time. **Delay Types**: | Type | Description | | ------------------ | ------------------------------------------------------------------ | | **Duration** | Wait for a specific amount of time (days, hours, minutes, seconds) | | **Scheduled Date** | Wait until a specific date and time | **Configuration for Duration**: * Set days, hours, minutes, and/or seconds * Combine multiple units (e.g., 2 days and 4 hours) **Configuration for Scheduled Date**: * Select a date and time * Can reference a date field from a previous step (e.g., follow up 3 days after a meeting) **Use cases**: * Wait 24 hours before sending a follow-up email * Pause until an opportunity's close date * Schedule actions for business hours The scheduled date cannot be in the past. If a date field from a previous step is used and the date has already passed, the workflow will fail. **Limits & Credits**: * **No maximum duration limit**—you can set delays of minutes, days, weeks, or longer * **1 credit consumed** when the Delay node executes, regardless of duration * **No credits consumed** while waiting—a 5-minute delay costs the same as a 5-day delay ## Communication Actions ### Send Email Sends an email from your workflow. This is great for templated group emails. Emails will look like the ones you send from your mailbox. It's not suited for newsletters (which require richer formatting) or automated email sequences. **Prerequisites**: Add an email account in Settings → Accounts **Configuration**: * Select the sender email account You can only send emails from mailboxes synced to your own Twenty account. Sending from other team members' mailboxes (e.g., the account owner's email) is on the roadmap. For all the following steps, you can reference variables from previous steps for personalization. * Enter recipient email address. Only one recipient is possible at the moment. * Set subject line. * Compose message body. You can format links, create numbered lists, bullet point lists, and add attachments. Adding HTML signatures is not possible at the moment. ### Form Prompts a form during workflow execution to collect user input. The responses can then be used in subsequent steps to create records, send emails, or execute any other action based on the input. **Forms are designed for manual triggers only**. For workflows with other triggers (Record Created, Updated, etc.), forms are only accessible via the workflow run interface, which is not the expected user experience. A notification center will be released in 2026 to properly support forms in automated workflows. **Configuration**: * Configure the fields that users will be asked to fill. For each field, choose * a type among text, number, date, a given record, a select field. Fields from all objects are available for selection. * a label * a default value under `Placeholder` (optional) * Edit the form title **Output**: Form responses are available for use in subsequent steps. **Example**: The "Quick Lead" workflow is available by default in all workspaces, available anywhere in the Command Menu `Cmd+K`. **How to fill the form**: * Trigger your manual workflow from the Command Menu `Cmd+K` * Fill the form that is displayed in the side panel and click `Submit`. The fields cannot be made mandatory. ## Integration Actions ### Code Runs custom JavaScript within your workflow. Behind the scenes, each Code action is backed by its own **logic function** — a server-side TypeScript function that runs on the Twenty platform. When you add a Code action, Twenty creates a dedicated logic function for that step and exposes its editor inline, so the code you write lives with the workflow. **Configuration**: * Access variables from previous steps. You can edit the variables' names dynamically. * Write JavaScript code in the editor * Return variables for use in subsequent steps * Test code directly in the step If you need to use external API keys in your code, you must input them directly in the function body. You cannot configure API keys elsewhere and reference them in the logic function. **Working with arrays?** Arrays from external systems or previous steps may come as strings. See [How to handle arrays in Code actions](/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions) for the solution. Click the square icon at the top right of the code editor to display it in full screen — helpful since the default editor width is limited. #### Reusing a logic function across workflows The inline Code action is great for one-off logic, but the code it holds belongs to that single step. When you want to share the same logic across several workflows — or maintain it as versioned source in an app — define a **reusable logic function** instead of copy-pasting code into each Code action. A logic function is defined once in an app (using the SDK's `defineLogicFunction`) and exposed to the workflow builder by adding `workflowActionTriggerSettings`. Once your app is installed, that function appears as its own action in the workflow builder, alongside the built-in actions. Selecting it renders the input fields you declared (with variable pickers, just like other actions) and runs your shared code — no inline JavaScript required. | | Code action | Reusable logic function | | ------------------------ | ---------------------------------------- | ----------------------------------------------- | | **Where the code lives** | Inline editor, tied to one workflow step | Defined in an app, versioned in source control | | **Reuse** | Copy-pasted per step | Added as an action in any workflow | | **Inputs** | Variables wired in the editor | Fields rendered from your declared input schema | | **Best for** | Quick, workflow-specific logic | Shared logic used across multiple workflows | Both run on the same logic function runtime, so a Code action you've prototyped inline can later be promoted into a reusable logic function with minimal changes. For the developer-facing reference on defining logic functions and exposing them to the workflow builder, see [Logic Functions](/developers/extend/apps/logic/logic-functions) — in particular the **Exposing a function as an AI tool or workflow action** section and `workflowActionTriggerSettings`. ### HTTP Request Sends a request to an external API as part of your workflow. **Configuration**: * Enter the API endpoint URL. Using parameters from previous steps is possible. * Select HTTP method (GET, POST, PUT, PATCH, DELETE) * Add required headers and values * Provide sample response for structure preview ## AI Actions ### AI Agent - Coming Soon Runs an AI agent within your workflow to perform intelligent tasks. **Configuration**: * **Agent**: Select an existing AI agent or use the default agent * **Prompt**: Write the instruction for the AI agent * Reference variables from previous steps in the prompt **What AI Agents can do**: * Analyze and summarize data * Classify or categorize records * Generate text content * Make decisions based on data * Interact with your CRM data using tools **Output**: The AI agent's response is available for use in subsequent steps. If the agent has a structured output schema, the response will follow that format. AI Agent actions consume workflow credits based on the AI model used. See [Workflow Credits](/user-guide/workflows/capabilities/workflow-credits) for details. AI agents respect role-based permissions. You can assign specific roles to agents under **Settings → Members → Roles** to control what data they can access. See [Permissions](/user-guide/permissions-access/capabilities/permissions) for details. # Workflow Branches Source: https://docs.twenty.com/user-guide/workflows/capabilities/workflow-branches Create parallel paths and conditional logic in your workflows. Branches allow you to split your workflow into multiple paths that can run simultaneously or conditionally based on your data. ## How Branches Work When you create multiple connections from a single node, each path becomes a branch. By default, **all branches execute in parallel**—they don't wait for each other. ## Creating Branches ### Add a New Branch 1. **Right-click on the main canvas** of the workflow (not on an existing node) 2. Click **Add node** 3. Choose the node type for your new branch 4. Drag an arrow from the bottom of the previous step to the top of this new action 5. Repeat to add more branches from the same node Each branch is independent. Adding a branch doesn't affect other existing paths from that node. ### Visual Layout Branches appear as parallel paths in the workflow editor. You can drag nodes to rearrange the visual layout without affecting execution. ## Conditional Branches Since all branches run by default, use **Filter** nodes to control which paths actually execute: | Branch | Filter Condition | Action | | ------ | --------------------- | -------------------------- | | A | Stage = "Won" | Send congratulations email | | B | Stage = "Lost" | Create follow-up task | | C | Stage = "Negotiation" | Notify manager | 1. Create branches from your trigger or action 2. Add a **Filter** node as the first step of each branch 3. Configure each filter with mutually exclusive conditions 4. Add your actions after each filter Only the branch(es) where the filter condition is met will continue executing. ## Merging Branches **Branches don't automatically merge.** Each branch runs independently until it ends. You have full flexibility in how you handle this: * **Option 1: Keep branches separate** Each branch handles its own follow-up actions independently. This is the simplest approach when branches don't need to converge. * **Option 2: Merge branches manually** When creating your workflow, you can manually connect multiple branches to the same downstream action. Simply drag arrows from the end of each branch to a common node. While you can use a [Delay](/user-guide/workflows/capabilities/workflow-actions#delay) node to pause execution, it's not currently configurable to wait "until another branch ends." ## Related * [How to Use Branches in Workflows](/user-guide/workflows/capabilities/use-branches-in-workflows) - Step-by-step guide * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) - Available actions including Filter # Workflow Credits Source: https://docs.twenty.com/user-guide/workflows/capabilities/workflow-credits Understand workflow credit consumption and management. Workflow credits power your automations in Twenty. Understanding how they work helps you optimize costs and manage your automation budget effectively. ## Credit Allocation Workflow credits are allocated based on your billing cycle, not your plan tier: | Billing Cycle | Credits | | ------------------------ | ------------------- | | **Monthly subscription** | 5 credits per month | | **Yearly subscription** | 50 credits per year | 5 monthly credits are generous for standard automations. Most teams won't exceed this limit with typical workflow usage. Additional credits are primarily needed for advanced Code actions and AI-powered workflows. ## How Credit Consumption Works Credits are consumed when workflows execute, not when you create them. Each workflow action consumes credits based on its complexity: ### Credit Consumption by Action Type * **Basic internal operations**: Very low credit consumption * Search Records * Create Record * Update Record * Delete Record * Form actions * **Complex operations**: Higher credit consumption * Code actions (JavaScript execution) * HTTP Requests to external services * **AI features**: Higher credit consumption * AI Agent actions consume credits based on the AI model used * More complex prompts and longer outputs use more credits * **Delay actions**: Minimal credit consumption * The Delay node consumes **1 credit** when it executes * **No credits are consumed** during the wait period * A 5-minute delay costs the same as a 5-day delay ### Real-Time Deduction Credits are deducted in real-time as workflows execute. This means: * Draft workflows don't consume credits * Only active, running workflows use your credit allocation * Failed workflows still consume credits for completed steps ## Managing Credits ### Check Credit Usage 1. Go to **Settings → Billing** 2. View your current credit consumption and remaining balance 3. Monitor usage patterns to optimize your workflows ### Purchasing Additional Credits If you need more credits beyond your plan allocation: 1. Go to **Settings → Billing** 2. Click on the option to purchase additional credits. Packages of different sizes are available. 3. Credits are added to your current balance ## Best Practices * **Batch Processing**: Use bulk operations and Iterator actions efficiently to reduce individual action calls * **Manual Trigger Optimization**: For manual triggers, choose `Bulk` availability to process multiple records in a single workflow run * Optimize Code actions for efficiency # Workflow Runs Source: https://docs.twenty.com/user-guide/workflows/capabilities/workflow-runs Monitor and manage workflow executions. ## About Runs A **Run** is a record of a workflow execution. Every time a workflow is triggered—whether by a record event, schedule, manual action, or webhook—a new run is created. ## Viewing Runs ### From the Workflow Editor 1. Open the workflow you want to monitor 2. Click the **Runs** panel on the right side 3. See a list of recent runs with their status ### From the Workflow Runs View 1. Go to **Workflow Runs** in the sidebar 2. View runs across all workflows 3. Filter by status, workflow, or date ## Run Statuses | Status | Description | | ------------- | ------------------------------------------------------------------------ | | **Running** | Workflow is currently executing | | **Completed** | Workflow finished successfully | | **Failed** | Workflow encountered an error and stopped | | **Waiting** | Workflow is paused (e.g., waiting for a Delay action or Form submission) | ## Run Details Click on any run to see: * **Status**: Current state of the run * **Started at**: When the run began * **Duration**: How long the run took * **Trigger data**: The input that started the workflow * **Step outputs**: Data returned by each step * **Error messages**: If the run failed, what went wrong ## Step-by-Step Execution Each run shows the progression through your workflow: 1. See which steps completed successfully 2. Identify where failures occurred 3. View the data passed between steps 4. Debug issues by examining step inputs and outputs ## Error Handling When a run fails: 1. Open the failed run 2. Find the step that caused the failure 3. Check the error message for details Common issues: * Missing required fields * Invalid data format * External API errors * Permission issues ## Re-running Workflows If a run fails, you can: * Fix the underlying issue and wait for the next trigger * For manual workflows, trigger again with the same or updated data * Review the workflow logic to prevent future failures ## Performance Tips ### Managing Run History * Runs are retained for historical reference * Very old runs may be archived automatically * Export run data if you need to keep records ### Monitoring Best Practices * Check runs regularly after activating new workflows * Review failed runs to identify patterns ## Related * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Workflow Troubleshooting](/user-guide/workflows/how-tos/need-more-help/workflow-troubleshooting) # Workflow Triggers Source: https://docs.twenty.com/user-guide/workflows/capabilities/workflow-triggers Learn about the different triggers that start your workflows. ## About Triggers Workflows always start with a single trigger that defines when the automation should run. **Advanced objects are supported!** Beyond standard CRM objects (People, Companies, Opportunities), you can also trigger workflows and perform actions on: * Workspace Members * Calendar Events * Messages (Emails) * Tasks, Notes, and many other system objects This opens up powerful automations like notifying team members when calendar events are created, or processing incoming emails automatically. ## Record is Created Starts the workflow when a new record is created in a selected object (People, Companies, Opportunities, or any custom object). **Configuration**: Select the object type to monitor for new records. * This trigger is great for records created by CSV, mailbox and calendar synchronization, or API. * **It is not recommended for records created manually**: with this trigger, workflows start as soon as the record is created. Since Twenty UI offers auto-save on the fly (there is no edit mode and then a validation to save records), the workflow will be triggered before the user inputs all the fields. To trigger this workflow on records created manually, it is recommended to use the trigger `Record is created or updated` instead. ## Record is Updated Starts the workflow when changes are made to an existing record. **Configuration**: * Select the object type * Optionally specify which fields to monitor for changes ## Record is Updated or Created Starts the workflow when a record is either created or updated in a selected object. **Why This Matters**: This trigger is particularly helpful because records created via different methods behave differently: * **API/CSV imports**: Records are created with all fields populated immediately * **Manual creation**: Records are created first, then fields are added in subsequent updates **Configuration**: * Select the object type to monitor * Optionally specify which fields to monitor for changes * The workflow will trigger both on initial creation and any subsequent updates ## Record is Deleted Starts the workflow when a record is removed from an object. **Configuration**: Select the object type to monitor for deletions. ## Manual Trigger Starts the workflow when triggered by a user action. This trigger can be accessed through the `Cmd+K` menu or via a custom button that will be displayed in the top navbar after selecting record(s). **Availability Configuration**: Choose how the workflow should handle record selection: * **Global**: No record is required to trigger this workflow. The workflow is triggered from the command menu `Cmd+K` anywhere (from any object) and does not use record(s) as input. For **Single** and **Bulk** availability, the selected record(s) will be passed to your workflow. This is configured for a given object, and several records can be selected before triggering the workflow. * **Single**: The workflow will run from beginning to end as many times as there are records selected. **Soft limit: 100 runs/minute**. Beyond this, workflows remain in "Not Started" status and are processed gradually—either by a background job or when another workflow enters the queue. This means you can select more than 100 records with a Single trigger; execution will just be slower. * **Bulk**: The workflow will run once, providing the entire list of records as input. This means the workflow needs to contain an [Iterator action](/user-guide/workflows/capabilities/workflow-actions#iterator). This is more advanced, and best for people who want to optimize the number of workflow runs. **Additional Configuration**: * Select the target object (for Single and Bulk availability) * Choose a command icon for the workflow trigger * Configure navbar placement (Pinned or Not Pinned) **Access Methods**: * `Cmd+K` menu to find and launch manual workflows * Custom button in the top navbar (if configured) ## Time-Based Trigger: On a Schedule Starts the workflow on a recurring basis you define. **Configuration**: * Select time unit (minutes, hours, days) * Enter a value or use custom cron expressions for advanced scheduling **Timezone**: Scheduled workflows run in **UTC**. When setting hours for daily schedules, convert your local time to UTC. ## External Trigger: Webhook Starts the workflow when a GET or POST request is received from an external service. **Configuration**: * The workflow provides a unique webhook URL—copy this and add it to your external system as the endpoint to call. * For POST requests, define the expected body structure so Twenty knows what data to expect. Add here the fields you will receive that will be needed below in your workflow. * Configure authentication (coming soon). ## Choosing the Right Trigger | Use Case | Recommended Trigger | | --------------------------- | ---------------------------- | | New leads need processing | Record is Created | | Data changes need sync | Record is Updated | | Import/manual data handling | Record is Updated or Created | | Cleanup after deletion | Record is Deleted | | User-initiated action | Launch Manually | | Recurring reports | On a Schedule | | External integration | Webhook or On a Schedule | # Workflow Versions Source: https://docs.twenty.com/user-guide/workflows/capabilities/workflow-versions Manage workflow versions and drafts. ## About Versions Every time you activate a workflow, a new version is created. This allows you to track changes over time and revert to previous configurations if needed. ## Version Statuses | Status | Description | | --------------- | -------------------------------------- | | **Draft** | Being edited, not yet published | | **Active** | Live version responding to triggers | | **Deactivated** | Previously active but manually stopped | | **Archived** | Past versions kept for history | ## Working with Drafts When you edit an active workflow, your changes are saved as a **draft**. The active version continues to run while you work on updates. Once you're done editing, you can: * **Activate**: Publish the draft as the new active version (the previous version is archived) * **Discard**: Delete the draft and keep the current active version ## Version History ### Viewing Past Versions 1. Open the workflow 2. Click the **Versions** tab 3. See all previous versions with timestamps ### Restoring a Version 1. Find the version you want to restore 2. Click **Use as draft** 3. The version is copied to a new draft 4. Make any necessary updates 5. Activate when ready ## Best Practices ### Version Management * Activate only when ready for production * Keep meaningful changes between versions * Document major changes in workflow names or descriptions * Test in draft mode before activating ### Reverting Changes * If a new version causes issues, restore the previous version * Use the version history to track what changed * Always test restored versions before activating ## Common Workflows ### Quick Edit 1. Make minor changes to an active workflow 2. Test in draft mode 3. Activate the new version ### Major Revision 1. Use the previous version as a starting point 2. Make significant changes in draft 3. Thoroughly test all scenarios 4. Activate when confident ### Rollback 1. Identify the issue with the current version 2. Find the last working version in history 3. Click **Use as draft** 4. Activate to restore the old behavior ## Related * [Getting Started with Workflows](/user-guide/workflows/overview) * [Workflow Runs](/user-guide/workflows/capabilities/workflow-runs) # Handle Arrays in Code Actions Source: https://docs.twenty.com/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions Learn how to properly handle array inputs in workflow Code actions. When working with arrays in Code actions, you may encounter two common challenges: 1. **Arrays passed as strings** — data from external systems or previous steps arrives as a string instead of an actual array 2. **Can't select individual items** — you can only select the entire array, not specific fields within it Both can be solved with a Code node. ## Parsing Arrays from Strings Arrays are often passed between workflow steps as strings or JSON rather than native arrays. This happens when: * Receiving data from external APIs via HTTP Request * Processing webhook payloads * Passing data between workflow steps **Solution**: Add this pattern at the start of your Code action: ```javascript theme={null} export const main = async (params: { users: any; }): Promise => { const { users } = params; // Handle input that may come as a string or an array const usersFormatted = typeof users === "string" ? JSON.parse(users) : users; // Now you can safely work with usersFormatted as an array return { users: usersFormatted.map((user) => ({ ...user, activityStatus: String(user.activityStatus).toUpperCase(), })), }; }; ``` The key line `typeof users === "string" ? JSON.parse(users) : users` checks if the input is a string, parses it if needed, or uses it directly if it's already an array. ## Extracting Individual Fields from Arrays A webhook might return an array like `answers: [...]`, but in subsequent workflow steps you can only select the **entire array** — not individual items within it. **Solution**: Add a Code node to extract specific fields and return them as a structured object: ```javascript theme={null} export const main = async (params: { answers: any; }): Promise => { const { answers } = params; // Handle input that may come as a string or an array const answersFormatted = typeof answers === "string" ? JSON.parse(answers) : answers; // Extract specific fields from the array const firstname = answersFormatted[0]?.text || ""; const name = answersFormatted[1]?.text || ""; return { answer: { firstname, name } }; }; ``` The Code node returns a structured object instead of an array. In subsequent steps, you can now select individual fields like `answer.firstname` and `answer.name` from the variable picker. **Want to loop over the array instead of extracting fields?** When a Code or Logic Function step returns a top-level array, you can feed it straight into an [Iterator](/user-guide/workflows/capabilities/use-iterator): select the step's **Whole list** option as the Iterator's input, then reference each element with `{{iterator.currentItem}}` inside the loop. In that case you don't need to restructure the array into an object. Click the square icon at the top right of the code editor to display it in full screen — helpful since the default editor width is limited. # Bring Product Data into Twenty Source: https://docs.twenty.com/user-guide/workflows/how-tos/connect-to-other-tools/bring-product-data-in-twenty Sync product catalog data from a data warehouse into your CRM on a schedule. Use this pattern to keep Twenty in sync with product data from your data warehouse (e.g., Snowflake, BigQuery, PostgreSQL). ## Workflow Structure 1. **Trigger**: On a Schedule 2. **Code**: Query your data warehouse 3. **Code** (optional): Format data as array 4. **Iterator**: Loop through each product 5. **Upsert Record**: Create or update in Twenty ## Step 1: Schedule the Trigger Set the workflow to run at a frequency matching your data freshness needs: * Every 5 minutes for near real-time sync * Every hour for less critical data * Daily for batch updates ## Step 2: Query Your Data Warehouse Add a **Code** action to fetch recent data: ```javascript theme={null} export const main = async () => { const intervalMinutes = 10; // Match your schedule frequency const cutoffTime = new Date(Date.now() - intervalMinutes * 60 * 1000).toISOString(); // Replace with your actual data warehouse connection const response = await fetch("https://your-warehouse-api.com/query", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ query: ` SELECT id, name, sku, price, stock_quantity, updated_at FROM products WHERE updated_at >= '${cutoffTime}' ` }) }); const data = await response.json(); return { products: data.results }; }; ``` Filter by `updated_at >= last X minutes` to retrieve only recently changed records. This keeps the sync efficient. ## Step 3: Format Data (Optional) If your warehouse returns data in a format that needs transformation, add another **Code** action. Common transformations include type conversions, field renaming, and data cleanup. ### Example: User Data with Boolean and Status Fields ```javascript theme={null} export const main = async (params: { users: any; }): Promise => { const { users } = params; const usersFormatted = typeof users === "string" ? JSON.parse(users) : users; // Convert string "true"/"false" to actual booleans const toBool = (v: any) => v === true || v === "true"; return { users: usersFormatted.map((user) => ({ ...user, activityStatus: String(user.activityStatus).toUpperCase(), isActiveLast30d: toBool(user.isActiveLast30d), isActiveLast7d: toBool(user.isActiveLast7d), isActiveLast24h: toBool(user.isActiveLast24h), isTwenty: toBool(user.isTwenty), })), }; }; ``` ### Example: Product Data with Type Conversions ```javascript theme={null} export const main = async (params: { products: any }) => { const products = typeof params.products === "string" ? JSON.parse(params.products) : params.products; return { products: products.map(product => ({ externalId: product.id, name: product.name, sku: product.sku, price: parseFloat(product.price), // String → Number stockQuantity: parseInt(product.stock_quantity), isActive: product.status === "active" // String → Boolean })) }; }; ``` ### Example: Date and Currency Formatting ```javascript theme={null} export const main = async (params: { deals: any }) => { const deals = typeof params.deals === "string" ? JSON.parse(params.deals) : params.deals; return { deals: deals.map(deal => ({ ...deal, // Convert Unix timestamp to ISO date closedAt: deal.closed_timestamp ? new Date(deal.closed_timestamp * 1000).toISOString() : null, // Ensure amount is a number (remove currency symbols) amount: parseFloat(String(deal.amount).replace(/[^0-9.-]/g, "")), // Normalize stage names stage: deal.stage?.toLowerCase().replace(/_/g, " ") })) }; }; ``` ### Common Transformations | Source Format | Target Format | Code | | -------------------- | ---------------- | ---------------------------------------- | | `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` | | `"123.45"` | `123.45` | `parseFloat(value)` | | `"active"` | `"ACTIVE"` | `value.toUpperCase()` | | `1704067200` (Unix) | ISO date | `new Date(v * 1000).toISOString()` | | `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` | | `null` / `undefined` | `""` | `value \|\| ""` | ## Step 4: Iterate Through Products Add an **Iterator** action: * Input: `{{code.products}}` This loops through each product in the array. ## Step 5: Upsert Each Record Inside the iterator, add an **Upsert Record** action: | Setting | Value | | ------------ | -------------------------------------- | | **Object** | Your custom Product object | | **Match by** | External ID or SKU (unique identifier) | | **Name** | `{{iterator.item.name}}` | | **SKU** | `{{iterator.item.sku}}` | | **Price** | `{{iterator.item.price}}` | Use **Upsert** (update or create) instead of building separate branches for create vs. update. It's faster to build and easier to debug. ## Example Use Cases | Source | Data | | ----------------------- | ----------------------------------- | | **ERP system** | Product catalog, pricing, inventory | | **E-commerce platform** | Orders, customers, product updates | | **Data warehouse** | Aggregated metrics, enriched data | | **Inventory system** | Stock levels, reorder alerts | ## Related * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Handle Arrays in Code Actions](/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions) # Bring Typeform Submissions into Twenty Source: https://docs.twenty.com/user-guide/workflows/how-tos/connect-to-other-tools/bring-typeform-submissions-in-twenty Handle Typeform's webhook payload to create leads from form submissions. For standard webhook setup, see [Set Up a Webhook Trigger](/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). This article covers the specific handling required for Typeform's custom payload structure. ### Step 1: Create a Webhook Workflow 1. Go to **Settings → Workflows** 2. Click **+ New Workflow** 3. Select **Webhook** as the trigger 4. Copy the webhook URL ### Step 2: Configure Typeform 1. In Typeform, open your form 2. Go to **Connect → Webhooks** 3. Paste your Twenty webhook URL 4. Save ### Step 3: Understand the Typeform Payload Typeform sends a nested JSON structure. Here's a simplified example: ```json theme={null} { "event_type": "form_response", "form_response": { "form_id": "abc123", "submitted_at": "2025-01-15T10:30:00Z", "answers": [ { "text": "Jane", "type": "text", "field": { "id": "field1", "type": "short_text", "title": "First Name" } }, { "text": "Smith", "type": "text", "field": { "id": "field2", "type": "short_text", "title": "Last Name" } }, { "text": "Acme Corp", "type": "text", "field": { "id": "field3", "type": "short_text", "title": "Company" } }, { "email": "jane@acme.com", "type": "email", "field": { "id": "field4", "type": "email", "title": "Email" } }, { "type": "choice", "field": { "id": "field5", "type": "dropdown", "title": "Team Size" }, "choice": { "label": "10-50" } } ] } } ``` Key things to note: * Form data is nested under `form_response` * **Answers are returned as an array**, not as named fields * Each answer includes the field type and title for reference ### Step 4: Extract Fields from the Answers Array Since `answers` is an array, you can only select the entire array in subsequent steps — not individual fields. Add a **Code** action to extract the fields you need: ```javascript theme={null} export const main = async (params: { answers: any; }): Promise => { const { answers } = params; // Handle input that may come as a string or an array const answersFormatted = typeof answers === "string" ? JSON.parse(answers) : answers; // Extract fields by position or by finding the field type const firstName = answersFormatted[0]?.text || ""; const lastName = answersFormatted[1]?.text || ""; const company = answersFormatted[2]?.text || ""; const email = answersFormatted.find(a => a.type === "email")?.email || ""; const teamSize = answersFormatted.find(a => a.type === "choice")?.choice?.label || ""; return { contact: { firstName, lastName, company, email, teamSize } }; }; ``` Now in subsequent steps, you can select `contact.firstName`, `contact.email`, etc. from the variable picker. For more details on handling arrays in Code actions, see [Handle Arrays in Code Actions](/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions). ### Step 5: Create the Record Add a **Create Record** action: | Field | Value | | -------------- | ---------------------------------------------------- | | **Object** | People | | **First Name** | `{{code.contact.firstName}}` | | **Last Name** | `{{code.contact.lastName}}` | | **Email** | `{{code.contact.email}}` | | **Company** | Search or create based on `{{code.contact.company}}` | ### Step 6: Test and Activate 1. Submit a test response in Typeform 2. Check the workflow run to verify data was captured 3. Activate the workflow ## Related * [Set Up a Webhook Trigger](/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger) * [Handle Arrays in Code Actions](/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions) # Generate a PDF from Twenty Source: https://docs.twenty.com/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty Create a workflow to generate and attach a PDF (such as a quote) to a record. Automatically generate or fetch a PDF and attach it to a record in Twenty. This is commonly used to create quotes, invoices, or reports that are linked to Companies, Opportunities, or other objects. ## Overview This workflow uses a **Manual Trigger** so users can generate a PDF on demand for any selected record. A **Logic Function** handles: 1. Downloading the PDF from a URL (from a PDF generation service) 2. Uploading the file to Twenty 3. Creating an Attachment linked to the record ## Prerequisites Before setting up the workflow: 1. **Create an API Key**: Go to **Settings → APIs** and create a new API key. You'll need this token for the Logic Function. 2. **Set up a PDF generation service** (optional): If you want to dynamically generate PDFs (e.g., quotes), use a service like Carbone, PDFMonkey, or DocuSeal to create the PDF and get a download URL. ## Step-by-Step Setup ### Step 1: Configure the Trigger 1. Go to **Workflows** and create a new workflow 2. Select **Manual Trigger** 3. Choose the object you want to attach PDFs to (e.g., **Company** or **Opportunity**) With a Manual Trigger, users can run this workflow using a button that appears on the top right once a record is selected — this runs the workflow to generate and attach a PDF. ### Step 2: Add a Logic Function 1. Add a **Code** action (Logic Function) 2. Create a new function with the code below 3. Configure the input parameters #### Input Parameters | Parameter | Value | | ----------- | ----------------------- | | `companyId` | `{{trigger.object.id}}` | If attaching to a different object (Person, Opportunity, etc.), rename the parameter accordingly (e.g., `personId`, `opportunityId`) and update the logic function. #### Logic Function Code ```typescript theme={null} export const main = async ( params: { companyId: string }, ) => { const { companyId } = params; // Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records) // Cloud: https://api.twenty.com/graphql // Self-hosted: https://your-domain.com/graphql const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata'; const dataGraphqlEndpoint = 'https://api.twenty.com/graphql'; // Replace with your API key from Settings → APIs const authToken = 'YOUR_API_KEY'; // Replace with your PDF URL // This could be from a PDF generation service or a static URL const pdfUrl = 'https://your-pdf-service.com/generated-quote.pdf'; const filename = 'quote.pdf'; // Step 1: Download the PDF file const pdfResponse = await fetch(pdfUrl); if (!pdfResponse.ok) { throw new Error(`Failed to download PDF: ${pdfResponse.status}`); } const pdfBlob = await pdfResponse.blob(); const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' }); const fieldMetadataIdQuery = ` query FindUploadFileFieldMetadataId { objects { edges { node { nameSingular fieldsList { id name } } } } } `; // Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API const response = await fetch(metadataGraphqlEndpoint, { method: 'POST', headers: { Authorization: `Bearer ${authToken}` }, body: { query: fieldMetadataIdQuery, } }); const result = await response.json(); const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id; // Step 3: Upload the file via GraphQL multipart upload const uploadMutation = ` mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) { uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) { id } } `; const uploadForm = new FormData(); uploadForm.append('operations', JSON.stringify({ query: uploadMutation, variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId }, })); uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] })); uploadForm.append('0', pdfFile); const uploadResponse = await fetch(metadataGraphqlEndpoint, { method: 'POST', headers: { Authorization: `Bearer ${authToken}` }, body: uploadForm, }); const uploadResult = await uploadResponse.json(); if (uploadResult.errors?.length) { throw new Error(`Upload failed: ${uploadResult.errors[0].message}`); } const fileId = uploadResult.data?.uploadFilesFieldFile?.id; if (!fileId) { throw new Error('No file id returned from upload'); } // Step 4: Create the attachment linked to the company const attachmentMutation = ` mutation CreateOneAttachment($data: AttachmentCreateInput!) { createAttachment(data: $data) { id name } } `; const attachmentResponse = await fetch(dataGraphqlEndpoint, { method: 'POST', headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ query: attachmentMutation, variables: { data: { name: filename, targetCompanyId: companyId, file: [ { fileId: fileId, label: filename } ] }, }, }), }); const attachmentResult = await attachmentResponse.json(); if (attachmentResult.errors?.length) { throw new Error(`Attachment creation failed: ${attachmentResult.errors[0].message}`); } return attachmentResult.data?.createAttachment; }; ``` ### Step 3: Customize for Your Use Case #### To attach to a different object Replace `targetCompanyId` with the appropriate field: | Object | Field Name | | ------------- | -------------------------- | | Company | `targetCompanyId` | | Person | `targetPersonId` | | Opportunity | `targetOpportunityId` | | Custom Object | `targetYourCustomObjectId` | Update both the function parameter and the `variables.data` object in the attachment mutation. #### To use a dynamic PDF URL If using a PDF generation service, you can: 1. First make an HTTP Request action to generate the PDF 2. Pass the returned PDF URL to the logic function as a parameter ```typescript theme={null} export const main = async ( params: { companyId: string; pdfUrl: string; filename: string }, ) => { const { companyId, pdfUrl, filename } = params; // ... rest of the function }; ``` ### Step 4: Test and Activate 1. Save the workflow 2. Navigate to a Company record 3. Click the **⋮** menu and select your workflow 4. Check the **Attachments** section on the record to verify the PDF was attached 5. Activate the workflow ## Combining with PDF Generation Services For creating dynamic quotes or invoices: ### Example: Generate Quote → Attach PDF | Step | Action | Purpose | | ---- | ------------------------ | ---------------------------------------- | | 1 | Manual Trigger (Company) | User initiates on a record | | 2 | Search Record | Get Opportunity or line item details | | 3 | HTTP Request | Call PDF generation API with record data | | 4 | Logic Function | Download and attach the generated PDF | ### Popular PDF Generation Services * **Carbone** - Template-based document generation * **PDFMonkey** - Dynamic PDF creation from templates * **DocuSeal** - Document automation platform * **Documint** - API-first document generation Each service provides an API that returns a PDF URL, which you can then pass to the Logic Function. ## Troubleshooting | Issue | Solution | | ---------------------------- | ---------------------------------------------------------- | | "Failed to download PDF" | Check the PDF URL is accessible and returns a valid PDF | | "Upload failed" | Verify your API key is valid and has write permissions | | "Attachment creation failed" | Ensure the object ID field name matches your target object | ## Related * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Logic Functions](/user-guide/workflows/capabilities/workflow-actions#code) * [Generate a Quote or Invoice from Twenty](/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty) # Generate a Quote or Invoice from Twenty Source: https://docs.twenty.com/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty Automatically create invoices in external tools when deals close. Automatically send deal data to your invoicing system (Stripe, QuickBooks, Xero, etc.) when an opportunity is won. ## Workflow Structure 1. **Trigger**: Record is Updated (Opportunity) 2. **Filter**: Stage = Closed Won 3. **Search Record**: Get Company details 4. **Code** (optional): Format payload 5. **HTTP Request**: Send to invoicing system ## Step 1: Set Up the Trigger 1. Create a new workflow 2. Select **Record is Updated** trigger 3. Choose **Opportunity** as the object ## Step 2: Filter for Closed Won Add a **Filter** action to only continue when the deal is won: | Setting | Value | | ------------- | --------------------------------- | | **Field** | Stage | | **Condition** | Equals | | **Value** | `CLOSED_WON` (or your stage name) | The trigger fires on any Opportunity update. The Filter ensures the workflow only continues when the stage changes to Closed Won. ## Step 3: Get Company Details The Opportunity record may not include all Company fields you need for the invoice. Add a **Search Record** action: | Setting | Value | | ------------ | ---------------------------------------- | | **Object** | Company | | **Match by** | ID equals `{{trigger.object.companyId}}` | This retrieves the full Company record with billing address, tax ID, etc. ## Step 4: Format the Payload (Optional) If your invoicing system expects a specific format, add a **Code** action: ```javascript theme={null} export const main = async (params: { opportunity: any; company: any; }): Promise => { const { opportunity, company } = params; return { invoice: { // Customer info from Company customer_name: company.name, customer_email: company.email || "", billing_address: { line1: company.address?.street || "", city: company.address?.city || "", postal_code: company.address?.postalCode || "", country: company.address?.country || "" }, tax_id: company.taxId || null, // Invoice details from Opportunity amount: opportunity.amount, currency: opportunity.currency || "USD", description: `Invoice for ${opportunity.name}`, due_days: 30, // Reference back to Twenty metadata: { opportunity_id: opportunity.id, company_id: company.id } } }; }; ``` ## Step 5: Send to Invoicing System Add an **HTTP Request** action: | Setting | Value | | ----------- | ----------------------------------------- | | **Method** | POST | | **URL** | Your invoicing API endpoint | | **Headers** | `Authorization: Bearer YOUR_API_KEY` | | **Body** | `{{code.invoice}}` or map fields directly | ### Example: Stripe Invoice ``` POST https://api.stripe.com/v1/invoices Headers: Authorization: Bearer sk_live_xxx Content-Type: application/x-www-form-urlencoded Body: customer: {{company.stripeCustomerId}} collection_method: send_invoice days_until_due: 30 ``` ### Example: QuickBooks Invoice ``` POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice Headers: Authorization: Bearer YOUR_ACCESS_TOKEN Content-Type: application/json Body: {{code.invoice}} ``` ## Complete Workflow Summary | Step | Action | Purpose | | ---- | ----------------------- | ------------------------------------ | | 1 | Trigger: Record Updated | Fires when any Opportunity changes | | 2 | Filter | Only proceed if Stage = Closed Won | | 3 | Search Record | Get full Company details for billing | | 4 | Code | Format data for invoicing API | | 5 | HTTP Request | Create invoice in external system | ## Tips * **Store external IDs**: Save the invoice ID returned by the API back to the Opportunity using an **Update Record** action * **Error handling**: Add a branch to send a notification if the HTTP request fails * **Test first**: Use your invoicing system's sandbox/test mode before going live ## Related * [Generate a PDF from Twenty](/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — attach generated PDFs to records * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Closed Won Automations](/user-guide/workflows/how-tos/crm-automations/closed-won-automations) # Set Up a Webhook Trigger Source: https://docs.twenty.com/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger Receive data from external services to trigger workflows. Webhook triggers allow external services to start your workflows by sending data to a unique URL. Use them to connect forms, third-party apps, and custom integrations. ## When to Use Webhooks | Use Case | Example | | ----------------------- | --------------------------------------- | | **Web forms** | Contact form submissions create leads | | **Third-party apps** | Stripe payment → create customer record | | **Custom integrations** | Your app → Twenty automation | | **No-code tools** | Zapier, Make, n8n connections | ## Step-by-Step Setup ### Step 1: Create the Workflow 1. Go to **Settings → Workflows** 2. Click **+ New Workflow** 3. Name it (e.g., "Website Form Submission") ### Step 2: Configure the Webhook Trigger 1. Click on the trigger block 2. Select **Webhook** 3. You'll receive a unique webhook URL like: ``` https://api.twenty.com/webhooks/workflow/abc123... ``` 4. Copy this URL—you'll need it for your external service ### Step 3: Define Expected Data Structure For **POST** requests, define the expected body structure: 1. Click **Define expected body** 2. Enter a sample JSON that matches what your service will send: ```json theme={null} { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "company": "Acme Inc", "message": "Interested in your product" } ``` 3. Click **Save**—this creates variables you can use in subsequent steps ### Step 4: Add Actions Now add actions that use the webhook data: **Example: Create a Person record** 1. Add **Create Record** action 2. Select **People** object 3. Map fields: | Field | Value | | ---------- | ---------------------------------------------------- | | First Name | `{{trigger.body.firstName}}` | | Last Name | `{{trigger.body.lastName}}` | | Email | `{{trigger.body.email}}` | | Company | Search or create based on `{{trigger.body.company}}` | ### Step 5: Test the Webhook Before activating, test your webhook: **Using cURL**: ```bash theme={null} curl -X POST https://api.twenty.com/webhooks/workflow/abc123... \ -H "Content-Type: application/json" \ -d '{"firstName":"Test","lastName":"User","email":"test@example.com"}' ``` **Using Postman or similar**: 1. Create a POST request to your webhook URL 2. Set Content-Type header to `application/json` 3. Add your test JSON body 4. Send and check workflow runs ### Step 6: Activate Once tested, click **Activate** to make the workflow live. ## Handling Different Data Structures ### Nested Data If your webhook sends nested data: ```json theme={null} { "contact": { "name": "John Doe", "email": "john@example.com" }, "source": "website" } ``` Reference with: `{{trigger.body.contact.email}}` ### Arrays If data includes arrays: ```json theme={null} { "items": [ {"name": "Product A", "qty": 2}, {"name": "Product B", "qty": 1} ] } ``` How you handle arrays depends on your use case: **Unknown number of items → Use Iterator** If you need to process each item in the array (e.g., create a record for each), add a **Code** action to parse the array, then use **Iterator**: ```javascript theme={null} export const main = async (params: { items: any }) => { const items = typeof params.items === "string" ? JSON.parse(params.items) : params.items; return { items }; }; ``` Then use Iterator to loop through: `{{code.items}}` **Known/specific fields → Extract to named fields** If the array contains specific fields you want to access individually (e.g., form answers where position 0 is always "first name", position 1 is always "last name"), add a **Code** action to extract them: ```javascript theme={null} export const main = async (params: { items: any }) => { const items = typeof params.items === "string" ? JSON.parse(params.items) : params.items; return { product: { name: items[0]?.name || "", qty: items[0]?.qty || 0 } }; }; ``` Now you can select `product.name` and `product.qty` individually in subsequent steps. For more details on handling arrays, see [Handle Arrays in Code Actions](/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions). ## Related * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) # Auto-Reply to Inbound Emails Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails Build a workflow that uses AI to triage incoming emails and send threaded replies automatically. Respond to inbound emails in seconds — not hours. This workflow uses an AI Agent to filter out noise (newsletters, spam, auto-replies) and draft a personalized response to real messages. Then, AI sends it as a threaded reply inside the original conversation. ## How email threading works Every email carries a hidden `Message-ID` header — a unique fingerprint assigned by the sender's mail server. When you reply to an email, your mail client sets an `In-Reply-To` header referencing that fingerprint. That's how Gmail, Outlook, and every other client groups messages into threads. In Twenty, that fingerprint is stored as `headerMessageId` on the Message object. Your workflow grabs it and passes it into the Send Email action's In-Reply-To field. ## Building the workflow ### Step 1: Create a new workflow Head to **Settings -> Workflows** and click **+ New Workflow**. ### Step 2: Trigger on incoming messages Pick **When a Record is Created** and choose **Messages**. Every time an email lands in Twenty, this fires. ### Step 3: Look up who sent it Add a **Search Records** action. The sender's address isn't on the message itself — it's on the related Message Participant record. | Field | Value | | ---------- | ------------------------------- | | **Object** | Message Participants | | **Filter** | Message **is** `{{trigger.id}}` | | **Filter** | Role **is** From | | **Limit** | 1 | This gives you the sender's email in `handle` and their name in `displayName`. ### Step 4: Triage and draft a reply with AI Add an **AI Agent** action. This single step does two things: decides whether the email deserves a reply, and if so, writes one. Use a prompt like: ``` You are an email triage assistant for a sales team. Read the following inbound email and decide if it deserves a reply. Subject: {{trigger.subject}} Body: {{trigger.text}} From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}}) If this email is spam, a newsletter, an automated notification, or otherwise does not need a human reply, respond with exactly: SKIP Otherwise, write a short, professional reply (3-4 sentences max) that: - Acknowledges their specific message - Lets them know someone from the team will follow up shortly - Is warm but not overly casual Respond with only the reply text, no subject line or greeting prefix. ``` The AI Agent outputs its response in a `response` field that the next steps can reference. ### Step 5: Branch on the AI decision Add an **If/Else** action to check whether the AI decided to reply or skip. | Field | Value | | ------------- | ----------------------------------------------- | | **Condition** | AI Agent `response` **does not contain** `SKIP` | | **If true** | Continue to Send Email | | **Else** | Do nothing (workflow ends) | Spam, newsletters, and auto-generated messages get dropped. Everything else moves to the next step. ### Step 6: Send a threaded reply Add a **Send Email** action on the "if true" branch. Click **Advanced options**, then **Add In-Reply-To**. | Field | Value | | --------------- | -------------------------------------- | | **To** | `{{Find Sender.first.handle}}` | | **Subject** | `Re: {{trigger.subject}}` | | **Body** | `{{AI Triage & Draft Reply.response}}` | | **In-Reply-To** | `{{trigger.headerMessageId}}` | The In-Reply-To field is what makes this a reply instead of a new conversation. The recipient sees it threaded under the original email in Gmail, Outlook, or any other client. **In-Reply-To** expects a `message.headerMessageId` from the trigger — it's the email's unique fingerprint, not a recipient address. If you leave it empty, the email still sends, just as a standalone message. Gmail uses the subject line to group messages into threads. The subject **must** start with `Re:` (including the colon and space) for Gmail to display the reply inside the original thread. Without it, the reply will appear as a separate conversation — even if the In-Reply-To header is set correctly. ### Step 7: Test and activate Hit **Test**, then check your email client. The reply should appear nested under the original message. Activate when you're happy with it. ## Ideas to build on * **Only reply to VIPs** — add a branch that checks the sender's domain or whether they exist as a Contact in Twenty * **Route by intent** — use separate AI Agent prompts to handle sales inquiries differently from support requests * **Enrich before replying** — add a Search Records step to pull the sender's company or deal history into the AI prompt for more personalized replies # Closed Won Automations Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/closed-won-automations Automate post-win activities when opportunities close. When a deal closes, multiple things need to happen: update company status, notify team members, create onboarding tasks. Automate all of this with a single workflow. ## The Problem When an opportunity moves to "Closed Won": * Company type needs to change from "Prospect" to "Customer" * Onboarding tasks need to be created * Customer success team needs to be notified * Sales rep needs confirmation Doing this manually is time-consuming and error-prone. ## The Solution Create a workflow that handles all post-win activities automatically. ## Complete Workflow Setup ### Step 1: Create the Workflow 1. Go to **Settings → Workflows** 2. Click **+ New Workflow** 3. Name it "Deal Won - Post-Win Automation" ### Step 2: Configure the Trigger 1. Select **Record is Updated** 2. Choose **Opportunities** 3. Under "Fields to monitor", select **Stage** ### Step 3: Add Stage Filter 1. Add **Filter** action 2. Condition: `{{trigger.object.stage}}` equals "Closed Won" ### Step 4: Update Company Type 1. Add **Update Record** action 2. Configure: | Field | Value | | ------------------- | ------------------------------- | | **Object** | Companies | | **Record** | `{{trigger.object.company.id}}` | | **Type** | Customer | | **First Deal Date** | `{{trigger.object.closedAt}}` | | **Account Owner** | `{{trigger.object.owner.id}}` | ### Step 5: Create Onboarding Task 1. Add **Create Record** action 2. Configure: | Field | Value | | ----------------------- | ---------------------------------------------------------------------------------------------------- | | **Object** | Tasks | | **Title** | `Onboarding: {{trigger.object.name}}` | | **Assignee** | Customer Success team member | | **Due Date** | 3 days from now | | **Priority** | High | | **Related Company** | `{{trigger.object.company.id}}` | | **Related Opportunity** | `{{trigger.object.id}}` | | **Description** | `New customer onboarding for {{trigger.object.company.name}}. Deal value: {{trigger.object.amount}}` | ### Step 6: Notify Customer Success 1. Add **Send Email** action 2. Configure: | Field | Value | | ----------- | --------------------------------------------------------------------------- | | **To** | [customer-success@yourcompany.com](mailto:customer-success@yourcompany.com) | | **Subject** | `🎉 New Customer: {{trigger.object.company.name}}` | | **Body** | See example below | **Email body example**: ``` Hi CS Team, We have a new customer! Company: {{trigger.object.company.name}} Deal: {{trigger.object.name}} Value: {{trigger.object.amount}} Sales Rep: {{trigger.object.owner.name}} Close Date: {{trigger.object.closedAt}} An onboarding task has been created automatically. Let's give them a great start! ``` ### Step 7: Confirm to Sales Rep 1. Add another **Send Email** action 2. Configure: | Field | Value | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | **To** | `{{trigger.object.owner.email}}` | | **Subject** | `✅ Deal Closed: {{trigger.object.name}}` | | **Body** | Congratulations! Your deal has been processed. The customer success team has been notified and onboarding has begun. | ### Step 8: Test and Activate 1. Test by moving a test opportunity to "Closed Won" 2. Verify: * Company type changed to "Customer" * Onboarding task created * CS team received email * Sales rep received confirmation 3. Activate when ready ## Handling Closed Lost Create a similar workflow for lost deals: ### Trigger * Record is Updated (Opportunities, Stage = "Closed Lost") ### Actions 1. **Create Record**: Task for "Lost Deal Analysis" 2. **Update Record**: Add lost reason to company record 3. **Send Email**: Notify manager of lost deal ## Advanced: Multi-Step Onboarding For complex onboarding, create multiple tasks: ```javascript theme={null} export const main = async (params) => { const tasks = [ { title: "Welcome call", daysFromNow: 1, assignee: "CS" }, { title: "Send onboarding materials", daysFromNow: 2, assignee: "CS" }, { title: "Technical setup", daysFromNow: 5, assignee: "Support" }, { title: "30-day check-in", daysFromNow: 30, assignee: "CS" } ]; return { tasks }; }; ``` Use **Iterator** to create each task from the array. ## Customization Ideas ### Keep your other tools up-to-date * Create customer in billing system with an **HTTP Request** ### Conditional actions Use **Filter** actions to: * Different onboarding for enterprise vs SMB * Different assignees based on region * Skip notifications for small deals ### Include Deal details Use **Code** action to format: * Deal summary documents * Handoff notes for CS team * Custom onboarding checklists ## Related * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Send Emails from Workflows](/user-guide/workflows/capabilities/send-emails-from-workflows) # Detect Stale Opportunities Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities Automatically notify managers when opportunities haven't been updated. Keep your pipeline healthy by alerting managers when opportunities go stale. This workflow checks for opportunities that haven't been updated in a specified number of days. ## The Problem Opportunities sitting without updates lead to: * Deals going cold * Unreliable forecasts * Lost revenue ## The Solution Create a scheduled workflow that finds stale opportunities and emails their managers. ## Step-by-Step Setup ### Step 1: Create the Workflow 1. Go to **Settings → Workflows** 2. Click **+ New Workflow** 3. Name it "Stale Opportunity Alert" ### Step 2: Configure the Trigger 1. Select **On a Schedule** 2. Set to run daily (e.g., every day at 8 AM) ### Step 3: Search for Stale Opportunities 1. Add **Search Records** action 2. Configure: | Field | Value | | ---------- | ----------------------------------------------- | | **Object** | Opportunities | | **Filter** | Updated At is before (today - 7 days) | | **Filter** | Stage is not "Closed Won" AND not "Closed Lost" | | **Limit** | 100 | ### Step 4: Check If Any Found 1. Add **Filter** action 2. Condition: `{{searchRecords.length}}` is greater than 0 3. If no stale opportunities, the workflow stops here ### Step 5: Format the Alert (Code Action) Add a **Code** action to format the email: ```javascript theme={null} export const main = async (params) => { const opportunities = params.opportunities; // Group opportunities by owner const byOwner = {}; opportunities.forEach(opp => { const ownerEmail = opp.owner?.email || 'unassigned'; if (!byOwner[ownerEmail]) { byOwner[ownerEmail] = []; } byOwner[ownerEmail].push({ name: opp.name, amount: opp.amount, lastUpdated: opp.updatedAt, stage: opp.stage }); }); // Format summary for manager let summary = "Stale Opportunities Report\n\n"; Object.entries(byOwner).forEach(([owner, opps]) => { summary += `${owner}: ${opps.length} stale opportunities\n`; opps.forEach(opp => { summary += ` - ${opp.name} (${opp.stage})\n`; }); summary += "\n"; }); return { summary, totalCount: opportunities.length }; }; ``` ### Step 6: Send Alert Email Add **Send Email** action: | Field | Value | | ----------- | --------------------------------------------------------------------- | | **To** | [sales-manager@yourcompany.com](mailto:sales-manager@yourcompany.com) | | **Subject** | `🚨 {{code.totalCount}} Stale Opportunities Need Attention` | | **Body** | `{{code.summary}}` | ### Step 7: Test and Activate 1. Click **Test** to run the workflow 2. Check that the email contains the right data 3. Activate when ready ## Customization Options ### Change Staleness Threshold Modify the Search Records filter to change from 7 days to your preferred period: * 3 days for high-velocity sales * 14 days for enterprise deals * 30 days for long sales cycles ### Alert Individual Reps Instead of one manager email, use **Iterator** to send personalized emails to each rep about their own stale deals. ### Add Escalation Create multiple workflows with increasing severity: 1. Day 7: Email to rep 2. Day 14: Email to rep + manager 3. Day 21: Create task for manager to intervene ### Include in Slack Use **HTTP Request** to post to a Slack webhook instead of or in addition to email. ## Related * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Send Emails from Workflows](/user-guide/workflows/capabilities/send-emails-from-workflows) # Display Number of Emails Received Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/display-number-of-emails-received Create a workflow to automatically count and display the number of emails received from each contact. ## Overview This workflow triggers every time a new email is received and updates a custom field on the Person record with the total count of emails from that sender. ## Prerequisites Before setting up this workflow, create a custom field on the **People** object: 1. Go to **Settings → Data Model → People** 2. Add a new **Number** field 3. Name it something like "Number of emails received from this person" ## Step-by-Step Setup ### Step 1: Configure the Trigger 1. Go to **Workflows** and create a new workflow 2. Select **Record is Created** as the trigger 3. Choose **Message Participants** (available under Advanced objects) A Message Participant is a combination of a message ID and a person ID, creating one unique record per message. This is easier to track than Messages directly because we can access the `handle` field, which contains the sender's (or recipient's) email address. ### Step 2: Filter on Role 1. Add a **Filter** action 2. Set the condition: **Role** equals **FROM** This ensures you only count messages sent by this person, not messages sent to them. ### Step 3: Search All Message Participants with Same Handle 1. Add a **Search Records** action 2. Select **Message Participants** as the object 3. Add filters: **Handle** equals the handle from the trigger (the sender's email address) and **Role** equals **FROM** 4. Increase the **Limit** from 1 to **200** (the maximum) This finds all messages from this email address to get the total count. The Search Records action is limited to returning 200 records maximum. However, since you're only using the `totalCount` value (not the individual records), this step will return the total number of emails sent by this person. ### Step 4: Update the Person Record with a Create or Update Record action 1. Add a **Create or Update Record** action 2. Select **People** as the object 3. Find the person by matching their email to the `handle` from the Message Participant 4. Set your custom "Number of emails received" field to `{{searchRecords.totalCount}}` The `totalCount` value from the Search Records action represents the total number of emails received from this person. ## Related * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Create Custom Fields](/user-guide/data-model/how-tos/customize-your-data-model) * [Search Records Action](/user-guide/workflows/capabilities/workflow-actions#search-records) # Display Related Record Data Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/display-related-record-data Show data from related records (e.g., Company info on Opportunities) using workflows. Display data from related records directly on your records — for example, show the employee count from a Company on its Opportunities. This workflow workaround is useful until nested fields are natively available. ## Common Use Cases | Source | Destination | Fields to Copy | | ----------- | ----------- | ------------------------------- | | Company | Opportunity | Industry, Company Size, ARR | | Person | Opportunity | Email, Phone, Title | | Opportunity | Company | Last Deal Amount, Last Won Date | ## Basic Field Copy ### Example: Copy Contact Email to Opportunity **Goal**: When setting a Point of Contact on an opportunity, copy their email to the opportunity for easy access. ### Prerequisite Create the destination fields in **Settings → Data Model → Opportunities** before building the workflow: * Contact Email (type: Email) * Contact Phone (type: Phone) ### Setup 1. **Trigger**: Record is Updated (Opportunities, Point of Contact field) 2. **Filter**: Check that Point of Contact is not empty 3. **Search Records**: Find the linked person * Object: People * Filter: ID equals `{{trigger.object.pointOfContact.id}}` 4. **Update Record**: * Object: Opportunities * Record: `{{trigger.object.id}}` * Contact Email: `{{searchRecords[0].email}}` * Contact Phone: `{{searchRecords[0].phone}}` ## Copy Multiple Fields ### Example: Sync Company Info to All Related Opportunities **Goal**: When company details change, update all related opportunities. ### Setup 1. **Trigger**: Record is Updated (Companies) * Fields: Industry, Company Size, Annual Revenue 2. **Search Records**: Find all opportunities for this company * Object: Opportunities * Filter: Company ID equals `{{trigger.object.id}}` 3. **Iterator**: Loop through each opportunity 4. **Update Record** (inside iterator): * Object: Opportunities * Record: `{{iterator.currentItem.id}}` * Company Industry: `{{trigger.object.industry}}` * Company Size: `{{trigger.object.companySize}}` * Company ARR: `{{trigger.object.annualRevenue}}` ## Copy on Record Creation ### Example: Pre-fill Opportunity with Company Data **Goal**: When creating an opportunity linked to a company, automatically copy key company info. ### Prerequisite Create the destination fields in **Settings → Data Model → Opportunities**: * Company Industry (type: Text) * Company Size (type: Number) ### Setup 1. **Trigger**: Record is Created (Opportunities) * Filter: Company is not empty 2. **Search Records**: Get the linked company's details * Object: Companies * Filter: ID equals `{{trigger.object.company.id}}` 3. **Update Record**: * Object: Opportunities * Record: `{{trigger.object.id}}` * Company Industry: `{{searchRecords[0].industry}}` * Company Size: `{{searchRecords[0].employees}}` **Tasks and Notes limitation**: Relations on Tasks and Notes are hardcoded as many-to-many and are not yet available in workflow triggers or actions. To access these relations, use the [API](/developers/extend/api) instead. ## Bidirectional Sync ### Example: Keep Primary Contact in Sync **Goal**: When a company's primary contact changes, update the contact. When a person becomes primary, update the company. ### Workflow 1: Company → Person 1. **Trigger**: Record is Updated (Companies, Primary Contact field) 2. **Update Record**: Set person's "Is Primary Contact" to true 3. **Search Records**: Find previous primary contact 4. **Update Record**: Set previous contact's "Is Primary Contact" to false ### Workflow 2: Person → Company 1. **Trigger**: Record is Updated (People, Is Primary Contact = true) 2. **Update Record**: Set company's Primary Contact to this person Be careful with bidirectional syncs to avoid infinite loops. Use filters to check if the value actually changed before updating. ## Using Code for Complex Mapping ### Example: Transform Data During Copy **Goal**: Copy and format phone number from person to opportunity. ```javascript theme={null} export const main = async (params) => { const { phone } = params; if (!phone) return { formattedPhone: null }; // Remove non-numeric characters const digits = phone.replace(/\D/g, ''); // Format as (XXX) XXX-XXXX const formatted = digits.length === 10 ? `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}` : phone; return { formattedPhone: formatted }; }; ``` ## Best Practices ### Avoid Loops * Don't create workflows that trigger each other endlessly * Use specific field conditions * Add checks to see if value actually changed ### Handle Missing Data * Always check if source record exists before copying * Provide default values for optional fields * Use filters to skip when source field is empty ### Performance * Batch updates when copying to many records * Use scheduled workflows for bulk sync operations * Consider using Iterator for multiple record updates ## Related * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) # Formula Fields Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/formula-fields Create formula fields using workflows until native support is available. Twenty doesn't yet support native formula fields (coming in 2026), but you can achieve the same result using workflows. This workaround lets you automatically calculate and populate field values—from simple concatenations to complex business logic. ## Common Use Cases | Use Case | Formula Example | | ------------------- | --------------------------------- | | **Full name** | First Name + " " + Last Name | | **Expected amount** | Amount × Probability | | **Days until due** | Due Date - Today | | **Days in stage** | Today - Stage Entry Date | | **Lead score** | Points based on multiple criteria | For a complete example of tracking time in pipeline stages, see [Track How Long Opportunities Stay in Each Stage](/user-guide/views-pipelines/how-tos/track-time-in-stage). ## Basic Formula: Concatenation ### Example: Auto-Fill Full Name **Goal**: Automatically combine first and last name into a full name field. ### Setup 1. **Trigger**: Record is Updated or Created (People) 2. **Filter**: Check that first name or last name changed 3. **Code action**: ```javascript theme={null} export const main = async (params) => { const { firstName, lastName } = params; const fullName = [firstName, lastName] .filter(Boolean) .join(' '); return { fullName }; }; ``` 4. **Update Record**: Set Full Name to `{{code.fullName}}` ## Numeric Formula: Expected Amount ### Example: Calculate Expected Revenue **Goal**: Multiply opportunity amount by probability to get expected amount. See [How to Show Expected Amount in Pipeline](/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline) for the complete workflow. ### Setup 1. **Trigger**: Record is Updated (Opportunities, Amount OR Probability field) 2. **Code action**: ```javascript theme={null} export const main = async (params) => { const { amount, probability } = params; const expectedAmount = (amount || 0) * (probability || 0) / 100; return { expectedAmount }; }; ``` 3. **Update Record**: Set Expected Amount to `{{code.expectedAmount}}` ## Date Formula: Days Calculation ### Example: Days Until Task Due **Goal**: Calculate how many days remain until a task's due date. ### Setup 1. **Trigger**: Record is Updated or Created (Tasks, Due Date field) 2. **Code action**: ```javascript theme={null} export const main = async (params) => { const { dueDate } = params; if (!dueDate) { return { daysUntilDue: null }; } const due = new Date(dueDate); const today = new Date(); const diffTime = due - today; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return { daysUntilDue: diffDays }; }; ``` 3. **Update Record**: Set Days Until Due to `{{code.daysUntilDue}}` Negative values indicate overdue tasks. You can use this field to filter or sort tasks by urgency. ## Conditional Formula: Lead Score ### Example: Calculate Lead Score Based on Criteria **Goal**: Score leads based on company size, industry, and engagement. ### Setup 1. **Trigger**: Record is Updated (People or Companies) 2. **Code action**: ```javascript theme={null} export const main = async (params) => { const { companySize, industry, hasEmail, hasPhone, source } = params; let score = 0; // Company size scoring if (companySize === 'Enterprise') score += 30; else if (companySize === 'Mid-Market') score += 20; else if (companySize === 'SMB') score += 10; // Industry scoring const targetIndustries = ['Technology', 'Finance', 'Healthcare']; if (targetIndustries.includes(industry)) score += 25; // Contact info scoring if (hasEmail) score += 10; if (hasPhone) score += 15; // Source scoring if (source === 'Referral') score += 20; else if (source === 'Website') score += 10; return { leadScore: score }; }; ``` 3. **Update Record**: Set Lead Score to `{{code.leadScore}}` ## Text Formula: Domain Extraction ### Example: Extract Domain from Email **Goal**: Automatically extract and store the email domain. ### Setup 1. **Trigger**: Record is Updated (People, Email field) 2. **Code action**: ```javascript theme={null} export const main = async (params) => { const { email } = params; if (!email) return { domain: null }; const domain = email.split('@')[1]?.toLowerCase(); return { domain }; }; ``` 3. **Update Record**: Set Domain field to `{{code.domain}}` ## Best Practices ### Performance * Only trigger on relevant field changes * Use filters to skip records that don't need calculation * Avoid complex calculations in high-volume workflows ### Error Handling * Check for null/undefined values before calculations * Use default values when data is missing * Return clear error messages when calculations fail ### Testing * Test with edge cases (empty fields, zero values) * Verify calculations manually before activating * Monitor workflow runs for unexpected results ## Related * [How to Show Expected Amount in Pipeline](/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline) * [How to Track Time in Stage](/user-guide/views-pipelines/how-tos/track-time-in-stage) * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) # Notify Teammates of a Note to Review Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/notify-teammates-of-note-to-review Send automatic email notifications when assigning a colleague to review a note. **Tagging teammates directly in notes is not yet supported.** This feature is planned for the second half of 2026. In the meantime, this workflow provides a practical workaround. ## The Challenge You want to tag a colleague in a note so they receive a notification to review it. Today, Twenty doesn't support @mentions in notes or tasks. ## The Workaround Instead of tagging in the note text, you can: 1. Create a **Reviewer** relation field on the Notes object 2. Assign the reviewer when creating or editing the note 3. Have a workflow automatically send an email to the reviewer with a link to the note **Limitation**: This approach allows assigning one reviewer per field. If you need to notify multiple people, create additional relation fields (e.g., Reviewer 1, Reviewer 2). This won't scale well if you need to tag many collaborators at once. ## Step-by-Step Setup ### Step 1: Create the Reviewer Field 1. Go to **Settings → Data Model → Notes** 2. Click **Add Field** 3. Configure: | Setting | Value | | ------------------ | --------------------------------- | | **Name** | Reviewer | | **Type** | Relation | | **Related Object** | Workspace Member | | **Relation Type** | Many Notes → One Workspace Member | 4. Save the field ### Step 2: Create the Workflow #### Create the Workflow Trigger 1. Go to the main view and create a new workflow 2. Select **Record Updated** as the trigger 3. Choose **Note** as the object and specify the Reviewer field. 4. This will trigger whenever the reviewer of a note is updated. #### Add a Filter to Check Reviewer Was Just Assigned Add a **Filter** action to ensure the workflow only continues when a reviewer is assigned: | Condition | Value | | ------------ | ------------ | | **Reviewer** | is not empty | #### Search for the Workspace Member 1. Add a **Search Records** action 2. Select **Workspace Member** (under Advanced Objects) 3. Add a filter: | Condition | Value | | --------- | ---------------------------- | | **ID** | equals `{{note.reviewerId}}` | This retrieves the full workspace member record, including their email address and name. #### Send the Notification Email 1. Add a **Send Email** action 2. Configure: | Field | Value | | ----------- | --------------------------------------------------- | | **To** | Search Record → First Workspace Member → User Email | | **Subject** | A note needs your review | | **Body** | See example below | **Email body example:** ```text theme={null} Hi {{searchRecord.firstWorkspaceMember.name.firstName}}, You've been assigned to review a note. View the note here: https://yourSubDomain.twenty.com/object/note/{{recordIsUpdated.id}} Best, Twenty ``` Replace `yourSubDomain` with your actual Twenty workspace subdomain. #### Test and Activate 1. Click **Test** to run the workflow manually 2. Create or edit a note and assign a reviewer 3. Check the reviewer's inbox for the notification email 4. Activate the workflow ## Usage Once set up, your team can use this workflow by: 1. Creating a new note (or editing an existing one) 2. Selecting a colleague in the **Reviewer** field 3. The colleague automatically receives an email with a direct link to the note ## Related * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Send Emails from Workflows](/user-guide/workflows/capabilities/send-emails-from-workflows) * [Create Custom Fields](/user-guide/data-model/how-tos/create-custom-fields) * [Create Relation Fields](/user-guide/data-model/how-tos/create-relation-fields) # Send Email Alerts with Tasks Due Source: https://docs.twenty.com/user-guide/workflows/how-tos/crm-automations/send-email-alerts-with-tasks-due Automatically notify team members about their upcoming or overdue tasks. Send daily email reminders to each team member about their tasks due today. ## Overview This workflow runs on a schedule and: 1. Fetches all workspace members 2. Loops through each member 3. Finds their tasks due today 4. Formats and sends a personalized email ## Step-by-Step Setup ### Step 1: Configure the Trigger 1. Go to **Settings → Workflows** and create a new workflow 2. Select **On a Schedule** as the trigger 3. Use a cron expression for daily at 8:00 AM: `0 8 * * *` ### Step 2: Search for All Workspace Members 1. Add a **Search Records** action 2. Select **Workspace Members** (under advanced objects) 3. No filters needed — this returns all members ### Step 3: Add an Iterator 1. Add an **Iterator** action 2. Set the input array to the workspace members from the previous step 3. All actions inside the iterator will run once per member ### Step 4: Search for Tasks Due Today (Inside Iterator) 1. Inside the iterator, add a **Search Records** action 2. Select **Tasks** as the object 3. Add filters: * **Assignee** = current workspace member (from the iterator) * **Due Date** = today ### Step 5: Format Tasks into Email Body (Inside Iterator) Add a **Code** action to format the tasks into a readable list with links: ```javascript theme={null} export const main = async (params: { tasksDue?: Array<{ id: string; title: string }> | null | string; }) => { const tasksDue = typeof params.tasksDue === "string" ? JSON.parse(params.tasksDue) : params.tasksDue; if (!Array.isArray(tasksDue) || tasksDue.length === 0) { return { formattedTasks: "No tasks due today." }; } const formattedTasks = tasksDue .map( t => `${t.title}\nhttps://yourSubDomain.twenty.com/object/task/${t.id}` ) .join("\n\n"); return { formattedTasks }; }; ``` Replace `yourSubDomain` with your actual Twenty workspace subdomain. ### Step 6: Send Email (Inside Iterator) 1. Add a **Send Email** action (still inside the iterator) 2. Configure: | Field | Value | | ----------- | --------------------------------------------------------------- | | **To** | `{{iterator.currentItem.userEmail}}` (workspace member's email) | | **Subject** | Your Tasks Due Today | | **Body** | `{{code.formattedTasks}}` | ### Step 7: Test and Activate 1. Click **Test** to run the workflow manually 2. Check inboxes for the emails 3. Activate the workflow ## Related * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [Send Emails from Workflows](/user-guide/workflows/capabilities/send-emails-from-workflows) * [Handle Arrays in Code Actions](/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions) # Professional Services Source: https://docs.twenty.com/user-guide/workflows/how-tos/need-more-help/professional-services Get professional help building complex workflows and automations from Twenty's team and certified partners. ## When Do You Need Professional Help? Consider professional services for: * Complex multi-system integrations * Advanced business logic and automation rules * Large-scale data processing workflows * Custom API development * Team training and workflow optimization * When you don't have internal resources ## Service Options ### Implementation Partners Work with certified partners for advanced customizations. Contact us at [contact@twenty.com](mailto:contact@twenty.com) to connect with our [implementation partners](https://twenty.com/partners). # Workflow Troubleshooting Source: https://docs.twenty.com/user-guide/workflows/how-tos/need-more-help/workflow-troubleshooting Common workflow issues and how to resolve them. ## Common Issues and Solutions ### Workflow Not Triggering **Symptoms**: Your workflow doesn't run when you expect it to. **Possible Causes**: 1. **Workflow not activated**: Ensure the workflow is set to "Active" not "Draft" 2. **Trigger conditions not met**: Verify the trigger matches your expected event 3. **Field not monitored**: For "Record is Updated" triggers, ensure the specific field is being watched 4. **Permissions**: Check you have permission to run workflows **Solutions**: * Verify workflow status in the workflow list * Test with the specific action you expect to trigger it * Review trigger configuration * Contact your admin about permissions ### Workflow Triggers Too Early (Empty Fields) **Symptoms**: When manually creating a record in the UI, your workflow triggers before you've had time to fill in all the fields. The workflow runs with mostly empty field values. **Why this happens**: Twenty saves everything in real-time — there's no separate "edit" vs "read" mode. When you create a record, it's saved immediately, triggering the "Record is created" event before you can fill in additional fields. **When "Record is created" works well**: * Records created via API calls (fields are populated in a single request) * Records created via import * Automated record creation from other workflows **Solution**: For records created manually in the UI, use **"Record is created or updated"** as your trigger instead. This way: * The workflow triggers after the user has finished filling in and saving the fields * You get the complete data rather than empty values If you only want the workflow to run once per record, add a Filter action to check a field like `createdAt equals updatedAt` (first save) or use a custom checkbox field to track if the workflow has already run. ### Actions Failing **Symptoms**: Workflow runs but some actions fail. **Possible Causes**: 1. **Missing data**: Required fields are empty 2. **Invalid references**: Variables from previous steps don't exist 3. **API errors**: External services returning errors 4. **Permission issues**: Action requires permissions you don't have **Solutions**: * Check the workflow run details for error messages * Verify all required fields have values * Test API connections independently * Review role permissions ### HTTP Request Errors **Symptoms**: HTTP Request actions fail or return unexpected results. **Common Error Codes**: * **400**: Bad request - check your request body format * **401**: Unauthorized - verify API key * **403**: Forbidden - check API permissions * **404**: Not found - verify endpoint URL * **429**: Too many requests - implement rate limiting * **500**: Server error - external service issue **Solutions**: * Verify API endpoint URL * Check authentication headers * Test the API call outside of Twenty first * Add error handling in Code actions ### Code Action Errors **Symptoms**: JavaScript code fails to execute. **Common Issues**: 1. **Syntax errors**: Typos or invalid JavaScript 2. **Undefined variables**: Referencing variables that don't exist 3. **Type errors**: Operations on wrong data types 4. **Timeouts**: Code taking too long to execute **Solutions**: * Use the built-in code editor validation * Test code logic in a JavaScript console first * Add console.log statements for debugging * Simplify complex operations ### Email Not Sending **Symptoms**: Send Email action doesn't deliver emails. **Possible Causes**: 1. **No email account connected**: Check Settings → Accounts 2. **Invalid email address**: Recipient email is malformed 3. **Sending limits**: Email provider rate limits reached 4. **Spam filters**: Emails being blocked **Solutions**: * Verify email account connection * Validate recipient email addresses * Check email provider limits * Review email content for spam triggers ## Debugging Workflows ### Using Workflow Runs 1. Go to the workflow editor 2. Open the **Runs** panel 3. Find the failed run 4. Click to see step-by-step details 5. Review error messages and output data ### Testing Individual Steps 1. For Code actions, use the **Test** button 2. For HTTP requests, test the endpoint separately 3. Create test records to trigger workflows 4. Use manual triggers for controlled testing ### Common Debugging Patterns **Add logging**: Use Code actions to log intermediate values for debugging. **Isolate steps**: Test each step independently to identify failures. **Check data flow**: Verify that each step receives the expected input data. ## Best Practices to Avoid Issues ### Before Activation * Test thoroughly in draft mode * Validate all API connections * Review trigger conditions carefully * Document expected behavior ### During Development * Use descriptive step names * Add comments in Code actions * Test with realistic data * Plan for edge cases ### After Activation * Monitor initial runs closely * Set up alerts for failures * Review run history regularly * Keep workflows simple when possible # Workflows FAQ Source: https://docs.twenty.com/user-guide/workflows/how-tos/need-more-help/workflows-faq Frequently asked questions about workflows in Twenty. This is likely a permissions issue. You need access to workflows to create and activate them. **Solution**: Contact your workspace administrator to grant you workflow access under **Settings → Members → Roles**. If you don't see the Workflows section at all in your sidebar, this confirms it's a permissions issue. Manual workflows only appear in the navbar if properly configured: 1. The workflow must be **activated** (not in draft mode) 2. The navbar placement must be set to **Pinned** 3. For Single/Bulk triggers, you must be on the correct object page **To check**: Open the workflow → click the trigger → verify "Navbar placement" is set to "Pinned". You can always access manual workflows via **Cmd + K** (or **Ctrl + K**) regardless of navbar settings. | Type | Records Required | Workflow Runs | | ---------- | -------------------- | ---------------------------------- | | **Global** | None | Once, no record input | | **Single** | One or more selected | Once per selected record | | **Bulk** | One or more selected | Once, with all records as an array | * **Global**: Use when the workflow doesn't need any record context (e.g., generate a report) * **Single**: Use when you want to process each selected record independently (e.g., send individual emails) * **Bulk**: Use when you need to process records together or optimize credit usage (requires Iterator action) See [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) for details. An explicit If/Else node is not yet available but is on our roadmap. **Current workaround**: Create multiple branches from your step, each starting with a **Filter** action: ``` Step 1 │ ├── Branch A: Filter (condition = true) → Actions... │ └── Branch B: Filter (condition = false) → Actions... ``` Only the branch where the filter condition passes will execute its subsequent actions. See [How to Use Branches](/user-guide/workflows/capabilities/workflow-branches) for a step-by-step guide. **Yes**, branches run in parallel by default. If you want only one branch to execute: * Add a **Filter** action at the start of each branch * Set opposite conditions (e.g., Branch A: status = "Open", Branch B: status ≠ "Open") Branches that fail their filter condition stop executing, while others continue. **Yes**. After your parallel branches complete, you can add a step that both branches connect to. In the workflow editor: 1. Complete your branched actions 2. Add a new step after the branches 3. Drag connections from the end of each branch to this new step The merged step will execute after all connected branches complete. **Search Records returns a maximum of 200 records.** If you need to process more: * Add more specific filters to reduce results * Use scheduled workflows to process in batches * Consider using the API for bulk operations For most workflows, 200 records is sufficient. If you regularly hit this limit, consider restructuring your automation. **Not yet.** CC and BCC fields for the Send Email action are on our roadmap. **Current workaround**: Add multiple Send Email actions to send to additional recipients, or use an HTTP Request to send via an external email service that supports CC. Every action produces output data that can be used in subsequent steps. **To reference previous step data**: * Use the variable picker when configuring a field * Or type `{{stepName.fieldName}}` directly **Examples**: * Trigger data: `{{trigger.object.email}}` * Search results: `{{searchRecords[0].name}}` * Code output: `{{code.calculatedValue}}` Hover over any field in the action configuration to see available variables from previous steps. **Iterator requires an array input.** Common issues: 1. **Input is not an array**: Ensure you're passing results from Search Records or another action that returns an array 2. **Array is empty**: Add a filter before Iterator to check `{{searchRecords.length}} > 0` 3. **Wrong variable selected**: Make sure you select the array itself, not a single record **Correct setup**: 1. Search Records (returns array) 2. Filter: length > 0 3. Iterator: select `{{searchRecords}}` 4. Actions inside iterator use `{{iterator.currentItem.fieldName}}` Code actions (logic functions) have a **default timeout of 5 minutes** (300 seconds). The maximum configurable timeout is **15 minutes** (900 seconds). If your code exceeds this limit, the action will fail with a timeout error. **Tips to avoid timeouts**: * Break large operations into smaller chunks using Iterator * Avoid heavy computations; use external services via HTTP Request for intensive processing * Optimize your code to reduce execution time * If you need longer processing, consider using scheduled workflows that process data in batches Workflow runs show the execution history and help you debug issues. **Access runs**: * In workflow editor → **Runs** panel on the right * Or go to **Workflow Runs** in the sidebar **Understanding a run**: * **Status**: Running, Completed, Failed, Waiting * **Steps**: See which steps executed and their output * **Errors**: Click failed steps to see error messages * **Data**: View input/output data at each step See [Workflow Runs](/user-guide/workflows/capabilities/workflow-runs) for details. Workflow runs might be failing immediately due to rate limits. Twenty enforces a hard limit of 5,000 runs per hour per workspace. If you exceed this limit, workflows are immediately marked as failed and won't appear in your runs list. **Common scenarios that hit this limit**: * Selecting more than 5,000 records with a Single manual trigger * Multiple workflows running simultaneously across your workspace * High-frequency automated triggers (e.g., Record Updated on a busy object) **Solutions**: * Use **Bulk** triggers instead of Single to process many records in one run * Space out large batch operations * Use filters to reduce trigger frequency * Schedule heavy workflows during off-peak hours Twenty has two rate limits to ensure system stability: | Limit | Value | Behavior | | -------------- | --------------- | ------------------------------------------------------- | | **Soft limit** | 100 runs/minute | Runs queue in "Not Started" status, processed gradually | | **Hard limit** | 5,000 runs/hour | Runs immediately fail | **Soft limit (100/min)**: Your workflows won't fail—they just wait in the queue and are processed over time. You can trigger more than 100 records; execution will be slower. **Hard limit (5,000/hr)**: This applies to your entire workspace. If all your workflows combined exceed 5,000 runs in an hour, additional runs will fail immediately. **Tips to stay within limits**: * Use Bulk triggers with Iterator instead of Single triggers for large batches * Combine related automations into fewer workflows * Use scheduled workflows to spread load over time **No, there is no automatic retry functionality at the moment.** If a workflow run fails, you'll need to: 1. Review the error in **Settings → Workflows → \[Your Workflow] → Runs** 2. Fix the issue (data, configuration, or external service) 3. Manually trigger the workflow again on the affected record(s) **Tips to reduce failures**: * Add **Filter** nodes to validate data before actions * Use **Search Records** to check if related records exist * Test thoroughly with a few records before bulk operations Automatic retry functionality is on our roadmap for a future release. **Yes, if your workflows are triggered by record creation or updates.** When you import data via CSV, each record created or updated can trigger workflows. A large import (thousands of records) could: * Hit the 5,000 runs/hour limit * Consume significant workflow credits * Send unexpected emails or notifications * Create duplicate tasks or records **Before a mass import**: 1. Go to **Settings → Workflows** 2. Identify workflows triggered by the object you're importing 3. **Deactivate** them temporarily 4. Run your CSV import 5. **Reactivate** the workflows when done **Alternative**: If you need the workflows to run on imported data, import in smaller batches to stay within rate limits. If your workflow canvas looks messy with nodes scattered around, you can automatically organize it: 1. Right-click anywhere on the workflow canvas 2. Click **Tidy up workflow** This will automatically rearrange all nodes into a clean, organized layout. # Workflows Source: https://docs.twenty.com/user-guide/workflows/overview Learn how to build automations in Twenty. ## Why Workflows Matter Twenty was built to bring maximum flexibility to its users. Rather than forcing you to adapt your business processes to rigid, pre-built features, workflows enable you to build automations that create the CRM that best supports your unique business use cases. Workflows are Twenty's in-app feature for building these automations. They give you the building blocks to create exactly what your business needs, when it needs it. ## What can I do with workflows? We recommend building automations for two main purposes: 1. **Internal automations to facilitate your team's day-to-day**: Reduce the amount of manual entries and repetitive tasks that slow down your team. 2. **Bring data in and out of Twenty**: Connect Twenty via API calls and webhooks to your database and other tools. ## Building Your First Workflow ### Step 1: Create a New Workflow 1. Go to **Workflows** accessible below the other objects 2. Click **+ New Record** 3. Give your workflow a name ### Step 2: Add a Trigger Every workflow starts with a trigger. Choose from: * **Record events**: When a record is created, updated, or deleted * **Schedule**: Run at specific times (daily, weekly, etc.) * **Manual**: Triggered by a user action * **Webhook**: Triggered by a webhook ### Step 3: Add Actions After your trigger, add one or more actions: * **Create Record**: Add new records to any object * **Update Record**: Modify existing record data * **Delete Record**: Remove records from objects * **Search Records**: Find records matching criteria * **Upsert Record**: Create or update based on matching criteria * **Iterator**: Loop through arrays of records * **Filter**: Control which records proceed * **Delay**: Wait before continuing (duration or scheduled date) * **Send Email**: Send emails via your connected account * **Code**: Run custom JavaScript * **HTTP Request**: Call external APIs * **Form**: Get inputs from users within Twenty UI at the time of execution * **AI Agent** (Coming soon): Run intelligent AI tasks ### Step 4: Test and Activate 1. Use the **Test** button to run your workflow with sample data 2. Review the results to ensure it works as expected 3. Toggle the workflow **Active** when ready ## Workflow Best Practices * **Edit step names**: Rename your workflow steps to clearly describe what each one does. This helps with maintenance and makes it easier to hand off to coworkers * **Leverage previous step data**: You can use fields from records returned by any previous step in your workflow * **Start simple**: Begin with basic workflows and add complexity over time as you become more comfortable with the system * **Plan before building**: Map out your workflow logic before you start building to avoid getting stuck halfway through ## Next Steps * [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers) * [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions) * [CRM Automations](/user-guide/workflows/how-tos/crm-automations/closed-won-automations) ## Need Help? Want help automating your workflows? [Find a certified Twenty partner](https://twenty.com/partners/list?categories=SOLUTIONING\&ref=docs-workflows) to design and build your automations end-to-end. # Local Setup Source: https://docs.twenty.com/developers/contribute/capabilities/local-setup The guide for contributors (or curious developers) who want to run Twenty locally. ## Prerequisites Before you can install and use Twenty, make sure you install the following on your computer: * [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [Node v24.5.0](https://nodejs.org/en/download) * [yarn v4](https://yarnpkg.com/getting-started/install) * [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) `npm` won't work, you should use `yarn` instead. Yarn is now shipped with Node.js, so you don't need to install it separately. You only have to run `corepack enable` to enable Yarn if you haven't done it yet. 1. Install WSL Open PowerShell as Administrator and run: ```powershell theme={null} wsl --install ``` You should now see a prompt to restart your computer. If not, restart it manually. Upon restart, a PowerShell window will open and install Ubuntu. This may take up some time. You'll see a prompt to create a username and password for your Ubuntu installation. 2. Install and configure git ```bash theme={null} sudo apt-get install git git config --global user.name "Your Name" git config --global user.email "youremail@domain.com" ``` 3. Install nvm, node.js and yarn Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all contributors use the same version. ```bash theme={null} sudo apt-get install curl curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash ``` Close and reopen your terminal to use nvm. Then run the following commands. ```bash theme={null} nvm install # installs recommended node version nvm use # use recommended node version corepack enable ``` *** ## Step 1: Git Clone In your terminal, run the following command. If you haven't already set up SSH keys, you can learn how to do so [here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh). ```bash theme={null} git clone git@github.com:twentyhq/twenty.git ``` ```bash theme={null} git clone https://github.com/twentyhq/twenty.git ``` ## Step 2: Position yourself at the root ```bash theme={null} cd twenty ``` You should run all commands in the following steps from the root of the project. ## Step 3: Set up a PostgreSQL Database **Option 1 (preferred):** To provision your database locally: Use the following link to install PostgreSQL on your Linux machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/) ```bash theme={null} psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;" ``` Note: You might need to add `sudo -u postgres` to the command before `psql` to avoid permission errors. **Option 2:** If you have docker installed: ```bash theme={null} make -C packages/twenty-docker postgres-on-docker ``` **Option 1 (preferred):** To provision your database locally with `brew`: ```bash theme={null} brew install postgresql@16 export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH" brew services start postgresql@16 psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;" ``` You can verify if the PostgreSQL server is running by executing: ```bash theme={null} brew services list ``` The installer might not create the `postgres` user by default when installing via Homebrew on macOS. Instead, it creates a PostgreSQL role that matches your macOS username (e.g., "john"). To check and create the `postgres` user if necessary, follow these steps: ```bash theme={null} # Connect to PostgreSQL psql postgres or psql -U $(whoami) -d postgres ``` Once at the psql prompt (postgres=#), run: ```bash theme={null} # List existing PostgreSQL roles \du ``` You'll see output similar to: ```bash theme={null} Role name | Attributes | Member of -----------+-------------+----------- john | Superuser | {} ``` If you do not see a `postgres` role listed, proceed to the next step. Create the `postgres` role manually: ```bash theme={null} CREATE ROLE postgres WITH SUPERUSER LOGIN; ``` This creates a superuser role named `postgres` with login access. ```bash theme={null} Role name | Attributes | Member of -----------+-------------+----------- postgres | Superuser | {} john | Superuser | {} ``` **Option 2:** If you have docker installed: ```bash theme={null} make -C packages/twenty-docker postgres-on-docker ``` All the following steps are to be run in the WSL terminal (within your virtual machine) **Option 1:** To provision your PostgreSQL locally: Use the following link to install PostgreSQL on your Linux virtual machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/) ```bash theme={null} psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;" ``` Note: You might need to add `sudo -u postgres` to the command before `psql` to avoid permission errors. **Option 2:** If you have docker installed: Running Docker on WSL adds an extra layer of complexity. Only use this option if you are comfortable with the extra steps involved, including turning on [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl). ```bash theme={null} make -C packages/twenty-docker postgres-on-docker ``` You can now access the database at `localhost:5432`. If you used the Docker option above, the default credentials are user `postgres` and password `postgres`. For native PostgreSQL installations, use the credentials and roles configured on your machine. ## Step 4: Set up a Redis Database (cache) Twenty requires a Redis cache to provide the best performance. **Option 1:** To provision your Redis locally: Use the following link to install Redis on your Linux machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/) **Option 2:** If you have docker installed: ```bash theme={null} make -C packages/twenty-docker redis-on-docker ``` **Option 1 (preferred):** To provision your Redis locally with `brew`: ```bash theme={null} brew install redis ``` Start your Redis server: ```bash theme={null} brew services start redis ``` **Option 2:** If you have docker installed: ```bash theme={null} make -C packages/twenty-docker redis-on-docker ``` **Option 1:** To provision your Redis locally: Use the following link to install Redis on your Linux virtual machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/) **Option 2:** If you have docker installed: ```bash theme={null} make -C packages/twenty-docker redis-on-docker ``` If you need a client GUI, we recommend [Redis Insight](https://redis.io/insight/) (free version available). ## Step 5: Set up environment variables Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup). Copy the `.env.example` files in `/front` and `/server`: ```bash theme={null} cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env ``` **Multi-Workspace Mode:** By default, Twenty runs in single-workspace mode where only one workspace can be created. To enable multi-workspace support (useful for testing subdomain-based features), set `IS_MULTIWORKSPACE_ENABLED=true` in your server `.env` file. See [Multi-Workspace Mode](/developers/self-host/capabilities/setup#multi-workspace-mode) for details. ## Step 6: Installing dependencies To build Twenty server and seed some data into your database, run the following command: ```bash theme={null} yarn ``` Note that `npm` or `pnpm` won't work ## Step 7: Running the project Depending on your Linux distribution, Redis server might be started automatically. If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro. Redis should already be running. If not, run: ```bash theme={null} brew services start redis ``` Depending on your Linux distribution, Redis server might be started automatically. If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro. Set up your database with the following command: ```bash theme={null} npx nx database:reset twenty-server ``` Start the server, the worker and the frontend services: ```bash theme={null} npx nx start twenty-server npx nx worker twenty-server npx nx start twenty-front ``` Alternatively, you can start all services at once: ```bash theme={null} npx nx start ``` ## Step 8: Use Twenty **Frontend** Twenty's frontend will be running at [http://localhost:3001](http://localhost:3001). You can log in using the default demo account: `tim@apple.dev` (password: `tim@apple.dev`) **Backend** * Twenty's server will be up and running at [http://localhost:3000](http://localhost:3000) * The GraphQL API can be accessed at [http://localhost:3000/graphql](http://localhost:3000/graphql) * The REST API can be reached at [http://localhost:3000/rest](http://localhost:3000/rest) ## Troubleshooting If you encounter any problem, check [Troubleshooting](/developers/self-host/capabilities/troubleshooting) for solutions. # Commands Source: https://docs.twenty.com/developers/contribute/commands Useful commands for developing Twenty. Commands can be run from the repository root using `npx nx`. Use `npx nx run {project}:{command}` for explicit targeting. ## Starting the App ```bash theme={null} npx nx start twenty-front # Frontend dev server (http://localhost:3001) npx nx start twenty-server # Backend server (http://localhost:3000) npx nx run twenty-server:worker # Background worker ``` ## Database ```bash theme={null} npx nx database:reset twenty-server # Reset and seed database npx nx run twenty-server:database:migrate:prod # Run migrations npx nx run twenty-server:database:migrate:generate --name --type # Generate a migration ``` ## Linting ```bash theme={null} npx nx lint:diff-with-main twenty-front # Lint changed files (fastest) npx nx lint:diff-with-main twenty-server npx nx lint twenty-front --configuration=fix # Auto-fix ``` ## Type Checking ```bash theme={null} npx nx typecheck twenty-front npx nx typecheck twenty-server ``` ## Testing ```bash theme={null} # Frontend npx nx test twenty-front # Jest unit tests npx nx storybook:build twenty-front # Build Storybook npx nx storybook:test twenty-front # Storybook tests # Backend npx nx run twenty-server:test:unit # Unit tests npx nx run twenty-server:test:integration # Integration tests npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset # Single file (fastest) npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs ``` ## GraphQL ```bash theme={null} npx nx run twenty-front:graphql:generate # Regenerate types npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema ``` ## Translations ```bash theme={null} npx nx run twenty-front:lingui:extract # Extract strings npx nx run twenty-front:lingui:compile # Compile translations ``` ## Build ```bash theme={null} npx nx build twenty-shared # Must be built first npx nx build twenty-front npx nx build twenty-server ``` # Style Guide Source: https://docs.twenty.com/developers/contribute/style-guide Code conventions and best practices for contributing to Twenty. ## React ### Functional components only Always use TSX functional components with named exports. ```tsx theme={null} // ❌ Bad const MyComponent = () => { return
Hello World
; }; export default MyComponent; // ✅ Good export function MyComponent() { return
Hello World
; }; ``` ### Props Create a type named `{ComponentName}Props`. Use destructuring. Don't use `React.FC`. ```tsx theme={null} type MyComponentProps = { name: string; }; export const MyComponent = ({ name }: MyComponentProps) =>
Hello {name}
; ``` ### No single-variable prop spreading ```tsx theme={null} // ❌ Bad const MyComponent = (props: MyComponentProps) => ; // ✅ Good const MyComponent = ({ prop1, prop2 }: MyComponentProps) => ; ``` ## State Management ### Jotai atoms for global state ```tsx theme={null} import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; export const myAtomState = createAtomState({ key: 'myAtomState', defaultValue: 'default value', }); ``` * Prefer atoms over prop drilling * Don't use `useRef` for state — use `useState` or atoms * Use atom families and selectors for lists ### Avoid unnecessary re-renders * Extract `useEffect` and data fetching into sibling sidecar components * Prefer event handlers (`handleClick`, `handleChange`) over `useEffect` * Don't use `React.memo()` — fix the root cause instead * Limit `useCallback` / `useMemo` usage ```tsx theme={null} // ❌ Bad — useEffect in the same component causes re-renders export const Page = () => { const [data, setData] = useAtomState(dataState); const [dep] = useAtomState(depState); useEffect(() => { setData(dep); }, [dep]); return
{data}
; }; // ✅ Good — extract into sibling export const PageData = () => { const [data, setData] = useAtomState(dataState); const [dep] = useAtomState(depState); useEffect(() => { setData(dep); }, [dep]); return <>; }; export const Page = () => { const [data] = useAtomState(dataState); return
{data}
; }; ``` ## TypeScript * **`type` over `interface`** — more flexible, easier to compose * **String literals over enums** — except for GraphQL codegen enums and internal library APIs * **No `any`** — strict TypeScript enforced * **No type imports** — use regular imports (enforced by Oxlint `typescript/consistent-type-imports`) * **Use [Zod](https://github.com/colinhacks/zod)** for runtime validation of untyped objects ## JavaScript ```tsx theme={null} // Use nullish-coalescing (??) instead of || const value = process.env.MY_VALUE ?? 'default'; // Use optional chaining onClick?.(); ``` ## Naming * **Variables**: camelCase, descriptive (`email` not `value`, `fieldMetadata` not `fm`) * **Constants**: SCREAMING\_SNAKE\_CASE * **Types/Classes**: PascalCase * **Files/directories**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`) * **Event handlers**: `handleClick` (not `onClick` for the handler function) * **Component props**: prefix with component name (`ButtonProps`) * **Styled components**: prefix with `Styled` (`StyledTitle`) ## Styling Use [Linaria](https://github.com/callstack/linaria) styled components. Use theme values — avoid hardcoded `px`, `rem`, or colors. ```tsx theme={null} // ❌ Bad const StyledButton = styled.button` color: #333333; font-size: 1rem; margin-left: 4px; `; // ✅ Good const StyledButton = styled.button` color: ${({ theme }) => theme.font.color.primary}; font-size: ${({ theme }) => theme.font.size.md}; margin-left: ${({ theme }) => theme.spacing(1)}; `; ``` ## Imports Use aliases instead of relative paths: ```tsx theme={null} // ❌ Bad import { Foo } from '../../../../../testing/decorators/Foo'; // ✅ Good import { Foo } from '~/testing/decorators/Foo'; import { Bar } from '@/modules/bar/components/Bar'; ``` ## Folder Structure ``` front └── modules/ # Feature modules │ └── module1/ │ ├── components/ │ ├── constants/ │ ├── contexts/ │ ├── graphql/ (fragments, queries, mutations) │ ├── hooks/ │ ├── states/ (atoms, selectors) │ ├── types/ │ └── utils/ └── pages/ # Route-level components └── ui/ # Reusable UI components (display, input, feedback, ...) ``` * Modules can import from other modules, but `ui/` should stay dependency-free * Use `internal/` subfolders for module-private code * Components under 300 lines, services under 500 lines # APIs Source: https://docs.twenty.com/developers/extend/api REST and GraphQL APIs generated from your workspace schema. ## Schema-per-tenant APIs There is no static API reference for Twenty. Each workspace has its own schema — when you add a custom object (say `Invoice`), it immediately gets REST and GraphQL endpoints identical to built-in objects like `Company` or `Person`. The API is generated from the schema, so endpoints use your object and field names directly — no opaque IDs. Your workspace-specific API documentation is available under **Settings → API & Webhooks** after creating an API key. It includes an interactive playground where you can execute real calls against your data. ## Two APIs **Core API** — `/rest/` and `/graphql/` CRUD on records: People, Companies, Opportunities, your custom objects. Query, filter, traverse relations. **Metadata API** — `/rest/metadata/` and `/metadata/` Schema management: create/modify/delete objects, fields, and relations. This is how you programmatically change your data model. Both are available as REST and GraphQL. GraphQL adds batch upserts and the ability to traverse relations in a single query. Same underlying data either way. ## Base URLs | Environment | Base URL | | ----------- | ------------------------- | | Cloud | `https://api.twenty.com/` | | Self-Hosted | `https://{your-domain}/` | ## Authentication ``` Authorization: Bearer YOUR_API_KEY ``` Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. Keys can be scoped to a specific role under **Settings → Members → Roles → Assignment tab** to limit what they can access. For OAuth-based access (external apps acting on behalf of users), see [OAuth](/developers/extend/oauth). ## Batch operations Both REST and GraphQL support batching up to 60 records per request — create, update, or delete. GraphQL also supports batch upsert (create-or-update in one call) using plural names like `CreateCompanies`. ## Rate limits | Limit | Value | | ---------- | ------------------- | | Requests | 100 per minute | | Batch size | 60 records per call | # Application Config Source: https://docs.twenty.com/developers/extend/apps/config/application Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. Every app must have exactly one `defineApplication` call. It declares: * **Identity** — universal identifier, display name, description. * **Permissions** — which role its logic functions and front components run under. * **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. * **Pre-install / post-install / uninstall hooks** *(optional)* — see [Logic Functions](/developers/extend/apps/logic/logic-functions). ```ts src/application-config.ts theme={null} import { defineApplication } from 'twenty-sdk/define'; export default defineApplication({ universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', displayName: 'My Twenty App', description: 'My first Twenty app', applicationVariables: { DEFAULT_RECIPIENT_NAME: { universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', description: 'Default recipient name for postcards', value: 'Jane Doe', isSecret: false, }, }, }); ``` Notes: * `universalIdentifier` fields are deterministic IDs you own. Generate them once and keep them stable across syncs. * `applicationVariables` become environment variables for your functions and front components. In logic functions (server-side), they are available as `process.env.VARIABLE_NAME`. In front components, use `getApplicationVariable('VARIABLE_NAME')` from `twenty-sdk/front-component`. Variables marked with `isSecret: true` are only injected into logic functions. Front components receive only non-secret variables. * The default role is detected automatically from the role file marked with [`defineApplicationRole()`](/developers/extend/apps/config/roles) — you do not need to reference it from `defineApplication()`. * Pre-install, post-install, and uninstall functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. * Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`. * `serverVariables` are instance-scoped configuration and secrets (e.g. API keys). Unlike `applicationVariables`, they declare no value in the manifest — the workspace operator fills them in from the app's settings, and they are injected into logic functions only once set. * To render a custom configuration UI inside the app's **Settings** tab (in place of the default variable configuration section), declare a front component with [`defineSettingsFrontComponent()`](/developers/extend/apps/layout/front-components#custom-settings-component) in its own file. Only one is allowed per app. System-managed sections (auto-upgrade, App URL, connections) always remain visible. ## Variable types Both `applicationVariables` and `serverVariables` accept an optional `type` (and, for `SELECT` / `MULTI_SELECT`, an `options` list). Supported types: `TEXT` (default), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`. ```ts src/application-config.ts theme={null} import { defineApplication, FieldType } from 'twenty-sdk/define'; export default defineApplication({ // ...identity, role... applicationVariables: { MAX_POSTCARDS: { universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713', description: 'Maximum postcards per batch', type: FieldType.NUMBER, value: 10, }, DEFAULT_REGION: { universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227', description: 'Default shipping region', type: FieldType.SELECT, options: [ { label: 'Europe', value: 'eu' }, { label: 'United States', value: 'us' }, ], value: 'eu', }, }, }); ``` The `type` only affects **presentation and validation** — it selects the matching input in the workspace settings UI (a toggle, number field, dropdown, date picker, JSON editor, …) and lets the build validate your config (for example, `SELECT` / `MULTI_SELECT` must declare non-empty `options`). It does **not** change how the value reaches your code. Values are **always injected as strings** — this is inherent to environment variables (`process.env.*` is string-only). When your logic function runs, the executor serializes each value by its declared `type` while building `process.env`, so the string format is consistent no matter how the value was set (manifest default, settings UI, or a previous version): | Type | `process.env` string | | ------------------------------------- | -------------------------------------- | | `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | the raw value (`"eu"`, `"2026-01-01"`) | | `BOOLEAN` | `"true"` / `"false"` | | `NUMBER`, `NUMERIC` | decimal string (`"10"`, `"2.5"`) | | `MULTI_SELECT`, `ARRAY` | JSON array (`'["email","postcard"]'`) | | `RAW_JSON`, `RICH_TEXT` | JSON object (`'{"retries":3}'`) | Parse the string back into the type you expect: ```ts theme={null} const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10 const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"] const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 } ``` The same applies to front components reading values via `getApplicationVariable('VARIABLE_NAME')` — the returned value is a string; parse it as needed. ## Default function role The role declared with [`defineApplicationRole()`](/developers/extend/apps/config/roles) controls what the app's logic functions and front components can access: * The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. * The typed API client is restricted to the permissions granted to that role. * Follow least-privilege: declare only the permissions your functions need. When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/developers/extend/apps/config/roles) for the full reference. ## Marketplace metadata If you plan to [publish your app](/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: | Field | Description | | ------------------ | -------------------------------------------------------------------------------------------------------------------- | | `author` | Author or company name | | `category` | App category for marketplace filtering | | `logo` | Path to your app logo bundled in `public/` (e.g., `public/logo.png`) | | `galleryImages` | Array of gallery image paths bundled in `public/` (e.g., `public/screenshot-1.png`) | | `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | | `websiteUrl` | Link to your website | | `termsUrl` | Link to terms of service | | `emailSupport` | Support email address | | `issueReportUrl` | Link to issue tracker | `logoUrl` and `screenshots` are deprecated aliases of `logo` and `galleryImages`. External absolute URLs (`http://` or `https://`) are not supported for these fields: they are dropped with a warning at build time. Bundle the images in your app's `public/` folder instead. # Install Hooks Source: https://docs.twenty.com/developers/extend/apps/config/install-hooks Run logic during the install, upgrade, or uninstall lifecycle — seed data, back up records, validate the upgrade, clean up external resources. Install hooks are special logic functions that run during the install, upgrade, or uninstall lifecycle. They share the same handler runtime as regular [logic functions](/developers/extend/apps/logic/logic-functions), but they're declared with their own define functions and live outside the normal trigger model (HTTP, cron, database events). Install hooks receive an `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` is `undefined` on a fresh install); the uninstall hook receives an `UninstallPayload` (`{ version?: string }` — the version being removed). Each app may define **at most one** of each hook (pre-install, post-install, uninstall). The manifest build errors if more than one of any kind is detected. ``` ┌─────────────────────────────────────────────────────────────┐ │ install flow │ │ │ │ upload package → [pre-install] → metadata migration → │ │ generate SDK → [post-install] │ │ │ │ old schema visible new schema visible │ └─────────────────────────────────────────────────────────────┘ ``` ## At a glance | | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` | | ----------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Runs | Before the metadata migration — the **previous** schema and data are still intact | After the migration and SDK generation — the **new** schema is in place | | Execution | Always synchronous; blocks the install | Async by default (queued, 3 retries); sync opt-in via `shouldRunSynchronously: true` | | On failure | Install is **aborted** before any schema change | Async: retried up to 3 times. Sync: caller receives `POST_INSTALL_ERROR` (schema changes are **not** rolled back) | | Typical use | Back up or fix data a migration would lose; refuse a risky upgrade by throwing | Seed default data, configure the workspace, register external resources | **Rule of thumb:** default to post-install. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone. | You want to... | Use | | --------------------------------------------------------------------- | -------------------------------------------------------- | | Seed data, configure the workspace, register external resources | `post-install` | | Long-running work that shouldn't block the install response | `post-install` (default async mode, with worker retries) | | Fast setup the caller relies on immediately after the install returns | `post-install` with `shouldRunSynchronously: true` | | Read or back up data that the upcoming migration would lose | `pre-install` | | Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) | | Reconciliation on every upgrade | Either hook with `shouldRunOnVersionUpgrade: true` | ## Behavior shared by both hooks * The config is a `defineLogicFunction` config minus the trigger settings, plus `shouldRunOnVersionUpgrade`. * **When it runs**: fresh installs only, by default. Set `shouldRunOnVersionUpgrade: true` to also run on upgrades. Use `previousVersion` / `newVersion` to branch on the upgrade path. * **Idempotency matters**: async post-install may be retried, and either hook re-runs on upgrades when `shouldRunOnVersionUpgrade` is on. * The usual logic-function environment (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) is injected, so you can call the Twenty API with your app's token. * The hook is attached to the application manifest automatically at build time (`preInstallLogicFunction` / `postInstallLogicFunction`) — nothing to reference in [`defineApplication()`](/developers/extend/apps/config/application). * The default `timeoutSeconds` is 300 to allow longer setup tasks like data seeding. * **Not executed in dev mode**: `yarn twenty dev` skips the install flow and syncs files directly, so hooks never run there. Trigger them manually instead: ```bash filename="Terminal" theme={null} yarn twenty dev:function:exec --postInstall yarn twenty dev:function:exec --preInstall ``` Runs once your app has finished installing: metadata synchronized, SDK client generated, new schema queryable. Example — seed a default record on fresh installs: ```ts src/logic-functions/post-install.ts theme={null} import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async ({ previousVersion }: InstallPayload): Promise => { if (previousVersion) return; // fresh installs only const client = new CoreApiClient(); await client.mutation({ createPostCard: { __args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } }, id: true, }, }); }; export default definePostInstallLogicFunction({ universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', name: 'post-install', description: 'Seeds a welcome post card after install.', timeoutSeconds: 300, shouldRunOnVersionUpgrade: false, shouldRunSynchronously: false, handler, }); ``` The `shouldRunSynchronously` flag controls the execution model: * `false` *(default)* — enqueued on the message queue (`retryLimit: 3`) and run by a worker. The install response returns as soon as the job is enqueued. **Use for long-running work** — seeding large datasets, slow third-party APIs. * `true` — executed inline during the install flow. The install request blocks until the handler finishes; a thrown error surfaces as `POST_INSTALL_ERROR` to the caller (no retries). **Use for fast, must-complete-before-response work.** The migration has already been applied at this point, so a failure does not roll back schema changes — it only surfaces the error. Runs before the metadata migration, against the **previous** schema — the right place to back up data a migration would lose, or to refuse a risky upgrade. Before executing, the server runs a purely additive "pared-down sync" that registers only the new version's pre-install function; everything else — the previous version's objects, fields, and data — is untouched when your handler runs. Pre-install is always **synchronous** and blocks the install. If the handler throws, the install is aborted before any schema change — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade. Example — copy a legacy field's values before the migration drops it: ```ts src/logic-functions/pre-install.ts theme={null} import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { // Only the 1.x → 2.x upgrade drops the legacy `notes` field. if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { return; } const client = new CoreApiClient(); const { postCards } = await client.query({ postCards: { __args: { filter: { notes: { isNot: null } } }, edges: { node: { id: true, notes: true } }, }, }); // Copy legacy `notes` into `description` before the migration drops the // column. If this fails, the upgrade aborts and the workspace stays on v1. for (const { node } of postCards.edges) { await client.mutation({ updatePostCard: { __args: { id: node.id, data: { description: node.notes } }, id: true, }, }); } }; export default definePreInstallLogicFunction({ universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', name: 'pre-install', description: 'Backs up legacy notes into description before the v2 migration.', timeoutSeconds: 300, shouldRunOnVersionUpgrade: true, handler, }); ``` ## Uninstall hook `defineUninstallLogicFunction` declares a hook that runs when a user uninstalls your app. It executes **before** the app's metadata, data, and code are removed — once the deletion migration runs there is nothing left to execute — so your handler can still query the app's objects and records. Use it for cleanup of external resources: deprovision API resources, delete remaining bots, revoke webhooks. Notes: * The hook is best-effort: it runs synchronously, but a failure is logged and **never blocks the uninstall** — cleanup must not make an app impossible to remove. * It receives `UninstallPayload` (`{ version?: string }` — the version being removed). * It does **not** run when a failed fresh install is rolled back — the app never finished installing. * The hook cannot run after the app is gone, so external cleanup that depends on app data (e.g. bot IDs stored in records) belongs here, not in an external scheduled job. * Like the install hooks, it is **not executed in dev mode** — trigger it manually instead: ```bash filename="Terminal" theme={null} yarn twenty dev:function:exec --uninstall ``` ```ts src/logic-functions/uninstall.ts theme={null} import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define'; import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async (_payload: UninstallPayload): Promise => { const client = new CoreApiClient(); const { meetingBots } = await client.query({ meetingBots: { edges: { node: { id: true, externalBotId: true } } }, }); // Delete the provider-side bots so nothing keeps recording after uninstall. for (const { node } of meetingBots.edges) { await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` }, }); } }; export default defineUninstallLogicFunction({ universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc', name: 'uninstall', description: 'Deletes remaining recorder bots when the app is uninstalled.', timeoutSeconds: 300, handler, }); ``` # Overview Source: https://docs.twenty.com/developers/extend/apps/config/overview Configure the app itself — its identity, default permissions, and what runs at install time. A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*. ```text theme={null} ┌────────────────────────────────────────────────────────┐ │ Application — identity, default role, variables, │ │ marketplace metadata │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Role — what the app's logic functions can read │ │ │ │ and write (referenced by Application) │ │ │ └──────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────┘ │ ▼ (at install / upgrade time) ┌──────────────────────────────────┐ │ Pre-install hook │ before metadata migration └──────────────────────────────────┘ ┌──────────────────────────────────┐ │ Post-install hook │ after metadata migration └──────────────────────────────────┘ │ ▼ (at uninstall time) ┌──────────────────────────────────┐ │ Uninstall hook │ before app removal └──────────────────────────────────┘ ``` ## In this section `defineApplication` — identity, default role, variables, marketplace metadata. `defineRole` — declare what your app's logic functions can read and write. `definePreInstallLogicFunction`, `definePostInstallLogicFunction`, and `defineUninstallLogicFunction` — back up data, seed defaults, validate upgrades, clean up on removal. ## How the pieces relate * **Application** is the entry point. Every app has exactly one `defineApplication()` call, and it points at one **Role** as its default. * The **Role** controls what the app's logic functions and front components can read and write. Follow least-privilege: only grant the permissions your code actually needs. * **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema). The uninstall hook runs right before the app is removed, so it can clean up external resources while the app's data is still readable. Install hooks share the [logic function](/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events). # Public Assets Source: https://docs.twenty.com/developers/extend/apps/config/public-assets Ship static files — images, icons, fonts — alongside your app via the public/ folder. The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server. Files placed in `public/` are: * **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them. * **Available in front components** — use asset URLs to display images, icons, or any media inside your React components. * **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic. * **Used for marketplace metadata** — the `logo` and `galleryImages` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published. External absolute URLs are ignored for these fields — bundle the images in `public/` instead. Each image must not exceed 10 MB. * **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed. * **Included in builds** — `yarn twenty dev:build` bundles all public assets into the distribution output. ## Accessing public assets with `getPublicAssetUrl` Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**. **In a logic function:** ```ts src/logic-functions/send-invoice.ts theme={null} import { defineLogicFunction } from 'twenty-sdk/define'; import { getPublicAssetUrl } from 'twenty-sdk/utils'; const handler = async (): Promise => { const logoUrl = getPublicAssetUrl('logo.png'); const invoiceUrl = getPublicAssetUrl('templates/invoice.png'); // Fetch the file content (no auth required — public endpoint) const response = await fetch(invoiceUrl); const buffer = await response.arrayBuffer(); return { logoUrl, size: buffer.byteLength }; }; export default defineLogicFunction({ universalIdentifier: 'a1b2c3d4-...', name: 'send-invoice', description: 'Sends an invoice with the app logo', timeoutSeconds: 10, handler, }); ``` **In a front component:** ```tsx src/front-components/company-card.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { getPublicAssetUrl } from 'twenty-sdk/utils'; const CompanyCard = () => { const logoUrl = getPublicAssetUrl('logo.png'); return App logo; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'company-card', component: CompanyCard, }); ``` The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present. # Roles & Permissions Source: https://docs.twenty.com/developers/extend/apps/config/roles Declare what objects and fields your app's logic functions and front components can read and write. A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role marked with `defineApplicationRole()` (see [The default function role](#the-default-function-role) below). ```ts src/roles/restricted-company-role.ts theme={null} import { defineRole, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, SystemPermissionFlag, } from 'twenty-sdk/define'; export default defineRole({ universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6', label: 'My new role', description: 'A role that can be used in your workspace', canReadAllObjectRecords: false, canUpdateAllObjectRecords: false, canSoftDeleteAllObjectRecords: false, canDestroyAllObjectRecords: false, canUpdateAllSettings: false, canBeAssignedToAgents: false, canBeAssignedToUsers: false, canBeAssignedToApiKeys: false, objectPermissions: [ { objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, }, ], fieldPermissions: [ { objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, fieldUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name .universalIdentifier, canReadFieldValue: false, canUpdateFieldValue: false, }, ], permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS], }); ``` ## Row-level security Object and field permissions decide *which objects and fields* a role can touch. **Row-level permission predicates** go further and decide *which records* a role can see and act on — for example, a self-service role where each external user sees only their own records. Declare predicates with `rowLevelPermissionPredicates` on the role. Like the rest of the manifest, each predicate carries its own `universalIdentifier`, and references an object and a field by their `universalIdentifier`, an `operand`, and (optionally) a workspaceMember field whose value is injected at query time — so you can express "the record's owner relation **is** the current workspace member": ```ts src/roles/partner-role.ts theme={null} import { defineRole, RowLevelPermissionPredicateOperand, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field'; export default defineRole({ universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343', label: 'Partner', description: 'External partner — sees only its own records', canBeAssignedToUsers: true, objectPermissions: [ { objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, canReadObjectRecords: true, canUpdateObjectRecords: true, }, ], rowLevelPermissionPredicates: [ { universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111', objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER, operand: RowLevelPermissionPredicateOperand.IS, workspaceMemberFieldUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id .universalIdentifier, }, ], }); ``` Because predicates ship in the manifest, they are created, updated, and removed together with the role on every install and upgrade — there is no separate post-install step to keep in sync. ### Combining predicates with groups By default a role's predicates are combined with `AND`. To combine some of them with `OR` (or to nest logic), declare a `rowLevelPermissionPredicateGroups` entry and point each predicate at it via `predicateGroupUniversalIdentifier`. This role lets a partner see an Opportunity it either **owns** or is the **point of contact** for: ```ts src/roles/partner-opportunities-role.ts theme={null} import { defineRole, RowLevelPermissionPredicateGroupLogicalOperator, RowLevelPermissionPredicateOperand, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity; const CURRENT_MEMBER = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id .universalIdentifier; export default defineRole({ universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222', label: 'Partner (opportunities)', canBeAssignedToUsers: true, objectPermissions: [ { objectUniversalIdentifier: OPPORTUNITY.universalIdentifier, canReadObjectRecords: true, }, ], rowLevelPermissionPredicateGroups: [ { universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333', objectUniversalIdentifier: OPPORTUNITY.universalIdentifier, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR, }, ], rowLevelPermissionPredicates: [ { universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444', objectUniversalIdentifier: OPPORTUNITY.universalIdentifier, fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier, operand: RowLevelPermissionPredicateOperand.IS, workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER, predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333', }, { universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555', objectUniversalIdentifier: OPPORTUNITY.universalIdentifier, fieldUniversalIdentifier: OPPORTUNITY.fields.pointOfContact.universalIdentifier, operand: RowLevelPermissionPredicateOperand.IS, workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER, predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333', }, ], }); ``` Notes: * Give every predicate and group a stable `universalIdentifier` (any uuid) — it keys the entity across upgrades, and predicates reference groups by it. * Predicates can reference objects and fields owned by your app or by Twenty's standard objects. * Row-level security is enforced for workspaces on plans that include it; the predicates still sync on other plans, they are simply not enforced. ## The default function role When you scaffold a new app, the CLI creates a default role file declared with `defineApplicationRole()`: ```ts src/roles/default-role.ts theme={null} import { defineApplicationRole } from 'twenty-sdk/define'; export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineApplicationRole({ universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: true, canUpdateAllObjectRecords: false, canSoftDeleteAllObjectRecords: false, canDestroyAllObjectRecords: false, canUpdateAllSettings: false, canBeAssignedToAgents: false, canBeAssignedToUsers: false, canBeAssignedToApiKeys: false, objectPermissions: [], fieldPermissions: [], permissionFlagUniversalIdentifiers: [], }); ``` `defineApplicationRole()` is a thin wrapper around `defineRole()` that flags **the** role used as your application's default at install time. Validation is identical to `defineRole`, but the build pipeline auto-wires its `universalIdentifier` into the application manifest's `defaultRoleUniversalIdentifier` — so you do not need to reference it from [`defineApplication`](/developers/extend/apps/config/application) yourself. Notes: * Exactly **one** `defineApplicationRole(...)` is allowed per app — the manifest build will fail if it finds more than one. * Use `defineRole()` (not `defineApplicationRole()`) for any **additional** roles your app ships. * Setting `defaultRoleUniversalIdentifier` explicitly on `defineApplication()` is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`. ## Best practices * Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production. * Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need. * `permissionFlagUniversalIdentifiers` control access to platform-level capabilities. Keep them minimal. * See a working example: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts). # Extending Objects Source: https://docs.twenty.com/developers/extend/apps/data/extending-objects Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField. Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend. ```ts src/fields/company-loyalty-tier.field.ts theme={null} import { defineField, FieldType } from 'twenty-sdk/define'; export default defineField({ universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890', objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object name: 'loyaltyTier', type: FieldType.SELECT, label: 'Loyalty Tier', icon: 'IconStar', options: [ { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' }, { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' }, { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' }, ], }); ``` ## Key points * `objectUniversalIdentifier` identifies the target object. For standard Twenty objects, import the constant from `twenty-sdk`: ```ts theme={null} import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier // … ``` * When defining fields **inline inside `defineObject()`**, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object. * `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`. * File location is up to you. The convention is `src/fields/.field.ts`, but the SDK detects fields anywhere in `src/`. * To add a tab to a standard page layout (e.g. the Task or Company detail page), use [`definePageLayoutTab`](/developers/extend/apps/layout/page-layouts#definepagelayouttab) with `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` from `twenty-sdk/define`. ## Adding a relation to an existing object To add a relation field (e.g. linking your custom object to a standard `Person`), use `defineField()` with `FieldType.RELATION`. The pattern is the same as for inline relations but with `objectUniversalIdentifier` set explicitly. See [Relations](/developers/extend/apps/data/relations) for the bidirectional pattern. # Objects Source: https://docs.twenty.com/developers/extend/apps/data/objects Declare new record types — custom tables with their own fields — using defineObject. Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys. ```ts src/objects/post-card.object.ts theme={null} import { defineObject, FieldType } from 'twenty-sdk/define'; enum PostCardStatus { DRAFT = 'DRAFT', SENT = 'SENT', DELIVERED = 'DELIVERED', RETURNED = 'RETURNED', } export default defineObject({ universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', nameSingular: 'postCard', namePlural: 'postCards', labelSingular: 'Post Card', labelPlural: 'Post Cards', description: 'A post card object', icon: 'IconMail', fields: [ { universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', name: 'content', type: FieldType.TEXT, label: 'Content', description: "Postcard's content", icon: 'IconAbc', }, { universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', name: 'recipientName', type: FieldType.FULL_NAME, label: 'Recipient name', icon: 'IconUser', }, { universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', name: 'recipientAddress', type: FieldType.ADDRESS, label: 'Recipient address', icon: 'IconHome', }, { universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', name: 'status', type: FieldType.SELECT, label: 'Status', icon: 'IconSend', defaultValue: `'${PostCardStatus.DRAFT}'`, options: [ { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' }, { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' }, { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' }, { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' }, ], }, { universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', name: 'deliveredAt', type: FieldType.DATE_TIME, label: 'Delivered at', icon: 'IconCheck', isNullable: true, defaultValue: null, }, ], }); ``` ## Key points * The `universalIdentifier` must be unique and stable across deployments. * Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`. * The `fields` array is optional — you can define objects without custom fields. * `openRecordIn` sets where records of this object open when clicked: `ObjectOpenRecordIn.USER_CHOICE` (the default, following each workspace member's own preference from Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL`, or `ObjectOpenRecordIn.RECORD_PAGE`. Pin it to `RECORD_PAGE` for records that need a full page to be usable, the way workflows and dashboards do, or to `SIDE_PANEL` for records that only make sense as a quick panel, the way calendar events do. * Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/developers/extend/apps/data/extending-objects) to add fields to objects you don't own. * You can scaffold new objects with `yarn twenty dev:add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/developers/extend/apps/getting-started/scaffolding). **Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea. ## Field types The full set of `FieldType` values, exported from `twenty-sdk/define`: | Category | Types | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | Text | `TEXT`, `RICH_TEXT`, `ARRAY` (of strings), `RAW_JSON` | | Numeric | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (arbitrary precision), `RATING`, `POSITION` | | Dates | `DATE`, `DATE_TIME` | | Choice | `BOOLEAN`, `SELECT`, `MULTI_SELECT` | | Composite | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` | | Identifiers & relations | `UUID`, `RELATION`, `MORPH_RELATION` (see [Relations](/developers/extend/apps/data/relations)) | | System | `TS_VECTOR` (full-text search vector, managed by the server) | Composite types store multiple sub-fields (e.g. `FULL_NAME` = first + last name; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` and `MULTI_SELECT` require an `options` array as in the example above. ## Default values Literal string defaults must be wrapped in single quotes **inside** the string — `defaultValue: "'Draft'"`, not `defaultValue: "Draft"`. That's why the `status` field above uses `` `'${PostCardStatus.DRAFT}'` ``. Unquoted strings are reserved for computed defaults, evaluated when a record is created: * `'uuid'` — generates a UUID (for `UUID` fields) * `'now'` — the current timestamp (for `DATE_TIME` fields) The same convention applies to string sub-fields of composite defaults (e.g. `{ source: "'MANUAL'" }` on an `ACTOR` field) and to `SELECT`/`MULTI_SELECT` values. A literal string default left unquoted raises a warning when your app is built. ## Nullability `isNullable` controls whether a field accepts `NULL`. It defaults to `true` — omit it for optional fields. Set `isNullable: false` to make a field required at the database level. Changes to `isNullable` are applied on every sync, including syncs that update an existing field — so you can flip a field's nullability by editing the manifest and re-syncing. **Making an existing field non-nullable requires a default value.** When you change a field to `isNullable: false`, you must also provide a non-null `defaultValue`. The default backfills any existing `NULL` rows before the `NOT NULL` constraint is applied; without it the sync fails with `Default value cannot be null for non-nullable fields`. Relation fields and `TS_VECTOR` fields are always nullable, so `isNullable` has no effect on them. ```ts theme={null} { universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000', name: 'reference', type: FieldType.TEXT, label: 'Reference', isNullable: false, defaultValue: "'N/A'", } ``` ## What's next * **Connect this object to others** — see [Relations](/developers/extend/apps/data/relations) for the bidirectional relation pattern. * **Add fields to objects from other apps** — see [Extending Objects](/developers/extend/apps/data/extending-objects) for `defineField()`. * **Display this object in the UI** — see [Navigation Menu Items](/developers/extend/apps/layout/navigation-menu-items) to add a sidebar entry; see [Views](/developers/extend/apps/layout/views) to add custom list configurations. # Overview Source: https://docs.twenty.com/developers/extend/apps/data/overview Shape the data your app adds to a workspace — objects, fields, and relations. A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other. ```text theme={null} ┌──────────────────────────────────────────────────┐ │ Object — a record type, e.g. PostCard │ │ ├─ Field (name, type, label) │ │ ├─ Field │ │ └─ Relation (link to another object) │ └──────────────────────────────────────────────────┘ │ ├── lives in your app, OR │ ▼ ┌──────────────────────────────────────────────────┐ │ Standard / other apps' objects │ │ └─ Field added by your app via defineField │ └──────────────────────────────────────────────────┘ ``` ## In this section `defineObject` — declare new record types with their own fields. `defineField` — add fields to standard or other apps' objects. Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects. ## Entities at a glance | Entity | Purpose | Defined with | | ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` | | **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` | | **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` | | **Index** | A database index to speed up a recurring query on one of your objects | `defineIndex()` | The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/`, `src/fields/`, and `src/indexes/`. Stable `universalIdentifier` UUIDs tie everything together across deploys. ## Indexes (optional) Apps can ship indexes alongside their objects to keep recurring queries fast. The most common case is a status or foreign-key column that you read frequently. ```ts src/indexes/post-card-status.index.ts theme={null} import { defineIndex } from 'twenty-sdk/define'; import { POST_CARD_UNIVERSAL_IDENTIFIER, STATUS_FIELD_UNIVERSAL_IDENTIFIER, } from '../objects/post-card.object'; export default defineIndex({ universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0', objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, fields: [ { universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1', fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER, }, ], }); ``` ### Unique indexes `defineIndex` accepts `isUnique: true` for both single- and multi-column uniqueness. This is the recommended primitive — `defineField({ isUnique: true })` is deprecated and will be removed in a future release. ```ts theme={null} defineIndex({ universalIdentifier: '…', objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER, isUnique: true, fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }], }); ``` ### Other constraints * Partial `WHERE` clauses stay under admin control — apps can't declare them. * Each object is capped at 10 custom indexes (the framework's own indexes don't count). Order the `fields` array the way Postgres should use it — leftmost column first, like a phone book. Indexes are not free: every write to the table updates them. Add one only when you have a query that needs it. Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/developers/extend/apps/logic/connections). # Relations Source: https://docs.twenty.com/developers/extend/apps/data/relations Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations. Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other. | Relation type | Description | Has foreign key? | | ------------- | ------------------------------------------------------------- | ---------------------- | | `MANY_TO_ONE` | Many records of this object point to one record of the target | Yes (`joinColumnName`) | | `ONE_TO_MANY` | One record of this object has many records of the target | No (the inverse side) | ## How relations work Every relation requires **two fields** that reference each other: 1. The **MANY\_TO\_ONE** side — lives on the object that holds the foreign key. 2. The **ONE\_TO\_MANY** side — lives on the object that owns the collection. Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`. ## Example: Post Card has many Recipients A `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card. **Step 1: Define the ONE\_TO\_MANY side on PostCard** (the "one" side): ```ts src/fields/post-card-recipients-on-post-card.field.ts theme={null} import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; // Export so the other side can reference it export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111'; // Import from the other side import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field'; export default defineField({ universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, type: FieldType.RELATION, name: 'postCardRecipients', label: 'Post Card Recipients', icon: 'IconUsers', relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID, universalSettings: { relationType: RelationType.ONE_TO_MANY, }, }); ``` **Step 2: Define the MANY\_TO\_ONE side on PostCardRecipient** (the "many" side — holds the foreign key): ```ts src/fields/post-card-on-post-card-recipient.field.ts theme={null} import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object'; import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object'; // Export so the other side can reference it export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222'; // Import from the other side import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field'; export default defineField({ universalIdentifier: POST_CARD_FIELD_ID, objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER, type: FieldType.RELATION, name: 'postCard', label: 'Post Card', icon: 'IconMail', relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, universalSettings: { relationType: RelationType.MANY_TO_ONE, onDelete: OnDeleteAction.CASCADE, joinColumnName: 'postCardId', }, }); ``` **Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. The build system resolves these at compile time. ## Relating to standard objects To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`: ```ts src/fields/person-on-self-hosting-user.field.ts theme={null} import { defineField, FieldType, RelationType, OnDeleteAction, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object'; export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333'; export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444'; export default defineField({ universalIdentifier: PERSON_FIELD_ID, objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER, type: FieldType.RELATION, name: 'person', label: 'Person', description: 'Person matching with the self hosting user', isNullable: true, relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID, universalSettings: { relationType: RelationType.MANY_TO_ONE, onDelete: OnDeleteAction.SET_NULL, joinColumnName: 'personId', }, }); ``` ## Relation field properties | Property | Required | Description | | ------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------- | | `type` | Yes | Must be `FieldType.RELATION` | | `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object | | `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object | | `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` | | `universalSettings.onDelete` | MANY\_TO\_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` | | `universalSettings.joinColumnName` | MANY\_TO\_ONE only | Database column name for the foreign key (e.g., `postCardId`) | ## Inline relation fields You can also declare a relation directly inside [`defineObject`](/developers/extend/apps/data/objects). When inline, omit `objectUniversalIdentifier` — it's inherited from the parent object: ```ts theme={null} export default defineObject({ universalIdentifier: '...', nameSingular: 'postCardRecipient', // ... fields: [ { universalIdentifier: POST_CARD_FIELD_ID, type: FieldType.RELATION, name: 'postCard', label: 'Post Card', relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID, universalSettings: { relationType: RelationType.MANY_TO_ONE, onDelete: OnDeleteAction.CASCADE, joinColumnName: 'postCardId', }, }, // … other fields ], }); ``` # Targeting System Metadata Source: https://docs.twenty.com/developers/extend/apps/data/system-fields Resolve the deterministic universal identifiers of the metadata Twenty provisions automatically on every object, so your app can reference it without hardcoding. Every object in Twenty comes with **system metadata** you never declare yourself, such as a set of fields and a main list view with its columns. The server creates all of it when the object is provisioned, and the set grows as Twenty does. Because you don't declare it, there's no `universalIdentifier` constant for you to import. Instead, the server **derives** each identifier deterministically, and `twenty-sdk` exposes the same derivation so your manifest can resolve the exact value the server uses. ## System fields The scalar fields present on every object, none of which you declare with [`defineField()`](/developers/extend/apps/data/extending-objects): `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector` So how do you reference `createdAt` as a column in a [view](/developers/extend/apps/layout/views)? ### The problem Since Twenty 2.19, a system field's universal identifier is **derived deterministically** by the server from three inputs: the application universal identifier, the object universal identifier, and the field name. Inventing an id and hardcoding it won't work: it matches nothing on the server, and the sync rejects the dangling reference: ``` Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found ``` ### The solution `getFieldUniversalIdentifier` is available from `twenty-sdk` 2.21 onward. Use `getFieldUniversalIdentifier` to resolve the exact same value the server uses. It takes the three inputs and returns the field's universal identifier: ```ts theme={null} import { getFieldUniversalIdentifier } from 'twenty-sdk/define'; const createdAtFieldId = getFieldUniversalIdentifier({ applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER, name: 'createdAt', }); ``` * `applicationUniversalIdentifier` is your app's identifier, the one you pass to [`defineApplication()`](/developers/extend/apps/config/application). * `objectUniversalIdentifier` is the identifier of the object the field belongs to. * `name` is the system field name, one of the values listed above. ### Example: a createdAt column in a view The typical case is adding a `createdAt` column to a view of one of your custom objects. Resolve the field id and reference it as any other `fieldMetadataUniversalIdentifier`: ```ts src/views/example-view.ts theme={null} import { defineView, getFieldUniversalIdentifier, } from 'twenty-sdk/define'; const APPLICATION_UNIVERSAL_IDENTIFIER = '0b04e15c-27b2-4741-9046-b32e07469072'; const MY_OBJECT_UNIVERSAL_IDENTIFIER = 'c782b61c-70fd-4c88-9cd6-4e61ab8d7591'; export default defineView({ universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0', name: 'All records', objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER, icon: 'IconList', position: 0, fields: [ { universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104', fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({ applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER, name: 'createdAt', }), position: 0, isVisible: true, size: 200, }, ], }); ``` The same resolved id works anywhere a `fieldMetadataUniversalIdentifier` is expected: view fields, filters, sorts, groups, and page-layout widgets. Resolve the id, don't hardcode it. Because the server derives the value from the application id, the object id and the field name, calling `getFieldUniversalIdentifier` keeps your reference correct even if those inputs change, and avoids drift if the derivation ever evolves. ### System relation fields `getSystemRelationFieldUniversalIdentifier` is available from `twenty-sdk` 2.23 onward and requires a Twenty server on 2.23 or later. Besides the scalar system fields above, the server also provisions four **system relation fields** on every object: `timelineActivities`, `attachments`, `noteTargets` and `taskTargets`, each pointing at the matching standard relation object. These fields are not resolved with `getFieldUniversalIdentifier`: their identifier is derived **name-free**, from the object hosting the field and the object the field points to. That way renaming an object never changes the identifiers of its relation fields. Use `getSystemRelationFieldUniversalIdentifier` to resolve them: ```ts theme={null} import { getSystemRelationFieldUniversalIdentifier, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; // rocket.attachments — the relation field hosted on your custom object const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({ applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER, relationTargetObjectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier, }); ``` * `objectUniversalIdentifier` is the object **hosting** the field. * `relationTargetObjectUniversalIdentifier` is the object the field **points to**. The direction is encoded by the argument order. To resolve the reverse side (e.g. `attachment.targetRocket`, the morph field the server creates on the standard relation object), swap the two: ```ts theme={null} // attachment.targetRocket — the reverse morph field on Attachment const attachmentTargetRocketFieldId = getSystemRelationFieldUniversalIdentifier({ applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier, relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER, }); ``` As with scalar system fields, the resolved id works anywhere a `fieldMetadataUniversalIdentifier` is expected. ## System views `getSystemViewUniversalIdentifier` and `getSystemViewFieldUniversalIdentifier` are available from `twenty-sdk` 2.26 onward and require a Twenty server on 2.26 or later. The server also provisions a **system view** on every object: the main list view (`All {objectLabelPlural}`, keyed on `ViewKey.INDEX`), with one column per displayable field. Like system relation fields, their identifiers are derived **name-free**, so renaming an object or a field never changes them. Use `getSystemViewUniversalIdentifier` to resolve the view: ```ts theme={null} import { getSystemViewUniversalIdentifier, ViewKey } from 'twenty-sdk/define'; const rocketIndexViewId = getSystemViewUniversalIdentifier({ objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER, viewKey: ViewKey.INDEX, }); ``` * `objectMetadataApplicationUniversalIdentifier` is the application owning the **object**, which is what the view is namespaced by. * `objectUniversalIdentifier` is the object the view lists. * `viewKey` is the system view key, `ViewKey.INDEX` today. The resolved id works anywhere a `viewUniversalIdentifier` is expected, such as a [`NavigationMenuItemType.VIEW`](/developers/extend/apps/layout/navigation-menu-items) sidebar entry. To simply open an object's main list, prefer `NavigationMenuItemType.OBJECT` with `targetObjectUniversalIdentifier`: it needs no derivation. `getSystemViewFieldUniversalIdentifier` resolves a single **column** on a system view, from the view and the field it displays: ```ts theme={null} import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define'; const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({ fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, viewUniversalIdentifier: rocketIndexViewId, fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER, }); ``` Note the first argument: a column is namespaced by the application owning the **field it displays**, not the one owning the view. A field your app adds to a standard object gets its column derived under your application, on a view owned by Twenty. System views and their columns are **server-owned**: resolve their identifiers to reference them, never to declare them. `key` on [`defineView()`](/developers/extend/apps/layout/views) is deprecated and ignored, so a manifest view can never claim the `INDEX` key, and the server already provisions a column for every field you add, so declaring your own `defineViewField()` for that same field on a system view conflicts with it. ## Standard Twenty objects For a **standard** Twenty object (Person, Company, Opportunity, …), you don't need to derive anything: the identifiers are pre-computed constants you can import directly, for both fields and views. ```ts theme={null} import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier // STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier ``` Reach for the helpers above when the object is one **your app** defines with [`defineObject()`](/developers/extend/apps/data/objects), where no such constant exists. `name` is a **default** field, not a system field. It keeps its own hardcoded universal identifier and is not resolved through `getFieldUniversalIdentifier`. On objects you define, reference the `name` field by the identifier you gave it in `defineObject()`. # Concepts Source: https://docs.twenty.com/developers/extend/apps/getting-started/concepts How Twenty apps work — entity model, sandboxing, and the install lifecycle. Twenty apps are TypeScript packages that extend your workspace with custom objects, logic, UI components, and AI capabilities. They run on the Twenty platform with full sandboxing and permission controls. ## How apps work An app is a collection of **entities** declared using `defineEntity()` functions from the `twenty-sdk` package. The SDK detects these declarations via AST analysis at build time and produces a **manifest** — a complete description of what your app adds to a workspace. These functions validate your configuration at build time and provide IDE autocompletion and type safety. ``` your-app/ ├── src/ │ ├── application-config.ts ← defineApplication (required, one per app) │ ├── roles/ ← defineRole │ ├── objects/ ← defineObject │ ├── fields/ ← defineField │ ├── logic-functions/ ← defineLogicFunction │ ├── front-components/ ← defineFrontComponent │ ├── skills/ ← defineSkill │ ├── agents/ ← defineAgent │ ├── views/ ← defineView │ ├── navigation-menu-items/ ← defineNavigationMenuItem │ └── page-layouts/ ← definePageLayout ├── public/ ← Static assets (images, icons) └── package.json ``` **File organization is up to you.** Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. The folder structure above is a convention, not a requirement. ## Entity types | Entity | Purpose | Docs | | ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- | | **Application** | App identity, default role, variables | [Application Config](/developers/extend/apps/config/application) | | **Role** | Permission sets on objects and fields | [Roles & Permissions](/developers/extend/apps/config/roles) | | **Object** | Custom record types with fields | [Objects](/developers/extend/apps/data/objects) | | **Field** | Add fields to objects from other apps | [Extending Objects](/developers/extend/apps/data/extending-objects) | | **Relation** | Bidirectional links between objects | [Relations](/developers/extend/apps/data/relations) | | **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/developers/extend/apps/logic/logic-functions) | | **Skill** | Reusable AI agent instructions | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) | | **Agent** | AI assistants with custom prompts | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) | | **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/developers/extend/apps/logic/connections) | | **View** | Pre-configured record list views | [Views](/developers/extend/apps/layout/views) | | **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/developers/extend/apps/layout/navigation-menu-items) | | **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/developers/extend/apps/layout/page-layouts) | | **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/developers/extend/apps/layout/front-components) | | **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/developers/extend/apps/layout/command-menu-items) | ## Sandboxing * **Logic functions** run in isolated Node.js processes on the server. They only access data through the typed API client, scoped to the app's role permissions. * **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes). They communicate with Twenty via a message-passing host API. * **Permissions** are enforced at the API level. The runtime token (`TWENTY_APP_ACCESS_TOKEN`) is derived from the role defined in `defineApplication()`. ## App lifecycle ``` ┌─────────────────────────────────────────────────────────┐ │ Development │ │ npx create-twenty-app → yarn twenty dev (live sync) │ ├─────────────────────────────────────────────────────────┤ │ Build & Deploy │ │ yarn twenty dev:build → yarn twenty app:publish │ ├─────────────────────────────────────────────────────────┤ │ Install flow │ │ upload → [pre-install] → metadata migration → │ │ generate SDK → [post-install] → … → [uninstall] │ ├─────────────────────────────────────────────────────────┤ │ Publish │ │ npm publish → appears in Twenty marketplace │ └─────────────────────────────────────────────────────────┘ ``` * **`yarn twenty dev`** — watches your source files and live-syncs changes to a connected Twenty server. The typed API client is regenerated automatically when the schema changes. * **`yarn twenty dev:build`** — compiles TypeScript, bundles logic functions and front components with esbuild, and produces a manifest. * **Pre/post-install and uninstall hooks** — optional functions that run during installation or right before removal. See [Install Hooks](/developers/extend/apps/config/install-hooks) for details. ## Next steps Application identity, default role, and install and uninstall hooks. Objects, fields, and bidirectional relations. Logic functions, skills, agents, and OAuth connections. Views, navigation, page layouts, front components. CLI, testing, remotes, CI, and publishing your app. # Local Server Source: https://docs.twenty.com/developers/extend/apps/getting-started/local-server Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup. ## Managing the local server Use `yarn twenty docker:*` to control the local Twenty container: | Command | What it does | | -------------------------------------- | -------------------------------------------- | | `yarn twenty docker:start` | Start the server (pulls the image if needed) | | `yarn twenty docker:start 2.2.0` | Start a specific server version | | `yarn twenty docker:start --port 3030` | Start on a custom port | | `yarn twenty docker:stop` | Stop the server (preserves data) | | `yarn twenty docker:status` | Show URL, version, and login credentials | | `yarn twenty docker:logs` | Stream server logs | | `yarn twenty docker:reset` | Wipe data and start fresh | | `yarn twenty docker:upgrade` | Pull the latest `twenty-app-dev` image | | `yarn twenty docker:upgrade 2.2.0` | Upgrade to a specific version | Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything. ## Pinning the server version When no version is passed, `docker:start` resolves the version from your app's `engines.twenty` range in `package.json` — the same range the server validates against when your app is installed. It starts the newest published `twenty-app-dev` image that satisfies the range, falling back to `latest` when the field is absent or no published version matches: ```json filename="package.json" theme={null} { "engines": { "twenty": ">=2.2.0" } } ``` Pass a version explicitly to override the range for a single run: `yarn twenty docker:start 2.3.0`. If a container already exists on a different version, `docker:start` upgrades it in place (recreating the container while preserving your data volumes). ## Upgrading the server image `yarn twenty docker:upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty docker:start` afterward to wait for it to become healthy. ```bash filename="Terminal" theme={null} yarn twenty docker:upgrade # Latest yarn twenty docker:upgrade 2.2.0 # Specific version ``` Verify the running version with `yarn twenty docker:status` (it shows the `APP_VERSION` baked into the container). ## Running a parallel test instance Pass `--test` to any `docker:*` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data: | Command | What it does | | ----------------------------------- | ----------------------------------------------- | | `yarn twenty docker:start --test` | Start the test instance (defaults to port 2021) | | `yarn twenty docker:stop --test` | Stop it | | `yarn twenty docker:status --test` | Show its status | | `yarn twenty docker:logs --test` | Stream its logs | | `yarn twenty docker:reset --test` | Wipe its data | | `yarn twenty docker:upgrade --test` | Upgrade its image | The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021. ## Manual setup (without the scaffolder) Skip the scaffolder if you're adding the SDK to an existing project: ```bash filename="Terminal" theme={null} yarn add twenty-sdk twenty-client-sdk ``` Add the script to `package.json`: ```json filename="package.json" theme={null} { "scripts": { "twenty": "twenty" } } ``` You can now run `yarn twenty dev`, `yarn twenty docker:start`, and the rest. Don't install `twenty-sdk` globally — pin it per project so each app uses its own version. # Project Structure Source: https://docs.twenty.com/developers/extend/apps/getting-started/project-structure What's inside a scaffolded Twenty app — files, folders, and what each one does. A new app generated by `npx create-twenty-app` looks like this: ```text filename="my-twenty-app/" theme={null} my-twenty-app/ package.json src/ application-config.ts # Required — your app's entry point default-role.ts # Permissions for logic functions constants/ universal-identifiers.ts # Auto-generated UUIDs and metadata front-components/ main-page.tsx # Welcome page component navigation-menu-items/ main-page.navigation-menu-item.ts # Sidebar entry for the welcome page page-layouts/ main-page.page-layout.ts # Standalone page hosting the component __tests__/ application-config.test.ts # Unit test global-setup.ts # Integration test setup (sync + uninstall) schema.integration-test.ts # Integration test against a live server .github/workflows/ ci.yml # Lint, typecheck, unit + integration tests cd.yml # Deploy + install on push to main publish.yml # Publish to npm on version tags (with provenance) public/ logo.svg # Static assets vitest.config.ts # Integration test runner config vitest.unit.config.ts # Unit test runner config tsconfig.json, tsconfig.spec.json .nvmrc, .yarnrc.yml, .oxlintrc.json README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md ``` ## Key files | File / Folder | Purpose | | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `src/application-config.ts` | **Required.** The main configuration file for your app. | | `src/default-role.ts` | Default role controlling what your logic functions can access. | | `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). | | `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | A starter welcome page: a front component rendered by a standalone page layout, reachable from the sidebar. | | `src/__tests__/` | A unit test plus an integration test (with its global setup) that syncs the app against a real server. | | `public/` | Static assets (images, fonts) served with your app. | | `AGENTS.md` / `CLAUDE.md` | Guidance for AI coding agents working on the app. | | `CHANGELOG.md` / `SETUP.md` | Changelog of notable changes and setup instructions for local development. | **File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives. ## Dependencies Both Twenty SDK packages belong under `devDependencies`, not `dependencies`: ```json filename="package.json" theme={null} { "dependencies": {}, "devDependencies": { "twenty-client-sdk": "2.20.0", "twenty-sdk": "2.20.0", "twenty-ui": "1.0.0-alpha.1" } } ``` The scaffolder pins `twenty-sdk` and `twenty-client-sdk` to its own version — keep the two in sync when upgrading. * **`twenty-sdk`** ships the `twenty` CLI and the build/scaffolding tooling. It only runs at development and build time and is never imported by your published app's runtime. * **`twenty-client-sdk`** *is* imported by your app code (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), but Twenty provides it at runtime — logic functions get it from a generated SDK layer, and front components resolve it from server-served modules. Your installed copy is only used for typechecking and the deploy-time build, so it never needs to ship in the deployed bundle. Keeping either package under `dependencies` pulls it into the installed app's runtime bundle, where it is dead weight. `twenty dev:build` emits a warning when either is still listed under `dependencies`. Add your app's own runtime dependencies (libraries your logic functions actually import at runtime) under `dependencies` as usual. Keep the tree lean: `dependencies` are installed into a runtime layer capped at 250MB unpacked, and a sync or install fails with a dependencies size error beyond that — UI libraries and dev tooling belong in `devDependencies`. # Quick Start Source: https://docs.twenty.com/developers/extend/apps/getting-started/quick-start Create your first Twenty app in minutes. ## Prerequisites * **Node.js 24.5+** — [Download](https://nodejs.org/) * **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable` * **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere. Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix. | Phase | What you do | Tool | Result | | ------------------- | ---------------------------------- | ----------------------------------- | ----------------------------- | | **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk | | **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty docker:start` | A running Twenty instance | | **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI | *** ## Phase 1 — Scaffold your project Create a new app from the template: ```bash filename="Terminal" theme={null} npx create-twenty-app@latest my-twenty-app ``` The scaffolder is non-interactive: the directory name becomes the app name. Pass `--display-name` and `--description` to customize the generated metadata (you can also edit it later in `src/constants/universal-identifiers.ts`). This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, CI/CD workflows, and an integration test. **After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2. *** ## Phase 2 — Run a local Twenty server Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI. The scaffolder starts one for you: with Docker running, it pulls the `twentycrm/twenty-app-dev` image, starts it on port `2020`, and authenticates the CLI against the pre-seeded demo workspace (`tim@apple.dev`) — no sign-in required. To connect to an existing Twenty server instead, pass `--url `. Remote servers authenticate with OAuth: a browser opens so you can sign in and click **Authorize**, which gives the CLI access to your workspace. (You can also opt into OAuth locally with `--authentication-method oauth` — sign in with `tim@apple.dev` / `tim@apple.dev`.)
Twenty login screen
Twenty CLI authorization screen
Your terminal will confirm everything is set up.
App scaffolded successfully
**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it. If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty docker:start` — no need to re-scaffold. *** ## Phase 3 — Sync your changes This is the inner loop you'll spend most of your time in. ```bash filename="Terminal" theme={null} cd my-twenty-app yarn twenty dev ``` This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a few seconds the server reflects the change. You'll see a live status panel in your terminal. For more detailed output (build logs, sync requests, error traces), add `--verbose`.
Dev mode terminal output
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**.
Your Apps list showing My twenty app
Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server.
Application registration details
Click **View installed app** to see the workspace install. The **About** tab shows version and management options.
Installed app
**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI. ### One-shot sync for CI and scripts Use `plan` and `apply` to run the same pipeline once, without a watcher: ```bash filename="Terminal" theme={null} yarn twenty plan # preview the metadata changes without applying them yarn twenty apply # show the plan, then apply it ``` | Command | Behavior | When to use | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. | | `yarn twenty apply` | Single build + sync, exits `0` on success, `1` on failure. Asks for confirmation on destructive changes (pass `--force` to skip). | CI, pre-commit hooks, AI agents, scripted workflows. | | `yarn twenty plan` | Builds and prints the metadata changes **without applying them**. | Inspecting what a sync would change before committing to it. | All modes need an authenticated remote. See [Syncing & recovery](/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) for more on `plan`. `yarn twenty dev --once` and `yarn twenty dev --once --dry-run` are deprecated aliases for `yarn twenty apply` and `yarn twenty plan`. ### Dev mode options | Flag | Description | | ----------------------- | --------------------------------------------------------------------- | | `--force` | Apply destructive changes (deletes) without confirmation. | | `--debounceMs ` | Set the file-change debounce delay in milliseconds (default: `1000`). | | `--verbose` / `--debug` | Show detailed build logs, sync requests, and error traces. | ## What you can build Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`: | Entity | What it does | | ---------------------- | ----------------------------------------------------------------------------------- | | **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields | | **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events | | **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) | | **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants | | **Views & Navigation** | Pre-configured list views and sidebar menu items | | **Page layouts** | Custom record detail pages with tabs and widgets | Full reference: [Concepts](/developers/extend/apps/getting-started/concepts). ## Next steps Application identity, default role, install and uninstall hooks, public assets. Objects, fields, and bidirectional relations. Logic functions, skills, agents, and OAuth connections. Views, navigation, page layouts, front components. CLI, testing, remotes, CI, and publishing your app. # Scaffolding Source: https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding Generate entity files interactively with yarn twenty dev:add — objects, fields, views, logic functions, and more. Instead of creating entity files by hand, use the interactive scaffolder: ```bash filename="Terminal" theme={null} yarn twenty dev:add ``` It prompts you to pick an entity type and walks you through the required fields, then writes a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call. You can also pass the entity type directly to skip the first prompt: ```bash filename="Terminal" theme={null} yarn twenty dev:add object yarn twenty dev:add logicFunction yarn twenty dev:add frontComponent ``` ## Available entity types | Entity type | Command | Generated file | | -------------------- | ---------------------------------------- | ------------------------------------- | | Object | `yarn twenty dev:add object` | `src/objects/.ts` | | Field | `yarn twenty dev:add field` | `src/fields/.ts` | | Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/.ts` | | Front component | `yarn twenty dev:add frontComponent` | `src/front-components/.tsx` | | Role | `yarn twenty dev:add role` | `src/roles/.ts` | | Skill | `yarn twenty dev:add skill` | `src/skills/.ts` | | Agent | `yarn twenty dev:add agent` | `src/agents/.ts` | | View | `yarn twenty dev:add view` | `src/views/.ts` | | Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/.ts` | | Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/.ts` | | Page layout tab | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/.ts` | | Command menu item | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/.ts` | | View field | `yarn twenty dev:add viewField` | `src/view-fields/.ts` | | Connection provider | `yarn twenty dev:add connectionProvider` | `src/connection-providers/.ts` | ## What the scaffolder generates Each entity type has its own template. For example, `yarn twenty dev:add object` asks for: 1. **Name (singular)** — e.g., `invoice` 2. **Name (plural)** — e.g., `invoices` 3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`) 4. **Label (plural)** — auto-populated (e.g., `Invoices`) 5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object. Other entity types have simpler prompts — most only ask for a name. The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`. ## Custom output path Use the `--path` flag to place the generated file in a custom location: ```bash filename="Terminal" theme={null} yarn twenty dev:add logicFunction --path src/custom-folder ``` # Troubleshooting Source: https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting Common first-run issues — Docker, Node version, Yarn, dependencies. * **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty docker:start`. The error message will show the right start command for your OS. * **Wrong Node version** — Need 24.5+ (`engines.node: ^24.5.0`). Check with `node -v`. * **Yarn 4 missing** — Run `corepack enable`. * **Dependencies broken** — `rm -rf node_modules && yarn install`. * **`twenty-sdk` errors after upgrading to v2.8.0** — It moved from `dependencies` to `devDependencies` in v2.8.0. See [Project Structure → Dependencies](/developers/extend/apps/getting-started/project-structure#dependencies). * **`twenty dev:build` warns about `twenty-client-sdk` under `dependencies`** — It is provided at runtime by Twenty, so it should be moved to `devDependencies` alongside `twenty-sdk`. See [Project Structure → Dependencies](/developers/extend/apps/getting-started/project-structure#dependencies). Stuck? Ask on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322). # Command Menu Items Source: https://docs.twenty.com/developers/extend/apps/layout/command-menu-items Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem. A **command menu item** is the bridge between the user and a [front component](/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page. ```ts src/command-menu-items/open-dashboard.command-menu-item.ts theme={null} import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', label: 'Open Dashboard', shortLabel: 'Dashboard', isPinned: true, availabilityType: 'GLOBAL', frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', }); ``` ## Configuration fields | Field | Required | Description | | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `universalIdentifier` | Yes | Stable unique ID for the command | | `label` | Yes | Full label shown in the command menu (Cmd+K) | | `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens | | `shortLabel` | No | Shorter label displayed on the pinned quick-action button | | `icon` | No | **Deprecated** — ignored in favor of the application icon; the build emits a warning if set | | `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page | | `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'GLOBAL_OBJECT_CONTEXT'` (only on pages with an object context — index and record pages), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) | | `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) | | `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) | ## Headless commands A command menu item paired with a [headless front component](/developers/extend/apps/layout/front-components#headless-vs-non-headless) is the idiomatic way to ship a one-click action — run code, navigate, or confirm and execute. The Front Components page covers the [SDK Command components](/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) that handle the action-and-unmount pattern. A typical flow: a headless component renders `` (see the [full example](/developers/extend/apps/layout/front-components#sdk-command-components)), and the command menu item points at it: ```ts src/command-menu-items/run-action.command-menu-item.ts theme={null} import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', label: 'Run my action', frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', }); ``` ## Conditional availability expressions The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions: ```ts src/command-menu-items/bulk-update.command-menu-item.ts theme={null} import { defineCommandMenuItem, objectPermissions, everyEquals, } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: '...', label: 'Bulk Update', availabilityType: 'RECORD_SELECTION', frontComponentUniversalIdentifier: '...', conditionalAvailabilityExpression: everyEquals( objectPermissions, 'canUpdateObjectRecords', true, ), }); ``` `RECORD_SELECTION` already implies a non-empty selection — use `numberOfSelectedRecords` only for specific counts (e.g. `>= 2`). ### Context variables These represent the current state of the page: | Variable | Type | Description | | ------------------------------ | --------- | ---------------------------------------------------------------- | | `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) | | `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel | | `numberOfSelectedRecords` | `number` | Number of currently selected records | | `isSelectAll` | `boolean` | Whether "select all" is active | | `selectedRecords` | `array` | The selected record objects | | `favoriteRecordIds` | `array` | IDs of favorited records | | `objectPermissions` | `object` | Permissions for the current object type | | `targetObjectReadPermissions` | `object` | Read permissions for the target object | | `targetObjectWritePermissions` | `object` | Write permissions for the target object | | `featureFlags` | `object` | Active feature flags | | `objectMetadataItem` | `object` | Metadata of the current object type | | `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter | ### Operators Combine variables into boolean expressions: | Operator | Description | | ----------------------------------- | ----------------------------------------------------------------- | | `isDefined(value)` | `true` if the value is not null/undefined | | `isNonEmptyString(value)` | `true` if the value is a non-empty string | | `includes(array, value)` | `true` if the array contains the value | | `includesEvery(array, prop, value)` | `true` if every item's property includes the value | | `every(array, prop)` | `true` if the property is truthy on every item | | `everyDefined(array, prop)` | `true` if the property is defined on every item | | `everyEquals(array, prop, value)` | `true` if the property equals the value on every item | | `some(array, prop)` | `true` if the property is truthy on at least one item | | `someDefined(array, prop)` | `true` if the property is defined on at least one item | | `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item | | `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item | | `none(array, prop)` | `true` if the property is falsy on every item | | `noneDefined(array, prop)` | `true` if the property is undefined on every item | | `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item | # Front Components Source: https://docs.twenty.com/developers/extend/apps/layout/front-components Build React components that render inside Twenty's UI with sandboxed isolation. Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code executes inside a sandboxed, opaque-origin iframe, yet its UI still renders natively in the page rather than being confined to that iframe. Front components are still under active development. Your code runs against a partial DOM, not a real browser page, so advanced usages can fail, often silently. See [Current limitations](#current-limitations). ## Where front components can be used Front components can render in three locations within Twenty: * **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu. * **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget. * **App settings** — Defined with [`defineSettingsFrontComponent()`](#custom-settings-component), the front component renders as a section inside the app's **Settings** tab, in place of the default variable configuration UI. A front component on its own isn't reachable from the UI — you need to *surface* it. The three ways to do that are: * **Pair it with a [command menu item](/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action. * **Embed it as a widget in a [page layout](/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard. * **Define it with [`defineSettingsFrontComponent()`](#custom-settings-component)** — renders it as a section inside the app's **Settings** tab, in place of the default variable configuration UI. ## Basic example The quickest way to see a front component in action is to pair it with a [`defineCommandMenuItem`](/developers/extend/apps/layout/command-menu-items), so it appears as a quick-action button in the top-right corner of the page: ```tsx src/front-components/hello-world.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; const HelloWorld = () => { return (

Hello from my app!

This component renders inside Twenty.

); }; export default defineFrontComponent({ universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', name: 'hello-world', description: 'A simple front component', component: HelloWorld, }); ``` ```ts src/command-menu-items/hello-world.command-menu-item.ts theme={null} import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', shortLabel: 'Hello', label: 'Hello World', isPinned: true, availabilityType: 'GLOBAL', frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', }); ``` After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty apply`), the quick action appears in the top-right corner of the page:
Quick action button in the top-right corner
Click it to render the component inline. ## Configuration fields | Field | Required | Description | | --------------------- | -------- | ------------------------------------------------------------ | | `universalIdentifier` | Yes | Stable unique ID for this component | | `component` | Yes | A React component function | | `name` | No | Display name | | `description` | No | Description of what the component does | | `isHeadless` | No | Set to `true` if the component has no visible UI (see below) | ## Placing a front component on a page Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/developers/extend/apps/layout/page-layouts) for details. ## Custom settings component To replace the auto-generated variable configuration UI in your app's **Settings** tab with your own component, define it with `defineSettingsFrontComponent` instead of `defineFrontComponent`. It takes the same [configuration fields](#configuration-fields) (except `isHeadless`, which is not accepted since a settings component always renders visible UI) and additionally marks the component as the app's settings UI. The component renders as a section **inside** the Settings tab, not as a replacement for the whole tab. Twenty's system-managed sections — auto-upgrade, App URL, and connections — always render above it and cannot be overridden by the app. ```tsx src/front-components/app-settings.tsx theme={null} import { defineSettingsFrontComponent } from 'twenty-sdk/define'; const AppSettings = () => { return (

My app settings

{/* render your own configuration UI here */}
); }; export default defineSettingsFrontComponent({ universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', name: 'app-settings', description: "Custom UI for the app's Settings tab", component: AppSettings, }); ``` Only one settings front component is allowed per app; declaring more than one fails the build. When present, the app's **Settings** tab renders this component in place of the default variable configuration UI. ## Headless vs non-headless Front components come in two rendering modes controlled by the `isHeadless` option: **Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted. **Headless (`isHeadless: true`)** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below. ```tsx src/front-components/sync-tracker.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component'; import { useEffect } from 'react'; const SyncTracker = () => { const [recordId] = useSelectedRecordIds(); useEffect(() => { enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); }, [recordId]); return null; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'sync-tracker', description: 'Tracks record views silently', isHeadless: true, component: SyncTracker, }); ``` Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API. ## SDK Command components The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done. Import them from `twenty-sdk/front-component`: * **`Command`** — Runs an async callback via the `execute` prop. * **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`. * **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. * **`CommandOpenSidePanelPage`** — Opens a side panel page. Props depend on `page` — e.g. `ViewRecord` takes `recordId` + `objectNameSingular` (plus an optional `tab` id to open the record on a specific tab), other pages take `pageTitle` + `pageIcon`. Here is a full example of a headless front component using `Command` to run an action from the command menu: ```tsx src/front-components/run-action.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { Command } from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; const RunAction = () => { const execute = async () => { const client = new CoreApiClient(); await client.mutation({ createTask: { __args: { data: { title: 'Created by my app' } }, id: true, }, }); }; return ; }; export default defineFrontComponent({ universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', name: 'run-action', description: 'Creates a task from the command menu', component: RunAction, isHeadless: true, }); ``` ```ts src/command-menu-items/run-action.command-menu-item.ts theme={null} import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', label: 'Run my action', frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', }); ``` And an example using `CommandModal` to ask for confirmation before executing: ```tsx src/front-components/delete-draft.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { CommandModal } from 'twenty-sdk/front-component'; const DeleteDraft = () => { const execute = async () => { // perform the deletion }; return ( ); }; export default defineFrontComponent({ universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', name: 'delete-draft', description: 'Deletes a draft with confirmation', component: DeleteDraft, isHeadless: true, }); ``` And an example using `CommandOpenSidePanelPage` to open the current record in the side panel on a specific tab. `tab` is a page layout tab id (default layouts use ids like `company-tab-emails` or `company-tab-timeline`; custom layouts use the tab's own id). If the id doesn't exist in the record's layout, the default tab opens instead: ```tsx src/front-components/open-company-emails.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { CommandOpenSidePanelPage, SidePanelPages, useSelectedRecordIds, } from 'twenty-sdk/front-component'; const OpenCompanyEmails = () => { const selectedRecordIds = useSelectedRecordIds(); const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null; if (!recordId) { return null; } return ( ); }; export default defineFrontComponent({ universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567', name: 'open-company-emails', description: 'Opens the current company on its Emails tab', component: OpenCompanyEmails, isHeadless: true, }); ``` ## Calling a logic function Front components run browser-side in a Web Worker sandboxed inside an opaque-origin iframe, while [logic functions](/developers/extend/apps/logic/logic-functions) run server-side. There is no direct in-process call between the two — instead, a front component reaches a logic function over HTTP. A logic function declared with `httpRouteTriggerSettings` is reachable over HTTP at its route path. `RestApiClient` treats paths starting with `/s/` as app routes, resolves them to the URL your functions are served from, and authenticates them with `TWENTY_APP_ACCESS_TOKEN`. > **On Twenty Cloud, HTTP-triggered logic functions are served on a dedicated per-workspace domain** at `https://.withtwenty.com`. For external callers, copy the exact URL from the function's **HTTP trigger** settings or the application's **Settings** tab. A headless front component can run the call on mount via the `Command` component, then unmount automatically: ```tsx src/front-components/sync-prs.tsx theme={null} import { RestApiClient } from 'twenty-client-sdk/rest'; import { defineFrontComponent } from 'twenty-sdk/define'; import { Command } from 'twenty-sdk/front-component'; const SyncPrs = () => { const execute = async () => { await new RestApiClient().post('/s/github/fetch-prs', { owner: 'twentyhq', repo: 'twenty', }); }; return ; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'sync-prs', description: 'Triggers the fetch-prs logic function', isHeadless: true, component: SyncPrs, }); ``` The path passed to `RestApiClient` is the logic function's `httpRouteTriggerSettings.path`, prefixed with `/s`. Keep `isAuthRequired: true`; the `TWENTY_APP_ACCESS_TOKEN` Twenty mints for your component authenticates the request: ```ts src/logic-functions/fetch-prs.logic-function.ts theme={null} import { defineLogicFunction } from 'twenty-sdk/define'; import type { RoutePayload } from 'twenty-sdk/logic-function'; const handler = async (event: RoutePayload) => { const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string }; // ...fetch from GitHub and persist records... return { ok: true }; }; export default defineLogicFunction({ universalIdentifier: '...', name: 'fetch-prs', handler, httpRouteTriggerSettings: { path: '/github/fetch-prs', httpMethod: 'POST', isAuthRequired: true, }, }); ``` `TWENTY_APP_ACCESS_TOKEN` is injected automatically — see [Application variables](#application-variables). Because secret application variables are never exposed to front components, keep API keys and other sensitive logic in the logic function, not in the front component. ### Calling the Twenty REST API To call app HTTP routes or read and write Twenty records from a front component, use `RestApiClient` from `twenty-client-sdk/rest`. It sends `/s/...` paths to your workspace's functions base URL and every other path, including `/rest/...`, to `TWENTY_API_URL`. | Method | Description | | --------------------------------- | --------------------------------------------------------------------- | | `get(path, options?)` | Sends a `GET` request | | `post(path, body?, options?)` | Sends a `POST` request | | `put(path, body?, options?)` | Sends a `PUT` request | | `patch(path, body?, options?)` | Sends a `PATCH` request | | `delete(path, options?)` | Sends a `DELETE` request | | `request(method, path, options?)` | Generic request with any HTTP method | | `resolveUrl(path, options?)` | Resolves a path to its full URL without sending a request (for links) | `options` accepts `headers`, `query` (a record of query-string params; nullish values are skipped), and an `AbortSignal` via `signal`. A non-`FormData` object `body` is JSON-serialized automatically. On a `401`, the client refreshes the access token once through the host and retries the request. The base URL and token are resolved from the environment by default. Pass overrides to the constructor when needed — for example in tests: ```ts theme={null} const client = new RestApiClient({ baseUrl: 'https://myworkspace.twenty.com', token: 'my-token', }); ``` Failed requests throw a `RestApiClientError` exposing `status`, `statusText`, `url`, and the parsed `body`: ```tsx theme={null} import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest'; const client = new RestApiClient(); try { const people = await client.get('/rest/people', { query: { limit: 10 }, }); } catch (error) { if (error instanceof RestApiClientError) { console.error(error.status, error.body); } } ``` ## Accessing runtime context Inside your component, use SDK hooks to access the current user, record, and component instance: ```tsx src/front-components/record-info.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { useUserId, useSelectedRecordIds, useFrontComponentId, } from 'twenty-sdk/front-component'; const RecordInfo = () => { const userId = useUserId(); const [recordId] = useSelectedRecordIds(); const componentId = useFrontComponentId(); return (

User: {userId}

Record: {recordId ?? 'No record context'}

Component: {componentId}

); }; export default defineFrontComponent({ universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', name: 'record-info', component: RecordInfo, }); ``` Available hooks: | Hook | Returns | Description | | --------------------------------------------- | --------------------- | ---------------------------------------------------------------- | | `useUserId()` | `string` or `null` | The current user's ID | | `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) | | `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead | | `useFrontComponentId()` | `string` | This component instance's ID | | `useColorScheme()` | `'light'` or `'dark'` | The host UI's active color scheme (`System` is already resolved) | | `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function | ## Application variables Application variables defined in [`defineApplication()`](/developers/extend/apps/config/application) with `isSecret: false` are available inside front components via the `getApplicationVariable` utility: ```tsx src/front-components/greeting.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { getApplicationVariable } from 'twenty-sdk/front-component'; const Greeting = () => { const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World'; return

Hello, {recipientName}!

; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'greeting', component: Greeting, }); ``` Secret variables (`isSecret: true`) are **not** exposed to front components. They are only available in [logic functions](/developers/extend/apps/logic/logic-functions), which run server-side. This prevents sensitive values like API keys from being sent to the browser. `getApplicationVariable` always returns a **string** (or `undefined`), regardless of the variable's declared `type`. The string is serialized consistently by type (booleans as `"true"` / `"false"`, numbers as decimal strings, arrays / objects as JSON), the same format used for logic-function `process.env` — parse it yourself (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). See [Variable types](/developers/extend/apps/config/application#variable-types). The following system variables are always available via `process.env`: | Variable | Description | | ------------------------- | ------------------------------------------- | | `TWENTY_API_URL` | Base URL of the Twenty core API | | `TWENTY_APP_ACCESS_TOKEN` | Short-lived token scoped to your app's role | ### `TWENTY_FUNCTIONS_URL` Twenty also injects `TWENTY_FUNCTIONS_URL` into front components and logic functions: the base URL your app's HTTP-triggered logic functions are served from. It exists because that URL is not always the Twenty server itself. On Twenty Cloud, app routes are served on a dedicated per-workspace domain (`https://.withtwenty.com`, or the application's primary public domain when one is configured) so that app-authored responses run on an isolated origin rather than on the Twenty app origin. Self-hosted and local instances serve app routes under the `/s` prefix on the server itself and may not set the variable at all. Since the base URL varies per workspace and per instance, your code cannot hard-code it — the server injects the right value at runtime. You rarely need to read it directly. Call your routes through `RestApiClient` with a `/s/`-prefixed path and the client resolves the URL for you: it strips the `/s` prefix and targets `TWENTY_FUNCTIONS_URL`, falling back to `/s` when the variable is not set. Use `resolveUrl('/s/')` to get the absolute URL without sending a request, e.g. for a link. Read the variable directly only when building a URL by hand: ```ts theme={null} const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`; ``` ## Host communication API Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`: | Function | Description | | ----------------------------------------------- | ----------------------------- | | `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app | | `openSidePanelPage(params)` | Open a side panel | | `closeSidePanel()` | Close the side panel | | `openCommandConfirmationModal(params)` | Show a confirmation dialog | | `enqueueSnackbar(params)` | Show a toast notification | | `unmountFrontComponent()` | Unmount the component | | `updateProgress(progress)` | Update a progress indicator | Here is an example that uses the host API to show a snackbar and close the side panel after an action completes: ```tsx src/front-components/archive-record.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; const ArchiveRecord = () => { const [recordId] = useSelectedRecordIds(); const handleArchive = async () => { const client = new CoreApiClient(); await client.mutation({ updateTask: { __args: { id: recordId, data: { status: 'ARCHIVED' } }, id: true, }, }); await enqueueSnackbar({ message: 'Record archived', variant: 'success', }); await closeSidePanel(); }; return (

Archive this record?

); }; export default defineFrontComponent({ universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', name: 'archive-record', description: 'Archives the current record', component: ArchiveRecord, }); ``` ### Working with multiple records Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations: ```tsx src/front-components/bulk-export.tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { useSelectedRecordIds } from 'twenty-sdk/front-component'; import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; const BulkExport = () => { const selectedRecordIds = useSelectedRecordIds(); const handleExport = async () => { const client = new CoreApiClient(); for (const recordId of selectedRecordIds) { await client.mutation({ updateTask: { __args: { id: recordId, data: { exported: true } }, id: true, }, }); } await enqueueSnackbar({ message: `Exported ${selectedRecordIds.length} records`, variant: 'success', }); await closeSidePanel(); }; return (

Export {selectedRecordIds.length} selected record(s)?

); }; export default defineFrontComponent({ universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', name: 'bulk-export', description: 'Export selected records', component: BulkExport, }); ``` Surface it with a [command menu item](/developers/extend/apps/layout/command-menu-items) restricted to record selections: ```ts src/command-menu-items/bulk-export.command-menu-item.ts theme={null} import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', label: 'Bulk Export', availabilityType: 'RECORD_SELECTION', frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', }); ``` ## Public assets Front components can access files from the app's `public/` directory using `getPublicAssetUrl`: ```tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { getPublicAssetUrl } from 'twenty-sdk/utils'; const Logo = () => Logo; export default defineFrontComponent({ universalIdentifier: '...', name: 'logo', component: Logo, }); ``` See the [public assets section](/developers/extend/apps/config/public-assets) for details. ## Styling Front components support multiple styling approaches. You can use: * **Inline styles** — `style={{ color: 'red' }}` * **Twenty UI components** — Twenty's own component library; see [Using Twenty UI components](#using-twenty-ui-components) below * **Emotion** — CSS-in-JS with `@emotion/react` * **Styled-components** — `styled.div` patterns * **Tailwind CSS** — utility classes * **Any CSS-in-JS library** compatible with React ## Using Twenty UI components Twenty ships its component library as the [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1) package. Front components can use it for buttons, tags, status pills, chips, avatars, icons, typography, and theme tokens that automatically match the workspace's light and dark theme. ### Installation Add the package to your app, pinned to the version your Twenty instance ships: ```bash theme={null} yarn add twenty-ui@1.0.0-alpha.1 ``` `twenty-ui` is bundled into your front component at build time, so it only needs to be a dependency of your app — there is nothing to configure at runtime. ### Importing components Import from the matching subpath rather than the package root, so only the components you use end up in your bundle: | Subpath | What it exports | | --------------------------- | -------------------------------------------------- | | `twenty-ui/input` | `Button` and form inputs | | `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar`, and more | | `twenty-ui/feedback` | `Callout`, `Banner`, `Info`, and more | | `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label`, and more | | `twenty-ui/icon` | `Icon*` components (e.g. `IconCheck`) | | `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` | ```tsx theme={null} import { defineFrontComponent } from 'twenty-sdk/define'; import { Status, Tag } from 'twenty-ui/data-display'; import { Button } from 'twenty-ui/input'; const StyledWidget = () => { return (
); }; export default defineFrontComponent({ universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', name: 'styled-widget', component: StyledWidget, }); ``` ### Icons Import individual icons from `twenty-ui/icon`: ```tsx theme={null} import { IconBox, IconCheck } from 'twenty-ui/icon'; ``` Each named icon is tree-shaken, so importing a handful adds little to your bundle. Avoid `IconsProvider`, `useIcons`, and `iconsState` — they pull in the full Tabler icon set (several MB). ### Theming and theme tokens Twenty UI components automatically match the workspace's light and dark theme — the renderer applies the active color scheme on the host, and the components resolve their colors against it. To use the same design tokens in your own inline styles, call the `useTheme()` hook. It returns Twenty's theme tokens (spacing, colors, radii, fonts) wired to the active theme, with no `ThemeProvider` setup needed in your component: ```tsx theme={null} import { useTheme } from 'twenty-ui/theme-constants'; const Card = () => { const theme = useTheme(); return (
Themed card
); }; ``` Because `useTheme()` is a hook, you read tokens inside the component body, so the values always reflect the live theme. The same token map is also exported as the `themeCssVariables` constant, but prefer `useTheme()` in front components — a module-level constant that dereferences `themeCssVariables` can be undefined while the app manifest is extracted. To branch on the active scheme explicitly, read it with `useColorScheme()` from `twenty-sdk/front-component`, which returns `'light'` or `'dark'`. ## Current limitations Front components are under active development. Rendering, styling and handling events works well. Anything that reaches *past* rendering (measuring an element, calling a DOM method on a ref, portaling outside your tree, touching browser storage) is missing or incomplete today, and most of it fails silently: no exception, and no TypeScript error either, since the scaffold is typed against the full browser DOM. If one of these blocks you, [open an issue](https://github.com/twentyhq/twenty/issues/new/choose) so it gets prioritized. ### Layout and measurement Nothing can measure itself yet. | API | What happens | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `getBoundingClientRect()`, `getClientRects()` | Throws | | `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Silently `undefined`, so `width ?? 0` yields `0` and `width > 600` is always false | | `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (`typeof` guards do work) | | `window.matchMedia()`, `window.getComputedStyle()` | Throws | | `window.innerWidth`, `innerHeight`, `devicePixelRatio` | Silently `undefined` | | `new MutationObserver(fn)` | Constructs, then `.observe()` throws | So recharts `ResponsiveContainer`, Floating UI / Popper, list virtualization and drag-to-resize do not work yet. Do layout in CSS instead: your stylesheet reaches the real page, so flexbox, grid, `aspect-ratio`, `clamp()` and `@container` all behave normally. `requestAnimationFrame`, `fetch`, `setTimeout` and `queueMicrotask` work without the `window.` prefix. Only `window.requestAnimationFrame(...)` and friends throw. ### DOM access A `ref` gives you a sandbox element, not an `HTMLElement`. | What you write | What happens | Use instead | | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Throws | Controlled components; read values from `event.target` | | `element.classList.add(...)` | Throws (`classList` is `undefined`) | Build the `className` string yourself | | `document.getElementById()`, `getElementsByClassName()`, `createTreeWalker()` | Throws | `querySelector()` / `querySelectorAll()`, which work | | `document.activeElement` | Always `undefined` | Track focus with `onFocus` / `onBlur` | | `` | Renders nothing, no error | SVG, or draw offscreen and show an `` | | `createPortal(node, document.body)` | Renders nothing, while `isConnected` reports success | Overlays inline with `position: absolute`, or pass the library your own container element | The portal gap is why Radix, Headless UI, MUI and react-select popovers render nothing by default. Most accept a container prop; point it at an element you rendered. ### Events Mouse, pointer, touch, drag, keyboard, focus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` and `animationend`/`transitionend` cross to the host, plus a few per element: `load`/`error` on ``, clipboard and composition on ``/`