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,82 @@
---
title: Manejar arrays en acciones de Code
description: Aprende a manejar correctamente las entradas de arrays en las acciones de Code de flujos de trabajo.
---
Al trabajar con arrays en acciones de Code, puedes encontrarte con dos desafíos comunes:
1. **Arrays pasados como cadenas** — los datos de sistemas externos o pasos anteriores llegan como una cadena en lugar de un array real
2. **No se pueden seleccionar elementos individuales** — solo puedes seleccionar el array completo, no campos específicos dentro de él
Ambos se pueden resolver con un nodo Code.
## Analizar arrays a partir de cadenas
A menudo, los arrays se pasan entre pasos de un flujo de trabajo como cadenas o JSON en lugar de arrays nativos. Esto ocurre cuando:
* Se reciben datos de APIs externas mediante HTTP Request
* Procesar cargas útiles de webhooks
* Pasar datos entre pasos del flujo de trabajo
**Solución**: agrega este patrón al comienzo de tu acción de Code:
```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(),
})),
};
};
```
La línea clave `typeof users === "string" ? JSON.parse(users) : users` verifica si la entrada es una cadena, la analiza si es necesario o la usa directamente si ya es un array.
## Extraer campos individuales de arrays
Un webhook podría devolver un array como `answers: [...]`, pero en pasos posteriores del flujo de trabajo solo puedes seleccionar el **array completo**, no elementos individuales dentro de él.
**Solución**: agrega un nodo Code para extraer campos específicos y devolverlos como un objeto estructurado:
```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
}
};
};
```
El nodo Code devuelve un objeto estructurado en lugar de un array. En los pasos posteriores, ahora puedes seleccionar campos individuales como `answer.firstname` y `answer.name` desde el selector de variables.
<Tip>
**¿Quieres iterar sobre el array en lugar de extraer campos?** Cuando un paso de Code o Logic Function devuelve un array de nivel superior, puedes pasarlo directamente a un [Iterator](/l/es/user-guide/workflows/capabilities/use-iterator): selecciona la opción **Whole list** del paso como la entrada del Iterator y luego haz referencia a cada elemento con `{{iterator.currentItem}}` dentro del bucle. En ese caso, no necesitas reestructurar el array en un objeto.
</Tip>
<Tip>
Haz clic en el icono cuadrado en la parte superior derecha del editor de código para mostrarlo en pantalla completa — es útil, ya que el ancho predeterminado del editor es limitado.
</Tip>

View File

@@ -0,0 +1,182 @@
---
title: Incorporar datos de productos en Twenty
description: Sincroniza los datos del catálogo de productos desde un almacén de datos a tu CRM según una programación.
---
Usa este patrón para mantener Twenty sincronizado con los datos de productos de tu almacén de datos (p. ej., Snowflake, BigQuery, PostgreSQL).
## Estructura del flujo de trabajo
1. **Disparador**: Según una programación
2. **Code**: Consultar tu almacén de datos
3. **Code** (opcional): Formatear los datos como array
4. **Iterator**: Recorrer cada producto
5. **Upsert Record**: Crear o actualizar en Twenty
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## Paso 1: Programar el disparador
Configura el flujo de trabajo para ejecutarse con una frecuencia acorde a tus necesidades de actualización de datos:
* Cada 5 minutos para una sincronización casi en tiempo real
* Cada hora para datos menos críticos
* Diariamente para actualizaciones por lotes
## Paso 2: Consulta tu almacén de datos
Añade una acción **Code** para obtener datos recientes:
```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>
Filtra por `updated_at >= last X minutes` para recuperar solo los registros cambiados recientemente. Esto mantiene la sincronización eficiente.
</Tip>
## Paso 3: Formatear los datos (opcional)
Si tu almacén devuelve datos en un formato que necesita transformación, añade otra acción **Code**. Las transformaciones comunes incluyen conversiones de tipo, cambio de nombre de campos y limpieza de datos.
### Ejemplo: Datos de usuario con campos booleanos y de estado
```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),
})),
};
};
```
### Ejemplo: Datos de producto con conversiones de tipo
```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
}))
};
};
```
### Ejemplo: Formato de fechas y divisas
```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, " ")
}))
};
};
```
### Transformaciones comunes
| Formato de origen | Formato de destino | Código |
| -------------------- | ------------------ | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | Fecha ISO | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## Paso 4: Recorrer los productos
Añade una acción **Iterator**:
* Entrada: `{{code.products}}`
Esto recorre cada producto del array.
## Paso 5: Insertar o actualizar cada registro
Dentro del iterador, añade una acción **Upsert Record**:
| Configuración | Valor |
| ----------------- | -------------------------------------- |
| **Objeto** | Tu objeto de producto personalizado |
| **Coincidir por** | ID externo o SKU (identificador único) |
| **Nombre** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Precio** | `{{iterator.item.price}}` |
<Tip>
Usa **Upsert** (actualizar o crear) en lugar de construir ramas separadas para crear frente a actualizar. Es más rápido de construir y más fácil de depurar.
</Tip>
## Casos de uso de ejemplo
| Fuente | Datos |
| -------------------------------------- | --------------------------------------------------- |
| **Sistema ERP** | Catálogo de productos, precios, inventario |
| **Plataforma de comercio electrónico** | Pedidos, clientes, actualizaciones de productos |
| **Almacén de datos** | Métricas agregadas, datos enriquecidos |
| **Sistema de inventario** | Niveles de existencias, alertas de reabastecimiento |
## Relacionado
* [Disparadores de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-triggers)
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)
* [Manejar arreglos en acciones de código](/l/es/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Importa las respuestas de Typeform a Twenty
description: Gestiona la carga útil del webhook de Typeform para crear clientes potenciales a partir de envíos de formularios.
---
Para la configuración estándar de webhooks, consulta [Configurar un disparador de Webhook](/l/es/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). Este artículo cubre el manejo específico requerido para la estructura de carga útil personalizada de Typeform.
### Paso 1: Crear un flujo de trabajo de Webhook
1. Ve a **Configuración → Flujos de trabajo**
2. Haz clic en **+ Nuevo flujo de trabajo**
3. Selecciona **Webhook** como el disparador
4. Copia la URL del webhook
### Paso 2: Configura Typeform
1. En Typeform, abre tu formulario
2. Ve a **Conectar → Webhooks**
3. Pega tu URL de webhook de Twenty
4. Guardar
### Paso 3: Comprender la carga útil de Typeform
Typeform envía una estructura JSON anidada. Aquí hay un ejemplo simplificado:
```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" }
}
]
}
}
```
Aspectos clave a tener en cuenta:
* Los datos del formulario están anidados bajo `form_response`
* **Las respuestas se devuelven como un arreglo**, no como campos con nombre
* Cada respuesta incluye el tipo y el título del campo como referencia
### Paso 4: Extraer campos del arreglo de respuestas
Dado que `answers` es un arreglo, en los pasos posteriores solo puedes seleccionar el arreglo completo — no campos individuales. Añade una acción **Code** para extraer los campos que necesitas:
```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
}
};
};
```
Ahora, en los pasos posteriores, puedes seleccionar `contact.firstName`, `contact.email`, etc., desde el selector de variables.
<Tip>
Para más detalles sobre cómo manejar arreglos en acciones de código, consulta [Cómo manejar arreglos en acciones de código](/l/es/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### Paso 5: Crear el registro
Añade una acción **Create Record**:
| Campo | Valor |
| ---------------------- | ----------------------------------------------- |
| **Objeto** | Personas |
| **Nombre** | `{{code.contact.firstName}}` |
| **Apellidos** | `{{code.contact.lastName}}` |
| **Correo electrónico** | `{{code.contact.email}}` |
| **Empresa** | Buscar o crear según `{{code.contact.company}}` |
### Paso 6: Probar y activar
1. Envía una respuesta de prueba en Typeform
2. Revisa la ejecución del flujo de trabajo para verificar que se capturaron los datos
3. Activa el flujo de trabajo
## Relacionado
* [Configurar un disparador de Webhook](/l/es/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Manejar arreglos en acciones de código](/l/es/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Generar un PDF desde Twenty
description: Cree un flujo de trabajo para generar y adjuntar un PDF (como una cotización) a un registro.
---
Genere u obtenga automáticamente un PDF y adjúntelo a un registro en Twenty. Esto se usa comúnmente para crear cotizaciones, facturas o informes vinculados a Empresas, Oportunidades u otros objetos.
## Resumen
Este flujo de trabajo utiliza un **Activador manual** para que los usuarios puedan generar un PDF bajo demanda para cualquier registro seleccionado. Una **Función de lógica** se encarga de:
1. Descargar el PDF desde una URL (de un servicio de generación de PDF)
2. Subir el archivo a Twenty
3. Crear un archivo adjunto vinculado al registro
## Prerrequisitos
Antes de configurar el flujo de trabajo:
1. **Cree una clave de API**: Vaya a **Settings → APIs** y cree una nueva clave de API. Necesitarás este token para la función lógica.
2. **Configure un servicio de generación de PDF** (opcional): Si desea generar PDFs dinámicamente (p. ej., cotizaciones), use un servicio como Carbone, PDFMonkey o DocuSeal para crear el PDF y obtener una URL de descarga.
## Configuración paso a paso
### Paso 1: Configurar el disparador
1. Vaya a **Flujos de trabajo** y cree un nuevo flujo de trabajo
2. Seleccione **Activador manual**
3. Elija el objeto al que desea adjuntar archivos PDF (p. ej., **Empresa** u **Oportunidad**)
<Tip>
Con un disparador manual, los usuarios pueden ejecutar este flujo de trabajo usando un botón que aparece en la parte superior derecha una vez seleccionado un registro, esto ejecuta el flujo de trabajo para generar y adjuntar un PDF.
</Tip>
### Paso 2: Añadir una Función de lógica
1. Agrega una acción de **código** (función lógica)
2. Cree una nueva función con el siguiente código
3. Configure los parámetros de entrada
#### Parámetros de entrada
| Parámetro | Valor |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
Si va a adjuntar a un objeto diferente (Persona, Oportunidad, etc.), cambie el nombre del parámetro en consecuencia (p. ej., `personId`, `opportunityId`) y actualice la función de lógica.
</Note>
#### Código de la Función de lógica
```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: Upload the file via GraphQL multipart upload
const uploadMutation = `
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
id
}
}
`;
const uploadForm = new FormData();
uploadForm.append('operations', JSON.stringify({
query: uploadMutation,
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
}));
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
uploadForm.append('0', pdfFile);
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
method: 'POST',
headers: { Authorization: `Bearer ${authToken}` },
body: uploadForm,
});
const uploadResult = await uploadResponse.json();
if (uploadResult.errors?.length) {
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
}
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
if (!fileId) {
throw new Error('No file id returned from upload');
}
// Step 4: Create the attachment linked to the company
const attachmentMutation = `
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
createAttachment(data: $data) {
id
name
}
}
`;
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: attachmentMutation,
variables: {
data: {
name: filename,
targetCompanyId: companyId,
file: [
{
fileId: fileId,
label: filename
}
]
},
},
}),
});
const attachmentResult = await attachmentResponse.json();
if (attachmentResult.errors?.length) {
throw new Error(`Attachment creation failed: ${attachmentResult.errors[0].message}`);
}
return attachmentResult.data?.createAttachment;
};
```
### Paso 3: Personalice para su caso de uso
#### Para adjuntar a un objeto diferente
Reemplace `targetCompanyId` por el campo correspondiente:
| Objeto | Nombre del campo |
| -------------------- | -------------------------- |
| Empresa | `targetCompanyId` |
| Persona | `targetPersonId` |
| Oportunidad | `targetOpportunityId` |
| Objeto personalizado | `targetYourCustomObjectId` |
Actualice tanto el parámetro de la función como el objeto `variables.data` en la mutación de adjunto.
#### Para usar una URL de PDF dinámica
Si usa un servicio de generación de PDF, puede:
1. Primero, cree una acción de solicitud HTTP para generar el PDF
2. Pase la URL de PDF devuelta a la función de lógica como parámetro
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### Paso 4: Probar y activar
1. Guarde el flujo de trabajo
2. Vaya a un registro de Empresa
3. Haga clic en el menú **⋮** y seleccione su flujo de trabajo
4. Revise la sección **Adjuntos** del registro para verificar que el PDF se haya adjuntado
5. Activa el flujo de trabajo
## Combinación con servicios de generación de PDF
Para crear cotizaciones o facturas dinámicas:
### Ejemplo: Generar cotización → Adjuntar PDF
| Paso | Acción | Propósito |
| ---- | -------------------------- | --------------------------------------------------------------- |
| 1 | Activador manual (Empresa) | El usuario lo inicia en un registro |
| 2 | Buscar registro | Obtener detalles de la oportunidad o de las partidas |
| 3 | Solicitud HTTP | Llamar a la API de generación de PDF con los datos del registro |
| 4 | Función de lógica | Descargar y adjuntar el PDF generado |
### Servicios populares de generación de PDF
* **Carbone** - Generación de documentos basada en plantillas
* **PDFMonkey** - Creación dinámica de PDF a partir de plantillas
* **DocuSeal** - Plataforma de automatización de documentos
* **Documint** - Generación de documentos con enfoque API-first
Cada servicio proporciona una API que devuelve una URL PDF, la cual puede pasar a la función lógica.
## Solución de Problemas
| Problema | Solución |
| --------------------------- | --------------------------------------------------------------------------------------- |
| "Error al descargar el PDF" | Compruebe que la URL del PDF sea accesible y devuelva un PDF válido |
| "Error al subir" | Verifique que su clave de API sea válida y tenga permisos de escritura |
| "Error al crear el adjunto" | Asegúrese de que el nombre del campo de ID del objeto coincida con su objeto de destino |
## Relacionado
* [Disparadores de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-triggers)
* [Funciones de lógica](/l/es/user-guide/workflows/capabilities/workflow-actions#code)
* [Generar una cotización o factura desde Twenty](/l/es/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: Generar una cotización o una factura desde Twenty
description: Crea automáticamente facturas en herramientas externas cuando se ganan las oportunidades.
---
Envía automáticamente los datos de la oportunidad a tu sistema de facturación (Stripe, QuickBooks, Xero, etc.) cuando se gana una oportunidad.
## Estructura del flujo de trabajo
1. **Disparador**: Se actualiza un registro (Oportunidad)
2. **Filtro**: Etapa = Cerrado ganado
3. **Search Record**: Obtener detalles de la empresa
4. **Code** (opcional): Formatear la carga útil
5. **HTTP Request**: Enviar al sistema de facturación
## Paso 1: Configurar el disparador
1. Crear un nuevo flujo de trabajo
2. Selecciona el disparador **Record is Updated**
3. Elige **Opportunity** como objeto
## Paso 2: Filtrar por Cerrado ganado
Añade una acción **Filter** para continuar solo cuando la oportunidad esté ganada:
| Configuración | Valor |
| ------------- | -------------------------------------- |
| **Campo** | Etapa |
| **Condición** | Igual a |
| **Valor** | `CLOSED_WON` (o el nombre de tu etapa) |
<Tip>
El disparador se activa con cualquier actualización de una Oportunidad. El filtro garantiza que el flujo de trabajo solo continúe cuando la etapa cambie a Cerrado ganado.
</Tip>
## Paso 3: Obtener detalles de la empresa
Es posible que el registro de Oportunidad no incluya todos los campos de Empresa que necesitas para la factura. Añade una acción **Search Record**:
| Configuración | Valor |
| ----------------- | ----------------------------------------- |
| **Objeto** | Empresa |
| **Coincidir por** | ID igual a `{{trigger.object.companyId}}` |
Esto recupera el registro completo de Empresa con dirección de facturación, ID fiscal, etc.
## Paso 4: Formatear la carga útil (opcional)
Si tu sistema de facturación exige un formato específico, añade una acción **Code**:
```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
}
}
};
};
```
## Paso 5: Enviar al sistema de facturación
Añade una acción **HTTP Request**:
| Configuración | Valor |
| --------------- | --------------------------------------------------- |
| **Método** | POST |
| **URL** | Tu endpoint de API de facturación |
| **Encabezados** | `Authorization: Bearer YOUR_API_KEY` |
| **Cuerpo** | `{{code.invoice}}` o asigna los campos directamente |
### Ejemplo: Factura de Stripe
```
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
```
### Ejemplo: Factura de QuickBooks
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## Resumen completo del flujo de trabajo
| Paso | Acción | Propósito |
| ---- | -------------------------------- | --------------------------------------------------------- |
| 1 | Disparador: Registro actualizado | Se activa cuando cambia cualquier Oportunidad |
| 2 | Filtro | Procede solo si la etapa = Cerrado ganado |
| 3 | Search Record | Obtener todos los detalles de Empresa para la facturación |
| 4 | Código | Dar formato a los datos para la API de facturación |
| 5 | Solicitud HTTP | Crear la factura en el sistema externo |
## Consejos
* **Almacenar IDs externos**: Guarda el ID de la factura devuelto por la API en la Oportunidad usando una acción **Update Record**
* **Gestión de errores**: Añade una rama para enviar una notificación si la solicitud HTTP falla
* **Prueba primero**: Utiliza el modo sandbox/pruebas de tu sistema de facturación antes de pasar a producción
## Relacionado
* [Generar un PDF desde Twenty](/l/es/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — adjuntar los PDF generados a los registros
* [Disparadores de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-triggers)
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)
* [Automatizaciones de Cerrado ganado](/l/es/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Configurar un disparador de webhook
description: Recibe datos de servicios externos para activar flujos de trabajo.
---
Los disparadores de webhook permiten que servicios externos inicien tus flujos de trabajo enviando datos a una URL única. Úsalos para conectar formularios, aplicaciones de terceros e integraciones personalizadas.
## Cuándo usar webhooks
| Caso de uso | Ejemplo |
| -------------------------------- | ---------------------------------------------------------------- |
| **Formularios web** | Los envíos del formulario de contacto crean clientes potenciales |
| **Aplicaciones de terceros** | Pago de Stripe → crear registro de cliente |
| **Integraciones personalizadas** | Tu aplicación → Automatización de Twenty |
| **Herramientas sin código** | Conexiones con Zapier, Make, n8n |
## Configuración paso a paso
### Paso 1: Crear el flujo de trabajo
1. Ve a **Configuración → Flujos de trabajo**
2. Haz clic en **+ Nuevo flujo de trabajo**
3. Ponle un nombre (p. ej., "Envío de formulario del sitio web")
### Paso 2: Configurar el disparador de webhook
1. Haz clic en el bloque del disparador
2. Selecciona **Webhook**
3. Recibirás una URL de webhook única como:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. Copia esta URL; la necesitarás para tu servicio externo
### Paso 3: Definir la estructura de datos esperada
Para solicitudes **POST**, define la estructura esperada del cuerpo:
1. Haz clic en **Definir cuerpo esperado**
2. Introduce un JSON de ejemplo que coincida con lo que enviará tu servicio:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. Haz clic en **Guardar**—esto crea variables que puedes usar en pasos posteriores
### Paso 4: Agregar acciones
Ahora agrega acciones que utilicen los datos del webhook:
**Ejemplo: Crear un registro de Persona**
1. Añade la acción **Crear registro**
2. Selecciona el objeto **Personas**
3. Asigna los campos:
| Campo | Valor |
| ------------------ | ----------------------------------------------- |
| Nombre | `{{trigger.body.firstName}}` |
| Apellidos | `{{trigger.body.lastName}}` |
| Correo electrónico | `{{trigger.body.email}}` |
| Empresa | Buscar o crear según `{{trigger.body.company}}` |
### Paso 5: Probar el webhook
Antes de activarlo, prueba tu webhook:
**Usando 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"}'
```
**Usando Postman o similar**:
1. Crea una solicitud POST a tu URL de webhook
2. Establece el encabezado Content-Type en `application/json`
3. Añade tu cuerpo JSON de prueba
4. Envía y verifica las ejecuciones del flujo de trabajo
### Paso 6: Activar
Una vez probado, haz clic en **Activar** para poner el flujo de trabajo en marcha.
## Cómo manejar diferentes estructuras de datos
### Datos anidados
Si tu webhook envía datos anidados:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Haz referencia con: `{{trigger.body.contact.email}}`
### Arrays
Si los datos incluyen arrays:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
Cómo manejes los arrays depende de tu caso de uso:
**Número desconocido de elementos → Usa Iterator**
Si necesitas procesar cada elemento del array (p. ej., crear un registro para cada uno), añade una acción **Code** para analizar el array y luego usa **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
Luego usa Iterator para iterar: `{{code.items}}`
**Campos conocidos/específicos → Extraer a campos con nombre**
Si el array contiene campos específicos a los que quieres acceder individualmente (p. ej., respuestas de formulario donde la posición 0 siempre es "nombre", la posición 1 siempre es "apellido"), añade una acción **Code** para extraerlos:
```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
}
};
};
```
Ahora puedes seleccionar `product.name` y `product.qty` individualmente en pasos posteriores.
<Tip>
Para más detalles sobre cómo manejar arrays, consulta [Cómo manejar arrays en acciones de código](/l/es/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Relacionado
* [Disparadores de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-triggers)
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)

View File

@@ -0,0 +1,121 @@
---
title: Respuesta automática a correos entrantes
description: Crea un flujo de trabajo que utilice IA para clasificar los correos entrantes y enviar respuestas en hilo automáticamente.
---
Responde a los correos entrantes en segundos — no en horas. Este flujo de trabajo utiliza un agente de IA para filtrar el ruido (boletines, spam, respuestas automáticas) y redactar una respuesta personalizada a los mensajes reales. Luego, AI lo envía como una respuesta en hilos dentro de la conversación original.
## Cómo funcionan los hilos de correo electrónico
Cada correo electrónico lleva un encabezado oculto `Message-ID` — un identificador único asignado por el servidor de correo del remitente. Cuando respondes a un correo electrónico, tu cliente de correo establece un encabezado `In-Reply-To` que hace referencia a esa huella. Así es como Gmail, Outlook y cualquier otro cliente agrupan los mensajes en hilos.
En Twenty, esa huella se almacena como `headerMessageId` en el objeto Message. Tu flujo de trabajo la toma y la pasa al campo In-Reply-To de la acción Send Email.
## Crear el flujo de trabajo
### Paso 1: Crear un nuevo flujo de trabajo
Ve a **Settings -> Workflows** y haz clic en **+ New Workflow**.
### Paso 2: Activar con mensajes entrantes
Selecciona **When a Record is Created** y elige **Messages**.
Cada vez que un correo electrónico llega a Twenty, esto se activa.
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
### Paso 3: Buscar quién lo envió
Añade una acción **Search Records**.
La dirección del remitente no está en el propio mensaje — está en el registro relacionado Message Participant.
| Campo | Valor |
| ---------- | ------------------------------- |
| **Objeto** | Participantes del mensaje |
| **Filtro** | Message **es** `{{trigger.id}}` |
| **Filtro** | Role **es** From |
| **Límite** | 1 |
Esto te da el correo electrónico del remitente en `handle` y su nombre en `displayName`.
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
### Paso 4: Trienal y redactar una respuesta con IA
Añade una acción **AI Agent**. Este único paso hace dos cosas: decide si el correo merece una respuesta y, en ese caso, redacta una.
Usa un prompt como:
```
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.
```
El AI Agent genera su respuesta en un campo `response` que los siguientes pasos pueden consultar.
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
### Paso 5: Ramificar según la decisión de la IA
Añade una acción **If/Else** para comprobar si la IA decidió responder u omitir.
| Campo | Valor |
| ------------------- | ------------------------------------------- |
| **Condición** | AI Agent `response` **no contiene** `SKIP` |
| **Si es verdadero** | Continuar con Send Email |
| **Si no** | No hacer nada (el flujo de trabajo termina) |
El spam, los boletines y los mensajes autogenerados se descartan. Todo lo demás pasa al siguiente paso.
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
### Paso 6: Enviar una respuesta en hilo
Añade una acción **Send Email** en la rama "si es verdadero". Haz clic en **Advanced options**, y luego en **Add In-Reply-To**.
| Campo | Valor |
| --------------- | -------------------------------------- |
| **Para** | `{{Find Sender.first.handle}}` |
| **Asunto** | `Re: {{trigger.subject}}` |
| **Cuerpo** | `{{AI Triage & Draft Reply.response}}` |
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
El campo In-Reply-To es lo que hace que esto sea una respuesta en lugar de una conversación nueva. El destinatario la ve en hilo debajo del correo original en Gmail, Outlook o cualquier otro cliente.
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
<Tip>
**In-Reply-To** espera un `message.headerMessageId` del disparador — es la huella única del correo, no una dirección del destinatario. Si lo dejas vacío, el correo igualmente se envía, solo que como un mensaje independiente.
</Tip>
<Warning>
Gmail usa la línea de asunto para agrupar los mensajes en hilos. El asunto **debe** comenzar con `Re:` (incluidos los dos puntos y el espacio) para que Gmail muestre la respuesta dentro del hilo original. Sin eso, la respuesta aparecerá como una conversación separada — incluso si el encabezado In-Reply-To está configurado correctamente.
</Warning>
### Paso 7: Probar y activar
Haz clic en **Test** y luego revisa tu cliente de correo. La respuesta debería aparecer anidada debajo del mensaje original.
Actívalo cuando estés satisfecho con el resultado.
## Ideas para ampliar
* **Responder solo a VIPs** — añade una rama que compruebe el dominio del remitente o si existe como Contact en Twenty
* **Enrutar por intención** — usa prompts de AI Agent separados para tratar las consultas de ventas de forma diferente a las solicitudes de soporte
* **Enriquecer antes de responder** — añade un paso Search Records para incorporar la empresa del remitente o el historial de oportunidades al prompt de IA para respuestas más personalizadas

View File

@@ -0,0 +1,179 @@
---
title: Automatizaciones de Cerrado Ganado
description: Automatiza las actividades posteriores al cierre cuando las oportunidades se cierran como ganadas.
---
Cuando se cierra un negocio, deben ocurrir varias cosas: actualizar el estado de la empresa, notificar a los miembros del equipo, crear tareas de incorporación. Automatiza todo esto con un único flujo de trabajo.
## Problema
Cuando una oportunidad pasa a "Cerrado ganado":
* El tipo de empresa debe cambiar de "Prospect" a "Customer"
* Se deben crear tareas de incorporación
* Se debe notificar al equipo de Customer Success
* El representante de ventas necesita una confirmación
Hacer esto manualmente consume tiempo y es propenso a errores.
## La solución
Crea un flujo de trabajo que gestione automáticamente todas las actividades posteriores al cierre ganado.
## Configuración completa del flujo de trabajo
### Paso 1: Crear el flujo de trabajo
1. Ve a **Configuración → Flujos de trabajo**
2. Haz clic en **+ Nuevo flujo de trabajo**
3. Asígnale el nombre "Deal Won - Post-Win Automation"
### Paso 2: Configurar el disparador
1. Selecciona **Record is Updated**
2. Elige **Oportunidades**
3. En "Campos a supervisar", selecciona **Etapa**
### Paso 3: Agregar filtro de etapa
1. Añade la acción **Filter**
2. Condición: `{{trigger.object.stage}}` es igual a "Closed Won"
### Paso 4: Actualizar el tipo de empresa
1. Añade la acción **Update Record**
2. Configurar:
| Campo | Valor |
| ---------------------------- | ------------------------------- |
| **Objeto** | Empresas |
| **Registro** | `{{trigger.object.company.id}}` |
| **Tipo** | Cliente |
| **Fecha del primer negocio** | `{{trigger.object.closedAt}}` |
| **Propietario de la cuenta** | `{{trigger.object.owner.id}}` |
### Paso 5: Crear tarea de incorporación
1. Añade la acción **Create Record**
2. Configurar:
| Campo | Valor |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Objeto** | Tareas |
| **Título** | `Incorporación: {{trigger.object.name}}` |
| **Asignado** | Miembro del equipo de Customer Success |
| **Fecha de vencimiento** | Dentro de 3 días |
| **Prioridad** | Alta |
| **Empresa relacionada** | `{{trigger.object.company.id}}` |
| **Oportunidad relacionada** | `{{trigger.object.id}}` |
| **Descripción** | `Incorporación de nuevo cliente para {{trigger.object.company.name}}. Valor del negocio: {{trigger.object.amount}}` |
### Paso 6: Notificar a Customer Success
1. Añade la acción **Send Email**
2. Configurar:
| Campo | Valor |
| ---------- | --------------------------------------------------- |
| **Para** | customer-success@yourcompany.com |
| **Asunto** | `🎉 Nuevo cliente: {{trigger.object.company.name}}` |
| **Cuerpo** | Consulta el ejemplo a continuación |
**Ejemplo de cuerpo del correo**:
```
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!
```
### Paso 7: Confirmar al representante de ventas
1. Añade otra acción **Send Email**
2. Configurar:
| Campo | Valor |
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Para** | `{{trigger.object.owner.email}}` |
| **Asunto** | `✅ Negocio cerrado: {{trigger.object.name}}` |
| **Cuerpo** | ¡Felicidades! Tu negocio ha sido procesado. Se ha notificado al equipo de Customer Success y la incorporación ha comenzado. |
### Paso 8: Probar y activar
1. Prueba moviendo una oportunidad de prueba a "Closed Won"
2. Verificar:
* El tipo de empresa cambió a "Customer"
* Tarea de incorporación creada
* El equipo de CS recibió el correo electrónico
* El representante de ventas recibió la confirmación
3. Actívalo cuando esté listo
## Cómo gestionar Closed Lost
Crea un flujo de trabajo similar para negocios perdidos:
### Disparador
* Record is Updated (Oportunidades, Etapa = "Closed Lost")
### Acciones
1. **Create Record**: Tarea para "Análisis de negocio perdido"
2. **Update Record**: Agrega el motivo de pérdida al registro de la empresa
3. **Send Email**: Notifica al gerente sobre el negocio perdido
## Avanzado: incorporación de varios pasos
Para una incorporación compleja, crea varias tareas:
```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 };
};
```
Usa **Iterator** para crear cada tarea a partir del array.
## Ideas de personalización
### Mantén tus otras herramientas actualizadas
* Crea el cliente en el sistema de facturación con una **HTTP Request**
### Acciones condicionales
Usa acciones **Filter** para:
* Incorporación diferente para empresas grandes frente a pymes
* Asignados diferentes según la región
* Omitir notificaciones para negocios pequeños
### Incluye detalles de la oferta
Usa la acción **Code** para formatear:
* Documentos de resumen del negocio
* Notas de traspaso para el equipo de CS
* Listas de verificación de incorporación personalizadas
## Relacionado
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)
* [Enviar correos electrónicos desde flujos de trabajo](/l/es/user-guide/workflows/capabilities/send-emails-from-workflows)

View File

@@ -0,0 +1,136 @@
---
title: Detectar oportunidades estancadas
description: Notifica automáticamente a los gerentes cuando las oportunidades no se han actualizado.
---
Mantén tu pipeline saludable informando a los gerentes cuando las oportunidades se estanquen. Este flujo de trabajo revisa las oportunidades que no se han actualizado en un número específico de días.
## Problema
Las oportunidades sin actualizaciones provocan:
* Acuerdos que se enfrían
* Pronósticos poco fiables
* Pérdida de ingresos
## La solución
Crea un flujo de trabajo programado que encuentre oportunidades estancadas y envíe correos electrónicos a sus gerentes.
## Configuración paso a paso
### Paso 1: Crear el flujo de trabajo
1. Ve a **Configuración → Flujos de trabajo**
2. Haz clic en **+ Nuevo flujo de trabajo**
3. Asígnale el nombre "Alerta de oportunidades estancadas"
### Paso 2: Configurar el disparador
1. Selecciona **On a Schedule**
2. Configúralo para ejecutarse diariamente (p. ej., todos los días a las 8 a. m.)
### Paso 3: Buscar oportunidades estancadas
1. Añade la acción **Search Records**
2. Configurar:
| Campo | Valor |
| ---------- | ---------------------------------------------- |
| **Objeto** | Oportunidades |
| **Filtro** | Updated At es anterior a (hoy - 7 días) |
| **Filtro** | Stage no es "Closed Won" Y no es "Closed Lost" |
| **Límite** | 100 |
### Paso 4: Comprobar si se encontró algo
1. Añade la acción **Filter**
2. Condición: `{{searchRecords.length}}` es mayor que 0
3. Si no hay oportunidades estancadas, el flujo de trabajo se detiene aquí
### Paso 5: Formatear la alerta (acción de código)
Añade una acción **Code** para formatear el correo electrónico:
```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
};
};
```
### Paso 6: Enviar correo de alerta
Añade la acción **Send Email**:
| Campo | Valor |
| ---------- | -------------------------------------------------------------------- |
| **Para** | sales-manager@yourcompany.com |
| **Asunto** | `🚨 {{code.totalCount}} oportunidades estancadas requieren atención` |
| **Cuerpo** | `{{code.summary}}` |
### Paso 7: Probar y activar
1. Haz clic en **Test** para ejecutar el flujo de trabajo
2. Verifica que el correo electrónico contenga los datos correctos
3. Actívalo cuando esté listo
## Opciones de personalización
### Cambiar el umbral de estancamiento
Modifica el filtro de Search Records para cambiar de 7 días a tu periodo preferido:
* 3 días para ventas de alto ritmo
* 14 días para acuerdos empresariales
* 30 días para ciclos de ventas largos
### Alertar a los representantes individuales
En lugar de un solo correo para el gerente, usa **Iterator** para enviar correos personalizados a cada representante sobre sus propios acuerdos estancados.
### Agregar escalamiento
Crea varios flujos de trabajo con severidad creciente:
1. Día 7: Correo al representante
2. Día 14: Correo al representante + gerente
3. Día 21: Crear una tarea para que el gerente intervenga
### Incluir en Slack
Usa **HTTP Request** para publicar en un webhook de Slack en lugar del correo electrónico o además de este.
## Relacionado
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)
* [Enviar correos electrónicos desde flujos de trabajo](/l/es/user-guide/workflows/capabilities/send-emails-from-workflows)

View File

@@ -0,0 +1,69 @@
---
title: Mostrar el número de correos electrónicos recibidos
description: Cree un flujo de trabajo para contar y mostrar automáticamente el número de correos electrónicos recibidos de cada contacto.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/workflow-number-of-emails-received.png" style={{width:'100%'}} />
## Resumen
Este flujo de trabajo se activa cada vez que se recibe un nuevo correo electrónico y actualiza un campo personalizado en el registro de Person con el recuento total de correos electrónicos de ese remitente.
## Prerrequisitos
Antes de configurar este flujo de trabajo, cree un campo personalizado en el objeto **People**:
1. Vaya a **Configuración → Modelo de datos → People**
2. Añada un nuevo campo **Number**
3. Asígnele un nombre como "Número de correos electrónicos recibidos de esta persona"
## Configuración paso a paso
<VimeoEmbed videoId="1148700336" title="Demostración en video" />
### Paso 1: Configurar el disparador
1. Vaya a **Flujos de trabajo** y cree un nuevo flujo de trabajo
2. Seleccione **Record is Created** como el disparador
3. Elija **Message Participants** (disponible en objetos avanzados)
<Note>
Un Message Participant es una combinación de un ID de mensaje y un ID de persona, creando un registro único por mensaje. Esto es más fácil de rastrear que Messages directamente porque podemos acceder al campo `handle`, que contiene la dirección de correo electrónico del remitente (o del destinatario).
</Note>
### Paso 2: Filtrar por Rol
1. Añada una acción **Filter**
2. Establezca la condición: **Role** es igual a **FROM**
Esto garantiza que solo cuente los mensajes enviados por esta persona, no los mensajes enviados a esa persona.
### Paso 3: Buscar todos los Message Participants con el mismo Handle
1. Añada una acción **Search Records**
2. Seleccione **Message Participants** como el objeto
3. Añada filtros: **Handle** es igual al handle del disparador (la dirección de correo electrónico del remitente) y **Role** es igual a **FROM**
4. Aumente el **Limit** de 1 a **200** (el máximo)
Esto encuentra todos los mensajes de esta dirección de correo electrónico para obtener el recuento total.
<Note>
La acción Search Records está limitada a devolver un máximo de 200 registros. Sin embargo, dado que solo está utilizando el valor `totalCount` (no los registros individuales), este paso devolverá el número total de correos electrónicos enviados por esta persona.
</Note>
### Paso 4: Actualizar el registro de Person con una acción Create or Update Record
1. Añada una acción **Create or Update Record**
2. Seleccione **People** como el objeto
3. Busque a la persona haciendo coincidir su correo electrónico con el `handle` del Message Participant
4. Establezca su campo personalizado "Número de correos electrónicos recibidos" en `{{searchRecords.totalCount}}`
El valor `totalCount` de la acción Search Records representa el número total de correos electrónicos recibidos de esta persona.
## Relacionado
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)
* [Cómo crear campos personalizados](/l/es/user-guide/data-model/how-tos/customize-your-data-model)
* [Acción Search Records](/l/es/user-guide/workflows/capabilities/workflow-actions#search-records)

View File

@@ -0,0 +1,170 @@
---
title: Mostrar datos de registros relacionados
description: Muestra datos de registros relacionados (p. ej., información de la empresa en Oportunidades) usando flujos de trabajo.
---
Muestra datos de registros relacionados directamente en tus registros — por ejemplo, muestra el recuento de empleados de una Empresa en sus Oportunidades. Esta solución provisional con flujos de trabajo es útil hasta que los campos anidados estén disponibles de forma nativa.
## Casos de Uso Comunes
| Fuente | Destino | Campos a copiar |
| ----------- | ----------- | ----------------------------------------------------------------------- |
| Empresa | Oportunidad | Industria, Tamaño de la empresa, ARR |
| Persona | Oportunidad | Correo electrónico, Teléfono, Cargo |
| Oportunidad | Empresa | Importe de la última oportunidad, Fecha de la última oportunidad ganada |
## Copia básica de campos
### Ejemplo: Copiar el correo electrónico del contacto a la oportunidad
**Objetivo**: Al establecer un Punto de contacto en una oportunidad, copia su correo electrónico a la oportunidad para un acceso sencillo.
### Requisito previo
Crea los campos de destino en **Configuración → Modelo de datos → Oportunidades** antes de crear el flujo de trabajo:
* Correo electrónico del contacto (tipo: Email)
* Teléfono del contacto (tipo: Phone)
### Configuración
1. **Desencadenante**: Se actualiza el registro (Oportunidades, campo Punto de contacto)
2. **Filtro**: Comprueba que el Punto de contacto no esté vacío
3. **Buscar registros**: Encuentra a la persona vinculada
* Objeto: Personas
* Filtro: ID igual a `{{trigger.object.pointOfContact.id}}`
4. **Actualizar registro**:
* Objeto: Oportunidades
* Registro: `{{trigger.object.id}}`
* Correo electrónico del contacto: `{{searchRecords[0].email}}`
* Teléfono del contacto: `{{searchRecords[0].phone}}`
## Copiar varios campos
### Ejemplo: Sincronizar la información de la empresa con todas las oportunidades relacionadas
**Objetivo**: Cuando cambien los detalles de la empresa, actualiza todas las oportunidades relacionadas.
### Configuración
1. **Desencadenante**: Se actualiza el registro (Empresas)
* Campos: Industria, Tamaño de la empresa, Ingresos anuales
2. **Buscar registros**: Encuentra todas las oportunidades de esta empresa
* Objeto: Oportunidades
* Filtro: ID de empresa igual a `{{trigger.object.id}}`
3. **Iterator**: Recorre cada oportunidad
4. **Actualizar registro** (dentro del Iterator):
* Objeto: Oportunidades
* Registro: `{{iterator.currentItem.id}}`
* Industria de la empresa: `{{trigger.object.industry}}`
* Tamaño de la empresa: `{{trigger.object.companySize}}`
* ARR de la empresa: `{{trigger.object.annualRevenue}}`
## Copiar al crear el registro
### Ejemplo: Rellenar previamente la oportunidad con datos de la empresa
**Objetivo**: Al crear una oportunidad vinculada a una empresa, copia automáticamente la información clave de la empresa.
### Requisito previo
Crea los campos de destino en **Configuración → Modelo de datos → Oportunidades**:
* Industria de la empresa (tipo: Texto)
* Tamaño de la empresa (tipo: Número)
### Configuración
1. **Desencadenante**: Se crea el registro (Oportunidades)
* Filtro: La empresa no está vacía
2. **Buscar registros**: Obtén los detalles de la empresa vinculada
* Objeto: Empresas
* Filtro: ID igual a `{{trigger.object.company.id}}`
3. **Actualizar registro**:
* Objeto: Oportunidades
* Registro: `{{trigger.object.id}}`
* Industria de la empresa: `{{searchRecords[0].industry}}`
* Tamaño de la empresa: `{{searchRecords[0].employees}}`
<Note>
**Limitación de Tareas y Notas**: Las relaciones en Tareas y Notas están codificadas como de muchos a muchos y aún no están disponibles en los desencadenantes o acciones de flujos de trabajo. Para acceder a estas relaciones, usa la [API](/l/es/developers/extend/api).
</Note>
## Sincronización bidireccional
### Ejemplo: Mantener sincronizado el contacto principal
**Objetivo**: Cuando cambie el contacto principal de una empresa, actualiza el contacto. Cuando una persona se convierta en contacto principal, actualiza la empresa.
### Flujo de trabajo 1: Empresa → Persona
1. **Desencadenante**: Se actualiza el registro (Empresas, campo Contacto principal)
2. **Actualizar registro**: Establece "Es contacto principal" de la persona en true
3. **Buscar registros**: Encuentra el contacto principal anterior
4. **Actualizar registro**: Establece "Es contacto principal" del contacto anterior en false
### Flujo de trabajo 2: Persona → Empresa
1. **Desencadenante**: Se actualiza el registro (Personas, Es contacto principal = true)
2. **Actualizar registro**: Establece el Contacto principal de la empresa en esta persona
<Note>
Ten cuidado con las sincronizaciones bidireccionales para evitar bucles infinitos. Usa filtros para comprobar si el valor realmente cambió antes de actualizar.
</Note>
## Uso de código para mapeo complejo
### Ejemplo: Transformar datos durante la copia
**Objetivo**: Copiar y formatear el número de teléfono de la persona a la oportunidad.
```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 };
};
```
## Mejores prácticas
### Evitar bucles
* No crees flujos de trabajo que se activen mutuamente sin fin
* Usa condiciones específicas de campo
* Añade comprobaciones para ver si el valor realmente cambió
### Gestionar datos faltantes
* Comprueba siempre que el registro de origen exista antes de copiar
* Proporciona valores predeterminados para los campos opcionales
* Usa filtros para omitir cuando el campo de origen esté vacío
### Rendimiento
* Procesa actualizaciones por lotes al copiar a muchos registros
* Usa flujos de trabajo programados para operaciones de sincronización masiva
* Considera usar Iterator para actualizaciones de múltiples registros
## Relacionado
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)
* [Disparadores de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-triggers)

View File

@@ -0,0 +1,202 @@
---
title: Campos de fórmula
description: Crea campos de fórmula usando flujos de trabajo hasta que haya soporte nativo disponible.
---
Veinte todavía no soporta campos de fórmula nativos (llegando en 2026), pero puede lograr el mismo resultado usando flujos de trabajo. Esta solución alternativa te permite calcular y rellenar automáticamente valores de campos—desde concatenaciones simples hasta lógica empresarial compleja.
## Casos de Uso Comunes
| Caso de uso | Ejemplo de fórmula |
| ----------------------------- | ---------------------------------- |
| **Nombre completo** | Nombre + " " + Apellido |
| **Importe esperado** | Importe × Probabilidad |
| **Días hasta el vencimiento** | Fecha de vencimiento - Hoy |
| **Días en la etapa** | Hoy - Fecha de entrada a la etapa |
| **Puntuación del lead** | Puntos basados en varios criterios |
<Tip>
Para ver un ejemplo completo de seguimiento del tiempo en etapas del pipeline, consulta [Cuánto tiempo permanecen las oportunidades en cada etapa](/l/es/user-guide/views-pipelines/how-tos/track-time-in-stage).
</Tip>
## Fórmula básica: concatenación
### Ejemplo: rellenar automáticamente el nombre completo
**Objetivo**: Combinar automáticamente nombre y apellido en un campo de nombre completo.
### Configuración
1. **Desencadenante**: Se actualiza o se crea el registro (Personas)
2. **Filtro**: Comprueba que el nombre o el apellido hayan cambiado
3. **Acción de código**:
```javascript
export const main = async (params) => {
const { firstName, lastName } = params;
const fullName = [firstName, lastName]
.filter(Boolean)
.join(' ');
return { fullName };
};
```
4. **Actualizar registro**: Establecer Nombre completo en `{{code.fullName}}`
## Fórmula numérica: importe esperado
### Ejemplo: calcular ingresos esperados
**Objetivo**: Multiplicar el importe de la oportunidad por la probabilidad para obtener el importe esperado.
Consulta [Cómo mostrar el importe esperado en el pipeline](/l/es/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline) para ver el flujo de trabajo completo.
### Configuración
1. **Desencadenante**: Se actualiza el registro (Oportunidades, campo Importe o Probabilidad)
2. **Acción de código**:
```javascript
export const main = async (params) => {
const { amount, probability } = params;
const expectedAmount = (amount || 0) * (probability || 0) / 100;
return { expectedAmount };
};
```
3. **Actualizar registro**: Establecer Importe esperado en `{{code.expectedAmount}}`
## Fórmula de fechas: cálculo de días
### Ejemplo: días hasta el vencimiento de la tarea
**Objetivo**: Calcular cuántos días faltan hasta la fecha de vencimiento de una tarea.
### Configuración
1. **Desencadenante**: Se actualiza o se crea el registro (Tareas, campo Fecha de vencimiento)
2. **Acción de código**:
```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. **Actualizar registro**: Establecer Días hasta el vencimiento en `{{code.daysUntilDue}}`
<Note>
Los valores negativos indican tareas vencidas. Puedes usar este campo para filtrar o ordenar tareas por urgencia.
</Note>
## Fórmula condicional: puntuación del lead
### Ejemplo: calcular la puntuación del lead según criterios
**Objetivo**: Puntuar leads en función del tamaño de la empresa, el sector y la interacción.
### Configuración
1. **Desencadenante**: Se actualiza el registro (Personas o Empresas)
2. **Acción de código**:
```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. **Actualizar registro**: Establecer Puntuación del lead en `{{code.leadScore}}`
## Fórmula de texto: extracción de dominio
### Ejemplo: extraer el dominio del correo electrónico
**Objetivo**: Extraer y almacenar automáticamente el dominio del correo electrónico.
### Configuración
1. **Desencadenante**: Se actualiza el registro (Personas, campo Correo electrónico)
2. **Acción de código**:
```javascript
export const main = async (params) => {
const { email } = params;
if (!email) return { domain: null };
const domain = email.split('@')[1]?.toLowerCase();
return { domain };
};
```
3. **Actualizar registro**: Establecer el campo Dominio en `{{code.domain}}`
## Mejores prácticas
### Rendimiento
* Activa solo ante cambios en campos relevantes
* Usa filtros para omitir registros que no necesiten cálculo
* Evita cálculos complejos en flujos de trabajo de alto volumen
### Manejo de errores
* Comprueba valores null/undefined antes de los cálculos
* Usa valores predeterminados cuando falten datos
* Devuelve mensajes de error claros cuando fallen los cálculos
### Pruebas
* Prueba con casos límite (campos vacíos, valores cero)
* Verifica los cálculos manualmente antes de activar
* Supervisa las ejecuciones de flujos de trabajo para detectar resultados inesperados
## Relacionado
* [Cómo mostrar el importe esperado en el pipeline](/l/es/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline)
* [Cómo realizar el seguimiento del tiempo en la etapa](/l/es/user-guide/views-pipelines/how-tos/track-time-in-stage)
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)

View File

@@ -0,0 +1,127 @@
---
title: Notificar a los compañeros de equipo que hay una nota para revisar
description: Enviar notificaciones de correo electrónico automáticas al asignar a un colega para revisar una nota.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<Note>
**Etiquetar directamente a los compañeros de equipo en las notas aún no está disponible.** Esta función está prevista para la segunda mitad de 2026. Mientras tanto, este flujo de trabajo ofrece una solución alternativa práctica.
</Note>
## El desafío
Desea etiquetar a un colega en una nota para que reciba una notificación para revisarla. Actualmente, Twenty no admite @mentions en notas ni tareas.
## La solución alternativa
En lugar de etiquetar en el texto de la nota, puede:
1. Cree un campo de relación **Reviewer** en el objeto Notes
2. Asigne al revisor al crear o editar la nota
3. Haz que un flujo de trabajo envíe automáticamente un correo electrónico al revisor con un enlace a la nota
<Warning>
**Limitación**: Este enfoque permite asignar un revisor por campo. Si necesita notificar a varias personas, cree campos de relación adicionales (p. ej., Reviewer 1, Reviewer 2). Esto no escalará bien si necesita etiquetar a muchos colaboradores a la vez.
</Warning>
## Configuración paso a paso
### Paso 1: Crear el campo Reviewer
<img src="/images/user-guide/workflows/reviewer-field-for-note.png" style={{width:'100%'}} />
1. Vaya a **Configuración → Modelo de datos → Notes**
2. Haga clic en **Add Field**
3. Configurar:
| Configuración | Valor |
| ------------------ | --------------------------------- |
| **Name** | Reviewer |
| **Tipo** | Relación |
| **Related Object** | Miembro del espacio de trabajo |
| **Relation Type** | Many Notes → One Workspace Member |
4. Guarde el campo
### Paso 2: Crear el flujo de trabajo
<VimeoEmbed videoId="1151897168" title="Demostración en video" />
#### Cree el disparador del flujo de trabajo
1. Vaya a la vista principal y cree un nuevo flujo de trabajo
2. Seleccione **Record Updated** como el disparador
3. Elija **Note** como objeto y especifique el campo Reviewer.
4. Esto se activará cada vez que se actualice el revisor de una nota.
#### Agregue un filtro para comprobar que el revisor se acaba de asignar
Añada una acción **Filter** para asegurarse de que el flujo de trabajo solo continúe cuando se asigne un revisor:
| Condición | Valor |
| ------------ | ------------- |
| **Reviewer** | no está vacío |
#### Busque al miembro del espacio de trabajo
1. Añada una acción **Search Records**
2. Seleccione **Workspace Member** (en Advanced Objects)
3. Añada un filtro:
| Condición | Valor |
| --------- | ----------------------------- |
| **ID** | igual a `{{note.reviewerId}}` |
Esto recupera el registro completo del miembro del espacio de trabajo, incluida su dirección de correo electrónico y su nombre.
#### Enviar el correo electrónico de notificación
1. Añada una acción **Send Email**
2. Configurar:
| Campo | Valor |
| ---------- | --------------------------------------------------- |
| **Para** | Search Record → First Workspace Member → User Email |
| **Asunto** | Una nota requiere su revisión |
| **Cuerpo** | Consulte el ejemplo a continuación |
**Ejemplo de cuerpo del correo:**
```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>
Reemplace `yourSubDomain` por el subdominio real de su espacio de trabajo de Twenty.
</Note>
#### Probar y activar
1. Haga clic en **Test** para ejecutar el flujo de trabajo manualmente
2. Cree o edite una nota y asigne un revisor
3. Compruebe la bandeja de entrada del revisor para ver el correo electrónico de notificación
4. Active el flujo de trabajo
## Uso
Una vez configurado, su equipo puede usar este flujo de trabajo de la siguiente manera:
1. Crear una nueva nota (o editar una existente)
2. Seleccionar a un colega en el campo **Reviewer**
3. El colega recibe automáticamente un correo electrónico con un enlace directo a la nota
## Relacionado
* [Disparadores de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-triggers)
* [Enviar correos electrónicos desde flujos de trabajo](/l/es/user-guide/workflows/capabilities/send-emails-from-workflows)
* [Crear campos personalizados](/l/es/user-guide/data-model/how-tos/create-custom-fields)
* [Crear campos de relación](/l/es/user-guide/data-model/how-tos/create-relation-fields)

View File

@@ -0,0 +1,106 @@
---
title: Enviar alertas por correo electrónico sobre tareas con fecha de vencimiento
description: Notifica automáticamente a los miembros del equipo sobre sus tareas próximas o vencidas.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/email_tasks_due.png" style={{width:'100%'}} />
Enviar recordatorios diarios por correo electrónico a cada miembro del equipo sobre sus tareas que vencen hoy.
## Resumen
Este flujo de trabajo se ejecuta según una programación y:
1. Obtiene todos los miembros del espacio de trabajo
2. Itera por cada miembro
3. Encuentra sus tareas que vencen hoy
4. Da formato y envía un correo electrónico personalizado
## Configuración paso a paso
<VimeoEmbed videoId="1147463514" title="Demostración en video" />
### Paso 1: Configurar el disparador
1. Ve a **Configuración → Flujos de trabajo** y crea un nuevo flujo de trabajo
2. Selecciona **On a Schedule** como el disparador
3. Usa una expresión cron para ejecutar diariamente a las 8:00 a. m.: `0 8 * * *`
### Paso 2: Buscar a todos los miembros del espacio de trabajo
1. Añade la acción **Search Records**
2. Selecciona **Workspace Members** (en objetos avanzados)
3. No se necesitan filtros — esto devuelve a todos los miembros
### Paso 3: Añadir un iterador
1. Añade la acción **Iterator**
2. Configura el arreglo de entrada con los miembros del espacio de trabajo del paso anterior
3. Todas las acciones dentro del iterador se ejecutarán una vez por miembro
### Paso 4: Buscar tareas que vencen hoy (dentro del iterador)
1. Dentro del iterador, añade una acción **Search Records**
2. Selecciona **Tasks** como el objeto
3. Agregar filtros:
* **Assignee** = miembro actual del espacio de trabajo (del iterador)
* **Due Date** = hoy
### Paso 5: Formatear las tareas para el cuerpo del correo (dentro del iterador)
Añade una acción **Code** para dar formato a las tareas como una lista legible con enlaces:
```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>
Reemplaza `yourSubDomain` por el subdominio real de tu espacio de trabajo de Twenty.
</Note>
### Paso 6: Enviar correo electrónico (dentro del iterador)
1. Añade una acción **Send Email** (aún dentro del iterador)
2. Configurar:
| Campo | Valor |
| ---------- | -------------------------------------------------------------------------------------------- |
| **Para** | `{{iterator.currentItem.userEmail}}` (correo electrónico del miembro del espacio de trabajo) |
| **Asunto** | Tus tareas que vencen hoy |
| **Cuerpo** | `{{code.formattedTasks}}` |
### Paso 7: Probar y activar
1. Haz clic en **Test** para ejecutar el flujo de trabajo manualmente
2. Revisa las bandejas de entrada para ver los correos electrónicos
3. Activa el flujo de trabajo
## Relacionado
* [Acciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-actions)
* [Enviar correos electrónicos desde flujos de trabajo](/l/es/user-guide/workflows/capabilities/send-emails-from-workflows)
* [Manejar arreglos en acciones de código](/l/es/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,21 @@
---
title: Servicios profesionales
description: Obtenga ayuda profesional para construir flujos de trabajo complejos y automatizaciones del equipo de Twenty y socios certificados.
---
## ¿Cuándo Necesita Ayuda Profesional?
Considere servicios profesionales para:
* Integraciones complejas de múltiples sistemas
* Lógica empresarial avanzada y reglas de automatización
* Flujos de trabajo de procesamiento de datos a gran escala
* Desarrollo de API a medida
* Capacitación del equipo y optimización del flujo de trabajo
* Cuando no tiene recursos internos
## Opciones de Servicio
### Socios de Implementación
Trabaje con socios certificados para personalizaciones avanzadas. Contáctanos en [contact@twenty.com](mailto:contact@twenty.com) para conectarte con nuestros [socios de implementación](https://twenty.com/partners).

View File

@@ -0,0 +1,170 @@
---
title: Resolución de problemas de flujo de trabajo
description: Problemas comunes de flujos de trabajo y cómo resolverlos.
---
## Problemas Comunes y Soluciones
### Flujo de trabajo no se activa
**Síntomas**: Tu flujo de trabajo no se ejecuta cuando lo esperas.
**Posibles causas**:
1. **Flujo de trabajo no activado**: Asegúrate de que el flujo de trabajo esté configurado como "Active", no como "Draft"
2. **Condiciones del disparador no cumplidas**: Verifica que el disparador coincida con el evento esperado
3. **Campo no monitoreado**: Para disparadores "Record is Updated", asegúrate de que se esté monitoreando el campo específico
4. **Permisos**: Verifica que tengas permiso para ejecutar flujos de trabajo
**Soluciones**:
* Verifica el estado del flujo de trabajo en la lista de flujos de trabajo
* Prueba con la acción específica que esperas que lo dispare
* Revisa la configuración del disparador
* Contacta a tu administrador acerca de los permisos
### Los disparadores del flujo de trabajo se activan demasiado pronto (campos vacíos)
**Síntomas**: Al crear un registro manualmente en la IU, tu flujo de trabajo se dispara antes de que tengas tiempo de completar todos los campos. El flujo de trabajo se ejecuta con valores de campo en su mayoría vacíos.
**Por qué sucede esto**: Twenty guarda todo en tiempo real; no hay un modo separado de "editar" y "lectura". Cuando creas un registro, se guarda de inmediato, lo que activa el evento "Record is created" antes de que puedas completar campos adicionales.
**Cuándo funciona bien "Record is created"**:
* Registros creados mediante llamadas de API (los campos se completan en una sola solicitud)
* Registros creados mediante importación
* Creación automática de registros desde otros flujos de trabajo
**Solución**: Para los registros creados manualmente en la IU, usa **"Record is created or updated"** como disparador. De esta forma:
* El flujo de trabajo se dispara después de que el usuario haya terminado de completar y guardar los campos
* Obtienes los datos completos en lugar de valores vacíos
<Tip>
Si solo quieres que el flujo de trabajo se ejecute una vez por registro, agrega una acción de filtro para comprobar un campo como `createdAt equals updatedAt` (primer guardado) o usa un campo de casilla de verificación personalizada para registrar si el flujo de trabajo ya se ejecutó.
</Tip>
### Acciones que fallan
**Síntomas**: El flujo de trabajo se ejecuta, pero algunas acciones fallan.
**Posibles causas**:
1. **Datos faltantes**: Los campos obligatorios están vacíos
2. **Referencias no válidas**: Las variables de pasos anteriores no existen
3. **Errores de API**: Los servicios externos devuelven errores
4. **Problemas de permisos**: La acción requiere permisos que no tienes
**Soluciones**:
* Revisa los detalles de la ejecución del flujo de trabajo para ver mensajes de error
* Verifica que todos los campos obligatorios tengan valores
* Prueba las conexiones de API de forma independiente
* Revisa los permisos del rol
### Errores de solicitudes HTTP
**Síntomas**: Las acciones de solicitud HTTP fallan o devuelven resultados inesperados.
**Códigos de error comunes**:
* **400**: Solicitud incorrecta: verifica el formato del cuerpo de la solicitud
* **401**: No autorizado: verifica la clave de API
* **403**: Prohibido: verifica los permisos de la API
* **404**: No encontrado: verifica la URL del endpoint
* **429**: Demasiadas solicitudes: implementa limitación de tasa
* **500**: Error del servidor: problema del servicio externo
**Soluciones**:
* Verifica la URL del endpoint de la API
* Verifica los encabezados de autenticación
* Primero prueba la llamada a la API fuera de Twenty
* Agrega manejo de errores en las acciones de código
### Errores en acciones de código
**Síntomas**: El código JavaScript no se ejecuta.
**Problemas comunes**:
1. **Errores de sintaxis**: errores tipográficos o JavaScript no válido
2. **Variables indefinidas**: hacer referencia a variables que no existen
3. **Errores de tipo**: operaciones sobre tipos de datos incorrectos
4. **Tiempos de espera agotados**: el código tarda demasiado en ejecutarse
**Soluciones**:
* Usa la validación integrada del editor de código
* Prueba primero la lógica del código en una consola de JavaScript
* Agrega sentencias console.log para depuración
* Simplifica las operaciones complejas
### El correo electrónico no se envía
**Síntomas**: La acción Send Email no entrega correos electrónicos.
**Posibles causas**:
1. **Ninguna cuenta de correo conectada**: verifica Configuración → Cuentas
2. **Dirección de correo electrónico no válida**: el correo electrónico del destinatario está mal formado
3. **Límites de envío**: se alcanzaron los límites de tasa del proveedor de correo electrónico
4. **Filtros de spam**: los correos electrónicos están siendo bloqueados
**Soluciones**:
* Verifica la conexión de la cuenta de correo electrónico
* Valida las direcciones de correo electrónico de los destinatarios
* Verifica los límites del proveedor de correo electrónico
* Revisa el contenido del correo electrónico en busca de desencadenantes de spam
## Depuración de flujos de trabajo
### Uso de las ejecuciones de flujos de trabajo
1. Ve al editor de flujos de trabajo
2. Abre el panel **Runs**
3. Busca la ejecución fallida
4. Haz clic para ver detalles paso a paso
5. Revisa los mensajes de error y los datos de salida
### Prueba de pasos individuales
1. Para las acciones de código, usa el botón **Test**
2. Para las solicitudes HTTP, prueba el endpoint por separado
3. Crea registros de prueba para disparar flujos de trabajo
4. Usa disparadores manuales para pruebas controladas
### Patrones comunes de depuración
**Agrega registros**:
Usa acciones de código para registrar valores intermedios para depuración.
**Aísla pasos**:
Prueba cada paso de forma independiente para identificar fallas.
**Verifica el flujo de datos**:
Comprueba que cada paso reciba los datos de entrada esperados.
## Mejores prácticas para evitar problemas
### Antes de la activación
* Prueba exhaustivamente en modo borrador
* Valida todas las conexiones de API
* Revisa cuidadosamente las condiciones del disparador
* Documenta el comportamiento esperado
### Durante el desarrollo
* Usa nombres de pasos descriptivos
* Agrega comentarios en las acciones de código
* Prueba con datos realistas
* Planifica casos límite
### Después de la activación
* Supervisa de cerca las ejecuciones iniciales
* Configura alertas para fallas
* Revisa el historial de ejecuciones con regularidad
* Mantén los flujos de trabajo simples cuando sea posible

View File

@@ -0,0 +1,236 @@
---
title: Preguntas frecuentes sobre flujos de trabajo
description: Preguntas frecuentes sobre los flujos de trabajo en Twenty.
---
<AccordionGroup>
<Accordion title="¿Por qué no puedo activar un flujo de trabajo?">
Probablemente se deba a un problema de permisos. Necesitas acceso a los flujos de trabajo para crearlos y activarlos.
**Solución**: Ponte en contacto con el administrador de tu espacio de trabajo para que te conceda acceso a flujos de trabajo en **Configuración → Miembros → Roles**.
Si no ves la sección de Flujos de trabajo en la barra lateral, esto confirma que es un problema de permisos.
</Accordion>
<Accordion title="¿Por qué no veo el botón de flujo de trabajo manual en la barra de navegación superior?">
Los flujos de trabajo manuales solo aparecen en la barra de navegación si están configurados correctamente:
1. El flujo de trabajo debe estar **activado** (no en modo borrador)
2. La ubicación en la barra de navegación debe estar configurada como **Pinned**
3. Para los disparadores Single/Bulk, debes estar en la página del objeto correcta
**Para comprobarlo**: Abre el flujo de trabajo → haz clic en el disparador → verifica que "Navbar placement" esté configurado como "Pinned".
Siempre puedes acceder a los flujos de trabajo manuales con **Cmd + K** (o **Ctrl + K**) independientemente de la configuración de la barra de navegación.
</Accordion>
<Accordion title="¿Cuál es la diferencia entre los disparadores manuales Single, Global y Bulk?">
| Tipo | Registros necesarios | Ejecuciones de flujos de trabajo |
| ---- | -------------------- | -------------------------------- |
\| **Global** | Ninguna | Una vez, sin entrada de registro |
\| **Single** | Uno o más seleccionados | Una vez por registro seleccionado |
\| **Bulk** | Uno o más seleccionados | Una vez, con todos los registros como matriz |
* **Global**: Úsalo cuando el flujo de trabajo no necesite ningún contexto de registros (p. ej., generar un informe)
* **Single**: Úsalo cuando quieras procesar cada registro seleccionado de forma independiente (p. ej., enviar correos electrónicos individuales)
* **Bulk**: Úsalo cuando necesites procesar registros en conjunto u optimizar el uso de créditos (requiere la acción Iterator)
Consulta [Disparadores de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-triggers) para más detalles.
</Accordion>
<Accordion title="No veo un nodo If/Else. ¿Qué debo hacer?">
Un nodo If/Else explícito todavía no está disponible, pero está en nuestra hoja de ruta.
**Solución provisional actual**: Crea varias ramas desde tu paso, cada una comenzando con una acción **Filter**:
```
Step 1
│
├── Branch A: Filter (condition = true) → Actions...
│
└── Branch B: Filter (condition = false) → Actions...
```
Solo la rama donde la condición del filtro se cumpla ejecutará sus acciones posteriores.
Consulta [Cómo usar ramas](/l/es/user-guide/workflows/capabilities/workflow-branches) para ver una guía paso a paso.
</Accordion>
<Accordion title="¿Las ramas del flujo de trabajo se ejecutan en paralelo?">
**Sí**, las ramas se ejecutan en paralelo de forma predeterminada.
Si quieres que solo se ejecute una rama:
* Añade una acción **Filter** al inicio de cada rama
* Establece condiciones opuestas (p. ej., Rama A: status = "Open", Rama B: status ≠ "Open")
Las ramas que no cumplan su condición de filtro dejan de ejecutarse, mientras que las demás continúan.
</Accordion>
<Accordion title="¿Puedo volver a unir las ramas?">
**Sí**. Una vez que tus ramas en paralelo hayan terminado, puedes añadir un paso al que se conecten ambas ramas.
En el editor de flujos de trabajo:
1. Completa tus acciones en las ramas
2. Añade un nuevo paso después de las ramas
3. Arrastra conexiones desde el final de cada rama a este nuevo paso
El paso combinado se ejecutará después de que hayan finalizado todas las ramas conectadas.
</Accordion>
<Accordion title="¿Cuál es el límite en los resultados de Search Records?">
**Search Records devuelve un máximo de 200 registros.**
Si necesitas procesar más:
* Añade filtros más específicos para reducir los resultados
* Usa flujos de trabajo programados para procesar por lotes
* Considera usar la API para operaciones masivas
Para la mayoría de los flujos de trabajo, 200 registros son suficientes. Si alcanzas este límite con frecuencia, considera reestructurar tu automatización.
</Accordion>
<Accordion title="¿Puedo poner en CC a personas en los correos electrónicos del flujo de trabajo?">
**Aún no.** Los campos CC y BCC para la acción Send Email están en nuestra hoja de ruta.
**Solución provisional actual**: Añade varias acciones Send Email para enviar a destinatarios adicionales, o usa un HTTP Request para enviar a través de un servicio de correo externo que admita CC.
</Accordion>
<Accordion title="¿Cómo uso las salidas de pasos anteriores?">
Cada acción produce datos de salida que pueden utilizarse en pasos posteriores.
**Para hacer referencia a los datos de un paso anterior**:
* Usa el selector de variables al configurar un campo
* O escribe `{{stepName.fieldName}}` directamente
**Ejemplos**:
* Datos del disparador: `{{trigger.object.email}}`
* Resultados de búsqueda: `{{searchRecords[0].name}}`
* Resultado de código: `{{code.calculatedValue}}`
Pasa el cursor sobre cualquier campo en la configuración de la acción para ver las variables disponibles de pasos anteriores.
</Accordion>
<Accordion title="¿Por qué falla mi Iterator?">
**Iterator requiere una entrada de tipo matriz.** Problemas comunes:
1. **La entrada no es una matriz**: Asegúrate de pasar resultados de Search Records u otra acción que devuelva una matriz
2. **La matriz está vacía**: Añade un filtro antes de Iterator para comprobar `{{searchRecords.length}} > 0`
3. **Variable incorrecta seleccionada**: Asegúrate de seleccionar la propia matriz, no un solo registro
**Configuración correcta**:
1. Search Records (devuelve una matriz)
2. Filter: length > 0
3. Iterator: selecciona `{{searchRecords}}`
4. Las acciones dentro de Iterator usan `{{iterator.currentItem.fieldName}}`
</Accordion>
<Accordion title="¿Cuál es el tiempo máximo de ejecución para las acciones Code?">
Las acciones Code (funciones sin servidor) tienen un **tiempo de espera predeterminado de 5 minutos** (300 segundos).
El tiempo de espera configurable máximo es de **15 minutos** (900 segundos).
Si tu código excede este límite, la acción fallará con un error por tiempo de espera.
**Consejos para evitar tiempos de espera**:
* Divide las operaciones grandes en partes más pequeñas usando Iterator
* Evita cálculos pesados; usa servicios externos mediante HTTP Request para el procesamiento intensivo
* Optimiza tu código para reducir el tiempo de ejecución
* Si necesitas un procesamiento más largo, considera usar flujos de trabajo programados que procesen datos por lotes
</Accordion>
<Accordion title="¿Cómo leo las ejecuciones de flujos de trabajo?">
Las ejecuciones de flujos de trabajo muestran el historial de ejecución y te ayudan a depurar problemas.
**Acceder a las ejecuciones**:
* En el editor de flujos de trabajo → panel **Runs** a la derecha
* O ve a **Workflow Runs** en la barra lateral
**Cómo entender una ejecución**:
* **Estado**: En ejecución, Completada, Fallida, En espera
* **Pasos**: Ve qué pasos se ejecutaron y su salida
* **Errores**: Haz clic en los pasos fallidos para ver los mensajes de error
* **Datos**: Visualiza los datos de entrada/salida en cada paso
Consulta [Ejecuciones de flujos de trabajo](/l/es/user-guide/workflows/capabilities/workflow-runs) para más detalles.
</Accordion>
<Accordion title="¿Por qué no veo mis ejecuciones de flujos de trabajo?">
Es posible que las ejecuciones de flujos de trabajo estén fallando de inmediato debido a los límites de tasa. Veinte impone un límite duro de 5.000 ejecuciones por hora por espacio de trabajo.
Si excede este límite, los flujos de trabajo se marcan inmediatamente como fallidos y no aparecerán en su lista de ejecuciones.
**Escenarios comunes que alcanzan este límite**:
* Seleccionar más de 5,000 registros con un disparador manual Single
* Múltiples flujos de trabajo ejecutándose simultáneamente en tu espacio de trabajo
* Disparadores automáticos de alta frecuencia (p. ej., Record Updated en un objeto con mucha actividad)
**Soluciones**:
* Usa disparadores **Bulk** en lugar de Single para procesar muchos registros en una sola ejecución
* Espacia las operaciones por lotes grandes
* Usa filtros para reducir la frecuencia de los disparadores
* Programa los flujos de trabajo pesados durante las horas de menor actividad
</Accordion>
<Accordion title="¿Cuáles son los límites de tasa de los flujos de trabajo?">
Twenty tiene dos límites de tasa para garantizar la estabilidad del sistema:
| Límite | Valor | Comportamiento |
| ------ | ----- | -------------- |
\| **Límite suave** | 100 ejecuciones/minuto | Las ejecuciones se encolan con estado "Not Started", se procesan gradualmente |
\| **Límite estricto** | 5,000 ejecuciones/hora | Las ejecuciones fallan de inmediato |
**Límite suave (100/min)**: Tus flujos de trabajo no fallarán; solo esperan en la cola y se procesan con el tiempo. Puedes activar más de 100 registros; la ejecución será más lenta.
**Límite estricto (5,000/h)**: Esto se aplica a todo tu espacio de trabajo. Si todos tus flujos de trabajo combinados superan 5,000 ejecuciones en una hora, las ejecuciones adicionales fallarán de inmediato.
**Consejos para mantenerse dentro de los límites**:
* Usa disparadores Bulk con Iterator en lugar de disparadores Single para lotes grandes
* Combina automatizaciones relacionadas en menos flujos de trabajo
* Usa flujos de trabajo programados para distribuir la carga en el tiempo
</Accordion>
<Accordion title="¿Puedo reintentar una ejecución de flujo de trabajo que falló?">
**No, en este momento no hay funcionalidad de reintento automático.**
Si una ejecución de flujo de trabajo falla, tendrás que:
1. Revisa el error en **Configuración → Flujos de trabajo → [Tu flujo de trabajo] → Ejecuciones**
2. Soluciona el problema (datos, configuración o servicio externo)
3. Activa manualmente el flujo de trabajo de nuevo en los registros afectados
**Consejos para reducir fallos**:
* Añade nodos **Filter** para validar datos antes de las acciones
* Usa **Search Records** para comprobar si existen registros relacionados
* Prueba exhaustivamente con unos pocos registros antes de operaciones masivas
La funcionalidad de reintento automático está en nuestra hoja de ruta para una versión futura.
</Accordion>
<Accordion title="¿Debo desactivar los flujos de trabajo antes de las importaciones CSV?">
**Sí, si tus flujos de trabajo se activan por la creación o actualización de registros.**
Cuando importas datos mediante CSV, cada registro creado o actualizado puede activar flujos de trabajo. Una importación grande (miles de registros) podría:
* Alcanzar el límite de 5,000 ejecuciones/hora
* Consumir una cantidad significativa de créditos de flujos de trabajo
* Enviar correos electrónicos o notificaciones inesperados
* Crear tareas o registros duplicados
**Antes de una importación masiva**:
1. Ve a **Configuración → Flujos de trabajo**
2. Identifica los flujos de trabajo activados por el objeto que estás importando
3. **Desactívalos** temporalmente
4. Ejecuta tu importación CSV
5. **Reactiva** los flujos de trabajo al terminar
**Alternativa**: Si necesitas que los flujos de trabajo se ejecuten con los datos importados, importa en lotes más pequeños para mantenerte dentro de los límites de tasa.
</Accordion>
<Accordion title="¿Cómo limpio el diseño de mi flujo de trabajo?">
Si tu lienzo de flujo de trabajo se ve desordenado con nodos dispersos, puedes organizarlo automáticamente:
1. Haz clic derecho en cualquier parte del lienzo del flujo de trabajo
2. Haz clic en **Tidy up workflow**
Esto reorganizará automáticamente todos los nodos en un diseño limpio y organizado.
</Accordion>
</AccordionGroup>