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: Импортируйте данные о товарах в Twenty
description: Синхронизируйте данные каталога товаров из хранилища данных с вашей CRM по расписанию.
---
Используйте этот шаблон, чтобы поддерживать синхронизацию Twenty с данными о товарах из вашего хранилища данных (например, Snowflake, BigQuery, PostgreSQL).
## Структура рабочего процесса
1. **Триггер**: По расписанию
2. **Code**: Запрос к вашему хранилищу данных
3. **Code** (необязательно): Отформатируйте данные как массив
4. **Iterator**: Цикл по каждому товару
5. **Upsert Record**: Создать или обновить в Twenty
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## Шаг 1: Запланируйте триггер
Настройте запуск рабочего процесса с частотой, соответствующей требованиям к актуальности данных:
* Каждые 5 минут для синхронизации, близкой к реальному времени
* Каждый час для менее критичных данных
* Ежедневно для пакетных обновлений
## Шаг 2: Выполните запрос к вашему хранилищу данных
Добавьте действие **Code** для получения последних данных:
```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>
Отфильтруйте по `updated_at >= last X minutes`, чтобы получать только недавно изменённые записи. Это делает синхронизацию эффективной.
</Tip>
## Шаг 3: Отформатируйте данные (необязательно)
Если ваше хранилище возвращает данные в формате, требующем преобразования, добавьте ещё одно действие **Code**. К распространённым преобразованиям относятся преобразование типов, переименование полей и очистка данных.
### Пример: данные пользователей с булевыми и статусными полями
```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),
})),
};
};
```
### Пример: данные о товарах с преобразованием типов
```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
}))
};
};
```
### Пример: форматирование даты и валюты
```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, " ")
}))
};
};
```
### Распространённые преобразования
| Исходный формат | Целевой формат | Код |
| -------------------- | ------------------ | ---------------------------------------- |
| `"true"` / `"false"` | `true` / `false` | `v === true \|\| v === "true"` |
| `"123.45"` | `123.45` | `parseFloat(value)` |
| `"active"` | `"ACTIVE"` | `value.toUpperCase()` |
| `1704067200` (Unix) | Дата в формате ISO | `new Date(v * 1000).toISOString()` |
| `"$1,234.56"` | `1234.56` | `parseFloat(v.replace(/[^0-9.-]/g, ""))` |
| `null` / `undefined` | `""` | `value \|\| ""` |
## Шаг 4: Переберите товары
Добавьте действие **Iterator**:
* Вход: `{{code.products}}`
Это проходит по каждому товару в массиве.
## Шаг 5: Выполните Upsert для каждой записи
Внутри итератора добавьте действие **Upsert Record**:
| Настройка | Значение |
| -------------------- | --------------------------------------------- |
| **Объект** | Ваш пользовательский объект Product |
| **Сопоставление по** | Внешний ID или SKU (уникальный идентификатор) |
| **Название** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Цена** | `{{iterator.item.price}}` |
<Tip>
Используйте **Upsert** (обновление или создание) вместо построения отдельных веток для создания и обновления. Так быстрее реализовать и проще отлаживать.
</Tip>
## Примеры случаев использования
| Источник | Данные |
| ----------------------------------- | ------------------------------------------ |
| **ERP-система** | Каталог товаров, цены, запасы |
| **Платформа электронной коммерции** | Заказы, клиенты, обновления товаров |
| **Хранилище данных** | Агрегированные метрики, обогащённые данные |
| **Система управления запасами** | Уровни запасов, оповещения о дозаказах |
## Связанные материалы
* [Триггеры рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-triggers)
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)
* [Обработка массивов в действиях Code](/l/ru/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Импорт ответов Typeform в Twenty
description: Обработайте полезную нагрузку вебхука Typeform, чтобы создавать лиды из отправок формы.
---
Для стандартной настройки вебхука см. [Настройка триггера вебхука](/l/ru/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger). В этой статье рассматривается специальная обработка, требуемая для пользовательской структуры полезной нагрузки Typeform.
### Шаг 1: Создайте рабочий процесс с вебхуком
1. Перейдите в **Настройки → Рабочие процессы**
2. Нажмите **+ Новый рабочий процесс**
3. Выберите **Webhook** в качестве триггера
4. Скопируйте URL вебхука
### Шаг 2: Настройте Typeform
1. В Typeform откройте свою форму
2. Перейдите в **Connect → Webhooks**
3. Вставьте URL вебхука Twenty
4. Сохранить
### Шаг 3: Разберитесь в полезной нагрузке Typeform
Typeform отправляет вложенную структуру JSON. Вот упрощенный пример:
```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" }
}
]
}
}
```
Ключевые моменты:
* Данные формы находятся в `form_response`
* **Ответы возвращаются в виде массива**, а не именованных полей
* Каждый ответ включает тип поля и его заголовок для справки
### Шаг 4: Извлеките поля из массива ответов
Поскольку `answers` — это массив, на последующих шагах можно выбрать только весь массив, а не отдельные поля. Добавьте действие **Code** для извлечения нужных полей:
```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
}
};
};
```
На последующих шагах теперь можно выбирать отдельные поля, такие как `contact.firstName` и `contact.email`, в селекторе переменных.
<Tip>
Для получения подробностей об обработке массивов в действиях Code см. [Обработка массивов в действиях Code](/l/ru/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
### Шаг 5: Создайте запись
Добавьте действие **Создать запись**:
| Поле | Значение |
| --------------------- | --------------------------------------------------------- |
| **Объект** | Люди |
| **Имя** | `{{code.contact.firstName}}` |
| **Фамилия** | `{{code.contact.lastName}}` |
| **Электронная почта** | `{{code.contact.email}}` |
| **Компания** | Найдите или создайте на основе `{{code.contact.company}}` |
### Шаг 6: Протестируйте и активируйте
1. Отправьте тестовый ответ в Typeform
2. Проверьте запуск рабочего процесса, чтобы убедиться, что данные были получены
3. Активируйте рабочий процесс
## Связанные материалы
* [Настройка триггера вебхука](/l/ru/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Обработка массивов в действиях Code](/l/ru/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Создание PDF в Twenty
description: Создайте рабочий процесс для создания и прикрепления PDF (например, коммерческого предложения) к записи.
---
Автоматически создавайте или получайте PDF и прикрепляйте его к записи в Twenty. Это часто используют для создания коммерческих предложений, счетов или отчетов, связанных с компаниями, сделками или другими объектами.
## Обзор
В этом рабочем процессе используется **Ручной триггер**, чтобы пользователи могли по запросу создавать PDF для любой выбранной записи. За обработку отвечает **Логическая функция**:
1. Загрузка PDF по URL (из сервиса генерации PDF)
2. Загрузка файла в Twenty
3. Создание вложения, связанного с записью
## Требования
Прежде чем настраивать рабочий процесс:
1. **Создайте ключ API**: перейдите в **Настройки → APIs** и создайте новый ключ API. Вам понадобится этот токен для логической функции.
2. **Настройте сервис генерации PDF** (необязательно): если вы хотите динамически генерировать PDF (например, коммерческие предложения), используйте такие сервисы, как Carbone, PDFMonkey или DocuSeal, чтобы создать PDF и получить URL для скачивания.
## Пошаговая настройка
### Шаг 1: Настройте триггер
1. Перейдите в **Рабочие процессы** и создайте новый рабочий процесс
2. Выберите **Ручной триггер**
3. Выберите объект, к которому нужно прикреплять PDF (например, **Company** или **Opportunity**)
<Tip>
С ручным триггером, пользователи могут запускать этот рабочий процесс, используя кнопку, которая появляется в правом верхнем углу после выбора записи, которая запускает рабочий процесс для создания и прикрепления PDF.
</Tip>
### Шаг 2: Добавьте логическую функцию
1. Добавить действие **код** (логическая функция)
2. Создайте новую функцию с кодом ниже
3. Настройте входные параметры
#### Входные параметры
| Параметр | Значение |
| ----------- | ----------------------- |
| `companyId` | `{{trigger.object.id}}` |
<Note>
Если прикрепляете к другому объекту (Person, Opportunity и т. д.), переименуйте параметр соответствующим образом (например, `personId`, `opportunityId`) и обновите логическую функцию.
</Note>
#### Код логической функции
```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;
};
```
### Шаг 3: Настройте под ваш сценарий
#### Чтобы прикреплять к другому объекту
Замените `targetCompanyId` на соответствующее поле:
| Объект | Имя поля |
| ----------------------- | -------------------------- |
| Компания | `targetCompanyId` |
| Контакт | `targetPersonId` |
| Сделка | `targetOpportunityId` |
| Пользовательский объект | `targetYourCustomObjectId` |
Обновите и параметр функции, и объект `variables.data` в мутации вложения.
#### Чтобы использовать динамический URL PDF
Если используете сервис генерации PDF, вы можете:
1. Сначала выполните действие HTTP Request, чтобы сгенерировать PDF
2. Передайте возвращенный URL PDF в логическую функцию как параметр
```typescript
export const main = async (
params: { companyId: string; pdfUrl: string; filename: string },
) => {
const { companyId, pdfUrl, filename } = params;
// ... rest of the function
};
```
### Шаг 4: Протестируйте и активируйте
1. Сохраните рабочий процесс
2. Перейдите к записи Company
3. Нажмите меню **⋮** и выберите ваш рабочий процесс
4. Проверьте раздел **Attachments** в записи, чтобы убедиться, что PDF прикреплен
5. Активируйте рабочий процесс
## Совмещение с сервисами генерации PDF
Для создания динамических коммерческих предложений или счетов:
### Пример: Сгенерировать коммерческое предложение → Прикрепить PDF
| Шаг | Действие | Назначение |
| --- | ------------------------ | ------------------------------------------ |
| 1 | Ручной триггер (Company) | Пользователь запускает на записи |
| 2 | Поиск записи | Получить сведения о сделке или позициях |
| 3 | HTTP-запрос | Вызвать API генерации PDF с данными записи |
| 4 | Логическая функция | Скачать и прикрепить сгенерированный PDF |
### Популярные сервисы генерации PDF
* **Carbone** — генерация документов на основе шаблонов
* **PDFMonkey** — динамическое создание PDF из шаблонов
* **DocuSeal** — платформа автоматизации документооборота
* **Documint** — генерация документов, ориентированная на API
Каждый сервис предоставляет API, который возвращает URL-адрес PDF, который затем можно передать в логическую функцию.
## Устранение неполадок
| Проблема | Решение |
| ----------------------------- | ----------------------------------------------------------------------------- |
| "Не удалось скачать PDF" | Проверьте, что URL PDF доступен и возвращает корректный PDF |
| "Не удалось загрузить" | Убедитесь, что ваш ключ API действителен и имеет права на запись |
| "Не удалось создать вложение" | Убедитесь, что имя поля идентификатора объекта соответствует целевому объекту |
## Связанные материалы
* [Триггеры рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-triggers)
* [Логические функции](/l/ru/user-guide/workflows/capabilities/workflow-actions#code)
* [Создание коммерческого предложения или счета в Twenty](/l/ru/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)

View File

@@ -0,0 +1,144 @@
---
title: Создайте коммерческое предложение или счёт из Twenty
description: Автоматически создавайте счёта во внешних инструментах при закрытии сделок.
---
Автоматически отправляйте данные по сделке в вашу систему выставления счетов (Stripe, QuickBooks, Xero и т. д.). когда сделка выиграна.
## Структура рабочего процесса
1. **Триггер**: Запись обновлена (объект 'Сделка')
2. **Фильтр**: Этап = Закрыто — выиграно
3. **Поиск записи**: Получить сведения о компании
4. **Код** (необязательно): Сформатировать полезную нагрузку
5. **HTTP-запрос**: Отправить в систему выставления счетов
## Шаг 1: Настройте триггер
1. Создайте новый рабочий процесс
2. Выберите триггер **Запись обновлена**
3. Выберите **Сделка** в качестве объекта
## Шаг 2: Отфильтруйте по «Закрыто — выиграно»
Добавьте действие **Фильтр**, чтобы продолжать только когда сделка выиграна:
| Настройка | Значение |
| ------------ | ---------------------------------------- |
| **Поле** | Этап |
| **Условие** | Равно |
| **Значение** | `CLOSED_WON` (или название вашего этапа) |
<Tip>
Триггер срабатывает при любом обновлении Сделки. Фильтр гарантирует, что рабочий процесс продолжится только когда этап изменится на «Закрыто — выиграно».
</Tip>
## Шаг 3: Получите сведения о компании
Запись Сделки может не содержать всех полей Компании, которые нужны для счёта. Добавьте действие **Поиск записи**:
| Настройка | Значение |
| -------------------- | --------------------------------------- |
| **Объект** | Компания |
| **Сопоставление по** | ID равен `{{trigger.object.companyId}}` |
Это вернёт полную запись Компании с платёжным адресом, налоговым идентификатором и т. д.
## Шаг 4: Сформатируйте полезную нагрузку (необязательно)
Если ваша система выставления счетов ожидает определённый формат, добавьте действие **Код**:
```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
}
}
};
};
```
## Шаг 5: Отправьте в систему выставления счетов
Добавьте действие **HTTP-запрос**:
| Настройка | Значение |
| -------------- | --------------------------------------------------- |
| **Метод** | POST |
| **URL-адрес** | Конечная точка API вашей системы выставления счетов |
| **Заголовки** | `Authorization: Bearer YOUR_API_KEY` |
| **Содержимое** | `{{code.invoice}}` или сопоставьте поля напрямую |
### Пример: счёт 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
```
### Пример: счёт QuickBooks
```
POST https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice
Headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Body: {{code.invoice}}
```
## Итоговая сводка рабочего процесса
| Шаг | Действие | Назначение |
| --- | ------------------------- | --------------------------------------------------------- |
| 1 | Триггер: запись обновлена | Срабатывает при любых изменениях в Сделке |
| 2 | Фильтр | Продолжать только если Этап = Закрыто — выиграно |
| 3 | Поиск записи | Получить полные сведения о компании для выставления счёта |
| 4 | Код | Сформатировать данные для API выставления счетов |
| 5 | HTTP-запрос | Создать счёт во внешней системе |
## Советы
* **Сохраняйте внешние ID**: Сохраните идентификатор счёта, возвращённый API, обратно в Сделку с помощью действия **Обновить запись**
* **Обработка ошибок**: Добавьте ветку, чтобы отправлять уведомление, если HTTP-запрос завершится ошибкой
* **Сначала протестируйте**: Используйте песочницу/тестовый режим вашей системы выставления счетов перед запуском в рабочую среду
## Связанные материалы
* [Сгенерировать PDF из Twenty](/l/ru/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — прикрепляйте сгенерированные PDF-файлы к записям
* [Триггеры рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-triggers)
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)
* [Автоматизации «Закрыто — выиграно»](/l/ru/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Настройка триггера вебхука
description: Получайте данные из внешних сервисов, чтобы запускать рабочие процессы.
---
Триггеры вебхуков позволяют внешним сервисам запускать ваши рабочие процессы, отправляя данные на уникальный URL. Используйте их для подключения форм, сторонних приложений и пользовательских интеграций.
## Когда использовать вебхуки
| Сценарий использования | Пример |
| ------------------------------- | ------------------------------------------ |
| **Веб-формы** | Отправки формы обратной связи создают лиды |
| **Сторонние приложения** | Платеж в Stripe → создать запись клиента |
| **Пользовательские интеграции** | Ваше приложение → автоматизация Twenty |
| **Инструменты без кода** | Подключения Zapier, Make, n8n |
## Пошаговая настройка
### Шаг 1: Создайте рабочий процесс
1. Перейдите в **Настройки → Рабочие процессы**
2. Нажмите **+ Новый рабочий процесс**
3. Дайте ему имя (например, "Отправка формы на сайте")
### Шаг 2: Настройте триггер вебхука
1. Нажмите на блок триггера
2. Выберите **Вебхук**
3. Вы получите уникальный URL вебхука, например:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. Скопируйте этот URL — он понадобится вашему внешнему сервису
### Шаг 3: Определите ожидаемую структуру данных
Для **POST**-запросов определите ожидаемую структуру тела:
1. Нажмите **Определить ожидаемое тело**
2. Введите пример JSON, который ваш сервис будет отправлять:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. Нажмите **Сохранить** — это создаст переменные, которые можно использовать на следующих шагах
### Шаг 4: Добавьте действия
Теперь добавьте действия, которые используют данные вебхука:
**Пример: Создать запись Person**
1. Добавьте действие **Создать запись**
2. Выберите объект **People**
3. Сопоставьте поля:
| Поле | Значение |
| ----------------- | --------------------------------------------------------- |
| Имя | `{{trigger.body.firstName}}` |
| Фамилия | `{{trigger.body.lastName}}` |
| Электронная почта | `{{trigger.body.email}}` |
| Компания | Найдите или создайте на основе `{{trigger.body.company}}` |
### Шаг 5: Протестируйте вебхук
Перед активацией протестируйте вебхук:
**С помощью 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"}'
```
**С помощью Postman или аналогов**:
1. Создайте POST-запрос на URL вашего вебхука
2. Установите заголовок Content-Type в `application/json`
3. Добавьте тестовое тело JSON
4. Отправьте и проверьте запуски рабочего процесса
### Шаг 6: Активируйте
После тестирования нажмите **Активировать**, чтобы сделать рабочий процесс активным.
## Обработка разных структур данных
### Вложенные данные
Если ваш вебхук отправляет вложенные данные:
```json
{
"contact": {
"name": "John Doe",
"email": "john@example.com"
},
"source": "website"
}
```
Обращайтесь так: `{{trigger.body.contact.email}}`
### Массивы
Если данные включают массивы:
```json
{
"items": [
{"name": "Product A", "qty": 2},
{"name": "Product B", "qty": 1}
]
}
```
То, как обрабатывать массивы, зависит от сценария использования:
**Неизвестное количество элементов → Используйте Iterator**
Если нужно обработать каждый элемент массива (например, создать запись для каждого), добавьте действие **Code** для разбора массива, затем используйте **Iterator**:
```javascript
export const main = async (params: { items: any }) => {
const items = typeof params.items === "string"
? JSON.parse(params.items)
: params.items;
return { items };
};
```
Затем используйте Iterator для перебора: `{{code.items}}`
**Известные/конкретные поля → Извлеките в именованные поля**
Если массив содержит конкретные поля, к которым нужно обращаться по отдельности (например, ответы формы, где позиция 0 — всегда "имя", позиция 1 — всегда "фамилия"), добавьте действие **Code** для их извлечения:
```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
}
};
};
```
Теперь вы можете выбирать `product.name` и `product.qty` по отдельности на следующих шагах.
<Tip>
Для получения подробностей об обработке массивов см. [Обработка массивов в действиях Code](/l/ru/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions).
</Tip>
## Связанные материалы
* [Триггеры рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-triggers)
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)