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: Traga dados de produtos para o Twenty
description: Sincronize, de forma agendada, os dados do catálogo de produtos do seu armazém de dados para o seu CRM.
---
Use este padrão para manter o Twenty sincronizado com os dados de produtos do seu armazém de dados (por exemplo, Snowflake, BigQuery, PostgreSQL).
## Estrutura do Fluxo de Trabalho
1. **Gatilho**: De forma agendada
2. **Código**: Consultar seu armazém de dados
3. **Código** (opcional): Formatar os dados como array
4. **Iterador**: Percorrer cada produto
5. **Inserir/atualizar registro**: Criar ou atualizar no Twenty
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## Etapa 1: Agendar o gatilho
Defina o fluxo de trabalho para executar em uma frequência que atenda às suas necessidades de atualização dos dados:
* A cada 5 minutos para sincronização quase em tempo real
* A cada hora para dados menos críticos
* Diariamente para atualizações em lote
## Etapa 2: Consultar seu armazém de dados
Adicione uma ação de **Código** para buscar dados recentes:
```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>
Filtre por `updated_at >= last X minutes` para recuperar apenas registros alterados recentemente. Isso mantém a sincronização eficiente.
</Tip>
## Etapa 3: Formatar os dados (opcional)
Se o seu armazém de dados retornar dados em um formato que precise de transformação, adicione outra ação de **Código**. Transformações comuns incluem conversões de tipo, renomeação de campos e limpeza de dados.
### Exemplo: Dados de usuário com campos booleanos e de status
```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),
})),
};
};
```
### Exemplo: Dados de produto com conversões 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
}))
};
};
```
### Exemplo: Formatação de data e moeda
```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, " ")
}))
};
};
```
### Transformações comuns
| Formato de origem | 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) | Data ISO | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## Etapa 4: Iterar pelos produtos
Adicione uma ação de **Iterador**:
* Entrada: `{{code.products}}`
Isso percorre cada produto no array.
## Etapa 5: Inserir/atualizar cada registro
Dentro do iterador, adicione uma ação de **Inserir/atualizar registro**:
| Configuração | Valor |
| ----------------------- | --------------------------------------- |
| **Objeto** | Seu objeto Produto personalizado |
| **Correspondência por** | ID externo ou SKU (identificador único) |
| **Nome** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Preço** | `{{iterator.item.price}}` |
<Tip>
Use **inserir/atualizar** (atualizar ou criar) em vez de construir ramificações separadas para criação vs. atualização. É mais rápido de construir e mais fácil de depurar.
</Tip>
## Casos de uso de exemplo
| Fonte | Data |
| ---------------------------- | ------------------------------------------- |
| **Sistema ERP** | Catálogo de produtos, preços, inventário |
| **Plataforma de e-commerce** | Pedidos, clientes, atualizações de produtos |
| **Armazém de dados** | Métricas agregadas, dados enriquecidos |
| **Sistema de inventário** | Níveis de estoque, alertas de reposição |
## Relacionados
* [Gatilhos de Fluxo de Trabalho](/l/pt/user-guide/workflows/capabilities/workflow-triggers)
* [Ações de Fluxo de Trabalho](/l/pt/user-guide/workflows/capabilities/workflow-actions)
* [Lidar com arrays em ações de código](/l/pt/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Importar envios do Typeform para o Twenty
description: Trate a carga útil de webhook do Typeform para criar leads a partir de envios de formulários.
---
Para a configuração padrão de webhook, consulte [Configurar um gatilho de Webhook](/l/pt/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). Este artigo aborda o tratamento específico necessário para a estrutura de carga útil personalizada do Typeform.
### Etapa 1: Criar um fluxo de trabalho de Webhook
1. Vá para **Configurações → Fluxos de trabalho**
2. Clique em **+ Novo fluxo de trabalho**
3. Selecione **Webhook** como o gatilho
4. Copie a URL do webhook
### Etapa 2: Configurar o Typeform
1. No Typeform, abra seu formulário
2. Vá para **Connect → Webhooks**
3. Cole a URL do webhook do Twenty
4. Salvar
### Etapa 3: Entender a carga útil do Typeform
O Typeform envia uma estrutura JSON aninhada. Aqui está um exemplo 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" }
}
]
}
}
```
Pontos importantes a observar:
* Os dados do formulário estão aninhados em `form_response`
* **As respostas são retornadas como um array**, não como campos nomeados
* Cada resposta inclui o tipo e o título do campo para referência
### Etapa 4: Extrair campos do array de respostas
Como `answers` é um array, você só pode selecionar o array inteiro nas etapas subsequentes — não campos individuais. Adicione uma ação de **Código** para extrair os campos necessários:
```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
}
};
};
```
Nas etapas subsequentes, agora você pode selecionar campos individuais como `contact.firstName` e `contact.email` no seletor de variáveis.
<Tip>
Para mais detalhes sobre como lidar com arrays, consulte [Tratar arrays em ações de código](/l/pt/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### Etapa 5: Criar o registro
Adicione a ação **Criar registro**:
| Campo | Valor |
| ----------- | --------------------------------------------------------- |
| **Objeto** | Pessoas |
| **Nome** | `{{code.contact.firstName}}` |
| **Apelido** | `{{code.contact.lastName}}` |
| **Email** | `{{code.contact.email}}` |
| **Empresa** | Pesquisar ou criar com base em `{{code.contact.company}}` |
### Etapa 6: Testar e ativar
1. Envie uma resposta de teste no Typeform
2. Verifique a execução do fluxo de trabalho para verificar se os dados foram capturados
3. Ative o fluxo de trabalho
## Relacionados
* [Configurar um gatilho de Webhook](/l/pt/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Lidar com arrays em ações de código](/l/pt/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Gerar um PDF a partir do Twenty
description: Crie um fluxo de trabalho para gerar e anexar um PDF (como um orçamento) a um registo.
---
Gere automaticamente ou obtenha um PDF e anexe-o a um registo no Twenty. Isto é normalmente utilizado para criar orçamentos, faturas ou relatórios que estão associados a Empresas, Oportunidades ou outros objetos.
## Visão Geral
Este fluxo de trabalho utiliza um **Gatilho Manual** para que os utilizadores possam gerar um PDF a pedido para qualquer registo selecionado. Uma **Função lógica** trata de:
1. Transferir o PDF a partir de um URL (de um serviço de geração de PDF)
2. Carregar o ficheiro para o Twenty
3. Criar um Anexo ligado ao registo
## Pré-requisitos
Antes de configurar o fluxo de trabalho:
1. **Crie uma Chave de API**: Vá a **Definições → APIs** e crie uma nova chave de API. Você precisará deste token para a função lógica.
2. **Configure um serviço de geração de PDF** (opcional): Se quiser gerar PDFs dinamicamente (por exemplo, orçamentos), utilize um serviço como o Carbone, o PDFMonkey ou o DocuSeal para criar o PDF e obter um URL de transferência.
## Configuração passo a passo
### Etapa 1: Configurar o gatilho
1. Vá a **Fluxos de Trabalho** e crie um novo fluxo de trabalho
2. Selecione **Gatilho Manual**
3. Escolha o objeto ao qual pretende anexar PDFs (por exemplo, **Empresa** ou **Oportunidade**)
<Tip>
Com um disparador Manual usuários podem executar este fluxo de trabalho usando um botão que aparece no topo direito, depois que um registro é selecionado — isso executa o fluxo de trabalho para gerar e anexar um PDF.
</Tip>
### Passo 2: Adicionar uma Função lógica
1. Adicionar uma ação **Código** (função lógica)
2. Crie uma nova função com o código abaixo
3. Configure os parâmetros de entrada
#### Parâmetros de Entrada
| Parâmetro | Valor |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
Se estiver a anexar a um objeto diferente (Pessoa, Oportunidade, etc.), renomeie o parâmetro em conformidade (por exemplo, `personId`, `opportunityId`) e atualize a função lógica.
</Note>
#### Código da Função 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;
};
```
### Passo 3: Personalizar para o seu caso de utilização
#### Para anexar a um objeto diferente
Substitua `targetCompanyId` pelo campo apropriado:
| Objeto | Nome do Campo |
| -------------------- | -------------------------- |
| Empresa | `targetCompanyId` |
| Pessoa | `targetPersonId` |
| Oportunidade | `targetOpportunityId` |
| Objeto personalizado | `targetYourCustomObjectId` |
Atualize tanto o parâmetro da função como o objeto `variables.data` na mutação de anexo.
#### Para utilizar um URL de PDF dinâmico
Se utilizar um serviço de geração de PDF, pode:
1. Primeiro, faça uma ação de Pedido HTTP para gerar o PDF
2. Passe o URL do PDF devolvido para a função 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
};
```
### Etapa 4: Testar e ativar
1. Guarde o fluxo de trabalho
2. Aceda a um registo de Empresa
3. Clique no menu **⋮** e selecione o seu fluxo de trabalho
4. Verifique a secção **Anexos** no registo para confirmar que o PDF foi anexado
5. Ative o fluxo de trabalho
## Combinar com Serviços de Geração de PDF
Para criar orçamentos ou faturas dinâmicos:
### Exemplo: Gerar Orçamento → Anexar PDF
| Etapa | Ação | Finalidade |
| ----- | ------------------------ | ------------------------------------------------------ |
| 1 | Gatilho Manual (Empresa) | O utilizador inicia num registo |
| 2 | Pesquisar Registo | Obter detalhes da Oportunidade ou dos itens de linha |
| 3 | Requisição HTTP | Chamar a API de geração de PDF com os dados do registo |
| 4 | Função lógica | Transferir e anexar o PDF gerado |
### Serviços Populares de Geração de PDF
* **Carbone** - Geração de documentos baseada em modelos
* **PDFMonkey** - Criação dinâmica de PDF a partir de modelos
* **DocuSeal** - Plataforma de automatização de documentos
* **Documint** - Geração de documentos orientada a API
Cada serviço fornece uma API que retorna uma URL em PDF, que você pode passar para a função lógica.
## Resolução de Problemas
| Problema | Solução |
| --------------------------- | ---------------------------------------------------------------------------- |
| "Falha ao transferir PDF" | Verifique se o URL do PDF é acessível e devolve um PDF válido |
| "Falha ao carregar" | Verifique se a sua chave de API é válida e tem permissões de escrita |
| "Falha na criação do anexo" | Garanta que o nome do campo de ID do objeto corresponde ao objeto de destino |
## Relacionados
* [Gatilhos de Fluxo de Trabalho](/l/pt/user-guide/workflows/capabilities/workflow-triggers)
* [Funções lógicas](/l/pt/user-guide/workflows/capabilities/workflow-actions#code)
* [Gerar um Orçamento ou Fatura a partir do Twenty](/l/pt/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: Gerar um orçamento ou fatura no Twenty
description: Crie faturas automaticamente em ferramentas externas quando os negócios forem fechados.
---
Envie automaticamente os dados do negócio para o seu sistema de faturação (Stripe, QuickBooks, Xero, etc.) quando uma oportunidade é ganha.
## Estrutura do Fluxo de Trabalho
1. **Gatilho**: Registo é atualizado (Oportunidade)
2. **Filtro**: Etapa = Closed Won
3. **Pesquisar Registo**: Obter detalhes da empresa
4. **Código** (opcional): Formatar o payload
5. **Pedido HTTP**: Enviar para o sistema de faturação
## Etapa 1: Configurar o Gatilho
1. Crie um novo fluxo de trabalho
2. Selecione o gatilho **Registo é atualizado**
3. Escolha **Oportunidade** como o objeto
## Etapa 2: Filtrar por Closed Won
Adicione uma ação de **Filtro** para só continuar quando o negócio for ganho:
| Configuração | Valor |
| ------------ | ------------------------------------- |
| **Campo** | Etapa |
| **Condição** | Igual a |
| **Valor** | `CLOSED_WON` (ou o nome da sua etapa) |
<Tip>
O gatilho é acionado em qualquer atualização de Oportunidade. O Filtro garante que o fluxo de trabalho só continue quando a etapa mudar para Closed Won.
</Tip>
## Etapa 3: Obter detalhes da empresa
O registo de Oportunidade pode não incluir todos os campos da Empresa de que precisa para a fatura. Adicione uma ação de **Pesquisar Registo**:
| Configuração | Valor |
| ----------------------- | ----------------------------------------- |
| **Objeto** | Empresa |
| **Correspondência por** | ID igual a `{{trigger.object.companyId}}` |
Isto obtém o registo completo da Empresa com morada de faturação, NIF, etc.
## Etapa 4: Formatar o Payload (Opcional)
Se o seu sistema de faturação esperar um formato específico, adicione uma ação de **Código**:
```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
}
}
};
};
```
## Etapa 5: Enviar para o Sistema de Faturação
Adicione uma ação de **Pedido HTTP**:
| Configuração | Valor |
| -------------- | -------------------------------------------------------------- |
| **Método** | POST |
| **URL** | O endpoint da API do seu sistema de faturação |
| **Cabeçalhos** | `Authorization: Bearer YOUR_API_KEY` |
| **Corpo** | `{{code.invoice}}` ou faça o mapeamento dos campos diretamente |
### Exemplo: Fatura 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
```
### Exemplo: Fatura QuickBooks
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## Resumo Completo do Fluxo de Trabalho
| Etapa | Ação | Finalidade |
| ----- | --------------------------- | -------------------------------------------------- |
| 1 | Gatilho: Registo atualizado | Dispara quando qualquer Oportunidade é alterada |
| 2 | Filtro | Só prossiga se Etapa = Closed Won |
| 3 | Pesquisar Registo | Obter detalhes completos da Empresa para faturação |
| 4 | Código | Formatar dados para a API de faturação |
| 5 | Requisição HTTP | Criar fatura no sistema externo |
## Dicas
* **Armazene IDs externos**: Guarde o ID da fatura retornado pela API na Oportunidade usando uma ação de **Atualizar Registo**
* **Tratamento de erros**: Adicione uma ramificação para enviar uma notificação se o pedido HTTP falhar
* **Teste primeiro**: Use o modo sandbox/de teste do seu sistema de faturação antes de ir para produção
## Relacionados
* [Gerar um PDF a partir do Twenty](/l/pt/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — anexar PDFs gerados aos registros
* [Gatilhos de Fluxo de Trabalho](/l/pt/user-guide/workflows/capabilities/workflow-triggers)
* [Ações de Fluxo de Trabalho](/l/pt/user-guide/workflows/capabilities/workflow-actions)
* [Automações de Closed Won](/l/pt/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Configurar um gatilho de Webhook
description: Receba dados de serviços externos para acionar fluxos de trabalho.
---
Gatilhos de Webhook permitem que serviços externos iniciem seus fluxos de trabalho enviando dados para uma URL exclusiva. Use-os para conectar formulários, aplicativos de terceiros e integrações personalizadas.
## Quando usar Webhooks
| Caso de uso | Exemplo |
| ------------------------------ | ----------------------------------------------- |
| **Formulários da web** | Envios de formulários de contato geram leads |
| **Aplicativos de terceiros** | Pagamento no Stripe → criar registro de cliente |
| **Integrações personalizadas** | Seu aplicativo → automação no Twenty |
| **Ferramentas no-code** | Conexões com Zapier, Make, n8n |
## Configuração passo a passo
### Etapa 1: Criar o fluxo de trabalho
1. Vá para **Configurações → Fluxos de trabalho**
2. Clique em **+ Novo fluxo de trabalho**
3. Dê um nome (por exemplo, "Envio de formulário do site")
### Etapa 2: Configurar o gatilho de Webhook
1. Clique no bloco de gatilho
2. Selecione **Webhook**
3. Você receberá uma URL de webhook exclusiva, como:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. Copie esta URL—você vai precisar dela no seu serviço externo
### Etapa 3: Definir a estrutura de dados esperada
Para solicitações **POST**, defina a estrutura esperada do corpo:
1. Clique em **Definir corpo esperado**
2. Insira um JSON de exemplo que corresponda ao que o seu serviço enviará:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. Clique em **Salvar**—isso cria variáveis que você pode usar nas etapas subsequentes
### Etapa 4: Adicionar ações
Agora adicione ações que usem os dados do webhook:
**Exemplo: Criar um registro de Pessoa**
1. Adicione a ação **Criar registro**
2. Selecione o objeto **Pessoas**
3. Mapeie os campos:
| Campo | Valor |
| --------- | --------------------------------------------------------- |
| Nome | `{{trigger.body.firstName}}` |
| Sobrenome | `{{trigger.body.lastName}}` |
| E-mail | `{{trigger.body.email}}` |
| Empresa | Pesquisar ou criar com base em `{{trigger.body.company}}` |
### Etapa 5: Testar o Webhook
Antes de ativar, teste o seu 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 o Postman ou similar**:
1. Crie uma solicitação POST para a sua URL de webhook
2. Defina o cabeçalho Content-Type como `application/json`
3. Adicione o corpo JSON de teste
4. Envie e verifique as execuções do fluxo de trabalho
### Etapa 6: Ativar
Depois de testar, clique em **Ativar** para ativar o fluxo de trabalho.
## Como lidar com diferentes estruturas de dados
### Dados aninhados
Se o seu webhook enviar dados aninhados:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Referencie com: `{{trigger.body.contact.email}}`
### Arrays
Se os dados incluírem arrays:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
Como você lida com arrays depende do seu caso de uso:
**Número desconhecido de itens → Use Iterator**
Se você precisar processar cada item no array (por exemplo, criar um registro para cada), adicione uma ação **Code** para analisar o array e, em seguida, use **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
Em seguida, use Iterator para percorrer: `{{code.items}}`
**Campos conhecidos/específicos → Extraia para campos nomeados**
Se o array contiver campos específicos aos quais você deseja acessar individualmente (por exemplo, respostas de formulário em que a posição 0 é sempre "nome", a posição 1 é sempre "sobrenome"), adicione uma ação **Code** para extraí-los:
```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
}
};
};
```
Agora você pode selecionar `product.name` e `product.qty` individualmente nas etapas subsequentes.
<Tip>
Para mais detalhes sobre como lidar com arrays, consulte [Tratar arrays em ações de código](/l/pt/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Relacionados
* [Gatilhos de Fluxo de Trabalho](/l/pt/user-guide/workflows/capabilities/workflow-triggers)
* [Ações de Fluxo de Trabalho](/l/pt/user-guide/workflows/capabilities/workflow-actions)