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 2s

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,163 @@
---
title: Send Emails from Workflows
description: 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
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 |
<Note>
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).
</Note>
## 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)

View File

@@ -0,0 +1,89 @@
---
title: Use Branches in Workflows
description: 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
<Note>
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.
</Note>
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

View File

@@ -0,0 +1,181 @@
---
title: Use Iterator
description: 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)

View File

@@ -0,0 +1,286 @@
---
title: Workflow Actions
description: Learn about the actions available in Twenty workflows.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## About Actions
Actions define what happens after a trigger fires. You can chain multiple actions together to build complex automations.
<Tip>
- 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
</Tip>
## Record Actions
<img src="/images/user-guide/workflows/workflow_actions.png" style={{width:'100%'}}/>
### 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.
<img src="/images/user-guide/workflows/update_record.png" style={{width:'100%'}}/>
**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.
<Note>
**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.
</Note>
**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.
<img src="/images/user-guide/workflows/upsert_double_identifier.png" style={{width:'100%'}}/>
**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.
<Note>
**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.
</Note>
<img src="/images/user-guide/workflows/upsert_simple_identifier.png" style={{width:'100%'}}/>
- 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.
<Note>
- 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.
</Note>
- 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.
<VimeoEmbed videoId="1146577247" title="Video demonstration" />
### 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
<Note>
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`.
</Note>
### 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
<Note>
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.
</Note>
**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
<Note>
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.
</Note>
For all the following steps, you can reference variables from previous steps for personalization.
- Enter recipient email address.
<Note>
Only one recipient is possible at the moment.
</Note>
- Set subject line.
- Compose message body. You can format links, create numbered lists, bullet point lists, and add attachments.
<Note>
Adding HTML signatures is not possible at the moment.
</Note>
### 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.
<Warning>
**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.
</Warning>
**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`.
<Note>
The fields cannot be made mandatory.
</Note>
<VimeoEmbed videoId="1147277989" title="Video demonstration" />
## 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.
<VimeoEmbed videoId="1147281795" title="Video demonstration" />
- Write JavaScript code in the editor
- Return variables for use in subsequent steps
- Test code directly in the step
<Note>
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.
</Note>
<Tip>
**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.
</Tip>
<Tip>
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.
</Tip>
#### 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.
<Tip>
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`.
</Tip>
### HTTP Request
Sends a request to an external API as part of your workflow.
<img src="/images/user-guide/workflows/http_action.png" style={{width:'100%'}}/>
**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.
<Note>
AI Agent actions consume workflow credits based on the AI model used. See [Workflow Credits](/user-guide/workflows/capabilities/workflow-credits) for details.
</Note>
<Note>
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.
</Note>

View File

@@ -0,0 +1,69 @@
---
title: Workflow Branches
description: 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.
<img src="/images/user-guide/workflows/branches.png" style={{width:'100%'}}/>
## 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
<Note>
Each branch is independent. Adding a branch doesn't affect other existing paths from that node.
</Note>
### 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.
<Note>
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."
</Note>
## 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

View File

@@ -0,0 +1,73 @@
---
title: Workflow Credits
description: 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 |
<Note>
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.
</Note>
## 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

View File

@@ -0,0 +1,84 @@
---
title: Workflow Runs
description: 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)

View File

@@ -0,0 +1,125 @@
---
title: Workflow Triggers
description: 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.
<img src="/images/user-guide/workflows/workflow_triggers.png" style={{width:'100%'}}/>
<Tip>
**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.
</Tip>
## 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.
<Note>
- 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.
</Note>
## 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).
<img src="/images/user-guide/workflows/add_to_newsletter_button.png" style={{width:'100%'}}/>
**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.
<Note>
**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.
</Note>
- **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).
<Note>
This is more advanced, and best for people who want to optimize the number of workflow runs.
</Note>
<img src="/images/user-guide/workflows/manual_trigger.png" style={{width:'100%'}}/>
**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
<Note>
**Timezone**: Scheduled workflows run in **UTC**. When setting hours for daily schedules, convert your local time to UTC.
</Note>
## External Trigger: Webhook
Starts the workflow when a GET or POST request is received from an external service.
<img src="/images/user-guide/workflows/webhook_trigger.png" style={{width:'100%'}}/>
**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|

View File

@@ -0,0 +1,76 @@
---
title: Workflow Versions
description: 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)

View File

@@ -0,0 +1,80 @@
---
title: Handle Arrays in Code Actions
description: 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
export const main = async (params: {
users: any;
}): Promise<object> => {
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
export const main = async (params: {
answers: any;
}): Promise<object> => {
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.
<Tip>
**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.
</Tip>
<Tip>
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.
</Tip>

View File

@@ -0,0 +1,182 @@
---
title: Bring Product Data into Twenty
description: 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
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}}/>
## 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
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 };
};
```
<Tip>
Filter by `updated_at >= last X minutes` to retrieve only recently changed records. This keeps the sync efficient.
</Tip>
## 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
export const main = async (params: {
users: any;
}): Promise<object> => {
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
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
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}}` |
<Tip>
Use **Upsert** (update or create) instead of building separate branches for create vs. update. It's faster to build and easier to debug.
</Tip>
## 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)

View File

@@ -0,0 +1,130 @@
---
title: Bring Typeform Submissions into Twenty
description: 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
{
"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
export const main = async (params: {
answers: any;
}): Promise<object> => {
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.
<Tip>
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).
</Tip>
### 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)

View File

@@ -0,0 +1,319 @@
---
title: Generate a PDF from Twenty
description: 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**)
<Tip>
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.
</Tip>
### 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}}` |
<Note>
If attaching to a different object (Person, Opportunity, etc.), rename the parameter accordingly (e.g., `personId`, `opportunityId`) and update the logic function.
</Note>
#### Logic Function Code
```typescript
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: Request an upload URL for the file
const createFileUploadMutation = `
mutation CreateFileUpload($filename: String!, $size: Float!, $fileFolder: FileFolder!, $fieldMetadataId: String) {
createFileUpload(filename: $filename, size: $size, fileFolder: $fileFolder, fieldMetadataId: $fieldMetadataId) {
fileId
uploadUrl
contentType
}
}
`;
const createFileUploadResponse = await fetch(metadataGraphqlEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: createFileUploadMutation,
variables: {
filename,
size: pdfFile.size,
fileFolder: 'FilesField',
fieldMetadataId: uploadFileFieldMetadataId,
},
}),
});
if (!createFileUploadResponse.ok) {
throw new Error(`Upload failed with status ${createFileUploadResponse.status}`);
}
const createFileUploadResult = await createFileUploadResponse.json();
if (createFileUploadResult.errors?.length) {
throw new Error(`Upload failed: ${createFileUploadResult.errors[0].message}`);
}
const uploadTarget = createFileUploadResult.data?.createFileUpload;
if (!uploadTarget) {
throw new Error('No upload target returned from createFileUpload');
}
const { fileId, uploadUrl, contentType } = uploadTarget;
// Step 4: Send the file bytes to the upload URL
const putResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: pdfFile,
});
if (!putResponse.ok) {
throw new Error(`Upload failed with status ${putResponse.status}`);
}
// Step 5: Confirm the upload so the file becomes usable
const completeFileUploadMutation = `
mutation CompleteFileUpload($fileId: String!) {
completeFileUpload(fileId: $fileId) {
id
}
}
`;
const completeFileUploadResponse = await fetch(metadataGraphqlEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: completeFileUploadMutation,
variables: { fileId },
}),
});
if (!completeFileUploadResponse.ok) {
throw new Error(`Upload failed with status ${completeFileUploadResponse.status}`);
}
const completeFileUploadResult = await completeFileUploadResponse.json();
if (completeFileUploadResult.errors?.length) {
throw new Error(`Upload failed: ${completeFileUploadResult.errors[0].message}`);
}
if (!completeFileUploadResult.data?.completeFileUpload?.id) {
throw new Error('No file id returned from completeFileUpload');
}
// Step 6: 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
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)

View File

@@ -0,0 +1,145 @@
---
title: Generate a Quote or Invoice from Twenty
description: 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) |
<Tip>
The trigger fires on any Opportunity update. The Filter ensures the workflow only continues when the stage changes to Closed Won.
</Tip>
## 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
export const main = async (params: {
opportunity: any;
company: any;
}): Promise<object> => {
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)

View File

@@ -0,0 +1,168 @@
---
title: Set Up a Webhook Trigger
description: 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
{
"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
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
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Reference with: `{{trigger.body.contact.email}}`
### Arrays
If data includes arrays:
```json
{
"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
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
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.
<Tip>
For more details on handling arrays, see [Handle Arrays in Code Actions](/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Related
- [Workflow Triggers](/user-guide/workflows/capabilities/workflow-triggers)
- [Workflow Actions](/user-guide/workflows/capabilities/workflow-actions)

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)

View File

@@ -0,0 +1,19 @@
---
title: Professional Services
description: 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).

View File

@@ -0,0 +1,156 @@
---
title: Workflow Troubleshooting
description: 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
<Tip>
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.
</Tip>
### 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

View File

@@ -0,0 +1,237 @@
---
title: Workflows FAQ
description: Frequently asked questions about workflows in Twenty.
---
<AccordionGroup>
<Accordion title="Why can't I activate a workflow?">
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.
</Accordion>
<Accordion title="Why don't I see my manual workflow button in the top navbar?">
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.
</Accordion>
<Accordion title="What's the difference between Single, Global, and Bulk manual triggers?">
| 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.
</Accordion>
<Accordion title="I don't see an If/Else node. What should I do?">
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.
</Accordion>
<Accordion title="Are workflow branches running in parallel?">
**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.
</Accordion>
<Accordion title="Can I merge branches back together?">
**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.
</Accordion>
<Accordion title="What's the limit on Search Records results?">
**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.
</Accordion>
<Accordion title="Can I CC people on workflow emails?">
**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.
</Accordion>
<Accordion title="How do I use outputs from previous steps?">
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.
</Accordion>
<Accordion title="Why does my Iterator fail?">
**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}}`
</Accordion>
<Accordion title="What's the maximum execution time for Code actions?">
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
</Accordion>
<Accordion title="How do I read workflow runs?">
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.
</Accordion>
<Accordion title="Why don't I see my workflow runs?">
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
</Accordion>
<Accordion title="What are the workflow rate limits?">
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
</Accordion>
<Accordion title="Can I retry a failed workflow run?">
**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.
</Accordion>
<Accordion title="Should I deactivate workflows before CSV imports?">
**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.
</Accordion>
<Accordion title="How do I clean up my workflow layout?">
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.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,77 @@
---
title: Workflows
description: 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
<img src="/images/user-guide/workflows/workflow_triggers.png" style={{width:'100%'}}/>
### 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
<img src="/images/user-guide/workflows/workflow_actions.png" style={{width:'100%'}}/>
### 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?
<Tip>
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.
</Tip>