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

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

View File

@@ -0,0 +1,182 @@
---
title: 製品データを Twenty に取り込む
description: データウェアハウスの製品カタログデータをスケジュールに基づいてCRMに同期します。
---
このパターンを使用して、データウェアハウス(例: Snowflake、BigQuery、PostgreSQL)の製品データとTwentyの同期を維持します。
## ワークフローの構成
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分ごと
* 重要度の低いデータには1時間ごと
* バッチ更新には毎日
## ステップ 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: 各レコードをアップサートする
Iterator 内で「**Upsert Record**」アクションを追加します:
| 設定 | 値 |
| ---------- | ------------------------- |
| **オブジェクト** | カスタムの Product オブジェクト |
| **一致条件** | 外部IDまたはSKU(一意の識別子) |
| **名前** | `{{iterator.item.name}}` |
| **SKU** | `{{iterator.item.sku}}` |
| **価格** | `{{iterator.item.price}}` |
<Tip>
作成と更新で分岐を作る代わりに、**Upsert**(更新または作成)を使用します。 その方が構築が速く、デバッグも容易です。
</Tip>
## ユースケース例
| ソース | データ |
| ----------------- | ------------------- |
| **ERP システム** | 製品カタログ、価格設定、在庫 |
| **Eコマースプラットフォーム** | 受注、顧客、製品更新 |
| **データウェアハウス** | 集計メトリクス、エンリッチされたデータ |
| **在庫管理システム** | 在庫水準、再発注アラート |
## 関連
* [ワークフローのトリガー](/l/ja/user-guide/workflows/capabilities/workflow-triggers)
* [ワークフローのアクション](/l/ja/user-guide/workflows/capabilities/workflow-actions)
* [Code アクションで配列を扱う](/l/ja/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/ja/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)を参照してください。 本記事では、Typeform のカスタムペイロード構造に必要となる特有の処理について説明します。
### ステップ 1: Webhook ワークフローを作成
1. 「**Settings → Workflows**」に移動
2. 「**+ New Workflow**」をクリック
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 配列からフィールドを抽出する
`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/ja/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)を参照してください。
</Tip>
### ステップ 5: レコードを作成
「**Create Record**」アクションを追加します:
| フィールド | 値 |
| ---------- | --------------------------------------- |
| **オブジェクト** | 人物 |
| **名** | `{{code.contact.firstName}}` |
| **姓** | `{{code.contact.lastName}}` |
| **メール** | `{{code.contact.email}}` |
| **会社** | `{{code.contact.company}}` に基づいて検索または作成 |
### ステップ 6: テストして有効化
1. Typeform でテスト回答を送信
2. ワークフローの実行を確認し、データが取り込まれたことを検証
3. ワークフローを有効化します
## 関連
* [Webhook トリガーを設定](/l/ja/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)
* [Code アクションで配列を扱う](/l/ja/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 を自動生成または取得し、レコードに添付します。 これは、会社、商談、またはその他のオブジェクトに関連付けられた見積書、請求書、レポートを作成する用途で一般的に使用されます。
## 概要
このワークフローでは **手動トリガー** を使用し、選択した任意のレコードに対してオンデマンドで PDF を生成できるようにします。 次を **ロジック関数** が処理します:
1. URL(PDF 生成サービスから)から PDF をダウンロードする
2. ファイルを Twenty にアップロードする
3. レコードに関連付けられた添付ファイルを作成する
## 前提条件
ワークフローを設定する前に:
1. **API キーを作成**: **Settings → APIs** に移動して新しい API キーを作成します。 ロジック関数にはこのトークンが必要です。
2. **PDF 生成サービスを設定**(任意):PDF を動的に生成したい場合(例: 見積書)は、Carbone、PDFMonkey、DocuSeal のようなサービスを使用して PDF を作成し、ダウンロード URL を取得します。
## ステップバイステップの設定
### ステップ 1: トリガーを設定
1. **Workflows** に移動し、新しいワークフローを作成します
2. **手動トリガー** を選択します
3. PDF を添付したい対象オブジェクト(例: **Company** や **Opportunity**)を選択します
<Tip>
手動トリガー付き ユーザーは、レコードが選択されると右上に表示されるボタンを使用してこのワークフローを実行できます — これは、PDFを生成して添付するためのワークフローを実行します。
</Tip>
### ステップ 2: ロジック関数を追加する
1. **コード** アクション (Logic Function) を追加します
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` オブジェクトの両方を更新します。
#### 動的な PDF URL を使用する場合
PDF 生成サービスを使用する場合、次のことができます:
1. まず HTTP Request アクションで PDF を生成します
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を提供し、ロジック関数に渡すことができます。
## トラブルシューティング
| 問題 | 解決策 |
| -------------------- | ----------------------------------------- |
| "PDF のダウンロードに失敗しました" | PDF URL にアクセスでき、有効な PDF が返されることを確認します |
| "アップロードに失敗しました" | API キーが有効で、書き込み権限があることを確認します |
| "添付ファイルの作成に失敗しました" | オブジェクト ID のフィールド名が対象オブジェクトに一致していることを確認します |
## 関連
* [ワークフローのトリガー](/l/ja/user-guide/workflows/capabilities/workflow-triggers)
* [ロジック関数](/l/ja/user-guide/workflows/capabilities/workflow-actions#code)
* [Twenty から見積書または請求書を生成する](/l/ja/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. **フィルター**: ステージ = Closed Won
3. **レコード検索**: 会社の詳細を取得
4. **コード** (任意): ペイロードを整形
5. **HTTP リクエスト**: 請求書発行システムに送信
## ステップ 1: トリガーを設定
1. 新しいワークフローを作成
2. **Record is Updated** トリガーを選択
3. オブジェクトとして **Opportunity** を選択
## ステップ 2: Closed Won でフィルター
商談が受注になったときのみ続行するように、**フィルター** アクションを追加:
| 設定 | 値 |
| --------- | --------------------------- |
| **フィールド** | ステージ |
| **条件** | 等しい |
| **値** | `CLOSED_WON` (またはあなたのステージ名) |
<Tip>
このトリガーは、商談の更新があるたびに発火します。 このフィルターにより、ステージが Closed Won に変わった場合にのみワークフローが続行されます。
</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 | トリガー: Record Updated | 任意の商談が変更されたときに発火 |
| 2 | フィルター | ステージが Closed Won の場合のみ続行 |
| 3 | レコード検索 | 請求用の会社詳細を取得 |
| 4 | コード | 請求書発行 API 向けにデータを整形 |
| 5 | HTTPリクエスト | 外部システムで請求書を作成 |
## ヒント
* **外部 ID を保存**: **Update Record** アクションを使用して、API から返された請求書 ID を商談に保存
* **エラー処理**: HTTP リクエストが失敗した場合に通知を送信する分岐を追加
* **まずテスト**: 本番運用前に、請求書発行システムのサンドボックス/テストモードを使用
## 関連
* [Twenty から PDF を生成する](/l/ja/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) — 生成された PDF をレコードに添付する
* [ワークフローのトリガー](/l/ja/user-guide/workflows/capabilities/workflow-triggers)
* [ワークフローのアクション](/l/ja/user-guide/workflows/capabilities/workflow-actions)
* [Closed Won の自動化](/l/ja/user-guide/workflows/how-tos/crm-automations/closed-won-automations)

View File

@@ -0,0 +1,170 @@
---
title: Webhook トリガーの設定
description: 外部サービスからデータを受け取り、ワークフローをトリガーします。
---
Webhook トリガーは、一意の URL にデータを送信することで外部サービスがあなたのワークフローを開始できるようにします。 フォーム、サードパーティアプリ、カスタム統合に接続するために使用します。
## Webhook を使用するタイミング
| ユースケース | 例 |
| -------------- | ----------------------- |
| **Web フォーム** | お問い合わせフォームの送信でリードを作成 |
| **サードパーティアプリ** | Stripe の支払い → 顧客レコードを作成 |
| **カスタム統合** | 自社アプリ → Twenty の自動化 |
| **ノーコードツール** | Zapier、Make、n8n との連携 |
## ステップバイステップの設定
### ステップ 1: ワークフローを作成
1. 「**Settings → Workflows**」に移動
2. 「**+ New Workflow**」をクリック
3. 名前を付ける(例: "Website Form Submission")
### ステップ 2: Webhook トリガーを設定
1. トリガーブロックをクリック
2. 「**Webhook**」を選択
3. 次のような一意の Webhook URL が発行されます:
```
https://api.twenty.com/webhooks/workflow/abc123...
```
4. この URL をコピーします—外部サービスで使用します
### ステップ 3: 想定されるデータ構造を定義
**POST** リクエストの場合、想定されるボディ構造を定義します:
1. 「**Define expected body**」をクリック
2. サービスが送信する内容に一致するサンプル JSON を入力します:
```json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Acme Inc",
"message": "Interested in your product"
}
```
3. 「**Save**」をクリック—これにより、後続のステップで使用できる変数が作成されます
### ステップ 4: アクションを追加
Webhook データを使用するアクションを追加します:
**例: Person レコードを作成**
1. 「**Create Record**」アクションを追加
2. 「**People**」オブジェクトを選択
3. フィールドをマッピング:
| フィールド | 値 |
| ----- | --------------------------------------- |
| 名 | `{{trigger.body.firstName}}` |
| 姓 | `{{trigger.body.lastName}}` |
| メール | `{{trigger.body.email}}` |
| 会社 | `{{trigger.body.company}}` に基づいて検索または作成 |
### ステップ 5: Webhook をテスト
有効化する前に、Webhook をテストします:
**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. Webhook URL に対して POST リクエストを作成
2. Content-Type ヘッダーを `application/json` に設定
3. テスト用の JSON ボディを追加
4. 送信してワークフローの実行を確認
### ステップ 6: 有効化
テストが完了したら、ワークフローを稼働させるために「**Activate**」をクリックします。
## さまざまなデータ構造の扱い
### ネストされたデータ
Webhook がネストされたデータを送信する場合:
```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/ja/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions)を参照してください。
</Tip>
## 関連
* [ワークフローのトリガー](/l/ja/user-guide/workflows/capabilities/workflow-triggers)
* [ワークフローのアクション](/l/ja/user-guide/workflows/capabilities/workflow-actions)