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

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

View File

@@ -0,0 +1,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)