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

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

View File

@@ -0,0 +1,82 @@
---
title: Работа с массивами в действиях Code
description: Узнайте, как правильно обрабатывать входные массивы в действиях Code в рабочих процессах.
---
При работе с массивами в действиях Code вы можете столкнуться с двумя распространёнными проблемами:
1. **Массивы, передаваемые как строки** — данные из внешних систем или предыдущих шагов приходят строкой, а не как настоящий массив
2. **Невозможно выбрать отдельные элементы** — можно выбрать только весь массив, а не конкретные поля внутри него
Обе проблемы решаются с помощью узла Code.
## Разбор массивов из строк
Массивы часто передаются между шагами рабочего процесса как строки или JSON, а не как нативные массивы. Это происходит, когда:
* Получение данных из внешних API через HTTP Request
* Обработка полезной нагрузки вебхуков
* Передача данных между шагами рабочего процесса
**Решение**: Добавьте этот шаблон в начало вашего действия Code:
```javascript
export const main = async (params: {
users: any;
}): Promise<object> => {
const { users } = params;
// Handle input that may come as a string or an array
const usersFormatted = typeof users === "string" ? JSON.parse(users) : users;
// Now you can safely work with usersFormatted as an array
return {
users: usersFormatted.map((user) => ({
...user,
activityStatus: String(user.activityStatus).toUpperCase(),
})),
};
};
```
Ключевая строка `typeof users === "string" ? JSON.parse(users) : users` проверяет, является ли входное значение строкой, парсит его при необходимости или использует напрямую, если это уже массив.
## Извлечение отдельных полей из массивов
Вебхук может вернуть массив вида `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 specific fields from the array
const firstname = answersFormatted[0]?.text || "";
const name = answersFormatted[1]?.text || "";
return {
answer: {
firstname,
name
}
};
};
```
Узел Code возвращает структурированный объект вместо массива. На последующих шагах теперь можно выбирать отдельные поля, такие как `answer.firstname` и `answer.name`, в селекторе переменных.
<Tip>
**Хотите перебирать массив в цикле вместо извлечения полей?** Когда шаг Code или Logic Function возвращает массив верхнего уровня, вы можете передать его напрямую в [Iterator](/l/ru/user-guide/workflows/capabilities/use-iterator): выберите параметр шага **Whole list** в качестве входных данных Iterator, а затем обращайтесь к каждому элементу с помощью `{{iterator.currentItem}}` внутри цикла. В этом случае вам не нужно преобразовывать массив в объект.
</Tip>
<Tip>
Нажмите квадратный значок в правом верхнем углу редактора кода, чтобы отобразить его на весь экран — это полезно, поскольку ширина редактора по умолчанию ограничена.
</Tip>

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)

View File

@@ -0,0 +1,121 @@
---
title: Автоответ на входящие письма
description: Создайте рабочий процесс, который использует ИИ для сортировки входящих писем и автоматически отправляет ответы в рамках цепочки.
---
Отвечайте на входящие письма за секунды — а не за часы. Этот рабочий процесс использует ИИ-агента, чтобы отфильтровать шум (рассылки, спам, автоответы) и подготовить персонализированный ответ на реальные сообщения. Затем ИИ отправляет его как ответ в цепочке внутри исходной переписки.
## Как работают цепочки писем
Каждое письмо содержит скрытый заголовок `Message-ID` — уникальный отпечаток, назначаемый почтовым сервером отправителя. Когда вы отвечаете на электронное письмо, ваш почтовый клиент устанавливает заголовок `In-Reply-To`, ссылающийся на этот отпечаток. Именно так Gmail, Outlook и другие клиенты группируют сообщения в цепочки.
В Twenty этот отпечаток хранится как `headerMessageId` в объекте Message. Ваш рабочий процесс получает его и передает в поле In-Reply-To действия Send Email.
## Создание рабочего процесса
### Шаг 1: Создайте новый рабочий процесс
Перейдите в **Настройки → Рабочие процессы** и нажмите **+ New Workflow**.
### Шаг 2: Настройте триггер на входящие сообщения
Выберите **When a Record is Created** и укажите **Messages**.
Каждый раз, когда письмо попадает в Twenty, этот триггер срабатывает.
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
### Шаг 3: Найдите, кто отправил письмо
Добавьте действие **Search Records**.
Адрес отправителя находится не в самом сообщении — он в связанной записи Message Participant.
| Поле | Значение |
| ---------- | ---------------------------------- |
| **Объект** | Участники сообщения |
| **Фильтр** | Message **равно** `{{trigger.id}}` |
| **Фильтр** | Role **равно** From |
| **Лимит** | 1 |
Это дает вам адрес электронной почты отправителя в `handle` и его имя в `displayName`.
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
### Шаг 4: Классификация и черновик ответа с помощью ИИ
Добавьте действие **AI Agent**. Этот шаг делает две вещи: решает, заслуживает ли письмо ответа, и, если да, составляет ответ.
Используйте подсказку вроде:
```
You are an email triage assistant for a sales team. Read the following
inbound email and decide if it deserves a reply.
Subject: {{trigger.subject}}
Body: {{trigger.text}}
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
If this email is spam, a newsletter, an automated notification, or
otherwise does not need a human reply, respond with exactly: SKIP
Otherwise, write a short, professional reply (3-4 sentences max) that:
- Acknowledges their specific message
- Lets them know someone from the team will follow up shortly
- Is warm but not overly casual
Respond with only the reply text, no subject line or greeting prefix.
```
AI Agent выводит свой ответ в поле `response`, к которому могут обращаться следующие шаги.
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
### Шаг 5: Ветвление по решению ИИ
Добавьте действие **If/Else**, чтобы проверить, решил ли ИИ ответить или пропустить.
| Поле | Значение |
| -------------- | ---------------------------------------------- |
| **Условие** | AI Agent `response` **не содержит** `SKIP` |
| **Если верно** | Перейти к Send Email |
| **Иначе** | Ничего не делать (рабочий процесс завершается) |
Спам, рассылки и автоматически сгенерированные сообщения отбрасываются. Все остальное переходит к следующему шагу.
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
### Шаг 6: Отправьте ответ в цепочке
Добавьте действие **Send Email** в ветке "Если верно". Нажмите **Advanced options**, затем **Add In-Reply-To**.
| Поле | Значение |
| --------------- | -------------------------------------- |
| **Кому** | `{{Find Sender.first.handle}}` |
| **Тема** | `Re: {{trigger.subject}}` |
| **Текст** | `{{AI Triage & Draft Reply.response}}` |
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
Поле In-Reply-To делает это письмом-ответом, а не новой перепиской. Получатель увидит его в той же цепочке под исходным письмом в Gmail, Outlook или любом другом клиенте.
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
<Tip>
**In-Reply-To** ожидает `message.headerMessageId` из триггера — это уникальный отпечаток письма, а не адрес получателя. Если оставить его пустым, письмо всё равно отправится, но как отдельное сообщение.
</Tip>
<Warning>
Gmail использует тему письма, чтобы группировать сообщения в цепочки. Тема письма **должна** начинаться с `Re:` (включая двоеточие и пробел), чтобы Gmail показывал ответ внутри исходной цепочки. Без этого ответ будет отображаться как отдельная переписка — даже если заголовок In-Reply-To установлен правильно.
</Warning>
### Шаг 7: Протестируйте и активируйте
Нажмите **Test**, затем проверьте ваш почтовый клиент. Ответ должен появиться под исходным сообщением.
Активируйте, когда будете довольны результатом.
## Идеи для развития
* **Отвечать только VIP-адресатам** — добавьте ветку, которая проверяет домен отправителя или то, существует ли он как Контакт в Twenty
* **Маршрутизация по намерению** — используйте отдельные подсказки для AI Agent, чтобы обрабатывать запросы по продажам иначе, чем обращения в поддержку
* **Обогатите данные перед ответом** — добавьте шаг Search Records, чтобы передать компанию отправителя или историю сделок в подсказку ИИ для более персонализированных ответов

View File

@@ -0,0 +1,179 @@
---
title: Автоматизации для стадии «Закрыта — выиграна»
description: Автоматизируйте действия после выигрыша, когда возможность закрывается.
---
Когда сделка закрывается, нужно сделать несколько вещей: обновить статус компании, уведомить участников команды, создать задачи по онбордингу. Автоматизируйте всё это одним рабочим процессом.
## Проблема
Когда возможность переходит в стадию «Закрыта — выиграна»:
* Тип компании должен измениться с «Потенциальный клиент» на «Клиент»
* Нужно создать задачи по онбордингу
* Команду Customer Success необходимо уведомить
* Менеджеру по продажам нужно подтверждение
Вручную это занимает много времени и приводит к ошибкам.
## Решение
Создайте рабочий процесс, который автоматически выполнит все действия после выигрыша.
## Полная настройка рабочего процесса
### Шаг 1: Создайте рабочий процесс
1. Перейдите в **Настройки → Рабочие процессы**
2. Нажмите **+ Новый рабочий процесс**
3. Назовите его "Deal Won - Post-Win Automation"
### Шаг 2: Настройте триггер
1. Выберите **Запись обновлена**
2. Выберите **Возможности**
3. В разделе "Поля для отслеживания" выберите **Стадия**
### Шаг 3: Добавьте фильтр по стадии
1. Добавьте действие **Фильтр**
2. Условие: `{{trigger.object.stage}}` равно «Закрыта выиграна»
### Шаг 4: Обновите тип компании
1. Добавьте действие **Обновить запись**
2. Настроить:
| Поле | Значение |
| ---------------------- | ------------------------------- |
| **Объект** | Компании |
| **Запись** | `{{trigger.object.company.id}}` |
| **Тип** | Клиент |
| **Дата первой сделки** | `{{trigger.object.closedAt}}` |
| **Владелец аккаунта** | `{{trigger.object.owner.id}}` |
### Шаг 5: Создайте задачу по онбордингу
1. Добавьте действие **Создать запись**
2. Настроить:
| Поле | Значение |
| ------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Объект** | Задачи |
| **Заголовок** | `Onboarding: {{trigger.object.name}}` |
| **Исполнитель** | Сотрудник команды Customer Success |
| **Срок исполнения** | Через 3 дня |
| **Приоритет** | Высокий |
| **Связанная компания** | `{{trigger.object.company.id}}` |
| **Связанная возможность** | `{{trigger.object.id}}` |
| **Описание** | `New customer onboarding for {{trigger.object.company.name}}. Deal value: {{trigger.object.amount}}` |
### Шаг 6: Уведомите команду Customer Success
1. Добавьте действие **Send Email**
2. Настроить:
| Поле | Значение |
| --------- | -------------------------------------------------- |
| **Кому** | customer-success@yourcompany.com |
| **Тема** | `🎉 New Customer: {{trigger.object.company.name}}` |
| **Текст** | См. пример ниже |
**Пример текста письма**:
```
Hi CS Team,
We have a new customer!
Company: {{trigger.object.company.name}}
Deal: {{trigger.object.name}}
Value: {{trigger.object.amount}}
Sales Rep: {{trigger.object.owner.name}}
Close Date: {{trigger.object.closedAt}}
An onboarding task has been created automatically.
Let's give them a great start!
```
### Шаг 7: Отправьте подтверждение менеджеру по продажам
1. Добавьте ещё одно действие **Send Email**
2. Настроить:
| Поле | Значение |
| --------- | ------------------------------------------------------------------------------------------ |
| **Кому** | `{{trigger.object.owner.email}}` |
| **Тема** | `✅ Deal Closed: {{trigger.object.name}}` |
| **Текст** | Поздравляем! Ваша сделка обработана. Команда Customer Success уведомлена, онбординг начат. |
### Шаг 8: Протестируйте и активируйте
1. Протестируйте, переместив тестовую возможность в стадию «Закрыта выиграна»
2. Проверить:
* Тип компании изменился на «Клиент»
* Задача по онбордингу создана
* Команда Customer Success получила письмо
* Менеджер по продажам получил подтверждение
3. Активируйте, когда будете готовы
## Обработка статуса «Закрыта проиграна»
Создайте аналогичный рабочий процесс для проигранных сделок:
### Триггер
* Запись обновлена (Возможности, стадия = «Закрыта проиграна»)
### Действия
1. **Создать запись**: Задача «Анализ проигранной сделки»
2. **Обновить запись**: Добавьте причину проигрыша в запись компании
3. **Send Email**: Уведомите менеджера о проигранной сделке
## Продвинутое: многошаговый онбординг
Для сложного онбординга создайте несколько задач:
```javascript
export const main = async (params) => {
const tasks = [
{ title: "Welcome call", daysFromNow: 1, assignee: "CS" },
{ title: "Send onboarding materials", daysFromNow: 2, assignee: "CS" },
{ title: "Technical setup", daysFromNow: 5, assignee: "Support" },
{ title: "30-day check-in", daysFromNow: 30, assignee: "CS" }
];
return { tasks };
};
```
Используйте **Iterator** для создания каждой задачи из массива.
## Идеи для настройки
### Держите другие инструменты в актуальном состоянии
* Создайте клиента в биллинговой системе с помощью **HTTP Request**
### Условные действия
Используйте действия **Фильтр**, чтобы:
* Разный онбординг для Enterprise и SMB
* Разные исполнители в зависимости от региона
* Пропускать уведомления для небольших сделок
### Включить информацию о сделке
Используйте действие **Code** для форматирования:
* Документы с краткой сводкой по сделке
* Заметки для передачи команде Customer Success
* Пользовательские контрольные списки онбординга
## Связанные материалы
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)
* [Отправка писем из рабочих процессов](/l/ru/user-guide/workflows/capabilities/send-emails-from-workflows)

View File

@@ -0,0 +1,136 @@
---
title: Выявление застойных возможностей
description: Автоматически уведомляйте менеджеров, когда возможности не обновлялись.
---
Поддерживайте воронку продаж в порядке, уведомляя менеджеров, когда возможности застаиваются. Этот рабочий процесс проверяет возможности, которые не обновлялись в течение указанного числа дней.
## Проблема
Возможности без обновлений приводят к:
* Остывающие сделки
* Ненадежные прогнозы
* Потеря выручки
## Решение
Создайте запланированный рабочий процесс, который находит застойные возможности и отправляет их менеджерам электронные письма.
## Пошаговая настройка
### Шаг 1: Создайте рабочий процесс
1. Перейдите в **Настройки → Рабочие процессы**
2. Нажмите **+ Новый рабочий процесс**
3. Назовите его «Оповещение о застойных возможностях»
### Шаг 2: Настройте триггер
1. Выберите **On a Schedule**
2. Установите ежедневный запуск (например, каждый день в 8:00)
### Шаг 3: Поиск застойных возможностей
1. Добавьте действие **Search Records**
2. Настроить:
| Поле | Значение |
| ---------- | ------------------------------------------ |
| **Объект** | Возможности |
| **Фильтр** | Updated At раньше, чем (сегодня - 7 дней) |
| **Фильтр** | Stage не "Closed Won" AND не "Closed Lost" |
| **Лимит** | 100 |
### Шаг 4: Проверьте, есть ли найденные записи
1. Добавьте действие **Фильтр**
2. Условие: `{{searchRecords.length}}` больше 0
3. Если застойных возможностей нет, рабочий процесс останавливается здесь
### Шаг 5: Сформатируйте оповещение (действие Code)
Добавьте действие **Code** для форматирования письма:
```javascript
export const main = async (params) => {
const opportunities = params.opportunities;
// Group opportunities by owner
const byOwner = {};
opportunities.forEach(opp => {
const ownerEmail = opp.owner?.email || 'unassigned';
if (!byOwner[ownerEmail]) {
byOwner[ownerEmail] = [];
}
byOwner[ownerEmail].push({
name: opp.name,
amount: opp.amount,
lastUpdated: opp.updatedAt,
stage: opp.stage
});
});
// Format summary for manager
let summary = "Stale Opportunities Report\n\n";
Object.entries(byOwner).forEach(([owner, opps]) => {
summary += `${owner}: ${opps.length} stale opportunities\n`;
opps.forEach(opp => {
summary += ` - ${opp.name} (${opp.stage})\n`;
});
summary += "\n";
});
return {
summary,
totalCount: opportunities.length
};
};
```
### Шаг 6: Отправьте письмо с оповещением
Добавьте действие **Send Email**:
| Поле | Значение |
| --------- | ---------------------------------------------------------------- |
| **Кому** | sales-manager@yourcompany.com |
| **Тема** | `🚨 {{code.totalCount}} застойных возможностей требуют внимания` |
| **Текст** | `{{code.summary}}` |
### Шаг 7: Протестируйте и активируйте
1. Нажмите **Test** для запуска рабочего процесса
2. Проверьте, что письмо содержит корректные данные
3. Активируйте, когда будете готовы
## Варианты настройки
### Измените порог застоя
Измените фильтр Search Records, чтобы заменить 7 дней на предпочитаемый вами период:
* 3 дня для быстрых продаж
* 14 дней для корпоративных сделок
* 30 дней для длинных циклов продаж
### Оповещайте отдельных менеджеров по продажам
Вместо одного письма менеджеру используйте **Iterator** для отправки персонализированных писем каждому менеджеру по продажам о его застойных сделках.
### Добавьте эскалацию
Создайте несколько рабочих процессов с возрастающим уровнем серьезности:
1. День 7: письмо менеджеру по продажам
2. День 14: письмо менеджеру по продажам и руководителю
3. День 21: создать задачу для руководителя о необходимости вмешаться
### Добавьте в Slack
Используйте **HTTP Request**, чтобы отправлять сообщения в Slack через вебхук вместо электронной почты или в дополнение к ней.
## Связанные материалы
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)
* [Отправка писем из рабочих процессов](/l/ru/user-guide/workflows/capabilities/send-emails-from-workflows)

View File

@@ -0,0 +1,69 @@
---
title: Отображение количества полученных электронных писем
description: Создайте рабочий процесс для автоматического подсчета и отображения количества электронных писем, полученных от каждого контакта.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/workflow-number-of-emails-received.png" style={{width:'100%'}} />
## Обзор
Этот рабочий процесс срабатывает каждый раз при получении нового электронного письма и обновляет пользовательское поле в записи контакта значением общего количества писем от этого отправителя.
## Требования
Прежде чем настраивать этот рабочий процесс, создайте пользовательское поле в объекте **Контакты**:
1. Перейдите в **Настройки → Модель данных → Контакты**
2. Добавьте новое поле **Число**
3. Назовите его, например, "Количество писем, полученных от этого контакта"
## Пошаговая настройка
<VimeoEmbed videoId="1148700336" title="Видеодемонстрация" />
### Шаг 1: Настройте триггер
1. Перейдите в **Рабочие процессы** и создайте новый рабочий процесс
2. Выберите **Запись создана** в качестве триггера
3. Выберите **Участники сообщений** (доступно в разделе «Дополнительные объекты»)
<Note>
Участник сообщения — это комбинация идентификатора сообщения и идентификатора контакта, создающая одну уникальную запись на каждое сообщение. Это проще отслеживать, чем работать напрямую с объектом Messages, потому что мы можем получить доступ к полю `handle`, которое содержит адрес электронной почты отправителя (или получателя).
</Note>
### Шаг 2: Фильтр по роли
1. Добавьте действие **Фильтр**
2. Задайте условие: **Role** равно **FROM**
Это гарантирует, что вы считаете только сообщения, отправленные этим контактом, а не сообщения, отправленные ему.
### Шаг 3: Поиск всех участников сообщений с тем же handle
1. Добавьте действие **Search Records**
2. Выберите **Участники сообщений** в качестве объекта
3. Добавьте фильтры: **Handle** равен значению handle из триггера (адрес электронной почты отправителя) и **Role** равно **FROM**
4. Увеличьте **Limit** с 1 до **200** (максимум)
Это позволит найти все письма с этого адреса электронной почты, чтобы получить общее количество.
<Note>
Действие Search Records ограничено возвратом максимум 200 записей. Однако, поскольку вы используете только значение `totalCount` (а не отдельные записи), этот шаг вернет общее количество писем, отправленных этим контактом.
</Note>
### Шаг 4: Обновите запись контакта с помощью действия Create or Update Record
1. Добавьте действие **Create or Update Record**
2. Выберите **Контакты** в качестве объекта
3. Найдите контакт, сопоставив его адрес электронной почты со значением `handle` из участника сообщения
4. Установите значение вашего пользовательского поля "Количество полученных писем" равным `{{searchRecords.totalCount}}`
Значение `totalCount` из действия Search Records представляет общее количество писем, полученных от этого контакта.
## Связанные материалы
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)
* [Как создать пользовательские поля](/l/ru/user-guide/data-model/how-tos/customize-your-data-model)
* [Действие Search Records](/l/ru/user-guide/workflows/capabilities/workflow-actions#search-records)

View File

@@ -0,0 +1,170 @@
---
title: Отображение данных связанных записей
description: Показывайте данные из связанных записей (например, информацию о компании в сделках) с помощью рабочих процессов.
---
Отображайте данные из связанных записей прямо в ваших записях — например, показывайте численность сотрудников из компании в её сделках. Этот обходной путь с рабочим процессом полезен, пока вложенные поля не поддерживаются нативно.
## Типичные случаи использования
| Источник | Назначение | Поля для копирования |
| ----------- | ----------- | -------------------------------------------------------- |
| Компания | Возможность | Отрасль, размер компании, ARR |
| Контакт | Возможность | Email, телефон, должность |
| Возможность | Компания | Сумма последней сделки, дата последней выигранной сделки |
## Базовое копирование полей
### Пример: копирование Email контакта в сделку
**Цель**: При указании контактного лица в сделке скопируйте его email в сделку для быстрого доступа.
### Предварительное условие
Создайте целевые поля в **Settings → Data Model → Opportunities** перед созданием рабочего процесса:
* Email контакта (тип: Email)
* Телефон контакта (тип: Phone)
### Настройка
1. **Триггер**: Запись обновлена (Сделки, поле Контактное лицо)
2. **Фильтр**: Проверьте, что поле Контактное лицо не пустое
3. **Поиск записей**: найдите связанного контакта
* Объект: Контакты
* Фильтр: ID равен `{{trigger.object.pointOfContact.id}}`
4. **Обновление записи**:
* Объект: Сделки
* Запись: `{{trigger.object.id}}`
* Email контакта: `{{searchRecords[0].email}}`
* Телефон контакта: `{{searchRecords[0].phone}}`
## Копирование нескольких полей
### Пример: синхронизация данных компании со всеми связанными сделками
**Цель**: Когда данные компании меняются, обновляйте все связанные сделки.
### Настройка
1. **Триггер**: Запись обновлена (Компании)
* Поля: отрасль, размер компании, годовой доход
2. **Поиск записей**: найдите все сделки для этой компании
* Объект: Сделки
* Фильтр: ID компании равен `{{trigger.object.id}}`
3. **Итератор**: цикл по каждой сделке
4. **Обновление записи** (внутри итератора):
* Объект: Сделки
* Запись: `{{iterator.currentItem.id}}`
* Отрасль компании: `{{trigger.object.industry}}`
* Размер компании: `{{trigger.object.companySize}}`
* ARR компании: `{{trigger.object.annualRevenue}}`
## Копирование при создании записи
### Пример: предзаполнение сделки данными компании
**Цель**: При создании сделки, связанной с компанией, автоматически копируйте ключевую информацию о компании.
### Предварительное условие
Создайте целевые поля в **Settings → Data Model → Opportunities**:
* Отрасль компании (тип: Text)
* Размер компании (тип: Number)
### Настройка
1. **Триггер**: Запись создана (Сделки)
* Фильтр: поле Компания не пустое
2. **Поиск записей**: получите сведения о связанной компании
* Объект: Компании
* Фильтр: ID равен `{{trigger.object.company.id}}`
3. **Обновление записи**:
* Объект: Сделки
* Запись: `{{trigger.object.id}}`
* Отрасль компании: `{{searchRecords[0].industry}}`
* Размер компании: `{{searchRecords[0].employees}}`
<Note>
**Ограничение для задач и заметок**: связи в задачах и заметках жёстко заданы как многие-ко-многим и пока недоступны в триггерах или действиях рабочего процесса. Чтобы получить доступ к этим связям, вместо этого используйте [API](/l/ru/developers/extend/api).
</Note>
## Двунаправленная синхронизация
### Пример: синхронизация основного контакта
**Цель**: Когда в компании меняется основной контакт, обновляйте контакт. Когда контакт становится основным, обновляйте компанию.
### Рабочий процесс 1: Компания → Контакт
1. **Триггер**: Запись обновлена (Компании, поле Основной контакт)
2. **Обновление записи**: установите у контакта "Is Primary Contact" в значение true
3. **Поиск записей**: найдите предыдущего основного контакта
4. **Обновление записи**: установите у предыдущего контакта "Is Primary Contact" в значение false
### Рабочий процесс 2: Контакт → Компания
1. **Триггер**: Запись обновлена (Контакты, Is Primary Contact = true)
2. **Обновление записи**: установите у компании поле Primary Contact на этого контакта
<Note>
Осторожнее с двунаправленными синхронизациями, чтобы избежать бесконечных циклов. Используйте фильтры, чтобы проверять, действительно ли значение изменилось, прежде чем обновлять.
</Note>
## Использование кода для сложного сопоставления
### Пример: преобразование данных при копировании
**Цель**: Скопировать и отформатировать номер телефона из контакта в сделку.
```javascript
export const main = async (params) => {
const { phone } = params;
if (!phone) return { formattedPhone: null };
// Remove non-numeric characters
const digits = phone.replace(/\D/g, '');
// Format as (XXX) XXX-XXXX
const formatted = digits.length === 10
? `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`
: phone;
return { formattedPhone: formatted };
};
```
## Лучшие практики
### Избегайте циклов
* Не создавайте рабочие процессы, которые бесконечно запускают друг друга
* Используйте конкретные условия для полей
* Добавляйте проверки, что значение действительно изменилось
### Обработка отсутствующих данных
* Всегда проверяйте, что исходная запись существует, перед копированием
* Задавайте значения по умолчанию для необязательных полей
* Используйте фильтры, чтобы пропускать случаи, когда исходное поле пусто
### Производительность
* Выполняйте пакетные обновления при копировании во множество записей
* Используйте запланированные рабочие процессы для массовых операций синхронизации
* Рассмотрите использование Итератора для обновления множества записей
## Связанные материалы
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)
* [Триггеры рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-triggers)

View File

@@ -0,0 +1,202 @@
---
title: Формульные поля
description: Создавайте формульные поля с помощью рабочих процессов, пока не появится встроенная поддержка.
---
В Twenty пока нет собственной поддержки формульных полей (планируется в 2026), но того же результата можно добиться с помощью рабочих процессов. Это обходное решение позволяет автоматически вычислять и заполнять значения полей — от простых конкатенаций до сложной бизнес-логики.
## Типичные случаи использования
| Сценарий использования | Пример формулы |
| ---------------------- | ------------------------------------ |
| **Полное имя** | Имя + " " + Фамилия |
| **Ожидаемая сумма** | Сумма × Вероятность |
| **Дней до срока** | Срок - Сегодня |
| **Дней на этапе** | Сегодня - Дата входа в этап |
| **Оценка лида** | Баллы на основе нескольких критериев |
<Tip>
Полный пример отслеживания времени на этапах воронки см. в разделе [Как отслеживать, сколько времени возможности находятся на каждом этапе](/l/ru/user-guide/views-pipelines/how-tos/track-time-in-stage).
</Tip>
## Базовая формула: конкатенация
### Пример: автозаполнение полного имени
**Цель**: автоматически объединять имя и фамилию в поле полного имени.
### Настройка
1. **Триггер**: запись обновлена или создана (Люди)
2. **Фильтр**: проверьте, что изменились имя или фамилия
3. **Действие кода**:
```javascript
export const main = async (params) => {
const { firstName, lastName } = params;
const fullName = [firstName, lastName]
.filter(Boolean)
.join(' ');
return { fullName };
};
```
4. **Обновить запись**: установить Полное имя равным `{{code.fullName}}`
## Числовая формула: ожидаемая сумма
### Пример: рассчитать ожидаемую выручку
**Цель**: умножить сумму возможности на вероятность, чтобы получить ожидаемую сумму.
Полный рабочий процесс см. в разделе [Как показать ожидаемую сумму в воронке](/l/ru/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline).
### Настройка
1. **Триггер**: запись обновлена (Возможности, поле Сумма ИЛИ Вероятность)
2. **Действие кода**:
```javascript
export const main = async (params) => {
const { amount, probability } = params;
const expectedAmount = (amount || 0) * (probability || 0) / 100;
return { expectedAmount };
};
```
3. **Обновить запись**: установить Ожидаемую сумму равной `{{code.expectedAmount}}`
## Формула даты: вычисление дней
### Пример: дни до срока задачи
**Цель**: вычислить, сколько дней осталось до срока задачи.
### Настройка
1. **Триггер**: запись обновлена или создана (Задачи, поле Срок)
2. **Действие кода**:
```javascript
export const main = async (params) => {
const { dueDate } = params;
if (!dueDate) {
return { daysUntilDue: null };
}
const due = new Date(dueDate);
const today = new Date();
const diffTime = due - today;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return { daysUntilDue: diffDays };
};
```
3. **Обновить запись**: установить поле «Дни до срока» равным `{{code.daysUntilDue}}`
<Note>
Отрицательные значения означают просроченные задачи. Это поле можно использовать для фильтрации или сортировки задач по срочности.
</Note>
## Условная формула: оценка лида
### Пример: расчет оценки лида на основе критериев
**Цель**: оценивать лидов на основе размера компании, отрасли и вовлеченности.
### Настройка
1. **Триггер**: запись обновлена (Люди или Компании)
2. **Действие кода**:
```javascript
export const main = async (params) => {
const { companySize, industry, hasEmail, hasPhone, source } = params;
let score = 0;
// Company size scoring
if (companySize === 'Enterprise') score += 30;
else if (companySize === 'Mid-Market') score += 20;
else if (companySize === 'SMB') score += 10;
// Industry scoring
const targetIndustries = ['Technology', 'Finance', 'Healthcare'];
if (targetIndustries.includes(industry)) score += 25;
// Contact info scoring
if (hasEmail) score += 10;
if (hasPhone) score += 15;
// Source scoring
if (source === 'Referral') score += 20;
else if (source === 'Website') score += 10;
return { leadScore: score };
};
```
3. **Обновить запись**: установить Оценку лида равной `{{code.leadScore}}`
## Текстовая формула: извлечение домена
### Пример: извлечение домена из email
**Цель**: автоматически извлечь и сохранить домен email.
### Настройка
1. **Триггер**: запись обновлена (Люди, поле Email)
2. **Действие кода**:
```javascript
export const main = async (params) => {
const { email } = params;
if (!email) return { domain: null };
const domain = email.split('@')[1]?.toLowerCase();
return { domain };
};
```
3. **Обновить запись**: установить поле Домен равным `{{code.domain}}`
## Лучшие практики
### Производительность
* Запускайте только при изменении релевантных полей
* Используйте фильтры, чтобы пропускать записи, которым не требуется вычисление
* Избегайте сложных вычислений в высоконагруженных рабочих процессах
### Обработка ошибок
* Проверяйте значения null/undefined перед вычислениями
* Используйте значения по умолчанию при отсутствии данных
* Возвращайте понятные сообщения об ошибках при сбое вычислений
### Тестирование
* Тестируйте на крайних случаях (пустые поля, нулевые значения)
* Проверьте вычисления вручную перед активацией
* Отслеживайте выполнения рабочих процессов на предмет неожиданных результатов
## Связанные материалы
* [Как показать ожидаемую сумму в воронке](/l/ru/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline)
* [Как отслеживать время на этапе](/l/ru/user-guide/views-pipelines/how-tos/track-time-in-stage)
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)

View File

@@ -0,0 +1,127 @@
---
title: Уведомление коллег о заметке для проверки
description: Отправляйте автоматические уведомления по email при назначении коллеги на проверку заметки.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<Note>
**Прямое упоминание коллег в заметках пока не поддерживается.** Эта функция запланирована на вторую половину 2026 года. Тем временем этот рабочий процесс предоставляет практичный обходной путь.
</Note>
## Проблема
Вы хотите упомянуть коллегу в заметке, чтобы коллеге пришло уведомление о необходимости её проверить. На данный момент Twenty не поддерживает @упоминания в заметках или задачах.
## Обходной путь
Вместо упоминания в тексте заметки вы можете:
1. Создайте поле связи **Reviewer** в объекте «Заметки»
2. Назначайте проверяющего при создании или редактировании заметки
3. Привлечь рабочий процесс автоматически отправить письмо рецензенту со ссылкой на заметку
<Warning>
**Ограничение**: этот подход позволяет назначать по одному проверяющему на поле. Если нужно уведомить нескольких людей, создайте дополнительные поля связи (например, Reviewer 1, Reviewer 2). Это плохо масштабируется, если нужно упомянуть сразу многих участников.
</Warning>
## Пошаговая настройка
### Шаг 1: Создайте поле Reviewer
<img src="/images/user-guide/workflows/reviewer-field-for-note.png" style={{width:'100%'}} />
1. Перейдите в **Настройки → Модель данных → Заметки**
2. Нажмите **Добавить поле**
3. Настроить:
| Настройка | Значение |
| -------------------- | --------------------------------------------------- |
| **Название** | Reviewer |
| **Тип** | Связь |
| **Связанный объект** | Участник рабочего пространства |
| **Тип связи** | Много заметок → Один участник рабочего пространства |
4. Сохраните поле
### Шаг 2: Создайте рабочий процесс
<VimeoEmbed videoId="1151897168" title="Видеодемонстрация" />
#### Создайте триггер рабочего процесса
1. Перейдите в основной вид и создайте новый рабочий процесс
2. Выберите **Record Updated** в качестве триггера
3. Выберите **Note** в качестве объекта и укажите поле Reviewer.
4. Это сработает каждый раз, когда у заметки обновляется проверяющий.
#### Добавьте фильтр, чтобы проверить, что проверяющий только что назначен
Добавьте действие **Фильтр**, чтобы рабочий процесс продолжался только когда назначен проверяющий:
| Условие | Значение |
| ------------ | -------- |
| **Reviewer** | не пусто |
#### Найдите участника рабочего пространства
1. Добавьте действие **Search Records**
2. Выберите **Workspace Member** (в разделе расширенных объектов)
3. Добавьте фильтр:
| Условие | Значение |
| ------- | --------------------------- |
| **ID** | равно `{{note.reviewerId}}` |
Это извлечёт полную запись участника рабочего пространства, включая адрес email и имя.
#### Отправьте уведомление по email
1. Добавьте действие **Send Email**
2. Настроить:
| Поле | Значение |
| --------- | --------------------------------------------------- |
| **Кому** | Search Record → First Workspace Member → User Email |
| **Тема** | Заметка требует вашей проверки |
| **Текст** | См. пример ниже |
**Пример текста письма:**
```text
Hi {{searchRecord.firstWorkspaceMember.name.firstName}},
You've been assigned to review a note.
View the note here:
https://yourSubDomain.twenty.com/object/note/{{recordIsUpdated.id}}
Best,
Twenty
```
<Note>
Замените `yourSubDomain` на фактический поддомен рабочего пространства Twenty.
</Note>
#### Протестируйте и активируйте
1. Нажмите **Test**, чтобы запустить рабочий процесс вручную
2. Создайте или отредактируйте заметку и назначьте проверяющего
3. Проверьте почтовый ящик проверяющего на письмо-уведомление
4. Активируйте рабочий процесс
## Использование
После настройки ваша команда сможет использовать этот рабочий процесс, выполняя следующее:
1. Создание новой заметки (или редактирование существующей)
2. Выбор коллеги в поле **Reviewer**
3. Коллега автоматически получает письмо с прямой ссылкой на заметку
## Связанные материалы
* [Триггеры рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-triggers)
* [Отправка писем из рабочих процессов](/l/ru/user-guide/workflows/capabilities/send-emails-from-workflows)
* [Как создать пользовательские поля](/l/ru/user-guide/data-model/how-tos/create-custom-fields)
* [Создание полей связи](/l/ru/user-guide/data-model/how-tos/create-relation-fields)

View File

@@ -0,0 +1,106 @@
---
title: Отправка уведомлений по электронной почте о задачах со сроком выполнения
description: Автоматически уведомляйте членов команды об их предстоящих или просроченных задачах.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<img src="/images/user-guide/workflows/email_tasks_due.png" style={{width:'100%'}} />
Отправляйте ежедневные напоминания по электронной почте каждому члену команды о задачах, срок выполнения которых сегодня.
## Обзор
Этот рабочий процесс запускается по расписанию и:
1. Получает всех участников рабочего пространства
2. Последовательно обрабатывает каждого участника
3. Находит их задачи, срок выполнения которых сегодня
4. Форматирует и отправляет персонализированное письмо
## Пошаговая настройка
<VimeoEmbed videoId="1147463514" title="Видеодемонстрация" />
### Шаг 1: Настройте триггер
1. Перейдите в **Настройки → Рабочие процессы** и создайте новый рабочий процесс
2. Выберите **On a Schedule** в качестве триггера
3. Используйте выражение cron для ежедневного запуска в 8:00: `0 8 * * *`
### Шаг 2: Поиск всех участников рабочего пространства
1. Добавьте действие **Search Records**
2. Выберите **Workspace Members** (в разделе расширенных объектов)
3. Фильтры не требуются — это вернет всех участников
### Шаг 3: Добавьте итератор
1. Добавьте действие **Iterator**
2. Укажите в качестве входного массива участников рабочего пространства из предыдущего шага
3. Все действия внутри итератора выполнятся один раз для каждого участника
### Шаг 4: Поиск задач со сроком на сегодня (внутри итератора)
1. Внутри итератора добавьте действие **Search Records**
2. Выберите **Задачи** в качестве объекта
3. Добавьте фильтры:
* **Assignee** = текущий участник рабочего пространства (из итератора)
* **Due Date** = сегодня
### Шаг 5: Отформатируйте задачи в текст письма (внутри итератора)
Добавьте действие **Code**, чтобы отформатировать задачи в удобочитаемый список со ссылками:
```javascript
export const main = async (params: {
tasksDue?: Array<{ id: string; title: string }> | null | string;
}) => {
const tasksDue =
typeof params.tasksDue === "string"
? JSON.parse(params.tasksDue)
: params.tasksDue;
if (!Array.isArray(tasksDue) || tasksDue.length === 0) {
return {
formattedTasks: "No tasks due today."
};
}
const formattedTasks = tasksDue
.map(
t =>
`${t.title}\nhttps://yourSubDomain.twenty.com/object/task/${t.id}`
)
.join("\n\n");
return { formattedTasks };
};
```
<Note>
Замените `yourSubDomain` на фактический поддомен рабочего пространства Twenty.
</Note>
### Шаг 6: Отправьте письмо (внутри итератора)
1. Добавьте действие **Send Email** (по-прежнему внутри итератора)
2. Настроить:
| Поле | Значение |
| --------- | ---------------------------------------------------------------------------------------- |
| **Кому** | `{{iterator.currentItem.userEmail}}` (электронная почта участника рабочего пространства) |
| **Тема** | Ваши задачи на сегодня |
| **Текст** | `{{code.formattedTasks}}` |
### Шаг 7: Протестируйте и активируйте
1. Нажмите **Test**, чтобы запустить рабочий процесс вручную
2. Проверьте входящие на наличие писем
3. Активируйте рабочий процесс
## Связанные материалы
* [Действия рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-actions)
* [Отправка писем из рабочих процессов](/l/ru/user-guide/workflows/capabilities/send-emails-from-workflows)
* [Обработка массивов в действиях Code](/l/ru/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,21 @@
---
title: Профессиональные услуги
description: Получите профессиональную помощь в создании сложных рабочих процессов и автоматизации от команды Twenty и сертифицированных партнеров.
---
## Когда вам нужна профессиональная помощь?
Рассмотрите профессиональные услуги для:
* Сложные интеграции с несколькими системами
* Продвинутая бизнес-логика и правила автоматизации
* Рабочие процессы массовой обработки данных
* Разработка пользовательского API
* Обучение команды и оптимизация рабочих процессов
* Когда у вас нет внутренних ресурсов
## Варианты услуг
### Партнеры по внедрению
Работайте с сертифицированными партнерами для расширенных настроек. Свяжитесь с нами по адресу [contact@twenty.com](mailto:contact@twenty.com), чтобы связаться с нашими [партнерами по внедрению](https://twenty.com/partners).

View File

@@ -0,0 +1,170 @@
---
title: Устранение неполадок рабочего процесса
description: Распространённые проблемы с рабочими процессами и способы их решения.
---
## Распространенные проблемы и решения
### Рабочий процесс не запускается
**Симптомы**: Ваш рабочий процесс не запускается, когда вы этого ожидаете.
**Возможные причины**:
1. **Рабочий процесс не активирован**: Убедитесь, что для рабочего процесса установлен статус "Active", а не "Draft"
2. **Условия триггера не выполняются**: Убедитесь, что триггер соответствует ожидаемому событию
3. **Поле не отслеживается**: Для триггеров "Record is Updated" убедитесь, что нужное поле отслеживается
4. **Разрешения**: Проверьте, есть ли у вас разрешение на запуск рабочих процессов
**Решения**:
* Проверьте статус рабочего процесса в списке рабочих процессов
* Протестируйте с конкретным действием, которое должно его запустить
* Проверьте конфигурацию триггера
* Свяжитесь с администратором по поводу разрешений
### Триггер рабочего процесса срабатывает слишком рано (пустые поля)
**Симптомы**: При ручном создании записи в интерфейсе рабочий процесс срабатывает до того, как вы успеете заполнить все поля. Рабочий процесс запускается с преимущественно пустыми значениями полей.
**Почему это происходит**: Twenty сохраняет всё в режиме реального времени — нет раздельных режимов "edit" и "read". Когда вы создаёте запись, она сохраняется сразу, что вызывает событие "Record is created" до того, как вы успеете заполнить дополнительные поля.
**Когда "Record is created" работает хорошо**:
* Записи, созданные через вызовы API (поля заполняются одним запросом)
* Записи, созданные через импорт
* Автоматическое создание записей из других рабочих процессов
**Решение**: Для записей, созданных вручную в интерфейсе, используйте в качестве триггера **"Record is created or updated"**. Таким образом:
* Рабочий процесс запускается после того, как пользователь завершил заполнение и сохранение полей
* Вы получаете полные данные, а не пустые значения
<Tip>
Если вы хотите, чтобы рабочий процесс запускался только один раз для каждой записи, добавьте действие Filter, чтобы проверить поле, например `createdAt equals updatedAt` (первое сохранение), или используйте пользовательское поле-флажок, чтобы отслеживать, запускался ли рабочий процесс ранее.
</Tip>
### Сбои действий
**Симптомы**: Рабочий процесс запускается, но некоторые действия завершаются с ошибкой.
**Возможные причины**:
1. **Отсутствующие данные**: Обязательные поля пустые
2. **Недопустимые ссылки**: Переменные из предыдущих шагов отсутствуют
3. **Ошибки API**: Внешние сервисы возвращают ошибки
4. **Проблемы с разрешениями**: Действие требует разрешений, которых у вас нет
**Решения**:
* Проверьте сведения о запуске рабочего процесса на наличие сообщений об ошибках
* Убедитесь, что все обязательные поля заполнены
* Протестируйте соединения с API отдельно
* Проверьте разрешения ролей
### Ошибки HTTP-запросов
**Симптомы**: Действия HTTP Request завершаются с ошибкой или возвращают неожиданные результаты.
**Распространённые коды ошибок**:
* **400**: Некорректный запрос — проверьте формат тела запроса
* **401**: Неавторизовано — проверьте ключ API
* **403**: Запрещено — проверьте разрешения API
* **404**: Не найдено — проверьте URL конечной точки
* **429**: Слишком много запросов — реализуйте ограничение частоты
* **500**: Ошибка сервера — проблема внешнего сервиса
**Решения**:
* Проверьте URL конечной точки API
* Проверьте заголовки аутентификации
* Сначала протестируйте вызов API вне Twenty
* Добавьте обработку ошибок в действиях Code
### Ошибки действий Code
**Симптомы**: Код JavaScript не выполняется.
**Распространённые проблемы**:
1. **Синтаксические ошибки**: Опечатки или недопустимый JavaScript
2. **Неопределённые переменные**: Обращение к переменным, которые не существуют
3. **Ошибки типов**: Операции с некорректными типами данных
4. **Тайм-ауты**: Код выполняется слишком долго
**Решения**:
* Используйте проверку в встроенном редакторе кода
* Сначала протестируйте логику кода в консоли JavaScript
* Добавьте операторы console.log для отладки
* Упростите сложные операции
### Письма не отправляются
**Симптомы**: Действие Send Email не доставляет письма.
**Возможные причины**:
1. **Не подключена учетная запись электронной почты**: Проверьте Settings → Accounts
2. **Недопустимый адрес электронной почты**: Адрес электронной почты получателя имеет неверный формат
3. **Ограничения на отправку**: Достигнуты лимиты скорости у провайдера электронной почты
4. **Спам-фильтры**: Письма блокируются
**Решения**:
* Проверьте подключение учетной записи электронной почты
* Проверьте адреса электронной почты получателей
* Проверьте лимиты провайдера электронной почты
* Проверьте содержимое письма на наличие признаков спама
## Отладка рабочих процессов
### Использование запусков рабочего процесса
1. Перейдите в редактор рабочего процесса
2. Откройте панель **Runs**
3. Найдите запуск со сбоем
4. Нажмите, чтобы увидеть подробности по шагам
5. Просмотрите сообщения об ошибках и выходные данные
### Тестирование отдельных шагов
1. Для действий Code используйте кнопку **Test**
2. Для HTTP-запросов протестируйте конечную точку отдельно
3. Создайте тестовые записи, чтобы запускать рабочие процессы
4. Используйте ручные триггеры для контролируемого тестирования
### Распространённые шаблоны отладки
**Добавьте логирование**:
Используйте действия Code, чтобы записывать промежуточные значения для отладки.
**Изолируйте шаги**:
Тестируйте каждый шаг отдельно, чтобы выявить сбои.
**Проверьте поток данных**:
Убедитесь, что каждый шаг получает ожидаемые входные данные.
## Рекомендации по предотвращению проблем
### Перед активацией
* Тщательно протестируйте в режиме черновика
* Проверьте все подключения к API
* Внимательно проверьте условия триггера
* Задокументируйте ожидаемое поведение
### Во время разработки
* Используйте понятные названия шагов
* Добавьте комментарии в действиях Code
* Тестируйте с реалистичными данными
* Планируйте обработку крайних случаев
### После активации
* Внимательно отслеживайте первые запуски
* Настройте оповещения о сбоях
* Регулярно просматривайте историю запусков
* По возможности делайте рабочие процессы простыми

View File

@@ -0,0 +1,236 @@
---
title: Часто задаваемые вопросы о рабочих процессах
description: Часто задаваемые вопросы о рабочих процессах в Twenty.
---
<AccordionGroup>
<Accordion title="Почему я не могу активировать рабочий процесс?">
Скорее всего, это проблема с правами доступа. Вам нужен доступ к рабочим процессам, чтобы создавать и активировать их.
**Решение**: Свяжитесь с администратором рабочего пространства, чтобы он предоставил вам доступ к рабочим процессам в разделе **Настройки → Участники → Роли**.
Если вы совсем не видите раздел "Рабочие процессы" в боковой панели, это подтверждает, что проблема в правах доступа.
</Accordion>
<Accordion title="Почему я не вижу кнопку ручного рабочего процесса в верхней панели навигации?">
Ручные рабочие процессы отображаются в панели навигации только при правильной настройке:
1. Рабочий процесс должен быть **активирован** (не в режиме черновика)
2. Размещение в панели навигации должно быть установлено в значение **Pinned**
3. Для триггеров Single/Bulk вы должны находиться на правильной странице объекта
**Как проверить**: Откройте рабочий процесс → нажмите на триггер → убедитесь, что "Navbar placement" установлено в значение "Pinned".
Вы всегда можете получить доступ к ручным рабочим процессам через **Cmd + K** (или **Ctrl + K**) независимо от настроек панели навигации.
</Accordion>
<Accordion title="В чем разница между ручными триггерами Single, Global и Bulk?">
| Тип | Требуемые записи | Запуски рабочего процесса |
| --- | ---------------- | ------------------------- |
\| **Global** | Нет | Один раз, без входной записи |
\| **Single** | Одна или несколько выбранных | По одному разу для каждой выбранной записи |
\| **Bulk** | Одна или несколько выбранных | Один раз, со всеми записями в виде массива |
* **Global**: Используйте, когда рабочему процессу не нужен контекст записи (например, для создания отчета)
* **Single**: Используйте, когда нужно обрабатывать каждую выбранную запись независимо (например, отправлять отдельные письма)
* **Bulk**: Используйте, когда нужно обрабатывать записи вместе или оптимизировать использование кредитов (требуется действие Iterator)
См. [Триггеры рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-triggers) для подробностей.
</Accordion>
<Accordion title="Я не вижу узла If/Else. Что мне делать?">
Явный узел If/Else пока недоступен, но находится в нашем плане развития.
**Текущее решение**: Создайте несколько веток от вашего шага, каждая начинается с действия **Filter**:
```
Step 1
│
├── Branch A: Filter (condition = true) → Actions...
│
└── Branch B: Filter (condition = false) → Actions...
```
Только ветка, в которой условие фильтра выполняется, будет выполнять последующие действия.
См. пошаговое руководство [Как использовать ветки](/l/ru/user-guide/workflows/capabilities/workflow-branches).
</Accordion>
<Accordion title="Ветки рабочего процесса выполняются параллельно?">
**Да**, ветки по умолчанию выполняются параллельно.
Если вы хотите, чтобы выполнялась только одна ветка:
* Добавьте действие **Filter** в начало каждой ветки
* Задайте противоположные условия (например, Ветка A: status = "Open", Ветка B: status ≠ "Open")
Ветки, не прошедшие свое условие фильтра, прекращают выполнение, остальные продолжают.
</Accordion>
<Accordion title="Можно ли объединить ветки обратно?">
**Да**. После завершения параллельных веток вы можете добавить шаг, к которому будут подключены обе ветки.
В редакторе рабочего процесса:
1. Завершите действия в ветках
2. Добавьте новый шаг после веток
3. Перетащите соединения с конца каждой ветки к этому новому шагу
Объединенный шаг выполнится после завершения всех подключенных веток.
</Accordion>
<Accordion title="Каково ограничение на результаты Search Records?">
**Search Records возвращает максимум 200 записей.**
Если нужно обработать больше:
* Добавьте более точные фильтры, чтобы сократить результаты
* Используйте запланированные рабочие процессы для пакетной обработки
* Рассмотрите использование API для массовых операций
Для большинства рабочих процессов 200 записей достаточно. Если вы регулярно упираетесь в этот предел, подумайте о переработке вашей автоматизации.
</Accordion>
<Accordion title="Можно ли добавлять получателей в копию (CC) в письмах рабочего процесса?">
**Пока нет.** Поля CC и BCC для действия Send Email в нашем плане развития.
**Текущее решение**: Добавьте несколько действий Send Email, чтобы отправить дополнительным получателям, или используйте HTTP Request для отправки через внешний почтовый сервис, поддерживающий CC.
</Accordion>
<Accordion title="Как использовать выходные данные из предыдущих шагов?">
Каждое действие формирует выходные данные, которые можно использовать в последующих шагах.
**Как ссылаться на данные предыдущих шагов**:
* Используйте выбор переменных при настройке поля
* Или введите `{{stepName.fieldName}}` напрямую
**Примеры**:
* Данные триггера: `{{trigger.object.email}}`
* Результаты поиска: `{{searchRecords[0].name}}`
* Вывод кода: `{{code.calculatedValue}}`
Наведите курсор на любое поле в настройках действия, чтобы увидеть доступные переменные из предыдущих шагов.
</Accordion>
<Accordion title="Почему Iterator завершается с ошибкой?">
**Iterator требует массив на входе.** Частые проблемы:
1. **Входные данные не являются массивом**: Убедитесь, что вы передаете результаты из Search Records или другого действия, возвращающего массив
2. **Массив пуст**: Добавьте фильтр перед Iterator, чтобы проверить `{{searchRecords.length}} > 0`
3. **Выбрана неверная переменная**: Убедитесь, что вы выбираете сам массив, а не одну запись
**Правильная настройка**:
1. Search Records (возвращает массив)
2. Filter: length > 0
3. Iterator: выберите `{{searchRecords}}`
4. Действия внутри Iterator используют `{{iterator.currentItem.fieldName}}`
</Accordion>
<Accordion title="Каково максимальное время выполнения для действий Code?">
Действия Code (функции бизнес-логики) имеют **тайм-аут по умолчанию в 5 минут** (300 секунд).
Максимально настраиваемый тайм-аут — **15 минут** (900 секунд).
Если ваш код превышает этот предел, действие завершится ошибкой тайм-аута.
**Советы по предотвращению тайм-аутов**:
* Разбейте большие операции на небольшие части с помощью Iterator
* Избегайте тяжелых вычислений; используйте внешние сервисы через HTTP Request для ресурсоемкой обработки
* Оптимизируйте ваш код, чтобы сократить время выполнения
* Если требуется более длительная обработка, рассмотрите использование запланированных рабочих процессов, которые обрабатывают данные пакетами
</Accordion>
<Accordion title="Как понимать запуски рабочего процесса?">
Запуски рабочего процесса показывают историю выполнения и помогают диагностировать проблемы.
**Доступ к запускам**:
* В редакторе рабочего процесса → панель **Запуски** справа
* Или перейдите в **Запуски рабочего процесса** в боковой панели
**Понимание запуска**:
* **Статус**: Running, Completed, Failed, Waiting
* **Шаги**: Просматривайте, какие шаги выполнились и их вывод
* **Ошибки**: Нажмите на шаги с ошибкой, чтобы увидеть сообщения об ошибках
* **Данные**: Просматривайте входные/выходные данные на каждом шаге
См. [Запуски рабочего процесса](/l/ru/user-guide/workflows/capabilities/workflow-runs) для подробностей.
</Accordion>
<Accordion title="Почему я не вижу свои запуски рабочего процесса?">
Запуски рабочего процесса могут сразу завершаться с ошибкой из-за ограничений скорости. Twenty устанавливает жесткий лимит в 5 000 запусков в час на рабочее пространство.
Если вы превышаете этот лимит, рабочие процессы сразу помечаются как завершившиеся с ошибкой и не появляются в вашем списке запусков.
**Распространенные сценарии, при которых достигается этот лимит**:
* Выбор более 5 000 записей с ручным триггером Single
* Одновременная работа нескольких рабочих процессов в вашем рабочем пространстве
* Высокочастотные автоматические триггеры (например, Record Updated на загруженном объекте)
**Решения**:
* Используйте триггеры **Bulk** вместо Single, чтобы обработать много записей за один запуск
* Разнесите во времени крупные пакетные операции
* Используйте фильтры, чтобы снизить частоту срабатываний
* Запускайте «тяжелые» рабочие процессы в часы низкой нагрузки
</Accordion>
<Accordion title="Каковы ограничения скорости для рабочих процессов?">
В Twenty есть два лимита скорости для обеспечения стабильности системы:
| Лимит | Значение | Поведение |
| ----- | -------- | --------- |
\| **Мягкий лимит** | 100 запусков/мин | Запуски ставятся в очередь со статусом "Not Started", обрабатываются постепенно |
\| **Жесткий лимит** | 5 000 запусков/час | Запуски сразу завершаются с ошибкой |
**Мягкий лимит (100/мин)**: Ваши рабочие процессы не будут завершаться с ошибкой — они просто ждут в очереди и обрабатываются со временем. Вы можете запускать более 100 записей; выполнение будет медленнее.
**Жесткий лимит (5 000/ч)**: Он применяется ко всему вашему рабочему пространству. Если суммарно все ваши рабочие процессы превышают 5 000 запусков за час, дополнительные запуски сразу завершатся с ошибкой.
**Советы, чтобы не выходить за пределы лимитов**:
* Используйте триггеры Bulk с Iterator вместо Single для больших пакетов
* Объединяйте связанные автоматизации в меньшее количество рабочих процессов
* Используйте запланированные рабочие процессы, чтобы распределить нагрузку во времени
</Accordion>
<Accordion title="Можно ли повторно запустить неудавшийся запуск рабочего процесса?">
**Нет, в настоящий момент нет функции автоматического повтора.**
Если запуск рабочего процесса завершился с ошибкой, вам нужно:
1. Просмотрите ошибку в **Настройки → Рабочие процессы → [Ваш рабочий процесс] → Запуски**
2. Исправьте проблему (данные, конфигурация или внешний сервис)
3. Повторно запустите рабочий процесс вручную для затронутой записи (записей)
**Советы по снижению количества ошибок**:
* Добавьте узлы **Filter** для проверки данных перед действиями
* Используйте **Search Records**, чтобы проверить, существуют ли связанные записи
* Тщательно протестируйте на нескольких записях перед массовыми операциями
Функция автоматического повтора находится в нашем плане развития для будущего релиза.
</Accordion>
<Accordion title="Нужно ли деактивировать рабочие процессы перед импортом CSV?">
**Да, если ваши рабочие процессы запускаются при создании или обновлении записей.**
При импорте данных через CSV каждая созданная или обновленная запись может запускать рабочие процессы. Крупный импорт (тысячи записей) может:
* Достигнуть лимита 5 000 запусков/час
* Потреблять значительное количество кредитов рабочего процесса
* Отправить неожиданные письма или уведомления
* Создать дублирующиеся задачи или записи
**Перед массовым импортом**:
1. Перейдите в **Настройки → Рабочие процессы**
2. Определите рабочие процессы, запускаемые объектом, который вы импортируете
3. **Деактивируйте** их временно
4. Выполните импорт CSV
5. **Повторно активируйте** рабочие процессы по завершении
**Альтернатива**: Если вам нужно, чтобы рабочие процессы запускались на импортируемых данных, импортируйте небольшими партиями, чтобы укладываться в лимиты скорости.
</Accordion>
<Accordion title="Как привести в порядок схему моего рабочего процесса?">
Если ваше полотно рабочего процесса выглядит беспорядочно, с разбросанными узлами, вы можете автоматически упорядочить его:
1. Щелкните правой кнопкой мыши в любом месте полотна рабочего процесса
2. Нажмите **Упорядочить рабочий процесс**
Это автоматически переупорядочит все узлы в чистое, упорядоченное расположение.
</Accordion>
</AccordionGroup>