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으로 전달되는 경우가 자주 있습니다. 다음과 같은 경우에 발생합니다:
* HTTP Request로 외부 API에서 데이터를 받을 때
* 웹훅 페이로드를 처리할 때
* 워크플로 단계 간에 데이터를 전달할 때
**해결책**: 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>
**배열에서 필드를 추출하는 대신 배열을 순회(loop)하고 싶으신가요?** Code 또는 Logic Function 단계가 최상위 배열을 반환하는 경우, 해당 결과를 바로 [Iterator](/l/ko/user-guide/workflows/capabilities/use-iterator)에 전달할 수 있습니다. 해당 단계의 **Whole list** 옵션을 Iterator의 입력으로 선택한 다음, 루프 안에서 각 요소를 `{{iterator.currentItem}}`으로 참조하면 됩니다. 그 경우에는 배열을 객체로 변환할 필요가 없습니다.
</Tip>
<Tip>
코드 편집기 오른쪽 상단의 사각형 아이콘을 클릭하여 전체 화면으로 표시하세요 — 기본 편집기 너비가 제한되어 있어 유용합니다.
</Tip>

View File

@@ -0,0 +1,182 @@
---
title: Twenty로 제품 데이터 가져오기
description: 일정에 따라 데이터 웨어하우스의 제품 카탈로그 데이터를 CRM으로 동기화하세요.
---
이 패턴을 사용하여 데이터 웨어하우스(예: Snowflake, BigQuery, PostgreSQL)의 제품 데이터와 Twenty를 동기화 상태로 유지하세요.
## 워크플로우 구조
1. **트리거**: 일정에 따라
2. **코드**: 데이터 웨어하우스에 질의
3. **코드**(선택): 데이터를 배열로 형식 지정
4. **Iterator**: 각 제품을 순회
5. **레코드 업서트**: Twenty에서 생성 또는 업데이트
<img src="/images/user-guide/workflows/create_users_workflow.png" style={{width:'100%'}} />
## 단계 1: 트리거 일정 설정
데이터 최신성 요구에 맞는 빈도로 워크플로우가 실행되도록 설정하세요:
* 거의 실시간 동기화를 위해 5분마다
* 중요도가 낮은 데이터의 경우 매시간
* 배치 업데이트의 경우 매일
## 단계 2: 데이터 웨어하우스 질의
최근 데이터를 가져오기 위해 **코드** 액션을 추가합니다:
```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: 데이터 형식 지정(선택)
웨어하우스가 변환이 필요한 형식으로 데이터를 반환하는 경우, **코드** 액션을 하나 더 추가하세요. 일반적인 변환에는 타입 변환, 필드 이름 변경, 데이터 정리가 포함됩니다.
### 예시: 불리언 및 상태 필드를 포함한 사용자 데이터
```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단계: 각 레코드를 업서트하기
이터레이터 내부에 **레코드 업서트** 액션을 추가합니다:
| 설정 | 값 |
| --------- | -------------------------- |
| **객체** | 사용자 지정 제품 객체 |
| **일치 기준** | External ID 또는 SKU(고유 식별자) |
| **Name** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **Price** | `{{iterator.item.price}}` |
<Tip>
생성과 업데이트를 위한 분기 흐름을 따로 만들기보다 **Upsert**(업데이트 또는 생성)를 사용하세요. 구현이 더 빠르고 디버깅이 더 쉽습니다.
</Tip>
## 사용 예시
| 소스 | 데이터 |
| ------------- | --------------- |
| **ERP 시스템** | 제품 카탈로그, 가격, 재고 |
| **전자상거래 플랫폼** | 주문, 고객, 제품 업데이트 |
| **데이터 웨어하우스** | 집계 지표, 보강된 데이터 |
| **재고 시스템** | 재고 수준, 재주문 알림 |
## 관련 항목
* [워크플로우 트리거](/l/ko/user-guide/workflows/capabilities/workflow-triggers)
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)
* [Code 액션에서 배열 처리하기](/l/ko/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,130 @@
---
title: Typeform 응답을 Twenty로 가져오기
description: Typeform의 Webhook 페이로드를 처리하여 폼 제출에서 리드를 생성합니다.
---
표준 Webhook 설정은 [Webhook 트리거 설정](/l/ko/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)을 참조하세요. 이 문서는 Typeform의 맞춤형 페이로드 구조에 필요한 특정 처리 방법을 다룹니다.
### 1단계: Webhook 워크플로우 생성
1. **설정 → 워크플로우**로 이동
2. **+ 새 워크플로우**를 클릭
3. 트리거로 **Webhook**을 선택합니다
4. Webhook URL을 복사합니다
### 2단계: Typeform 구성
1. Typeform에서 폼을 엽니다
2. **Connect → Webhooks**로 이동
3. Twenty Webhook URL을 붙여넣습니다
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 액션에서 배열 처리하기](/l/ko/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. 워크플로우를 활성화합니다
## 관련 항목
* [Webhook 트리거 설정](/l/ko/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Code 액션에서 배열 처리하기](/l/ko/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)

View File

@@ -0,0 +1,263 @@
---
title: Twenty에서 PDF 생성
description: "레코드에 PDF(예: 견적서)를 생성해 첨부하는 워크플로우를 만드세요."
---
Twenty에서 PDF를 자동으로 생성하거나 가져와 레코드에 첨부합니다. 이는 Company, Opportunity 또는 기타 객체에 연결된 견적서, 송장, 보고서를 생성할 때 흔히 사용됩니다.
## 개요
이 워크플로우는 **수동 트리거**를 사용하므로 사용자가 선택한 레코드에 대해 필요할 때 PDF를 생성할 수 있습니다. **로직 함수**가 다음을 처리합니다:
1. URL에서 PDF를 다운로드(PDF 생성 서비스에서 제공)
2. 파일을 Twenty에 업로드
3. 레코드에 연결된 첨부 파일 생성
## 사전 준비
워크플로우를 설정하기 전에:
1. **API 키 생성**: **Settings → APIs**로 이동하여 새 API 키를 만드세요. 이 토큰은 로직 함수에 필요합니다.
2. **PDF 생성 서비스 설정**(선택 사항): PDF를 동적으로 생성하려면(예: 견적서) Carbone, PDFMonkey, DocuSeal 같은 서비스를 사용해 PDF를 만들고 다운로드 URL을 받으세요.
## 단계별 설정
### 1단계: 트리거 구성
1. **워크플로우**로 이동해 새 워크플로우를 생성합니다
2. **수동 트리거**를 선택합니다
3. PDF를 첨부할 객체를 선택하세요(예: **Company** 또는 **Opportunity**).
<Tip>
수동 트리거를 사용하면 사용자가 레코드를 선택한 후 우측 상단에 표시되는 버튼으로 이 워크플로우를 실행해 PDF를 생성하고 첨부할 수 있습니다.
</Tip>
### 2단계: 로직 함수 추가
1. **Code** 액션(로직 함수)을 추가합니다
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` |
함수 매개변수와 첨부 생성 mutation의 `variables.data` 객체를 모두 업데이트하세요.
#### 동적 PDF URL을 사용하려면
PDF 생성 서비스를 사용하는 경우 다음을 수행할 수 있습니다:
1. 먼저 PDF를 생성하기 위해 HTTP 요청 작업을 만듭니다
2. 반환된 PDF URL을 로직 함수의 매개변수로 전달합니다
```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 | 레코드 검색 | Opportunity 또는 라인 아이템 세부정보 가져오기 |
| 3 | HTTP 요청 | 레코드 데이터를 사용해 PDF 생성 API 호출 |
| 4 | 로직 함수 | 생성된 PDF를 다운로드해 첨부 |
### 널리 사용되는 PDF 생성 서비스
* **Carbone** - 템플릿 기반 문서 생성
* **PDFMonkey** - 템플릿 기반 동적 PDF 생성
* **DocuSeal** - 문서 자동화 플랫폼
* **Documint** - API 우선 문서 생성
각 서비스는 PDF URL을 반환하는 API를 제공하며, 이 URL을 로직 함수에 전달할 수 있습니다.
## 문제 해결
| 문제 | 해결책 |
| ------------- | --------------------------------------- |
| "PDF 다운로드 실패" | PDF URL에 접근할 수 있으며 유효한 PDF를 반환하는지 확인하세요 |
| "업로드 실패" | API 키가 유효하고 쓰기 권한이 있는지 확인하세요 |
| "첨부 파일 생성 실패" | 객체 ID 필드 이름이 대상 객체와 일치하는지 확인하세요 |
## 관련 항목
* [워크플로우 트리거](/l/ko/user-guide/workflows/capabilities/workflow-triggers)
* [로직 함수](/l/ko/user-guide/workflows/capabilities/workflow-actions#code)
* [Twenty에서 견적서 또는 송장 생성](/l/ko/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. **필터**: Stage = Closed Won
3. **레코드 검색**: 회사 세부정보 가져오기
4. **코드**(선택): 페이로드 형식 지정
5. **HTTP 요청**: 인보이스 시스템으로 전송
## 단계 1: 트리거 설정
1. 새 워크플로우 만들기
2. **레코드가 업데이트됨** 트리거 선택
3. 객체로 **기회(Opportunity)** 선택
## 단계 2: Closed Won 필터링
거래가 성사된 경우에만 계속되도록 **필터** 작업을 추가합니다:
| 설정 | 값 |
| ------ | ------------------------------- |
| **필드** | 단계 |
| **조건** | 같음 |
| **값** | `CLOSED_WON` (또는 사용 중인 스테이지 이름) |
<Tip>
트리거는 모든 기회 업데이트에서 실행됩니다. 필터는 스테이지가 Closed Won으로 변경될 때만 워크플로우가 계속 진행되도록 보장합니다.
</Tip>
## 단계 3: 회사 세부정보 가져오기
기회 레코드에는 인보이스에 필요한 모든 회사 필드가 포함되지 않을 수 있습니다. **레코드 검색** 작업을 추가합니다:
| 설정 | 값 |
| --------- | -------------------------------------- |
| **객체** | 회사 |
| **일치 기준** | ID가 `{{trigger.object.companyId}}`와 같음 |
청구지 주소, 세금 ID 등과 함께 전체 회사 레코드를 가져옵니다.
## 단계 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 | 필터 | Stage = Closed Won인 경우에만 진행 |
| 3 | 레코드 검색 | 청구를 위한 전체 회사 세부정보 가져오기 |
| 4 | 코드 | 인보이스 API용 데이터 형식 지정 |
| 5 | HTTP 요청 | 외부 시스템에서 인보이스 생성 |
## 팁
* **외부 ID 저장**: **레코드 업데이트** 작업을 사용하여 API가 반환한 인보이스 ID를 기회에 다시 저장합니다
* **오류 처리**: HTTP 요청이 실패하면 알림을 보내는 분기 추가
* **먼저 테스트**: 실제 운영 전에 인보이스 시스템의 샌드박스/테스트 모드를 사용하세요
## 관련 항목
* [Twenty에서 PDF 생성](/l/ko/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — 생성된 PDF를 레코드에 첨부
* [워크플로우 트리거](/l/ko/user-guide/workflows/capabilities/workflow-triggers)
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)
* [Closed Won 자동화](/l/ko/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단계: 액션 추가
이제 웹훅 데이터를 사용하는 액션을 추가하세요:
**예시: 사람 레코드 생성**
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. 웹훅 URL로 POST 요청을 생성
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은 항상 "first name", 위치 1은 항상 "last name"인 폼 답변), 이를 추출하기 위해 **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/ko/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)를 참조하세요.
</Tip>
## 관련 항목
* [워크플로우 트리거](/l/ko/user-guide/workflows/capabilities/workflow-triggers)
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)

View File

@@ -0,0 +1,121 @@
---
title: 수신 이메일 자동 회신
description: AI를 사용해 수신 이메일을 분류하고 스레드형 답장을 자동으로 보내는 워크플로를 구축하세요.
---
수신 이메일에 몇 시간이 아니라 몇 초 만에 응답하세요. 이 워크플로는 AI 에이전트를 사용해 잡음(뉴스레터, 스팸, 자동 회신)을 걸러내고 실제 메시지에는 개인화된 응답 초안을 작성합니다. 그런 다음 AI가 원본 대화 내에서 스레드형 답장으로 이를 전송합니다.
## 이메일 스레드 작동 방식
모든 이메일에는 숨겨진 `Message-ID` 헤더가 있으며, 이는 발신자의 메일 서버가 부여하는 고유한 식별자입니다. 이메일에 회신할 때, 메일 클라이언트는 해당 지문을 참조하는 `In-Reply-To` 헤더를 설정합니다. Gmail, Outlook 및 기타 모든 클라이언트가 메시지를 스레드로 묶는 방식입니다.
Twenty에서는 그 지문이 Message 객체의 `headerMessageId`로 저장됩니다. 워크플로우가 이를 가져와 Send Email 작업의 In-Reply-To 필드에 전달합니다.
## 워크플로우 구성
### 1단계: 새 워크플로우 생성
**설정 -> 워크플로우**로 이동한 다음 **+ 새 워크플로우**를 클릭하세요.
### 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로 분류하고 응답 초안 작성
**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단계: AI 결정에 따라 분기
**If/Else** 작업을 추가하여 AI가 회신할지 건너뛸지를 확인합니다.
| 필드 | 값 |
| ----------- | ---------------------------------------- |
| **조건** | AI Agent `response`에 `SKIP`이 **포함되지 않음** |
| **참인 경우** | Send Email로 계속 |
| **그렇지 않으면** | 아무것도 하지 않음(워크플로우 종료) |
스팸, 뉴스레터, 자동 생성된 메시지는 제외됩니다. 그 외 모든 항목은 다음 단계로 이동합니다.
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
### 6단계: 스레드형 회신 보내기
"if true" 분기에서 **Send Email** 작업을 추가합니다. **Advanced options**를 클릭한 다음 **Add In-Reply-To**를 클릭합니다.
| 필드 | 값 |
| --------------- | -------------------------------------- |
| **받는 사람** | `{{Find Sender.first.handle}}` |
| **제목** | `Re: {{trigger.subject}}` |
| **Body** | `{{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은 제목 줄을 사용하여 메시지를 스레드로 그룹화합니다. Gmail이 원래 스레드 내에 회신을 표시하려면 제목이 `Re:`(콜론과 공백 포함)로 시작해야 합니다. 이 표시가 없으면, In-Reply-To 헤더가 올바르게 설정되어 있어도 회신은 별도의 대화로 표시됩니다.
</Warning>
### 7단계: 테스트 및 활성화
**Test**를 클릭한 다음 메일 클라이언트를 확인하세요. 회신은 원본 메시지 아래에 스레드로 표시되어야 합니다.
만족스러우면 활성화하세요.
## 확장 아이디어
* **VIP에게만 회신** — 발신자의 도메인을 확인하거나 Twenty의 연락처로 존재하는지 확인하는 분기를 추가하세요
* **의도별 라우팅** — 영업 문의와 지원 요청을 다르게 처리하도록 별도의 AI Agent 프롬프트를 사용하세요
* **회신 전 정보 보강** — 보다 개인화된 회신을 위해 발신자의 회사나 거래 이력을 AI 프롬프트에 포함하도록 Search Records 단계를 추가하세요

View File

@@ -0,0 +1,179 @@
---
title: Closed Won 자동화
description: 기회가 성사되면 사후 작업을 자동화합니다.
---
거래가 성사되면 여러 작업이 필요합니다: 회사 상태 업데이트, 팀 구성원 알림, 온보딩 작업 생성. 워크플로우 하나로 이 모든 것을 자동화하세요.
## 문제
기회가 "Closed Won" 단계로 이동하면:
* 회사 유형을 "Prospect"에서 "Customer"로 변경해야 합니다
* 온보딩 작업을 생성해야 합니다
* Customer Success 팀에 알림을 보내야 합니다
* 영업 담당자에게 확인을 보내야 합니다
이 작업을 수동으로 하면 시간도 많이 들고 오류가 발생하기 쉽습니다.
## 해결책
성사 후 모든 활동을 자동으로 처리하는 워크플로우를 만드세요.
## 전체 워크플로우 설정
### 1단계: 워크플로우 생성
1. **설정 → 워크플로우**로 이동
2. **+ 새 워크플로우**를 클릭
3. 이름을 "Deal Won - Post-Win Automation"으로 지정
### 2단계: 트리거 구성
1. **레코드가 업데이트됨**을 선택
2. **Opportunities**를 선택
3. "Fields to monitor"에서 **Stage**를 선택
### 3단계: Stage 필터 추가
1. **필터** 액션을 추가합니다
2. 조건: `{{trigger.object.stage}}`이 "Closed Won"과 같음
### 4단계: 회사 유형 업데이트
1. **Update Record** 액션을 추가
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}}` |
| **설명** | `신규 고객 온보딩: {{trigger.object.company.name}}. 거래 금액: {{trigger.object.amount}}` |
### 6단계: Customer Success에 알림 보내기
1. 이메일 보내기 액션 추가
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. 또 하나의 **이메일 보내기** 액션 추가
2. 구성:
| 필드 | 값 |
| --------- | ---------------------------------------------------------------- |
| **받는 사람** | `{{trigger.object.owner.email}}` |
| **제목** | `✅ Deal Closed: {{trigger.object.name}}` |
| **본문** | 축하합니다! 거래가 처리되었습니다. Customer Success 팀에 알림이 전송되었으며 온보딩이 시작되었습니다. |
### 8단계: 테스트 및 활성화
1. 테스트용 기회를 "Closed Won"으로 이동하여 테스트하세요
2. 확인:
* 회사 유형이 "Customer"로 변경됨
* 온보딩 작업 생성됨
* CS 팀이 이메일을 받음
* 영업 담당자가 확인을 받음
3. 준비되면 활성화합니다
## Closed Lost 처리
실패한 거래에 대해서도 유사한 워크플로우를 만드세요:
### 트리거
* 레코드가 업데이트됨 (Opportunities, Stage = "Closed Lost")
### 작업
1. **레코드 생성**: "Lost Deal Analysis"용 작업
2. **레코드 업데이트**: 회사 레코드에 실패 사유 추가
3. **이메일 보내기**: 실패한 거래를 관리자에게 알림
## 고급: 다단계 온보딩
복잡한 온보딩의 경우 여러 작업을 생성합니다:
```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**로 결제 시스템에 고객 생성
### 조건부 작업
**필터** 액션을 사용하여 다음을 수행:
* 엔터프라이즈와 SMB에 다른 온보딩 적용
* 지역에 따라 다른 담당자 지정
* 소액 거래는 알림 생략
### 거래 세부 정보 포함
형식을 지정하려면 **Code** 액션을 사용하세요:
* 거래 요약 문서
* CS 팀을 위한 인수인계 노트
* 맞춤형 온보딩 체크리스트
## 관련 항목
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)
* [워크플로우에서 이메일 보내기](/l/ko/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시).
### 3단계: 오래된 기회 검색
1. **레코드 검색** 액션을 추가합니다
2. 구성:
| 필드 | 값 |
| ------ | ------------------------------------------ |
| **객체** | 기회 |
| **필터** | Updated At이 (오늘 - 7일) 이전 |
| **필터** | Stage가 "Closed Won"이 아니며 "Closed Lost"도 아님 |
| **제한** | 100 |
### 4단계: 발견된 항목이 있는지 확인
1. **필터** 액션을 추가합니다
2. 조건: `{{searchRecords.length}}`가 0보다 큼
3. 오래된 기회가 없으면 워크플로우는 여기서 중지됩니다
### 5단계: 경고 형식 지정(코드 액션)
이메일을 형식 지정하기 위해 **코드** 액션을 추가합니다:
```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단계: 경고 이메일 보내기
이메일 보내기 액션 추가:
| 필드 | 값 |
| --------- | -------------------------------------------- |
| **받는 사람** | sales-manager@yourcompany.com |
| **제목** | `🚨 {{code.totalCount}}건의 오래된 기회에 주의가 필요합니다` |
| **본문** | `{{code.summary}}` |
### 7단계: 테스트 및 활성화
1. 워크플로우를 실행하려면 **Test**를 클릭합니다
2. 이메일에 올바른 데이터가 포함되어 있는지 확인합니다
3. 준비되면 활성화합니다
## 맞춤 설정 옵션
### 정체 임계값 변경
레코드 검색 필터를 수정하여 7일을 원하는 기간으로 변경합니다:
* 고속 영업의 경우 3일
* 엔터프라이즈 거래의 경우 14일
* 긴 영업 주기의 경우 30일
### 개별 영업 담당자 알림
관리자에게 보내는 단일 이메일 대신 **Iterator**를 사용하여 각 영업 담당자에게 자신의 오래된 거래에 대한 개인화된 이메일을 보냅니다.
### 에스컬레이션 추가
심각도가 점차 높아지도록 여러 개의 워크플로우를 만듭니다:
1. 7일차: 영업 담당자에게 이메일
2. 14일차: 영업 담당자 및 관리자에게 이메일
3. 21일차: 관리자가 개입할 수 있도록 작업 생성
### Slack에 포함
이메일 대신 또는 추가로 **HTTP Request**를 사용해 Slack 웹훅으로 게시합니다.
## 관련 항목
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)
* [워크플로우에서 이메일 보내기](/l/ko/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. **Message Participants**를 선택합니다(고급 객체에 있음)
<Note>
Message Participant는 메시지 ID와 사람 ID의 조합으로, 메시지마다 하나의 고유 레코드를 생성합니다. 이는 발신자(또는 수신자)의 이메일 주소가 들어 있는 `handle` 필드에 접근할 수 있으므로 메시지를 직접 추적하는 것보다 더 쉽습니다.
</Note>
### 2단계: Role 기준으로 필터링
1. **필터** 액션을 추가합니다
2. 조건을 설정합니다: **Role**이 **FROM**과 같음
이렇게 하면 해당 사람이 보낸 메시지만 집계되고, 그 사람에게 보낸 메시지는 제외됩니다.
### 3단계: 동일한 Handle을 가진 모든 Message Participants 검색
1. **레코드 검색** 액션을 추가합니다
2. 객체로 **Message Participants**를 선택합니다
3. 필터를 추가합니다: **Handle**이 트리거의 handle(발신자의 이메일 주소)과 같고, **Role**이 **FROM**과 같음
4. **Limit**을 1에서 최대값인 **200**으로 늘립니다
해당 이메일 주소에서 보낸 모든 메시지를 찾아 총 개수를 구합니다.
<Note>
Search Records 액션은 최대 200개의 레코드만 반환합니다. 그러나 개별 레코드가 아니라 `totalCount` 값만 사용하므로, 이 단계는 해당 사람이 보낸 이메일의 총 개수를 반환합니다.
</Note>
### 4단계: Create or Update Record 액션으로 사람 레코드를 업데이트합니다
1. **레코드 생성 또는 업데이트** 액션을 추가합니다
2. 객체로 **People**를 선택합니다
3. Message Participant의 `handle`과 해당 사람의 이메일을 일치시켜 사람을 찾습니다.
4. 사용자 지정 "수신한 이메일 수" 필드를 `{{searchRecords.totalCount}}`로 설정합니다
Search Records 액션의 `totalCount` 값은 이 사람에게서 받은 이메일의 총 개수를 나타냅니다.
## 관련 항목
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)
* [사용자 지정 필드 만들기](/l/ko/user-guide/data-model/how-tos/customize-your-data-model)
* [레코드 검색 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions#search-records)

View File

@@ -0,0 +1,170 @@
---
title: 관련 레코드 데이터 표시
description: "워크플로우를 사용해 관련 레코드의 데이터를 표시합니다(예: Opportunities에 회사 정보 표시)."
---
관련 레코드의 데이터를 내 레코드에 직접 표시하세요 — 예를 들어, Company의 직원 수를 해당 Opportunities에 표시합니다. 중첩 필드가 기본적으로 제공될 때까지 이 워크플로우 우회 방법이 유용합니다.
## 일반적인 사용 사례
| 소스 | 대상 | 복사할 필드 |
| -- | -- | ------------------ |
| 회사 | 기회 | 산업, 회사 규모, ARR |
| 사람 | 기회 | 이메일, 전화, 직함 |
| 기회 | 회사 | 최근 거래 금액, 최근 성사 일자 |
## 기본 필드 복사
### 예시: 연락처 이메일을 Opportunity로 복사
**목표**: Opportunity에서 담당자를 설정할 때, 쉽게 액세스할 수 있도록 해당 이메일을 Opportunity로 복사합니다.
### 사전 준비
워크플로우를 만들기 전에 **설정 → 데이터 모델 → Opportunities**에서 대상 필드를 생성하세요:
* 연락처 이메일(유형: Email)
* 연락처 전화(유형: Phone)
### 설정
1. **트리거**: 레코드가 업데이트됨 (Opportunities, 담당자 필드)
2. **필터**: 담당자가 비어 있지 않은지 확인
3. **레코드 검색**: 연결된 사람 찾기
* 객체: People
* 필터: ID가 `{{trigger.object.pointOfContact.id}}`와 같음
4. **레코드 업데이트**:
* 객체: Opportunities
* 레코드: `{{trigger.object.id}}`
* 연락처 이메일: `{{searchRecords[0].email}}`
* 연락처 전화: `{{searchRecords[0].phone}}`
## 여러 필드 복사
### 예시: 모든 관련 Opportunities에 회사 정보 동기화
**목표**: 회사 세부 정보가 변경되면 관련된 모든 Opportunities를 업데이트합니다.
### 설정
1. **트리거**: 레코드가 업데이트됨 (Companies)
* 필드: 산업, 회사 규모, 연간 매출
2. **레코드 검색**: 이 회사의 모든 Opportunities 찾기
* 객체: Opportunities
* 필터: 회사 ID가 `{{trigger.object.id}}`와 같음
3. **Iterator**: 각 Opportunity를 순회
4. **레코드 업데이트** (iterator 내부):
* 객체: Opportunities
* 레코드: `{{iterator.currentItem.id}}`
* 회사 산업: `{{trigger.object.industry}}`
* 회사 규모: `{{trigger.object.companySize}}`
* 회사 ARR: `{{trigger.object.annualRevenue}}`
## 레코드 생성 시 복사
### 예시: 회사 데이터로 Opportunity를 미리 채우기
**목표**: 회사와 연결된 Opportunity를 생성할 때 핵심 회사 정보를 자동으로 복사합니다.
### 사전 준비
**설정 → 데이터 모델 → Opportunities**에서 대상 필드를 생성하세요:
* 회사 산업(유형: Text)
* 회사 규모(유형: Number)
### 설정
1. **트리거**: 레코드가 생성됨 (Opportunities)
* 필터: 회사가 비어 있지 않음
2. **레코드 검색**: 연결된 회사의 세부 정보 가져오기
* 객체: Companies
* 필터: ID가 `{{trigger.object.company.id}}`와 같음
3. **레코드 업데이트**:
* 객체: Opportunities
* 레코드: `{{trigger.object.id}}`
* 회사 산업: `{{searchRecords[0].industry}}`
* 회사 규모: `{{searchRecords[0].employees}}`
<Note>
**작업 및 노트 제한 사항**: 작업과 노트의 관계는 다대다로 하드코딩되어 있으며, 워크플로우 트리거 또는 액션에서 아직 사용할 수 없습니다. 이러한 관계에 액세스하려면 대신 [API](/l/ko/developers/extend/api)를 사용하세요.
</Note>
## 양방향 동기화
### 예시: 기본 담당자 동기화 유지
**목표**: 회사의 기본 담당자가 변경되면 담당자를 업데이트합니다. 사람이 기본 담당자가 되면 회사를 업데이트합니다.
### 워크플로우 1: Company → Person
1. **트리거**: 레코드가 업데이트됨 (Companies, 기본 담당자 필드)
2. **레코드 업데이트**: 사람의 "Is Primary Contact"를 true로 설정
3. **레코드 검색**: 이전 기본 담당자 찾기
4. **레코드 업데이트**: 이전 담당자의 "Is Primary Contact"를 false로 설정
### 워크플로우 2: Person → Company
1. **트리거**: 레코드가 업데이트됨 (People, Is Primary Contact = true)
2. **레코드 업데이트**: 회사의 기본 담당자를 이 사람으로 설정
<Note>
무한 루프를 피하기 위해 양방향 동기화에 주의하세요. 업데이트하기 전에 값이 실제로 변경되었는지 필터로 확인하세요.
</Note>
## 복잡한 매핑에 코드 사용
### 예시: 복사 중 데이터 변환
**목표**: 사람의 전화번호를 Opportunity로 복사하고 형식을 지정합니다.
```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 };
};
```
## 모범 사례
### 루프 방지
* 서로를 끝없이 트리거하는 워크플로우를 만들지 마세요
* 특정 필드 조건 사용
* 값이 실제로 변경되었는지 확인을 추가하세요
### 누락된 데이터 처리
* 복사하기 전에 소스 레코드가 존재하는지 항상 확인하세요
* 선택적 필드에 기본값 제공
* 소스 필드가 비어 있을 때 건너뛰도록 필터 사용
### 성능
* 여러 레코드에 복사할 때 일괄 업데이트 사용
* 대량 동기화 작업에는 예약된 워크플로우를 사용하세요
* 다수의 레코드를 업데이트할 때 Iterator 사용을 고려하세요
## 관련 항목
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)
* [워크플로우 트리거](/l/ko/user-guide/workflows/capabilities/workflow-triggers)

View File

@@ -0,0 +1,202 @@
---
title: 수식 필드
description: 네이티브 지원이 제공될 때까지 워크플로우를 사용해 수식 필드를 생성하세요.
---
Twenty는 아직 네이티브 수식 필드를 지원하지 않습니다(2026년에 제공 예정이지만), 워크플로우를 사용하면 동일한 결과를 얻을 수 있습니다. 이 우회 방법을 사용하면 단순한 연결부터 복잡한 비즈니스 로직까지 필드 값을 자동으로 계산하고 채울 수 있습니다.
## 일반적인 사용 사례
| 사용 사례 | 수식 예시 |
| --------------- | ---------------------------- |
| **전체 이름** | First Name + " " + Last Name |
| **예상 금액** | Amount × Probability |
| **마감일까지 남은 일수** | Due Date - Today |
| **단계 경과 일수** | Today - Stage Entry Date |
| **리드 점수** | 여러 기준을 기반으로 한 점수 |
<Tip>
파이프라인 단계에서 시간을 추적하는 전체 예시는 [각 단계에서 기회가 머무는 기간 추적하기](/l/ko/user-guide/views-pipelines/how-tos/track-time-in-stage)를 참조하세요.
</Tip>
## 기본 수식: 연결
### 예시: 전체 이름 자동 채우기
**목표**: 이름과 성을 결합해 전체 이름 필드에 자동으로 입력합니다.
### 설정
1. **트리거**: 레코드가 업데이트되거나 생성됨 (People)
2. **필터**: 이름 또는 성이 변경되었는지 확인
3. **코드 액션**:
```javascript
export const main = async (params) => {
const { firstName, lastName } = params;
const fullName = [firstName, lastName]
.filter(Boolean)
.join(' ');
return { fullName };
};
```
4. **레코드 업데이트**: Full Name을 `{{code.fullName}}`(으)로 설정
## 수치 수식: 예상 금액
### 예시: 예상 매출 계산
**목표**: 기회 금액에 확률을 곱하여 예상 금액을 계산합니다.
전체 워크플로우는 [파이프라인에서 예상 금액 표시하기](/l/ko/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline)를 참조하세요.
### 설정
1. **트리거**: 레코드가 업데이트됨 (Opportunities, Amount 또는 Probability 필드)
2. **코드 액션**:
```javascript
export const main = async (params) => {
const { amount, probability } = params;
const expectedAmount = (amount || 0) * (probability || 0) / 100;
return { expectedAmount };
};
```
3. **레코드 업데이트**: Expected Amount를 `{{code.expectedAmount}}`(으)로 설정
## 날짜 수식: 일수 계산
### 예시: 작업 마감일까지 남은 일수
**목표**: 작업 마감일까지 남은 일수를 계산합니다.
### 설정
1. **트리거**: 레코드가 업데이트되거나 생성됨 (Tasks, Due Date 필드)
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. **레코드 업데이트**: Days Until Due를 `{{code.daysUntilDue}}`(으)로 설정
<Note>
음수 값은 기한이 지난 작업을 의미합니다. 이 필드를 사용하여 긴급도에 따라 작업을 필터링하거나 정렬할 수 있습니다.
</Note>
## 조건 수식: 리드 점수
### 예시: 기준에 따라 리드 점수 계산
**목표**: 회사 규모, 산업, 참여도에 따라 리드 점수를 산정합니다.
### 설정
1. **트리거**: 레코드가 업데이트됨 (People 또는 Companies)
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. **레코드 업데이트**: Lead Score를 `{{code.leadScore}}`(으)로 설정
## 텍스트 수식: 도메인 추출
### 예시: 이메일에서 도메인 추출
**목표**: 이메일 도메인을 자동으로 추출하여 저장합니다.
### 설정
1. **트리거**: 레코드가 업데이트됨 (People, 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. **레코드 업데이트**: Domain 필드를 `{{code.domain}}`(으)로 설정
## 모범 사례
### 성능
* 관련 필드 변경에만 트리거되도록 하세요.
* 계산이 필요 없는 레코드는 필터로 건너뛰세요.
* 대량 워크플로우에서는 복잡한 계산을 피하세요.
### 오류 처리
* 계산 전에 null/undefined 값을 확인하세요.
* 데이터가 없을 때는 기본값을 사용하세요.
* 계산이 실패하면 명확한 오류 메시지를 반환하세요.
### 테스트
* 엣지 케이스로 테스트하세요(빈 필드, 0 값 등).
* 활성화 전에 계산을 수동으로 검증하세요.
* 예상치 못한 결과가 있는지 워크플로우 실행을 모니터링하세요.
## 관련 항목
* [파이프라인에서 예상 금액 표시하기](/l/ko/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline)
* [단계에서 시간 추적하기](/l/ko/user-guide/views-pipelines/how-tos/track-time-in-stage)
* [워크플로우 액션](/l/ko/user-guide/workflows/capabilities/workflow-actions)

View File

@@ -0,0 +1,127 @@
---
title: 검토할 노트에 대해 팀원에게 알림 보내기
description: 노트 검토를 위해 동료를 지정할 때 자동 이메일 알림을 보냅니다.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<Note>
**노트에서 팀원을 직접 태그하는 기능은 아직 지원되지 않습니다.** 이 기능은 2026년 하반기에 제공될 예정입니다. 그동안 이 워크플로우가 실질적인 대안을 제공합니다.
</Note>
## 과제
노트에서 동료를 태그해 검토 알림을 받도록 하고 싶습니다. 현재 Twenty는 노트나 작업에서 @멘션을 지원하지 않습니다.
## 대안 방법
노트 본문에서 태그하는 대신 다음을 수행할 수 있습니다:
1. 노트 개체에 **검토자** 관계 필드를 생성합니다
2. 노트를 생성하거나 편집할 때 검토자를 지정합니다
3. 워크플로우가 검토자에게 노트 링크가 포함된 이메일을 자동으로 보내도록 설정하세요
<Warning>
**제한 사항**: 이 방법은 필드당 한 명의 검토자만 지정할 수 있습니다. 여러 사람에게 알림이 필요하다면 추가 관계 필드를 생성하세요(예: 검토자 1, 검토자 2). 한 번에 많은 공동 작업자를 태그해야 하는 경우 확장성이 떨어집니다.
</Warning>
## 단계별 설정
### 1단계: 검토자 필드 생성
<img src="/images/user-guide/workflows/reviewer-field-for-note.png" style={{width:'100%'}} />
1. **설정 → 데이터 모델 → 노트**로 이동
2. **필드 추가**를 클릭합니다
3. 구성:
| 설정 | 값 |
| --------- | ---------------------- |
| **Name** | 검토자 |
| **유형** | 관계 |
| **관련 개체** | 워크스페이스 멤버 |
| **관계 유형** | 여러 노트 → 하나의 워크스페이스 구성원 |
4. 필드를 저장합니다
### 2단계: 워크플로우 생성
<VimeoEmbed videoId="1151897168" title="동영상 시연" />
#### 워크플로우 트리거 만들기
1. 메인 화면으로 이동해 새 워크플로우를 생성합니다
2. 트리거로 **레코드가 업데이트됨**을 선택합니다
3. 개체로 **노트**를 선택하고 검토자 필드를 지정합니다.
4. 노트의 검토자가 업데이트될 때마다 트리거됩니다.
#### 방금 검토자가 지정되었는지 확인하는 필터 추가
검토자가 지정된 경우에만 워크플로우가 계속되도록 **필터** 작업을 추가합니다:
| 조건 | 값 |
| ------- | -------- |
| **검토자** | 비어 있지 않음 |
#### 워크스페이스 구성원 검색
1. **레코드 검색** 작업을 추가합니다
2. 고급 객체 아래의 **Workspace Member**를 선택합니다
3. 필터 추가:
| 조건 | 값 |
| ------ | ------------------------- |
| **ID** | `{{note.reviewerId}}`와 같음 |
이렇게 하면 이메일 주소와 이름을 포함한 전체 워크스페이스 구성원 레코드를 가져옵니다.
#### 알림 이메일 보내기
1. **이메일 보내기** 작업을 추가합니다
2. 구성:
| 필드 | 값 |
| --------- | ---------------------------------- |
| **받는 사람** | 레코드 검색 → 첫 번째 워크스페이스 구성원 → 사용자 이메일 |
| **제목** | 노트 검토가 필요합니다 |
| **본문** | 아래 예시를 참조하세요 |
**이메일 본문 예시:**
```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. **검토자** 필드에서 동료를 선택합니다
3. 동료는 노트로 바로 이동하는 링크가 포함된 이메일을 자동으로 받습니다
## 관련 항목
* [워크플로우 트리거](/l/ko/user-guide/workflows/capabilities/workflow-triggers)
* [워크플로우에서 이메일 보내기](/l/ko/user-guide/workflows/capabilities/send-emails-from-workflows)
* [사용자 지정 필드 만들기](/l/ko/user-guide/data-model/how-tos/create-custom-fields)
* [관계 필드 만들기](/l/ko/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. **Settings → Workflows**로 이동해 새 워크플로우를 생성합니다
2. 트리거로 **On a Schedule**를 선택합니다
3. 매일 오전 8시에 실행되도록 다음 cron 표현식을 사용하세요: `0 8 * * *`
### 2단계: 모든 워크스페이스 구성원 검색
1. **레코드 검색** 액션을 추가합니다
2. 고급 객체(advanced objects) 아래의 **Workspace Members**를 선택합니다
3. 필터는 필요 없습니다 — 모든 구성원이 반환됩니다
### 3단계: 이터레이터 추가
1. **Iterator** 액션을 추가합니다
2. 입력 배열을 이전 단계의 워크스페이스 구성원으로 설정합니다
3. 이터레이터 내부의 모든 액션은 구성원당 한 번씩 실행됩니다
### 4단계: 오늘 마감 작업 검색(이터레이터 내부)
1. 이터레이터 내부에 **레코드 검색** 액션을 추가합니다
2. 객체로 **Tasks**를 선택합니다
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/ko/user-guide/workflows/capabilities/workflow-actions)
* [워크플로우에서 이메일 보내기](/l/ko/user-guide/workflows/capabilities/send-emails-from-workflows)
* [Code 액션에서 배열 처리하기](/l/ko/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. **워크플로우가 활성화되지 않음**: 워크플로우가 "Draft"가 아니라 "Active"로 설정되어 있는지 확인하세요
2. **트리거 조건 불충족**: 트리거가 예상한 이벤트와 일치하는지 확인하세요
3. **필드가 모니터링되지 않음**: "Record is Updated" 트리거의 경우 특정 필드가 모니터링되고 있는지 확인하세요
4. **권한**: 워크플로우를 실행할 권한이 있는지 확인하세요
**해결책**:
* 워크플로우 목록에서 워크플로우 상태를 확인하세요
* 해당 트리거를 유발할 것으로 예상되는 특정 작업으로 테스트하세요
* 트리거 구성을 검토하세요
* 권한과 관련해 관리자에게 문의하세요
### 워크플로우 트리거가 너무 일찍 발생함(빈 필드)
**증상**: UI에서 레코드를 수동으로 생성할 때 모든 필드를 채우기 전에 워크플로우가 트리거됩니다. 대부분의 필드 값이 비어 있는 상태로 워크플로우가 실행됩니다.
**발생 원인**: Twenty는 모든 내용을 실시간으로 저장합니다 — 별도의 "edit" 및 "read" 모드는 없습니다. 레코드를 생성하면 즉시 저장되어 추가 필드를 채우기 전에 "Record is created" 이벤트가 트리거됩니다.
**"Record is created"가 적절한 경우**:
* API 호출로 생성된 레코드(단일 요청으로 필드가 채워짐)
* 가져오기로 생성된 레코드
* 다른 워크플로우에서 자동으로 생성된 레코드
**해결 방법**: UI에서 수동으로 생성된 레코드의 경우 트리거로 \*\*"Record is created or updated"\*\*를 사용하세요. 이렇게 하면:
* 사용자가 필드를 모두 작성하고 저장한 후에 워크플로우가 트리거됩니다
* 빈 값이 아닌 완전한 데이터를 얻을 수 있습니다
<Tip>
레코드당 한 번만 워크플로우를 실행하려면, `createdAt equals updatedAt`(첫 저장)과 같은 필드를 확인하는 필터 액션을 추가하거나, 워크플로우가 이미 실행되었는지 추적하기 위해 사용자 지정 체크박스 필드를 사용하세요.
</Tip>
### 액션 실패
**증상**: 워크플로우는 실행되지만 일부 액션이 실패합니다.
**가능한 원인**:
1. **데이터 누락**: 필수 필드가 비어 있음
2. **잘못된 참조**: 이전 단계의 변수가 존재하지 않음
3. **API 오류**: 외부 서비스가 오류를 반환함
4. **권한 문제**: 액션에 필요한 권한이 없음
**해결책**:
* 워크플로우 실행 세부 정보에서 오류 메시지를 확인하세요
* 모든 필수 필드에 값이 있는지 확인하세요
* API 연결을 별도로 테스트하세요
* 역할 권한을 검토하세요
### HTTP 요청 오류
**증상**: HTTP 요청 액션이 실패하거나 예상치 못한 결과를 반환합니다.
**일반적인 오류 코드**:
* **400**: 잘못된 요청 - 요청 본문 형식을 확인하세요
* **401**: 인증되지 않음 - API 키를 확인하세요
* **403**: 접근 거부 - API 권한을 확인하세요
* **404**: 찾을 수 없음 - 엔드포인트 URL을 확인하세요
* **429**: 너무 많은 요청 - 요청 속도 제한을 구현하세요
* **500**: 서버 오류 - 외부 서비스 문제
**해결책**:
* API 엔드포인트 URL을 확인하세요
* 인증 헤더를 확인하세요
* 먼저 Twenty 밖에서 API 호출을 테스트하세요
* 코드 액션에 오류 처리를 추가하세요
### 코드 액션 오류
**증상**: JavaScript 코드가 실행되지 않습니다.
**일반적인 문제**:
1. **구문 오류**: 오탈자 또는 유효하지 않은 JavaScript
2. **정의되지 않은 변수**: 존재하지 않는 변수를 참조함
3. **타입 오류**: 잘못된 데이터 타입에 대한 연산
4. **시간 초과**: 코드 실행에 너무 오래 걸림
**해결책**:
* 내장 코드 편집기의 유효성 검사를 사용하세요
* 먼저 JavaScript 콘솔에서 코드 로직을 테스트하세요
* 디버깅을 위해 console.log 문을 추가하세요
* 복잡한 연산을 단순화하세요
### 이메일이 전송되지 않음
**증상**: 이메일 보내기 액션이 이메일을 전송하지 않습니다.
**가능한 원인**:
1. **이메일 계정이 연결되지 않음**: Settings → Accounts를 확인하세요
2. **잘못된 이메일 주소**: 수신자 이메일이 형식에 맞지 않음
3. **전송 한도**: 이메일 제공업체의 속도 제한에 도달함
4. **스팸 필터**: 이메일이 차단됨
**해결책**:
* 이메일 계정 연결 상태를 확인하세요
* 수신자 이메일 주소를 검증하세요
* 이메일 제공업체의 제한을 확인하세요
* 스팸 트리거가 될 수 있는 이메일 콘텐츠를 검토하세요
## 워크플로우 디버깅
### 워크플로우 실행 활용
1. 워크플로우 편집기로 이동하세요
2. **Runs** 패널을 엽니다
3. 실패한 실행을 찾으세요
4. 클릭하여 단계별 세부 정보를 확인하세요
5. 오류 메시지와 출력 데이터를 검토하세요
### 개별 단계 테스트
1. 코드 액션의 경우 **Test** 버튼을 사용하세요
2. HTTP 요청의 경우 엔드포인트를 별도로 테스트하세요
3. 워크플로우를 트리거하기 위한 테스트 레코드를 생성하세요
4. 제어된 테스트를 위해 수동 트리거를 사용하세요
### 일반적인 디버깅 패턴
**로깅 추가**:
디버깅을 위해 코드 액션을 사용해 중간 값을 기록하세요.
**단계 격리**:
실패를 식별하기 위해 각 단계를 개별적으로 테스트하세요.
**데이터 흐름 확인**:
각 단계가 예상된 입력 데이터를 수신하는지 확인하세요.
## 문제를 피하기 위한 모범 사례
### 활성화 이전
* 초안 모드에서 충분히 테스트하세요
* 모든 API 연결을 검증하세요
* 트리거 조건을 주의 깊게 검토하세요
* 예상 동작을 문서화하세요
### 개발 중
* 알기 쉬운 단계 이름을 사용하세요
* 코드 액션에 주석을 추가하세요
* 현실적인 데이터로 테스트하세요
* 엣지 케이스를 대비하세요
### 활성화 이후
* 초기 실행을 면밀히 모니터링하세요
* 실패에 대한 알림을 설정하세요
* 실행 기록을 정기적으로 검토하세요
* 가능한 경우 워크플로우를 단순하게 유지하세요

View File

@@ -0,0 +1,236 @@
---
title: 워크플로우 FAQ
description: Twenty의 워크플로우에 관한 자주 묻는 질문입니다.
---
<AccordionGroup>
<Accordion title="워크플로우를 활성화할 수 없는 이유는 무엇인가요?">
이는 권한 문제일 가능성이 높습니다. 워크플로우를 생성하고 활성화하려면 워크플로우에 대한 접근 권한이 필요합니다.
**해결 방법**: 워크스페이스 관리자에게 문의하여 **Settings → Members → Roles**에서 워크플로 액세스 권한을 부여받으세요.
사이드바에서 워크플로우 섹션이 전혀 보이지 않는다면, 이는 권한 문제임이 확실합니다.
</Accordion>
<Accordion title="상단 내비게이션 바에서 수동 워크플로우 버튼이 보이지 않는 이유는 무엇인가요?">
수동 워크플로우는 올바르게 구성된 경우에만 내비게이션 바에 표시됩니다:
1. 워크플로우가 **활성화**되어 있어야 합니다(초안 모드 아님)
2. navbar 배치는 **Pinned**로 설정되어 있어야 합니다
3. Single/Bulk 트리거의 경우 올바른 오브젝트 페이지에 있어야 합니다
**확인 방법**: 워크플로우를 연 다음 → 트리거를 클릭 → "Navbar placement"가 "Pinned"로 설정되어 있는지 확인합니다.
내비게이션 바 설정과 관계없이 **Cmd + K**(또는 **Ctrl + K**)를 통해 언제든지 수동 워크플로우에 접근할 수 있습니다.
</Accordion>
<Accordion title="Single, Global, Bulk 수동 트리거의 차이점은 무엇인가요?">
| 유형 | 필요한 레코드 | Workflow 실행들 |
| -- | ------- | ------------ |
\| **Global** | 없음 | 한 번, 레코드 입력 없음 |
\| **Single** | 하나 이상 선택됨 | 선택된 레코드마다 한 번씩 |
\| **Bulk** | 하나 이상 선택됨 | 한 번, 모든 레코드를 배열로 처리 |
* **Global**: 워크플로우에 레코드 컨텍스트가 필요하지 않을 때 사용합니다(예: 리포트 생성)
* **Single**: 선택된 각 레코드를 개별적으로 처리하려는 경우에 사용합니다(예: 개별 이메일 발송)
* **Bulk**: 레코드를 함께 처리하거나 크레딧 사용을 최적화해야 할 때 사용합니다(Iterator 액션 필요)
자세한 내용은 [Workflow Triggers](/l/ko/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...
```
Filter 조건을 통과한 분기만 이후 액션을 실행합니다.
단계별 안내는 [How to Use Branches](/l/ko/user-guide/workflows/capabilities/workflow-branches)를 참고하세요.
</Accordion>
<Accordion title="워크플로우 분기는 병렬로 실행되나요?">
**예**, 분기는 기본적으로 병렬로 실행됩니다.
하나의 분기만 실행되도록 하려면:
* 각 분기의 시작 부분에 **Filter** 액션을 추가합니다
* 서로 반대되는 조건을 설정합니다(예: 분기 A: status = "Open", 분기 B: status ≠ "Open")
Filter 조건을 만족하지 못한 분기는 실행이 중지되고, 다른 분기는 계속 실행됩니다.
</Accordion>
<Accordion title="분기를 다시 병합할 수 있나요?">
**예**. 병렬 분기가 완료된 후, 두 분기 모두가 연결되는 단계를 추가할 수 있습니다.
워크플로우 편집기에서:
1. 분기된 액션을 완료합니다
2. 분기 이후에 실행될 새 단계를 추가합니다
3. 각 분기의 마지막 단계에서 이 새 단계로 연결선을 드래그합니다
모든 연결된 분기가 완료된 후 병합된 단계가 실행됩니다.
</Accordion>
<Accordion title="Search Records 결과의 한도는 얼마인가요?">
**Search Records는 최대 200개 레코드를 반환합니다.**
더 많은 레코드를 처리해야 하는 경우:
* 결과를 줄이기 위해 더 구체적인 필터를 추가합니다
* 배치 처리를 위해 예약된 워크플로우를 사용합니다
* 대량 작업에는 API 사용을 고려합니다
대부분의 워크플로우에는 200개 레코드면 충분합니다. 이 한도에 자주 도달한다면 자동화 구조를 재설계하는 것을 고려하세요.
</Accordion>
<Accordion title="워크플로우 이메일에 사람들을 CC로 포함할 수 있나요?">
**아직은 불가능합니다.** Send Email 액션의 CC 및 BCC 필드는 로드맵에 포함되어 있습니다.
**현재 우회 방법**: 추가 수신자에게 이메일을 보내기 위해 여러 개의 Send Email 액션을 추가하거나, CC를 지원하는 외부 이메일 서비스를 통해 전송하도록 HTTP Request를 사용하세요.
</Accordion>
<Accordion title="이전 단계의 출력을 어떻게 사용하나요?">
모든 액션은 이후 단계에서 사용할 수 있는 출력 데이터를 생성합니다.
**이전 단계 데이터를 참조하려면**:
* 필드를 구성할 때 변수 선택기를 사용합니다
* 또는 `{{stepName.fieldName}}`을 직접 입력합니다
**예시**:
* 트리거 데이터: `{{trigger.object.email}}`
* 검색 결과: `{{searchRecords[0].name}}`
* 코드 출력: `{{code.calculatedValue}}`
액션 구성에서 임의의 필드 위에 마우스를 올리면 이전 단계에서 사용할 수 있는 변수를 확인할 수 있습니다.
</Accordion>
<Accordion title="Iterator가 실패하는 이유는 무엇인가요?">
**Iterator에는 배열 입력이 필요합니다.** 일반적인 문제는 다음과 같습니다:
1. **입력이 배열이 아님**: Search Records 또는 배열을 반환하는 다른 액션의 결과를 전달하고 있는지 확인합니다
2. **배열이 비어 있음**: `{{searchRecords.length}} > 0`을 확인하는 Filter를 Iterator 앞에 추가하세요
3. **잘못된 변수가 선택됨**: 단일 레코드가 아니라 배열 자체를 선택했는지 확인합니다
**올바른 설정**:
1. Search Records(배열 반환)
2. Filter: length > 0
3. Iterator: `{{searchRecords}}` 선택
4. Iterator 내부의 액션은 `{{iterator.currentItem.fieldName}}`을 사용합니다
</Accordion>
<Accordion title="Code 액션의 최대 실행 시간은 얼마인가요?">
코드 액션(로직 함수)에는 **5분의 기본 제한 시간** (300초)이 있습니다.
최대로 설정할 수 있는 타임아웃은 **15분**(900초)입니다.
코드 실행 시간이 이 한도를 초과하면 액션은 타임아웃 오류와 함께 실패합니다.
**타임아웃을 피하기 위한 팁**:
* Iterator를 사용해 대규모 작업을 더 작은 단위로 나눕니다
* 무거운 연산은 피하고, 많은 연산이 필요한 경우 HTTP Request를 통해 외부 서비스를 사용하세요
* 코드를 최적화하여 실행 시간을 줄이세요
* 더 긴 처리가 필요하다면 데이터를 배치로 처리하는 예약된 워크플로우 사용을 고려하세요
</Accordion>
<Accordion title="워크플로우 실행 내역은 어떻게 읽나요?">
워크플로우 실행 내역은 실행 이력을 보여주며 문제를 디버깅하는 데 도움이 됩니다.
**실행 내역에 접근하려면**:
* 워크플로우 편집기에서 → 오른쪽의 **Runs** 패널을 엽니다
* 또는 사이드바에서 **워크플로우 실행**으로 이동합니다
**실행 내역 이해하기**:
* **Status**: Running, Completed, Failed, Waiting
* **Steps**: 어떤 단계가 실행되었고 어떤 출력을 생성했는지 확인합니다
* **Errors**: 실패한 단계를 클릭하여 오류 메시지를 확인합니다
* **Data**: 각 단계에서 입력 및 출력 데이터를 확인합니다
자세한 내용은 [Workflow Runs](/l/ko/user-guide/workflows/capabilities/workflow-runs)를 참조하세요.
</Accordion>
<Accordion title="워크플로우 실행 내역이 보이지 않는 이유는 무엇인가요?">
워크플로우 실행이 레이트 리밋 때문에 즉시 실패하고 있을 수 있습니다. Twenty는 워크스페이스마다 시간당 실행 횟수를 5,000회로 엄격히 제한합니다.
이 한도를 초과하면 워크플로가 즉시 실패로 표시되며 실행 목록에 표시되지 않습니다.
**이 한도에 도달하는 일반적인 시나리오**:
* Single 수동 트리거로 5,000개가 넘는 레코드를 선택하는 경우
* 여러 워크플로가 워크스페이스 전체에서 동시에 실행되는 경우
* 고빈도 자동 트리거(예: 사용량이 많은 오브젝트에서의 Record Updated)
**해결책**:
* 여러 레코드를 한 번에 처리할 때는 Single 대신 **Bulk** 트리거를 사용하세요.
* 대용량 배치 작업 사이에 간격을 두세요
* 필터를 사용해 트리거 빈도를 줄이세요
* 부하가 큰 워크플로는 피크 시간이 아닌 시간대에 예약 실행하세요
</Accordion>
<Accordion title="워크플로우 레이트 리밋은 무엇인가요?">
Twenty는 시스템 안정성을 보장하기 위해 두 가지 레이트 리밋을 적용합니다:
| 한도 | 값 | 동작 |
| -- | - | -- |
\| **Soft limit** | 분당 100회 실행 | "Not Started" 상태로 큐에 쌓인 뒤 점진적으로 처리 |
\| **Hard limit** | 시간당 5,000회 실행 | 실행이 즉시 실패 |
**Soft limit(분당 100회)**: 워크플로우가 실패하지는 않으며, 단지 큐에서 대기하다가 시간이 지나면서 처리됩니다. 100개가 넘는 레코드를 트리거할 수 있지만, 실행 속도는 느려집니다.
**Hard limit(시간당 5,000회)**: 워크스페이스 전체에 적용됩니다. 모든 워크플로우를 합쳐 한 시간에 5,000회 실행을 초과하면, 추가 실행은 즉시 실패합니다.
**한도 내에 머무르기 위한 팁**:
* 대규모 배치 작업에는 Single 트리거 대신 Iterator를 함께 사용하는 Bulk 트리거를 사용합니다
* 관련된 자동화를 더 적은 수의 워크플로우로 통합합니다
* 부하를 분산하기 위해 예약된 워크플로우를 사용하세요
</Accordion>
<Accordion title="실패한 워크플로우 실행을 다시 시도할 수 있나요?">
**현재는 자동 재시도 기능이 없습니다.**
워크플로우 실행이 실패한 경우 다음을 수행해야 합니다:
1. **Settings → Workflows → [Your Workflow] → Runs**에서 오류를 확인합니다
2. 문제(데이터, 설정, 외부 서비스)를 수정합니다
3. 영향을 받은 레코드(들)에 대해 워크플로우를 수동으로 다시 트리거합니다
**실패를 줄이기 위한 팁**:
* 액션 실행 전에 데이터를 검증하기 위해 **Filter** 노드를 추가합니다
* 관련 레코드가 존재하는지 확인하기 위해 **Search Records**를 사용합니다
* 대량 작업 전에 소수의 레코드로 충분히 테스트합니다
자동 재시도 기능은 향후 릴리스를 위한 로드맵에 포함되어 있습니다.
</Accordion>
<Accordion title="CSV 가져오기 전에 워크플로우를 비활성화해야 하나요?">
**예, 워크플로우가 레코드 생성 또는 업데이트에 의해 트리거되는 경우에는 비활성화하는 것이 좋습니다.**
CSV로 데이터를 가져올 때, 생성되거나 업데이트되는 각 레코드가 워크플로우를 트리거할 수 있습니다. 수천 개의 레코드를 포함한 대규모 가져오기는 다음과 같은 상황을 초래할 수 있습니다:
* 시간당 5,000회 실행 한도에 도달
* 상당한 워크플로우 크레딧 소모
* 예상치 못한 이메일 또는 알림 발송
* 중복 작업 또는 레코드 생성
**대량 가져오기 전에**:
1. **Settings → Workflows**로 이동합니다
2. 가져오려는 오브젝트에 의해 트리거되는 워크플로우를 식별합니다
3. 해당 워크플로우를 일시적으로 **비활성화**합니다
4. CSV 가져오기를 실행합니다
5. 작업이 끝나면 워크플로우를 다시 **활성화**합니다
**대안**: 가져온 데이터에 워크플로우를 실행해야 한다면, 레이트 리밋 내에 머물 수 있도록 더 작은 배치로 나누어 가져오세요.
</Accordion>
<Accordion title="워크플로우 레이아웃을 정리하려면 어떻게 해야 하나요?">
워크플로우 캔버스에 노드가 흩어져 지저분해 보이는 경우, 자동 정리 기능을 사용할 수 있습니다:
1. 워크플로우 캔버스의 아무 곳이나 마우스 오른쪽 버튼으로 클릭합니다
2. **Tidy up workflow**를 클릭합니다
그러면 모든 노드가 자동으로 재배치되어 깔끔하고 정돈된 레이아웃이 됩니다.
</Accordion>
</AccordionGroup>