snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
---
title: Auto-Reply to Inbound Emails
description: 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.
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}}/>
### 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`.
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}}/>
### 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.
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}}/>
### 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.
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}}/>
### 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.
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}}/>
<Tip>
**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.
</Tip>
<Warning>
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.
</Warning>
### 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

View File

@@ -0,0 +1,170 @@
---
title: Closed Won Automations
description: 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 |
| **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
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)

View File

@@ -0,0 +1,133 @@
---
title: Detect Stale Opportunities
description: 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
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 |
| **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)

View File

@@ -0,0 +1,70 @@
---
title: Display Number of Emails Received
description: Create a workflow to automatically count and display the number of emails received from each contact.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/workflow-number-of-emails-received.png" style={{width:'100%'}}/>
## 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
<VimeoEmbed videoId="1148700336" title="Video demonstration" />
### 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)
<Note>
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.
</Note>
### 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.
<Note>
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.
</Note>
### 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)

View File

@@ -0,0 +1,165 @@
---
title: Display Related Record Data
description: 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}}`
<Note>
**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.
</Note>
## 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
<Note>
Be careful with bidirectional syncs to avoid infinite loops. Use filters to check if the value actually changed before updating.
</Note>
## Using Code for Complex Mapping
### Example: Transform Data During Copy
**Goal**: Copy and format phone number from person to opportunity.
```javascript
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)

View File

@@ -0,0 +1,199 @@
---
title: Formula Fields
description: 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 |
<Tip>
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).
</Tip>
## 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
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
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
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}}`
<Note>
Negative values indicate overdue tasks. You can use this field to filter or sort tasks by urgency.
</Note>
## 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
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
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)

View File

@@ -0,0 +1,126 @@
---
title: Notify Teammates of a Note to Review
description: Send automatic email notifications when assigning a colleague to review a note.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<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.
</Note>
## 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
<Warning>
**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.
</Warning>
## Step-by-Step Setup
### Step 1: Create the Reviewer Field
<img src="/images/user-guide/workflows/reviewer-field-for-note.png" style={{width:'100%'}}/>
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
<VimeoEmbed videoId="1151897168" title="Video demonstration" />
#### 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
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
```
<Note>
Replace `yourSubDomain` with your actual Twenty workspace subdomain.
</Note>
#### 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)

View File

@@ -0,0 +1,105 @@
---
title: Send Email Alerts with Tasks Due
description: Automatically notify team members about their upcoming or overdue tasks.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/email_tasks_due.png" style={{width:'100%'}}/>
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
<VimeoEmbed videoId="1147463514" title="Video demonstration" />
### 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
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 };
};
```
<Note>
Replace `yourSubDomain` with your actual Twenty workspace subdomain.
</Note>
### 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)