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,55 @@
---
title: API
icon: plug
description: ワークスペースのスキーマから生成される REST と GraphQL の API。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## テナントごとのスキーマ API
Twenty に静的な API リファレンスはありません。 各ワークスペースには独自のスキーマがあり、カスタムオブジェクト(たとえば `Invoice`)を追加すると、`Company` や `Person` といった組み込みオブジェクトと同一の REST および GraphQL エンドポイントが即座に提供されます。 API はスキーマから生成されるため、エンドポイントはオブジェクト名やフィールド名を直接使用します — 不透明な ID はありません。
API キー作成後、**Settings → API & Webhooks** でワークスペース固有の API ドキュメントを利用できます。 実データに対して実際の呼び出しを実行できるインタラクティブなプレイグラウンドが含まれています。
## 2 種類の API
**コア API** — `/rest/` と `/graphql/`
レコードに対する CRUD:People、Companies、Opportunities、およびカスタムオブジェクト。 クエリ、フィルター、リレーションのトラバース。
**メタデータ API** — `/rest/metadata/` と `/metadata/`
スキーマ管理:オブジェクト、フィールド、リレーションの作成/変更/削除。 これは、プログラム的にデータモデルを変更する方法です。
どちらも REST と GraphQL で利用できます。 GraphQL では、バッチアップサートや、単一のクエリでリレーションをトラバースする機能が追加されています。 いずれの場合も、基盤となるデータは同じです。
## ベース URL
| 環境 | ベース URL |
| ------ | ------------------------- |
| クラウド | `https://api.twenty.com/` |
| セルフホスト | `https://{your-domain}/` |
## 認証
```
Authorization: Bearer YOUR_API_KEY
```
API キーは **Settings → API & Webhooks → + Create key** で作成します。 すぐにコピーしてください — 表示は一度きりです。 アクセス可能範囲を制限するため、キーは **Settings → Members → Roles → Assignment tab** で特定のロールにスコープできます。
<VimeoEmbed videoId="928786722" title="API キーの作成" />
OAuth ベースのアクセス(ユーザーに代わって動作する外部アプリ)については、[OAuth](/l/ja/developers/extend/oauth) を参照してください。
## バッチ操作
REST と GraphQL はどちらも、1 リクエストあたり最大 60 レコードのバッチ処理(作成、更新、削除)に対応しています。 GraphQL は、`CreateCompanies` のような複数形の名前を使用して、バッチアップサート(1 回の呼び出しで作成または更新)もサポートします。
## API レートリミット
| 制限 | 値 |
| ------ | ------------------- |
| リクエスト | 1 分あたり 100 回の呼び出し |
| バッチサイズ | 1 回の呼び出しあたり 60 レコード |

View File

@@ -0,0 +1,124 @@
---
title: アプリケーション設定
description: "`defineApplication` を使用して、アプリの識別情報、デフォルトのロール、変数、およびマーケットプレイスのメタデータを宣言します。"
icon: rocket
---
すべてのアプリは、`defineApplication` の呼び出しを厳密に 1 つ持つ必要があります。 ここでは次の内容を宣言します。
* **Identity** — ユニバーサル識別子、表示名、説明。
* **Permissions** — ロジック関数およびフロントコンポーネントがどのロールで実行されるか。
* **Variables** *(optional)* — コードから環境変数として利用できるキーと値のペア。
* **プレインストール / ポストインストール / アンインストール フック** *(任意)* — [Logic Functions](/l/ja/developers/extend/apps/logic/logic-functions) を参照してください。
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
注記:
* `universalIdentifier` フィールドは、あなたが所有する安定した ID です。 一度生成し、その後の同期でも安定して維持してください。
* `applicationVariables` は関数やフロントコンポーネントの環境変数になります。 ロジック関数(サーバーサイド)では、`process.env.VARIABLE_NAME` として利用できます。 フロントコンポーネントでは、`twenty-sdk/front-component` の `getApplicationVariable('VARIABLE_NAME')` を使用します。 `isSecret: true` が指定された変数は、ロジック関数にのみインジェクトされます。 フロントコンポーネントには、秘密ではない変数のみが渡されます。
* デフォルトのロールは、[`defineApplicationRole()`](/l/ja/developers/extend/apps/config/roles) でマークされたロールファイルから自動的に検出されます。`defineApplication()` から参照する必要はありません。
* プレインストール関数、ポストインストール関数、およびアンインストール関数は、マニフェストのビルド中に自動的に検出されるため、`defineApplication()` で参照する必要はありません。
* 後方互換性のために `defaultRoleUniversalIdentifier` を明示的に渡すことも依然としてサポートされていますが、`defineApplicationRole()` が推奨されるため、非推奨となっています。
* `serverVariables` はインスタンス単位の構成およびシークレット(例: API キー)です。 `applicationVariables` と異なり、マニフェスト内で値は宣言されません。ワークスペースのオペレーターがアプリの設定からそれらを入力し、設定された時点でのみロジック関数に注入されます。
* どちらの種類の変数も `isDeprecated: true` を受け付けます。 変数を削除する代わりに廃止するためにこれを使用します。キーの宣言を残しておくことで保存されている値が保持されます(削除するとオペレーターが入力した値は失われます)。また、その変数は依然としてインジェクトされるため、コードは `process.env.NEW_API_KEY ?? process.env.API_KEY` にフォールバックできます。 非推奨の変数は値がなくなるとアプリの設定から消え、アプリの構成チェックにも一切カウントされません。そのため、`isDeprecated` は `isRequired` より優先されます。
* アプリの **Settings** タブ内で(デフォルトの変数設定セクションの代わりに)カスタム設定 UI をレンダーするには、専用のファイル内で [`defineSettingsFrontComponent()`](/l/ja/developers/extend/apps/layout/front-components#custom-settings-component) を使用してフロントコンポーネントを宣言します。 アプリごとに 1 つのみ許可されています。 システム管理のセクション(自動アップグレード、App URL、接続)は常に表示されます。
## 変数の型
`applicationVariables` と `serverVariables` の両方は、オプションの `type`(および `SELECT` / `MULTI_SELECT` の場合は `options` リスト)を受け取ります。 サポートされている型: `TEXT` (既定), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`。
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
`type` は **表示とバリデーション** のみに影響します。つまり、ワークスペース設定 UI で対応する入力(トグル、数値フィールド、ドロップダウン、日付ピッカー、JSON エディタ など)を選択します。 また、ビルド時に設定を検証できるようにします(たとえば、`SELECT` / `MULTI_SELECT` では空でない `options` を宣言する必要があります)。 これは、値がコードに届く方法を**変更しません**。
値は **常に文字列として注入されます**。これは環境変数の性質によるものです(`process.env.*` は文字列のみです)。 ロジック関数が実行されるとき、エグゼキュータは宣言された `type` に従って各値をシリアライズしながら `process.env` を構築します。そのため、値がどのように設定されたか(マニフェストのデフォルト、設定 UI、あるいは以前のバージョン)に関係なく、文字列形式は一貫したものになります。
| タイプ | `process.env` の文字列 |
| ------------------------------------- | --------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | 生の値(`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | 10進数の文字列(`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | JSON 配列(`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | JSON オブジェクト(`'{"retries":3}'`) |
文字列を、想定している型に再度パースしてください:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
同じことが、`getApplicationVariable('VARIABLE_NAME')` で値を読み取るフロントコンポーネントにも当てはまります。返される値は文字列なので、必要に応じてパースしてください。
## デフォルトの関数ロール
[`defineApplicationRole()`](/l/ja/developers/extend/apps/config/roles) で宣言されたロールは、アプリのロジック関数とフロントコンポーネントがアクセスできる内容を制御します。
* ロジック関数に注入される実行時トークンは、このロールに基づいて生成されます。 人物として動作する呼び出しは、その人物が実行できる範囲によってさらに制限されるため、どちらか一方の権限を超えることはありません。 [呼び出しで使用されるアクセス権](/l/ja/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses)を参照してください。
* 型付き API クライアントは、そのロールに付与された権限に制限されます。
* 最小権限の原則に従い、関数に必要な権限のみを宣言してください。
新しいアプリをスキャフォルドすると、CLI は `src/roles/default-role.ts` にスターターロールファイルを作成します。 詳細は [Roles & Permissions](/l/ja/developers/extend/apps/config/roles) を参照してください。
## マーケットプレイスのメタデータ
アプリを[公開](/l/ja/developers/extend/apps/operations/publishing)する予定がある場合、これらの任意フィールドでマーケットプレイスでの表示方法を制御できます:
| フィールド | 説明 |
| ------------------ | -------------------------------------------------------------------------------- |
| `author` | 作成者または会社名 |
| `category` | マーケットプレイスのフィルタリング用のアプリカテゴリ |
| `logo` | `public/`にバンドルされているアプリのロゴのパス(例:`public/logo.png`) |
| `galleryImages` | `public/`にバンドルされているギャラリー画像パスの配列(例:`public/screenshot-1.png`) |
| `aboutDescription` | 「About」タブ向けの詳細な Markdown 説明。 省略した場合、マーケットプレイスは npm のパッケージにある `README.md` を使用します。 |
| `websiteUrl` | 自社ウェブサイトへのリンク |
| `termsUrl` | 利用規約へのリンク |
| `emailSupport` | サポート用メールアドレス |
| `issueReportUrl` | 課題トラッカーへのリンク |
<Note>
`logoUrl`と`screenshots`は、`logo`と`galleryImages`の別名は非推奨です。 外部絶対URL (`http://`または`https://`) はこれらのフィールドでサポートされていません: ビルド時に警告が表示されます。 アプリの `public/` フォルダに画像を束ねます。
</Note>

View File

@@ -0,0 +1,188 @@
---
title: インストールフック
description: インストール、アップグレード、アンインストールのライフサイクル中にロジックを実行し、データのシード、レコードのバックアップ、アップグレードの検証、外部リソースのクリーンアップを行います。
icon: wrench
---
インストールフックは、インストール、アップグレード、またはアンインストールのライフサイクル中に実行される特別なロジック関数です。 これらは通常の[ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions)と同じハンドラーランタイムを共有しますが、独自の define 関数で宣言され、通常のトリガーモデル (HTTP、cron、データベースイベント) の外側で動作します。 インストールフックは `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — 新規インストールでは `previousVersion` は `undefined`) を受け取り、アンインストールフックは `UninstallPayload` (`{ version?: string }` — 削除されるバージョン) を受け取ります。
各アプリは、各フック (プレインストール、ポストインストール、アンインストール) を **最大 1 つまで** 定義できます。 いずれかが複数検出された場合、マニフェストのビルドはエラーになります。
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## ひと目でわかる概要
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ------- | -------------------------------------------------------- | --------------------------------------------------------------------------------- |
| 実行タイミング | メタデータマイグレーションの前 — **以前の**スキーマとデータはまだそのまま残っている | マイグレーションと SDK 生成の後 — **新しい**スキーマが適用されている |
| 実行 | 常に同期的であり、インストールをブロックする | デフォルトでは非同期(キュー投入され、最大 3 回再試行);`shouldRunSynchronously: true` の指定で同期実行に切り替え可能 |
| 失敗時 | スキーマ変更の前にインストールが**中止**される | 非同期: 最大 3 回まで再試行される。 同期: 呼び出し元は `POST_INSTALL_ERROR` を受け取る(スキーマ変更はロールバック**されない**) |
| 典型的な用途 | マイグレーションで失われるデータのバックアップや修復を行う;スローすることでリスクの高いアップグレードを拒否する | デフォルトデータのシーディング、ワークスペースの構成、外部リソースの登録 |
**経験則:** 既定では post-install を使用する。 マイグレーション自体が破壊的で、消える前の状態を先に扱う必要がある場合にのみ、pre-install を使ってください。
| やりたいこと… | 使用 |
| ---------------------------------------- | -------------------------------------------------- |
| データのシーディング、ワークスペースの構成、外部リソースの登録 | `post-install` |
| インストール応答をブロックすべきでない長時間処理を実行する | `post-install`(既定の非同期モード。ワーカーによる再試行あり) |
| インストールが返った直後に呼び出し元がすぐに依存する高速なセットアップを実行する | `post-install`(`shouldRunSynchronously: true` を指定) |
| 次のマイグレーションで失われるデータを読み取る、またはバックアップする | `pre-install` |
| 既存データを破損させる恐れのあるアップグレードを拒否する | `pre-install`(ハンドラーからスロー) |
| すべてのアップグレードで調整処理を実行する | `shouldRunOnVersionUpgrade: true` を指定したいずれかのフック |
## 両方のフックに共通する動作
* 設定は、トリガー設定を除いた `defineLogicFunction` の設定に `shouldRunOnVersionUpgrade` を加えたものです。
* **実行タイミング**: 既定では新規インストール時のみ。 アップグレード時にも実行するには、`shouldRunOnVersionUpgrade: true` を設定します。 `previousVersion` / `newVersion` を使って、アップグレードパスに応じて分岐させます。
* **べき等性が重要です**: 非同期 post-install は再試行される可能性があり、さらに `shouldRunOnVersionUpgrade` が有効な場合はいずれのフックもアップグレード時に再実行されます。
* 通常のロジック関数の環境(`APPLICATION_ID`、`APP_ACCESS_TOKEN`、`API_URL`)が注入されるため、アプリのトークンを使って Twenty API を呼び出せます。
* フックはビルド時に自動的にアプリケーションマニフェストにアタッチされます(`preInstallLogicFunction` / `postInstallLogicFunction`)。[`defineApplication()`](/l/ja/developers/extend/apps/config/application) 内で参照する必要はありません。
* デフォルトの `timeoutSeconds` は 300 に設定されており、データシーディングのような長めのセットアップ作業を許容します。
* **開発モードでは実行されません**: `yarn twenty dev` はインストールフローをスキップしてファイルを直接同期するため、フックはそこで一切実行されません。 代わりに手動でトリガーしてください:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="ワークスペースのメタデータマイグレーションが適用された後に実行されます">
アプリのインストールが完了した後に実行されます: メタデータは同期され、SDK クライアントが生成され、新しいスキーマはクエリ可能な状態になります。 例 — 新規インストール時に既定のレコードをシードする:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
`shouldRunSynchronously` フラグは実行モデルを制御します:
* `false` *(既定)* — メッセージキューに投入され(`retryLimit: 3`)、ワーカーによって実行される。 ジョブがキューに投入されるとすぐにインストールのレスポンスが返ります。 **長時間実行される処理に使用** — 大規模データセットのシーディング、低速なサードパーティ API など。
* `true` — インストールフロー中にインラインで実行される。 ハンドラーが終了するまでインストールリクエストはブロックされます。スローされたエラーは `POST_INSTALL_ERROR` として呼び出し元に伝播します(再試行なし)。 **高速かつ、レスポンス前に完了している必要がある処理に使用します。** この時点ではマイグレーションはすでに適用済みであるため、失敗してもスキーマ変更はロールバックされず、エラーが表面化するだけです。
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="ワークスペースのメタデータマイグレーションが適用される前に実行されます">
メタデータマイグレーションの前、**以前の**スキーマに対して実行されます — マイグレーションで失われるデータをバックアップしたり、リスクの高いアップグレードを拒否したりするのに適した場所です。 実行前に、サーバーは純粋に追加のみの「簡易同期」を実行し、新しいバージョンのプレインストール関数だけを登録します。あなたのハンドラーが実行される際には、それ以外 — 以前のバージョンのオブジェクト、フィールド、データ — には一切手を触れません。
プレインストールは常に**同期的**であり、インストールをブロックします。 ハンドラーがスローした場合、スキーマ変更が行われる前にインストールは中止され、ワークスペースは一貫した状態のまま前のバージョンに留まります。 これは意図的な設計です。プレインストールは、リスクの高いアップグレードを拒否できる最後の機会です。
例 — マイグレーションでレガシーフィールドが削除される前に、その値をコピーする:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## アンインストールフック
`defineUninstallLogicFunction` は、ユーザーがアプリをアンインストールしたときに実行されるフックを宣言します。 これは、アプリのメタデータ、データ、コードが削除される **前に** 実行されます。削除マイグレーションが実行された後には、実行できるものは何も残らないため、ハンドラーはまだアプリのオブジェクトやレコードをクエリできます。 これを、外部リソースのクリーンアップに使用します。API リソースのプロビジョニング解除、残っているボットの削除、webhook の失効などです。
注記:
* このフックはベストエフォートで動作します。同期的に実行されますが、失敗してもログに記録されるだけであり、**アンインストールをブロックすることは決してありません**。クリーンアップ処理が原因でアプリを削除できなくなってはなりません。
* これは `UninstallPayload` (`{ version?: string }` — 削除されるバージョン) を受け取ります。
* 新規インストールの失敗によりロールバックされた場合には実行されません。アプリのインストールが最後まで完了していないためです。
* このフックは、アプリが削除された後には実行できないため、アプリデータ (レコードに保存されたボット ID など) に依存する外部クリーンアップ処理は、外部のスケジュールジョブではなく、ここに実装する必要があります。
* インストールフックと同様に、これは **dev モードでは実行されません**。代わりに手動でトリガーしてください:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: 概要
description: アプリ自体の設定 — アプリのアイデンティティ、デフォルト権限、およびインストール時に実行される内容を構成します。
icon: screwdriver-wrench
---
Twenty アプリの **config layer** は、アプリのアイデンティティ、保持する権限、インストールやアップグレード時に実行されるコードなど、アプリを *プラットフォームに対して* 説明するものです。 これらの宣言は新しいデータ構造や実行時の動作を追加するものではなく、アプリが *何者であるか*、そして *どのようにセットアップするか* を Twenty に伝える役割を持ちます。
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## このセクションについて
<CardGroup cols={2}>
<Card title="アプリケーション設定" icon="rocket" href="/l/ja/developers/extend/apps/config/application">
`defineApplication` — アイデンティティ、デフォルトロール、変数、マーケットプレイスのメタデータ。
</Card>
<Card title="ロールと権限" icon="shield-halved" href="/l/ja/developers/extend/apps/config/roles">
`defineRole` — アプリのロジック関数が読み書きできる内容を宣言します。
</Card>
<Card title="インストールフック" icon="wrench" href="/l/ja/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`、`definePostInstallLogicFunction`、および `defineUninstallLogicFunction` — データのバックアップ、デフォルト値の投入、アップグレードの検証、アンインストール時のクリーンアップを行います。
</Card>
</CardGroup>
## 各要素同士の関係
* **アプリケーション** がエントリポイントです。 すべてのアプリには `defineApplication()` 呼び出しが 1 つだけ存在し、その呼び出しは 1 つの **ロール** をデフォルトとして指します。
* **ロール** は、アプリのロジック関数とフロントコンポーネントが読み書きできる内容を制御します。 最小権限の原則に従い、コードが実際に必要とする権限だけを付与してください。
* **インストールフック** はインストールまたはアップグレード時に実行されます。メタデータのマイグレーション前にプレインストールが実行されるため(リスクの高いアップグレードを拒否できます)、マイグレーション後にポストインストールが実行され(新しいスキーマに対してデフォルトデータを投入できます)。 アンインストールフックはアプリが削除される直前に実行されるため、アプリのデータがまだ読み取り可能なうちに外部リソースをクリーンアップできます。
<Note>
インストールフックは [logic function](/l/ja/developers/extend/apps/logic/logic-functions) ランタイムを共有します。つまり、同じハンドラーシグネチャ、同じ環境変数、同じ型付き API クライアントを使用します。ただし、独自の define 関数で宣言され、通常のトリガーモデル(HTTP、cron、データベースイベント)とは別に存在します。
</Note>

View File

@@ -0,0 +1,67 @@
---
title: 公開アセット
description: 静的ファイル(画像、アイコン、フォントなど)を、public/ フォルダーを通じてアプリと一緒に配信します。
icon: folder-open
---
アプリのルートにある `public/` フォルダーには、実行時に必要な画像、アイコン、フォントなどの静的ファイルが格納されます。 これらのファイルはビルドに自動的に含まれ、開発モード中に同期され、サーバーにアップロードされます。
`public/` に配置されたファイルは次のとおりです:
* **公開アクセス可能** — サーバーに同期されると、アセットはパブリック URL で配信されます。 アクセスには認証は不要です。
* **フロントコンポーネントで利用可能** — アセットの URL を使用して、React コンポーネント内に画像、アイコン、その他のメディアを表示します。
* **ロジック関数で利用可能** — メール、API レスポンス、その他のサーバーサイドロジックでアセットの URL を参照します。
* **マーケットプレイスのメタデータに使用** — `logo`と`galleryImages`フィールドの`defineApplication()`参照ファイルのこのフォルダ(例:`public/logo.png`)。 アプリを公開すると、これらがマーケットプレイスに表示されます。 外部の絶対URLはこれらのフィールドでは無視されます — 代わりに`public/`で画像をバンドルします。 各画像のサイズは10 MBを超えてはいけません。
* **開発モードで自動同期** — `public/` のファイルを追加、更新、削除すると、自動的にサーバーへ同期されます。 再起動は不要です。
* **ビルドに含まれる** — `yarn twenty dev:build` はすべての公開アセットを配布物にバンドルします。
## `getPublicAssetUrl` で公開アセットにアクセスする
`twenty-sdk` の `getPublicAssetUrl` ヘルパーを使用して、`public/` ディレクトリ内のファイルのフル URL を取得します。 これはロジック関数とフロントコンポーネントの両方で機能します。
**ロジック関数内:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**フロントコンポーネント内:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
`path` 引数はアプリの `public/` フォルダーからの相対パスです。 `getPublicAssetUrl('logo.png')` と `getPublicAssetUrl('public/logo.png')` のどちらも同じ URL に解決されます—`public/` プレフィックスが付いている場合は自動的に取り除かれます。

View File

@@ -0,0 +1,203 @@
---
title: 役割と権限
description: アプリのロジック関数とフロントコンポーネントが読み取りおよび書き込みできるオブジェクトとフィールドを宣言します。
icon: shield-halved
---
**ロール**とは権限セットのことであり、アプリが読み取りまたは書き込みできるオブジェクト、参照できるフィールド、および使用できるプラットフォームレベルの機能を定義します。 すべてのアプリのロジック関数とフロントコンポーネントは、`defineApplicationRole()` でマークされたロールの権限を継承します(下記の[The default function role](#the-default-function-role)を参照してください)。
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## 行レベルセキュリティ
オブジェクトおよびフィールド権限は、ロールが操作できる *オブジェクトとフィールド* を決定します。 **行レベル
権限述語** はさらに進んで、ロールが閲覧および操作できる *レコード* を決定します。たとえば、各外部ユーザーが自分自身のレコードだけを閲覧できるセルフサービスロールのようなケースです。
ロールに対して `rowLevelPermissionPredicates` を使って述語を宣言します。 マニフェストの他の要素と同様に、各述語はそれ自身の `universalIdentifier` を持ち、オブジェクトおよびフィールドをそれぞれの `universalIdentifier`、`operand`、そして(オプションで)クエリ時に値が注入される workspaceMember フィールドによって参照します。これにより、「レコードの所有者リレーションが現在のワークスペースメンバー **である**」ことを表現できます。
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
述語はマニフェスト内に含まれて出荷されるため、インストールやアップグレードのたびに、ロールと一緒にまとめて作成・更新・削除されます。同期を保つための、別個のポストインストール手順は不要です。
### 述語とグループの組み合わせ
デフォルトでは、ロールの述語は `AND` で組み合わされます。 それらの一部を `OR` で組み合わせたり(あるいはロジックをネストしたり)するには、`rowLevelPermissionPredicateGroups` エントリを宣言し、各述語から
`predicateGroupUniversalIdentifier` を通じてそのエントリを参照させます。 このロールでは、パートナーが、自分が**所有している**か、あるいは**連絡窓口**となっている Opportunity を閲覧できます:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
注記:
* すべての述語とグループに安定した `universalIdentifier`(任意の uuid)を付与してください。これはアップグレードにまたがってエンティティを特定するキーとなり、述語はこの識別子でグループを参照します。
* 述語は、あなたのアプリが所有するオブジェクトやフィールド、または Twenty の標準オブジェクトを参照できます。
* 行レベルセキュリティは、それを含むプランのワークスペースに対して適用されます。その他のプランでも述語は引き続き同期されますが、単に適用されないだけです。
## デフォルトの関数ロール
新しいアプリをスキャフォルドすると、CLI は `defineApplicationRole()` で宣言されたデフォルトのロールファイルを作成します。
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` は `defineRole()` の軽量なラッパーであり、インストール時にアプリケーションのデフォルトとして使用されるロールをフラグ付けします。 検証は `defineRole` と同一ですが、ビルドパイプラインがその `universalIdentifier` をアプリケーションマニフェストの `defaultRoleUniversalIdentifier` に自動的に関連付けるため、[`defineApplication`](/l/ja/developers/extend/apps/config/application) から参照する必要はありません。
注記:
* 各アプリにつき `defineApplicationRole(...)` は **1 つだけ** 許可されます。2 つ以上見つかった場合、マニフェストのビルドは失敗します。
* アプリが提供する**追加**ロールには、`defineApplicationRole()` ではなく `defineRole()` を使用してください。
* `defineApplication()` で `defaultRoleUniversalIdentifier` を明示的に設定することは後方互換性のため引き続きサポートされていますが、非推奨であり、代わりに `defineApplicationRole()` の使用が推奨されます。
## ベストプラクティス
* スキャフォールドされたロールから開始し、徐々に制限を強めてください。デフォルトでは幅広い読み取りアクセスが付与されていますが、本番環境でこれをそのまま使いたいケースはほとんどありません。
* `objectPermissions` と `fieldPermissions` を、関数が実際に必要とするオブジェクトとフィールドだけに置き換えてください。
* `permissionFlagUniversalIdentifiers` はプラットフォームレベルの機能へのアクセスを制御します。 できるだけ最小限に保ってください。
* 動作例を参照してください。[`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts)。

View File

@@ -0,0 +1,52 @@
---
title: オブジェクトの拡張
description: 標準の Twenty オブジェクト(Person、Company など)にフィールドを追加します。 または、`defineField` を使用して他のアプリのオブジェクトにフィールドを追加します。
icon: wand-magic-sparkles
---
`defineField()` を使用すると、あなたが所有していないオブジェクト ― Person や Company のような標準の Twenty オブジェクトや、他のインストール済みアプリから提供されるオブジェクト ― にフィールドを追加できます。 [`defineObject`](/l/ja/developers/extend/apps/data/objects) 内で宣言されるインラインフィールドと異なり、スタンドアロンフィールドでは、どのオブジェクトを拡張するかを指定するために `objectUniversalIdentifier` が必要です。
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
## 主なポイント
* `objectUniversalIdentifier` は対象オブジェクトを識別します。 標準の Twenty オブジェクトの場合は、`twenty-sdk` から定数を import します:
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* フィールドを **`defineObject()` 内でインライン定義する** 場合、`objectUniversalIdentifier` は **不要** です — 親オブジェクトから継承されます。
* `defineField()` は、`defineObject()` で作成していないオブジェクトにフィールドを追加する唯一の方法です。
* ファイルの配置場所は任意です。 慣例としては `src/fields/\<name>.field.ts` ですが、SDK は `src/` 内の任意の場所にあるフィールドを検出します。
* `writability` はフィールド単位でも設定できます。`MetadataWritability.APPLICATION` は書き込みをアプリ自身のロジック関数に制限し、`MetadataWritability.SYSTEM` はプラットフォームによって管理されるデータ専用に予約されています。 フィールドの書き込み可能性は、そのフィールドが属するオブジェクトのレベルよりも厳しくすることしかできず、読み取りには影響しません。 詳細な仕様については [Objects](/l/ja/developers/extend/apps/data/objects) を参照してください。
* 標準ページレイアウト(例:Task や Company の詳細ページ)にタブを追加するには、`twenty-sdk/define` の `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` とともに [`definePageLayoutTab`](/l/ja/developers/extend/apps/layout/page-layouts#definepagelayouttab) を使用します。
## 既存のオブジェクトへのリレーションの追加
リレーションフィールド(例: カスタムオブジェクトを標準の `Person` にリンクする)を追加するには、`FieldType.RELATION` を指定して `defineField()` を使用します。 パターンはインラインのリレーションと同じですが、`objectUniversalIdentifier` を明示的に設定します。 双方向のパターンについては、[Relations](/l/ja/developers/extend/apps/data/relations) を参照してください。

View File

@@ -0,0 +1,143 @@
---
title: オブジェクト
description: defineObject を使用して、新しいレコードタイプ(独自のフィールドを持つカスタムテーブル)を宣言します。
icon: table
---
カスタム**オブジェクト**は、アプリがワークスペースに追加する新しいレコードタイプです。ポストカード、請求書、サブスクリプションなど、あなたのドメインに特化したあらゆるものを定義できます。 各オブジェクトは、そのスキーマ(フィールド、リレーション、デフォルト値)と、同期やデプロイをまたいで維持される安定したユニバーサル識別子を宣言します。
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
## 主なポイント
* `universalIdentifier` は、デプロイをまたいで一意かつ安定している必要があります。
* 各フィールドには、`name`、`type`、`label`、および自身の安定した `universalIdentifier` が必要です。
* `fields` 配列は任意です。カスタムフィールドなしでオブジェクトを定義できます。
* `openRecordIn` は、このオブジェクトのレコードがクリックされたときにどこで開くかを設定します。`ObjectOpenRecordIn.USER_CHOICE`(デフォルト。各ワークスペースメンバーの[Settings → Experience]での設定に従います)、`ObjectOpenRecordIn.SIDE_PANEL`、または `ObjectOpenRecordIn.RECORD_PAGE` のいずれかです。 ワークフローやダッシュボードのように、レコードを使うためにフルページが必要な場合には `RECORD_PAGE` にピン留めします。カレンダーイベントのように、クイックパネルとして表示する方が適しているレコードの場合には `SIDE_PANEL` にピン留めします。
* `writability` は、ロール権限が適用される前の段階で、そのオブジェクトのレコードを書き込むことができる対象を制御します。`MetadataWritability.OPEN`(デフォルト — ワークスペースロールが決定)、`MetadataWritability.APPLICATION`(あなたのアプリ独自のロジック関数だけがレコードを作成・更新・削除できます。これは、そのレコードが挙動を付与する設定的なオブジェクトに使用し、幅広いレコードアクセス権を持つワークスペースメンバーが API 経由でそれらを編集できないようにします)、`MetadataWritability.SYSTEM`(プラットフォームで管理されるデータ用に予約)があります。 読み取りには影響しません。これはサーバー側で強制されるのに対し、`isUIEditable` は UI 上の操作手段を非表示にするだけです。 これはフィールド単位でも存在し、その場合はオブジェクトレベルよりも厳しく設定することしかできません。
* ここで定義されたインラインフィールドには `objectUniversalIdentifier` は**不要**です — 親オブジェクトから継承されます。 所有していないオブジェクトにフィールドを追加するには、[`defineField()`](/l/ja/developers/extend/apps/data/extending-objects) を使用します。
* `yarn twenty dev:add object` を使用すれば、新しいオブジェクトをスキャフォルドできます。名前、フィールド、リレーションシップの設定をガイドしてくれます。 [アーキテクチャ → エンティティのスキャフォールディング](/l/ja/developers/extend/apps/getting-started/scaffolding) を参照してください。
<Note>
**ベースフィールドは自動的に追加されます。** カスタムオブジェクトを定義すると、Twenty は `id`、`name`、`createdAt`、`updatedAt`、`createdBy`、`updatedBy`、`deletedAt` などの標準フィールドを自動的に作成します。 これらを `fields` 配列で宣言する必要はありません。カスタムフィールドのみを追加してください。 同じ名前でフィールドを宣言することでデフォルトフィールドを上書きすることもできますが、これはほとんどの場合お勧めできません。
</Note>
## フィールドタイプ
`twenty-sdk/define` からエクスポートされる、`FieldType` 値の完全な一覧:
| カテゴリ | タイプ |
| ---------- | ------------------------------------------------------------------------------------------------------------ |
| テキスト | `TEXT`、`RICH_TEXT`、`ARRAY`(文字列の配列)、`RAW_JSON` |
| 数値 | `NUMBER`(`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`)、`NUMERIC`(任意精度)、`RATING`、`POSITION` |
| 日付 | `DATE`, `DATE_TIME` |
| 選択 | `BOOLEAN`、`SELECT`、`MULTI_SELECT` |
| 複合 | `FULL_NAME`、`ADDRESS`、`EMAILS`、`PHONES`、`LINKS`、`CURRENCY`、`ACTOR`、`FILES` |
| 識別子とリレーション | `UUID`、`RELATION`、`MORPH_RELATION`([Relations](/l/ja/developers/extend/apps/data/relations) を参照) |
| システム | `TS_VECTOR`(サーバーによって管理される全文検索ベクター) |
複合タイプは複数のサブフィールドを保持します(例: `FULL_NAME` = 名 + 姓、`CURRENCY` = `amountMicros` + `currencyCode`)。 `SELECT` と `MULTI_SELECT` は、上記の例のように `options` 配列を必要とします。
## デフォルト値
文字列リテラルのデフォルト値は、文字列**の内部で**シングルクォートで囲む必要があります。つまり、`defaultValue: "'Draft'"` のように書き、`defaultValue: "Draft"` のようには書きません。 そのため上記の `status` フィールドでは、`` `'${PostCardStatus.DRAFT}'` `` を使用しています。
引用符で囲まれていない文字列は、レコード作成時に評価される計算済みデフォルト値用に予約されています。
* `'uuid'` — `UUID` フィールド用に UUID を生成します。
* `'now'` — 現在のタイムスタンプ(`DATE_TIME` フィールド用)を表します。
同じ規則が、複合デフォルトの文字列サブフィールド(例: `ACTOR` フィールド上の `{ source: "'MANUAL'" }`)および `SELECT` / `MULTI_SELECT` の値にも適用されます。 引用符で囲まれていないリテラル文字列のデフォルト値がある場合、アプリのビルド時に警告が発生します。
## NULL 許容性
`isNullable` は、フィールドが `NULL` を受け付けるかどうかを制御します。 デフォルトは `true` で、任意のフィールドでは省略できます。 データベースレベルでフィールドを必須にするには、`isNullable: false` を設定します。
`isNullable` への変更は、既存フィールドを更新する同期を含め、すべての同期で適用されます。そのため、マニフェストを編集して再同期することで、フィールドの NULL 許容性を切り替えることができます。
<Note>
**既存のフィールドを NULL 非許容にするには、デフォルト値が必要です。** フィールドを `isNullable: false` に変更する場合は、NULL ではない `defaultValue` も指定する必要があります。 デフォルト値は、`NOT NULL` 制約が適用される前に既存の `NULL` 行をすべてバックフィルします。これがない場合、同期は `Default value cannot be null for non-nullable fields` というエラーで失敗します。 リレーションフィールドおよび `TS_VECTOR` フィールドは常に NULL を許容するため、`isNullable` はそれらには影響しません。
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## 次のステップ
* **このオブジェクトを他のオブジェクトに接続する** — 双方向リレーションパターンについては [Relations](/l/ja/developers/extend/apps/data/relations) を参照してください。
* **他のアプリのオブジェクトにフィールドを追加する** — `defineField()` については [Extending Objects](/l/ja/developers/extend/apps/data/extending-objects) を参照してください。
* **このオブジェクトを UI に表示します** — サイドバー項目を追加するには [Navigation Menu Items](/l/ja/developers/extend/apps/layout/navigation-menu-items) を参照し、カスタムリスト構成を追加するには [Views](/l/ja/developers/extend/apps/layout/views) を参照してください。

View File

@@ -0,0 +1,101 @@
---
title: 概要
description: ワークスペースにアプリが追加するデータ ― オブジェクト、フィールド、リレーション ― の形を整えます。
icon: database
---
Twenty アプリの**データレイヤー**とは、アプリがワークスペースに*追加*するデータのことです。宣言する新しいレコードタイプ、既存オブジェクトに追加するカラム、およびそれらのレコード同士がどのように接続されるかを指します。
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## このセクションについて
<CardGroup cols={2}>
<Card title="オブジェクト" icon="table" href="/l/ja/developers/extend/apps/data/objects">
`defineObject` — 固有のフィールドを持つ新しいレコードタイプを宣言します。
</Card>
<Card title="オブジェクトの拡張" icon="wand-magic-sparkles" href="/l/ja/developers/extend/apps/data/extending-objects">
`defineField` — 標準オブジェクトや他のアプリのオブジェクトにフィールドを追加します。
</Card>
<Card title="関連" icon="diagram-project" href="/l/ja/developers/extend/apps/data/relations">
オブジェクト間の双方向 `MANY_TO_ONE` / `ONE_TO_MANY` 接続。
</Card>
<Card title="タイムライン活動タイプ" icon="clock-rotate-left" href="/l/ja/developers/extend/apps/data/timeline-activity-types">
レコードタイムライン用の自動監査イベントと明示的なアプリイベントを定義します。
</Card>
</CardGroup>
## エンティティの概要
| エンティティ | 目的 | 定義方法 |
| --------------- | ------------------------------------------------------------------------------- | -------------------------------------- |
| **オブジェクト** | 固有のフィールドを持つ新しいカスタムレコードタイプ(例: PostCard、Invoice) | `defineObject()` |
| **フィールド** | オブジェクト上のカラム。 スタンドアロンフィールドは、作成していないオブジェクトも拡張できます(例: Company に `loyaltyTier` を追加) | `defineField()` |
| **リレーション** | 2 つのオブジェクト間の双方向リンク ― 両側がフィールドとして宣言されます | `defineField()` と `FieldType.RELATION` |
| **インデックス** | オブジェクトの 1 つに対する定期的なクエリを高速化するためのデータベースインデックス | `defineIndex()` |
| **タイムライン活動タイプ** | レコードタイムラインに表示される自動監査イベントまたは明示的なドメインイベント | `defineTimelineActivityType()` |
SDK はビルド時に AST 解析を通じてこれらを検出するため、ファイル構成は自由です — 慣例としては `src/objects/`、`src/fields/`、`src/indexes/`、`src/timeline-activity-types/` です。 安定した `universalIdentifier` UUID が、デプロイをまたいであらゆるものを結び付けます。
## インデックス(任意)
アプリは、オブジェクトと一緒にインデックスを同梱しておくことで、繰り返し実行されるクエリを高速に保つことができます。 最も一般的なケースは、頻繁に読み取るステータス列や外部キー列です。
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### 一意のインデックス
`defineIndex` は、単一列と複数列のどちらの一意性でも、`isUnique: true` を受け付けます。 これは推奨されるプリミティブです — `defineField({ isUnique: true })` は非推奨であり、今後のリリースで削除されます。
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### その他の制約
* 部分的な `WHERE` 句は管理者の管理下にあり、アプリ側で宣言することはできません。
* 各オブジェクトにはカスタムインデックスを最大 10 個まで設定できます(フレームワーク自身のインデックスはカウントされません)。
`fields` 配列は、Postgres に使わせたい順序に並べてください ― 電話帳のように、最も左の列が先になります。 インデックスにはコストがかかります。テーブルへの書き込みのたびに更新されます。 必要とするクエリがある場合にのみインデックスを追加してください。
<Note>
**Application Config** や **Roles & Permissions** をお探しですか? それらは、アプリが追加するデータではなくアプリ自体を記述するものであり、[Config](/l/ja/developers/extend/apps/config/overview) 配下にあります。 **Connections**(Linear、GitHub、Slack OAuth)をお探しですか? それらはロジック関数*から*呼び出されるために存在し、[Logic](/l/ja/developers/extend/apps/logic/connections) 配下にあります。
</Note>

View File

@@ -0,0 +1,184 @@
---
title: 関連
description: 双方向の MANY_TO_ONE / ONE_TO_MANY リレーションでオブジェクト同士を接続します。
icon: diagram-project
---
リレーションは 2 つのオブジェクト同士を関連付けます。 Twenty におけるリレーションは常に **双方向** です — すべてのリレーションには 2 つの側面があり、それぞれが相手を参照するフィールドとして宣言されます。
| リレーションタイプ | 説明 | 外部キーあり? |
| ------------- | --------------------------------- | -------------------- |
| `MANY_TO_ONE` | このオブジェクトの多数のレコードが、対象の 1 件のレコードを指す | はい(`joinColumnName`) |
| `ONE_TO_MANY` | このオブジェクトの 1 件のレコードが、対象の多数のレコードを持つ | いいえ(逆側) |
## リレーションの仕組み
すべてのリレーションには、互いを参照する**2 つのフィールド**が必要です:
1. **MANY_TO_ONE** 側 — 外部キーを保持するオブジェクト側に存在します。
2. **ONE_TO_MANY** 側 — コレクションを所有するオブジェクト側に存在します。
両方のフィールドは `FieldType.RELATION` を使用し、`relationTargetFieldMetadataUniversalIdentifier` を介して相互参照します。
## 例: PostCard は多数の Recipient を持つ
`PostCard` は多数の `PostCardRecipient` レコードに送信できます。 各受取人は厳密に 1 件の PostCard に属します。
**ステップ 1: PostCard に ONE_TO_MANY 側を定義**(「1」の側):
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**ステップ 2: PostCardRecipient に MANY_TO_ONE 側を定義**(「多」の側 — 外部キーを保持):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**循環インポート:** 両方のリレーションフィールドは互いの `universalIdentifier` を参照します。 循環インポートの問題を避けるため、各ファイルからフィールド ID を名前付き定数としてエクスポートし、相手のファイルでインポートしてください。 ビルドシステムがコンパイル時にこれらを解決します。
</Note>
## 中間リレーション
中間オブジェクトは、1つのソースレコードを1つ以上のターゲットレコードに接続します。 たとえば、`PostCardRecipient` は `postCard` リレーションと `recipient` リレーションの両方を保持できます。 ソースオブジェクトの `ONE_TO_MANY` フィールドで、`junctionTargetFieldUniversalIdentifier` を、ターゲットを指す中間オブジェクト上のリレーションフィールドに設定します:
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` は、中間オブジェクト上の `MANY_TO_ONE` または morph リレーションを識別する必要があります。 これにより、[タイムラインアクティビティのファンアウト](/l/ja/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events)などの機能が中間オブジェクトをたどる際に、どの関連レコードがターゲットであるかをTwentyに伝えます。
## 標準オブジェクトとのリレーション
組み込みの Twenty オブジェクト(Person、Company など)とリレーションを作成するには、`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` を使用します:
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
## リレーションフィールドのプロパティ
| プロパティ | 必須 | 説明 |
| ---------------------------------------------------------- | ------------------- | ------------------------------------------------------------------- |
| `type` | はい | `FieldType.RELATION` でなければなりません |
| `relationTargetObjectMetadataUniversalIdentifier` | はい | 対象オブジェクトの `universalIdentifier` |
| `relationTargetFieldMetadataUniversalIdentifier` | はい | 対象オブジェクト上の対応するフィールドの `universalIdentifier` |
| `universalSettings.relationType` | はい | `RelationType.MANY_TO_ONE` または `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE のみ | 参照先レコードが削除されたときの動作: `CASCADE`、`SET_NULL`、`RESTRICT`、または `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE のみ | 外部キーのデータベース列名(例: `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | 中間 `ONE_TO_MANY` のみ | ファンアウトターゲットを指す、中間オブジェクト上のリレーションフィールド |
## インラインのリレーションフィールド
[`defineObject`](/l/ja/developers/extend/apps/data/objects) の中で、リレーションを直接宣言することもできます。 インラインで宣言する場合は、`objectUniversalIdentifier` を省略します — 親オブジェクトから継承されます:
```ts
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// … other fields
],
});
```

View File

@@ -0,0 +1,201 @@
---
title: システムメタデータのターゲット指定
description: Twenty がすべてのオブジェクトに自動的にプロビジョニングするメタデータの決定論的なユニバーサル識別子を解決し、アプリがハードコーディングせずに参照できるようにします。
icon: gears
---
Twenty のすべてのオブジェクトには、フィールドのセットや、そのカラムを持つメインのリストビューなど、あなた自身が宣言する必要のない **システムメタデータ** が付属しています。 オブジェクトがプロビジョニングされるときに、そのすべてをサーバーが作成し、そのセットは Twenty の拡張に合わせて増えていきます。
あなたがそれを宣言しないため、インポートできる `universalIdentifier` 定数は存在しません。 その代わりに、サーバーはそれぞれの識別子を決定論的に**導出**し、`twenty-sdk` が同じ導出処理を公開することで、マニフェストがサーバーで使用される正確な値を解決できるようにします。
## システムフィールド
すべてのオブジェクトに存在し、いずれも [`defineField()`](/l/ja/developers/extend/apps/data/extending-objects) で宣言しないスカラー フィールドは次のとおりです。
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
では、[view](/l/ja/developers/extend/apps/layout/views) で `createdAt` をカラムとしてどのように参照すればよいのでしょうか。
### 問題
Twenty 2.19 以降、システムフィールドのユニバーサル識別子は、アプリケーションのユニバーサル識別子、オブジェクトのユニバーサル識別子、およびフィールド名という 3 つの入力からサーバーによって **決定的に導出** されます。 id を独自に作成してハードコードしても機能しません。サーバー上のどの値とも一致せず、同期時にそのぶら下がった参照は拒否されます。
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### 解決策
<Note>
`getFieldUniversalIdentifier` は `twenty-sdk` 2.21 以降で利用可能です。
</Note>
`getFieldUniversalIdentifier` を使用して、サーバーが使用するものとまったく同じ値を解決します。 この関数は 3 つの入力を受け取り、フィールドのユニバーサル識別子を返します。
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` はあなたのアプリの識別子であり、[`defineApplication()`](/l/ja/developers/extend/apps/config/application) に渡すものです。
* `objectUniversalIdentifier` は、そのフィールドが属するオブジェクトの識別子です。
* `name` はシステムフィールド名であり、上に挙げた値のいずれかです。
### 例: ビュー内の createdAt カラム
典型的なケースは、カスタムオブジェクトの 1 つのビューに `createdAt` カラムを追加することです。 フィールド id を解決し、他の `fieldMetadataUniversalIdentifier` と同様にそれを参照します。
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
同じように解決された id は、`fieldMetadataUniversalIdentifier` が必要とされるあらゆる場所、すなわちビューのフィールド、フィルター、ソート、グループ、ページレイアウトウィジェットでそのまま使用できます。
<Note>
id は解決して、ハードコードしないでください。 サーバーはその値をアプリケーション id、オブジェクト id、フィールド名から導出するため、`getFieldUniversalIdentifier` を呼び出すことで、それらの入力が変更された場合でも参照を正しい状態に保ち、導出方法が将来変わった場合のずれも防ぐことができます。
</Note>
### システムリレーションフィールド
<Note>
`getSystemRelationFieldUniversalIdentifier` は `twenty-sdk`
2.23 以降で利用可能であり、バージョン 2.23 以降の Twenty サーバーが必要です。
</Note>
上記のスカラー型システムフィールドに加えて、サーバーはすべてのオブジェクトに対して 4 つの**システムリレーションフィールド**を提供します。`timelineActivities`、`attachments`、`noteTargets`、`taskTargets` であり、それぞれが対応する標準リレーションオブジェクトを参照します。
これらのフィールドは `getFieldUniversalIdentifier` では解決されません。フィールドを保持しているオブジェクトと、そのフィールドが参照するオブジェクトから、**名前に依存せず**識別子が導出されます。 この方法により、オブジェクトの名称を変更しても、そのリレーションフィールドの識別子は決して変わりません。
それらを解決するには、`getSystemRelationFieldUniversalIdentifier` を使用します。
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` は、そのフィールドを **保持している** オブジェクトです。
* `relationTargetObjectUniversalIdentifier` は、そのフィールドが **参照している** オブジェクトです。
方向は引数の順序によってエンコードされます。 逆方向(例: `attachment.targetRocket`。これは、標準リレーションオブジェクト上でサーバーが作成する morph フィールド)を解決するには、2 つを入れ替えます。
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
スカラーのシステムフィールドと同様に、解決された ID は `fieldMetadataUniversalIdentifier` が想定されるあらゆる場所で使用できます。
## システムビュー
<Note>
`getSystemViewUniversalIdentifier` と `getSystemViewFieldUniversalIdentifier` は `twenty-sdk` 2.26 以降で利用可能であり、バージョン 2.26 以降の Twenty サーバーが必要です。
</Note>
サーバーは、すべてのオブジェクトに対して **システムビュー** もプロビジョニングします。つまり、表示可能なフィールドごとに 1 つのカラムを持つメインのリストビュー(`All {objectLabelPlural}`、キーは `ViewKey.INDEX`(`SYSTEM_VIEW_KEYS.INDEX` から導出))です。 システムリレーションフィールドと同様に、それらの識別子は **名前に依存しない** 形で導出されるため、オブジェクトやフィールドの名前を変更しても識別子は変わりません。
ビューを解決するには、`getSystemViewUniversalIdentifier` を使用します。
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` は **オブジェクト** を所有しているアプリケーションであり、ビューが名前空間分割されている単位です。
* `objectUniversalIdentifier` は、そのビューがリストするオブジェクトです。
* `viewKey` はシステムビューを識別するための判別子です。メインのリストビューには `SYSTEM_VIEW_KEYS.INDEX`、レコードページのフィールドウィジェットのビューには `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` を使用します。 これは導出のキーとなる値であり、ビューの行に保存されるのは `INDEX` のみです。
解決された ID は、[`NavigationMenuItemType.VIEW`](/l/ja/developers/extend/apps/layout/navigation-menu-items) のサイドバーエントリなど、`viewUniversalIdentifier` が必要とされるあらゆる場所で使用できます。 オブジェクトのメインリストを単に開くだけであれば、導出が不要な `NavigationMenuItemType.OBJECT` と `targetObjectUniversalIdentifier` を優先して使用してください。
`getSystemViewFieldUniversalIdentifier` は、ビューとそのビューが表示するフィールドから、システムビュー上の 1 つの**カラム**を解決します。
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
最初の引数に注目してください。カラムは、そのビューではなく、**表示しているフィールド** を所有するアプリケーションで名前空間分割されます。 あなたのアプリが標準オブジェクトに追加したフィールドは、Twenty が所有するビュー上で、そのアプリケーションの名前空間の下にカラムが導出されます。
<Warning>
システムビューとそのカラムは **サーバー所有** です。これらの識別子は、宣言ではなく参照のためにのみ解決してください。 [`defineView()`](/l/ja/developers/extend/apps/layout/views) の `key` は非推奨で無視されます。そのため、マニフェストのビューが `INDEX` キーを主張することはできません。また、サーバーはあなたが追加するすべてのフィールドに対してすでにカラムをプロビジョニングしているため、同じフィールドについてシステムビュー上で独自の `defineViewField()` を宣言すると競合が発生します。
</Warning>
## 標準の Twenty オブジェクト
**標準** の Twenty オブジェクト(Person、Company、Opportunity など)の場合、何も導出する必要はありません。フィールドおよびビューの識別子は、あらかじめ計算された定数として用意されており、直接インポートできます。
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
[`defineObject()`](/l/ja/developers/extend/apps/data/objects) で **あなたのアプリ** が定義するオブジェクトのように、そのような定数が存在しない場合には、ここまでで紹介したヘルパーを使用してください。
<Note>
`name` はシステムフィールドではなく、**デフォルト** フィールドです。 このフィールドは独自のハードコードされたユニバーサル識別子を持ち、`getFieldUniversalIdentifier` で解決されることはありません。 あなたが定義したオブジェクトでは、`name` フィールドは `defineObject()` で与えた識別子で参照してください。
</Note>

View File

@@ -0,0 +1,184 @@
---
title: タイムライン活動タイプ
description: 自動監査イベントと、レコードタイムラインでレンダリングされる明示的なアプリイベントを定義します。
icon: clock-rotate-left
---
タイムラインアクティビティタイプは、レコードのタイムラインに表示されるイベントの安定したボキャブラリーを定義します。 標準オブジェクトとアプリオブジェクトは同じコントラクトを使用します。タイプにはラベルとアイコンがあり、出力時にオプションで宣言できます。 アプリの [front components](/l/ja/developers/extend/apps/layout/front-components) を使ってレンダリングすることもできます。
<Note>
タイムラインアクティビティタイプはベータ版で、Twenty 2.34では間もなく公開されます。
APIは、アプリ開発者のユースケースから学びながら進化することができます。
</Note>
足場を使って1つ作成します:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
または直接定義します。
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## 自動および明示的なイベント
`emit` を追加すると、このタイプが自動的に作成されます。 `emit.on`は`created`、`updated`、`deleted`、`restored`、`linked`、`unlinked`、`emit.objectUniversalIdentifier`はソースオブジェクトを識別します。 ワークスペースで同じエミットキー(`on`、オブジェクト、オプション)を扱うことができるのは、有効な型が 1 つだけです。
`emit.through `がなければ、イベントはソースレコードの独自のタイムラインに書き込まれます。 関連するレコードにファンアウトするには、 \`emit.through を設定します。 elationFieldUniversalIdentifier: 直接多対一のリレーション、またはソースオブジェクトの [1対多の接合関係](/l/ja/developers/extend/apps/data/relations#junction-relations) のいずれかへ。 Morphリレーションは彼らのmorphグループのすべてのメンバーにファンアウトするので、1つの宣言はいくつかのオブジェクトタイプをターゲットにすることができます。
直接のリレーションのため、ソースレコードを作成または復元すると、 `linked` が生成され、削除すると `unlinked` が生成されます。 リレーションは前のターゲットに `unlinked` を加え、新しいターゲットに `linked` を加えます。 その他のソースアップデートではリンクイベントは発生しません。 これは添付ファイルが使用する契約で、ターゲットは直接モーフ関係です。
ジャンクションリレーションでは、`universalSettings.junctionTargetFieldUniversalIdentifier` はジャンクションオブジェクトからターゲットへのリレーションを識別します。 ジャンクション行を作成または削除すると、`linked` または `unlinked` が生成されます。 ジャンクション行のいずれかの側を再挿入すると、リンクイベントも生成されます。関連しないジャンクションフィールドへの更新は行われません。
`linked` と `unlinked` イベントは `emit.through `が必要です。トリガーは設定された直接またはジャンクションの関係を変更するためです。
例えば、これはメモ、タスク、メッセージ、カレンダーイベントで使用される一般的なコントラクトと同じです。
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
デフォルトでは、ソースの更新ごとにイベントの `updated` が発行されます。 選択したソースフィールドにのみ変更する場合、ターゲットタイムラインに「emit.through.triggerFieldUniversalIdentifiers」を設定します。
ロジック関数がそれ自身を作成する明示的なドメインイベントに対して「emit」を省略します。 これにより、同じ操作に対して自動監査行と明示的な行の両方を生成することができません。
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
[`createTimelineActivity()`] (/developers/extend/apps/logic/logic-functions#create-a-timeline-activity ) を使用して明示的なイベントを作成します。 アプリコードは安定したユニバーサル識別子を使用します; Twenty はインストール固有のメタデータ ID を解決します。
## カスタムレンダリング
Twenty は、フロントコンポーネントがない場合、タイプラベル、アイコン、およびリンクオブジェクトメタデータからネイティブの一般的な行をレンダリングします。 これはオブジェクト固有のレンダラーを持たない標準オブジェクトおよびカスタムオブジェクトに対して機能します。
カスタムの詳細については、 `frontComponentUniversalIdentifier` を同じアプリが所有するフロントコンポーネントに設定します。
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
ネイティブの行は、折りたたまれたプレゼンテーションのままです。 Twenty は、ユーザーがその行を展開した後にのみフロントコンポーネントをマウントし、すべての表示イベントに対して Sandbox と worker を回避します。 コンポーネント内で、`二十-sdk/front-component` から `useTimelineActivityId()` を呼び出し、行IDを読み込み、プレゼンテーションの必要なデータを取得します。 コンポーネントがタイムライン行の外側にレンダリングされると、 `null` を返します。
## 別のアプリケーションのプレゼンテーションを上書きする
アプリケーションは、独自のオブジェクトの自動タイムライン契約を所有します。 別のアプリが所有するオブジェクトでイベントをカスタマイズするには、既存の型を `replaceTimelineActivityTypeUniversalIdentifier` で明示的に宣言します。
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
explicit-only 型ではなく、既存の自動エミットスロットを置き換えるため、オーバーライドは `emit` を含める必要があります。 参照される型は、ターゲットオブジェクトのアプリケーションに属し、同じアクションとルートを記述する必要があります。 オーバーライドアプリを削除すると、ベースタイプが復元され、ベースタイプが削除されるとオーバーライドは無効になります。
## ワークスペースの上書きとミュート
ワークスペース管理者は、タイプのプレゼンテーションを変更したり、アプリケーションをフォークすることなく、自動および明示的なイベントをミュートすることができます。 インストール固有の型 ID を持つメタデータ API の `updateTimelineActivityType` 変更を使用します。
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
`label`と`icon`はワークスペースの上書きとして保存されるため、後でアプリケーションの更新は管理者の選択肢を上書きしません。 自動発行を停止し、そのタイプの新しい明示的イベントを拒否するには、`isActive` を `false` に設定します。 既存の行は表示されたままです。
アクティブ状態を含むアプリケーションのデフォルトを復元するには、次を使用します。
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## 設定フィールド
| フィールド | 必須 | 説明 |
| ------------------------------------------------ | ------------ | --------------------------------------------- |
| `universalIdentifier` | はい | インストールおよびアップグレードをまたいで安定したタイプの UUID |
| `name` | はい | アプリ内のプログラム用名前。アプリ内で一意である必要があります。 |
| `label` | はい | ネイティブレンダラーで使用されるユーザー向けアクションテキスト |
| `icon` | いいえ | イベントの横に表示される Twenty アイコン名 |
| `emit` | いいえ | 自動発行の宣言。明示的イベント専用のタイプでは省略します |
| `emit.on` | emit を使用する場合 | このタイプが書き込まれる原因となる監査アクション |
| `emit.objectUniversalIdentifier` | emit を使用する場合 | レコードがこのタイプを発行するオブジェクト |
| `emit.through.relationFieldUniversalIdentifier` | いいえ | 関連するタイムラインにファンアウトするために使用されるダイレクトまたはジャンクションの関係 |
| `emit.through.triggerFieldUniversalIdentifiers` | いいえ | `updated` 経由イベントをトリガーできるソースフィールド |
| `frontComponentUniversalIdentifier` | いいえ | 行が展開されたときにマウントされたアプリ所有のフロントコンポーネントです |
| `replaceTimelineActivityTypeUniversalIdentifier` | いいえ | 既存のエミットタイプを置き換えるには、 `emit` が必要です |
イベントが作成されたときに意味的なアイデンティティと耐久性のあるプレゼンテーションのフォールバックを20枚のスナップショット。 歴史的な行は、元のアクションとオブジェクトの意味を保持します。 タイプがインストールされている間は、現在翻訳されているラベル、アイコン、およびフロントコンポーネントがライブレンダリングされます。アンインストール後、スナップショットのフォールバックは行を読み取ることができます。 解像度は普遍的な識別子を使用するため、プレゼンテーションはアプリの再インストールも残ります。

View File

@@ -0,0 +1,102 @@
---
title: 概念
description: Twenty アプリの動作 — エンティティモデル、サンドボックス化、およびインストールライフサイクル。
icon: sitemap
---
Twenty アプリは、カスタムオブジェクト、ロジック、UI コンポーネント、AI 機能でワークスペースを拡張する TypeScript パッケージです。 それらは、完全なサンドボックス化と権限制御のもとで Twenty プラットフォーム上で実行されます。
## アプリの仕組み
アプリは、`twenty-sdk` パッケージの `defineEntity()` 関数を使って宣言された**エンティティ**の集合です。 SDK はビルド時に AST 解析によってこれらの宣言を検出し、**マニフェスト**を生成します—アプリがワークスペースに追加する内容を完全に記述したものです。 これらの関数はビルド時に設定を検証し、IDE の自動補完と型安全性を提供します。
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**ファイル構成は自由です。** エンティティの検出は AST ベースです—ファイルの場所に関係なく、SDK は `export default defineEntity(...)` の呼び出しを見つけます。 上記のフォルダー構成は慣例であり、必須条件ではありません。
</Note>
## エンティティタイプ
| エンティティ | 目的 | ドキュメント |
| ----------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **アプリケーション** | アプリの識別情報、デフォルトロール、変数 | [Application Config](/l/ja/developers/extend/apps/config/application) |
| **ロール** | オブジェクトとフィールドの権限セット | [Roles & Permissions](/l/ja/developers/extend/apps/config/roles) |
| **Object** | フィールドを持つカスタムレコードタイプ | [Objects](/l/ja/developers/extend/apps/data/objects) |
| **フィールド** | 他のアプリのオブジェクトにフィールドを追加 | [Extending Objects](/l/ja/developers/extend/apps/data/extending-objects) |
| **リレーション** | オブジェクト間の双方向リンク | [Relations](/l/ja/developers/extend/apps/data/relations) |
| **ロジック関数** | トリガーを備えたサーバーサイド TypeScript | [ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions) |
| **スキル** | 再利用可能な AI エージェントの指示 | [スキルとエージェント](/l/ja/developers/extend/apps/logic/skills-and-agents) |
| **エージェント** | カスタムプロンプトを備えた AI エージェント | [スキルとエージェント](/l/ja/developers/extend/apps/logic/skills-and-agents) |
| **接続プロバイダー** | サードパーティ API 用の OAuth 資格情報 | [Connections](/l/ja/developers/extend/apps/logic/connections) |
| **ビュー** | 事前設定済みのレコード一覧ビュー | [Views](/l/ja/developers/extend/apps/layout/views) |
| **ナビゲーションメニュー項目** | カスタムのサイドバー項目 | [Navigation Menu Items](/l/ja/developers/extend/apps/layout/navigation-menu-items) |
| **ページレイアウト** | レコードの詳細ページ上のタブとウィジェット | [Page Layouts](/l/ja/developers/extend/apps/layout/page-layouts) |
| **フロントコンポーネント** | Twenty 内でサンドボックス化された React UI | [フロントコンポーネント](/l/ja/developers/extend/apps/layout/front-components) |
| **共有依存関係** | アプリのフロントコンポーネント間で共有される依存関係で、package.json 内で宣言されます | [共有依存関係](/l/ja/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) |
| **コマンドメニュー項目** | クイックアクションと Cmd+K エントリ | [Command Menu Items](/l/ja/developers/extend/apps/layout/command-menu-items) |
## サンドボックス化
* **ロジック関数**は、サーバー上の分離された Node.js プロセスで実行されます。 これらは、実行をトリガーしたユーザーまたはアプリ自体として動作する型付き API クライアントを介してのみデータにアクセスします。 [呼び出しで使用されるアクセス権](/l/ja/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses)を参照してください。
* **フロントコンポーネント**は Remote DOM を用いた Web Workers 内で実行されます—メインページからサンドボックス化されつつ、ネイティブの DOM 要素(iframe ではありません)をレンダリングします。 メッセージパッシング型のホスト API を介して Twenty と通信します。
* **権限**は API レベルで強制されます。 `defineApplicationRole()` で宣言されたロールから派生した 2 つのランタイムトークンが挿入されます。デフォルトのトークンは、実行の背後にユーザーがいる場合は常に、そのトリガーとなったユーザーが実行できる操作にさらに制限されます。
## アプリのライフサイクル
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — ソースファイルを監視し、接続された Twenty サーバーへ変更をライブ同期します。 スキーマが変更されると、型付き API クライアントは自動的に再生成されます。
* **`yarn twenty dev:build`** — TypeScript をコンパイルし、esbuild でロジック関数とフロントコンポーネントをバンドルして、マニフェストを生成します。
* **プリ/ポストインストールおよびアンインストールフック** — インストール中またはアンインストール直前に実行されるオプションの関数。 詳細は [Install Hooks](/l/ja/developers/extend/apps/config/install-hooks) を参照してください。
## 次のステップ
<CardGroup cols={2}>
<Card title="設定" icon="screwdriver-wrench" href="/l/ja/developers/extend/apps/config/overview">
アプリの識別情報、デフォルトロール、およびインストールフックとアンインストールフック。
</Card>
<Card title="データ" icon="database" href="/l/ja/developers/extend/apps/data/overview">
オブジェクト、フィールド、および双方向リレーション。
</Card>
<Card title="ロジック" icon="bolt" href="/l/ja/developers/extend/apps/logic/overview">
ロジック関数、スキル、エージェント、OAuth 接続。
</Card>
<Card title="レイアウト" icon="table-columns" href="/l/ja/developers/extend/apps/layout/overview">
ビュー、ナビゲーション、ページレイアウト、フロントコンポーネント。
</Card>
<Card title="オペレーション" icon="rocket" href="/l/ja/developers/extend/apps/operations/overview">
CLI、テスト、リモート、CI、アプリの公開。
</Card>
</CardGroup>

View File

@@ -0,0 +1,87 @@
---
title: ローカルサーバー
description: ローカルの Twenty Docker サーバーを管理します — 起動、停止、アップグレード、並列テスト用インスタンス、および手動での SDK セットアップ。
icon: server
---
## ローカルサーバーの管理
ローカルの Twenty コンテナを操作するには `yarn twenty docker:*` を使用します:
| コマンド | 機能 |
| -------------------------------------- | --------------------------- |
| `yarn twenty docker:start` | サーバーを起動します(必要に応じてイメージを取得) |
| `yarn twenty docker:start 2.2.0` | 特定のサーバーバージョンを起動する |
| `yarn twenty docker:start --port 3030` | カスタムポートで起動 |
| `yarn twenty docker:stop` | サーバーを停止します(データは保持) |
| `yarn twenty docker:status` | URL、バージョン、ログイン資格情報を表示 |
| `yarn twenty docker:logs` | サーバーログをストリーム表示 |
| `yarn twenty docker:reset` | データを消去してクリーンに開始 |
| `yarn twenty docker:upgrade` | `twenty-app-dev` の最新イメージを取得 |
| `yarn twenty docker:upgrade 2.2.0` | 特定のバージョンにアップグレードする |
データは再起動をまたいで、2 つの Docker ボリューム(PostgreSQL 用の `twenty-app-dev-data`、ファイル用の `twenty-app-dev-storage`)に永続化されます。 `reset` を使用して、すべてを消去します。
## サーバーのバージョンを固定する
バージョンが指定されていない場合、`docker:start` は `package.json` 内のアプリの `engines.twenty` の範囲からバージョンを解決します。これは、アプリのインストール時にサーバーが検証に使用する範囲と同じです。 このコマンドは、その範囲を満たす最新の公開済み `twenty-app-dev` イメージを起動し、フィールドが存在しない場合や公開済みバージョンが一致しない場合は `latest` にフォールバックします。
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
単回の実行でその範囲を上書きするには、バージョンを明示的に指定します: `yarn twenty docker:start 2.3.0`。 すでに別バージョンのコンテナが存在する場合、`docker:start` はその場でアップグレードします(データボリュームを保持したままコンテナを再作成します)。
## サーバーイメージのアップグレード
`yarn twenty docker:upgrade` は最新のイメージを取得してダイジェストを比較し、実際に変更があった場合にのみコンテナを再作成します。 ボリュームは保持されます — 置き換えられるのはコンテナだけです。 新しいイメージが取得され、コンテナが稼働中だった場合、アップグレードは自動的に新しいコンテナを起動します。その後、`yarn twenty docker:start` を実行して、正常稼働になるまで待機します。
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
`yarn twenty docker:status` で実行中のバージョンを確認できます。コンテナの `APP_VERSION` が表示されます。
## テスト用インスタンスを並行実行する
任意の `docker:*` コマンドに `--test` を指定すると、完全に分離された第2のインスタンスを管理できます。統合テストの実行や、メインの開発データに触れることなく試す際に便利です。
| コマンド | 機能 |
| ----------------------------------- | -------------------------------- |
| `yarn twenty docker:start --test` | テスト用インスタンスを起動します(デフォルトはポート2021)。 |
| `yarn twenty docker:stop --test` | 停止する |
| `yarn twenty docker:status --test` | ステータスを表示 |
| `yarn twenty docker:logs --test` | ログをストリーム表示 |
| `yarn twenty docker:reset --test` | データを消去 |
| `yarn twenty docker:upgrade --test` | イメージをアップグレード |
テスト用インスタンスは、専用のボリューム(`twenty-app-dev-test-data`、`twenty-app-dev-test-storage`)と設定を備えた独自の Docker コンテナ(`twenty-app-dev-test`)内で実行されるため、メインのインスタンスと競合することなく並行して実行できます。 `--test` と `--port` を併用して、2021 を上書きできます。
## 手動セットアップ(スキャフォルダーなし)
既存プロジェクトに SDK を追加する場合はスキャフォルダーを省略します:
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
`package.json` にスクリプトを追加します:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
これで `yarn twenty dev`、`yarn twenty docker:start` などを実行できます。
<Note>
`twenty-sdk` をグローバルインストールしないでください — プロジェクトごとに固定し、各アプリがそれぞれのバージョンを使用するようにします。
</Note>

View File

@@ -0,0 +1,79 @@
---
title: プロジェクト構成
description: スキャフォールドされた Twenty アプリに含まれているもの — ファイルやフォルダー、およびそれぞれの役割について説明します。
icon: folder-tree
---
`npx create-twenty-app` で生成された新しいアプリは、次のような構成になります。
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md
```
## 主要ファイル
| ファイル / フォルダー | 目的 |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `src/application-config.ts` | **必須。** アプリのメイン設定ファイルです。 |
| `src/default-role.ts` | ロジック関数がアクセスできる範囲を制御するデフォルトのロールです。 |
| `src/constants/universal-identifiers.ts` | 自動生成される UUID とアプリのメタデータ(表示名、説明)。 |
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | スターター用のウェルカムページ:スタンドアロンのページレイアウトによってレンダリングされ、サイドバーからアクセスできるフロントコンポーネントです。 |
| `src/__tests__/` | ユニットテストと、(グローバルセットアップ付きで)実際のサーバーに対してアプリを同期する統合テスト。 |
| `public/` | アプリとともに提供される静的アセット(画像、フォント)。 |
| `AGENTS.md` / `CLAUDE.md` | アプリ上で作業する AI コーディングエージェント向けのガイダンス。 |
| `CHANGELOG.md` / `SETUP.md` | 主な変更の履歴とローカル開発用のセットアップ手順。 |
<Note>
**ファイル構成は自由です。** 上記のフォルダーはあくまで慣習であり、SDK はファイルがどこにあっても、`export default defineEntity(...)` 呼び出しに対する AST 解析によってエンティティを検出します。
</Note>
## 依存関係
Twenty の両方の SDK パッケージは、`dependencies` ではなく `devDependencies` の下に属します。
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "2.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
スキャフォルダーは `twenty-sdk` と `twenty-client-sdk` を自身のバージョンに固定します — アップグレードする際はこの 2 つを同期させてください。
* **`twenty-sdk`** は、`twenty` CLI とビルド/スキャフォールディング用のツールを提供します。 これは開発時とビルド時にのみ実行され、公開済みアプリのランタイムによってインポートされることは決してありません。
* **`twenty-client-sdk`** はアプリのコード(`CoreApiClient`、`MetadataApiClient`、`RestApiClient`)によってインポートされますが、Twenty がランタイムで提供します — ロジック関数は生成された SDK レイヤーからそれを取得し、フロントエンドコンポーネントはサーバー提供のモジュールから解決します。 インストール済みのコピーは型チェックとデプロイ時のビルドにのみ使用されるため、デプロイされたバンドルに同梱される必要はありません。
どちらかのパッケージを `dependencies` の下に置いたままだと、インストールされたアプリのランタイムバンドルに取り込まれてしまい、不要な重荷になります。 いずれかがまだ `dependencies` の下にリストされている場合、`twenty dev:build` は警告を出力します。
アプリ独自のランタイム依存関係(ロジック関数が実際にランタイムでインポートするライブラリ)は、通常どおり `dependencies` の下に追加してください。 ツリーを軽量に保ちましょう。`dependencies` は、展開後の上限が 250MB のランタイムレイヤーにインストールされます。それを超えると同期またはインストールは失敗し、`dependencies` のサイズエラーになります。UI ライブラリや開発ツールは `devDependencies` に含めてください。

View File

@@ -0,0 +1,166 @@
---
title: クイックスタート
icon: rocket
description: 数分で最初の Twenty アプリを作成しましょう。
---
## 前提条件
* **Node.js 24.5+** — [こちらからダウンロード](https://nodejs.org/)
* **Yarn 4** — Corepack 経由で Node.js に同梱されています。 有効化: `corepack enable`
* **Docker** — [こちらからダウンロード](https://www.docker.com/products/docker-desktop/)。 ローカルの Twenty サーバーを実行するために必要です。 すでに別の場所で Twenty が稼働している場合はスキップしてください。
Twenty アプリの構築は 3 つのフェーズで構成されます。 スキャフォルダーはそれらをハッピーパスの 1 つのコマンドにまとめますが、各フェーズは別個の概念です — 何かが失敗したとき、いまどのフェーズにいるかが分かると、直すべき箇所が特定できます。
| フェーズ | やること | ツール | 結果 |
| --------------- | ----------------------- | ----------------------------------- | ------------------------ |
| **1. スキャフォールド** | アプリのソースコードを生成する | `npx create-twenty-app` | ディスク上の TypeScript プロジェクト |
| **2. サーバーを起動** | 同期先となる Twenty サーバーを起動する | Docker + `yarn twenty docker:start` | 稼働中の Twenty インスタンス |
| **3. 同期** | コードをサーバーにライブ同期する | `yarn twenty dev` | 変更が UI に反映されます |
---
## フェーズ 1 — プロジェクトをスキャフォールドする
テンプレートから新しいアプリを作成します:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
スキャフォルダーは非対話型であり、ディレクトリ名がアプリ名になります。 生成されるメタデータをカスタマイズするには、`--display-name` と `--description` を指定します(後から `src/constants/universal-identifiers.ts` 内で編集することもできます)。 これにより、`my-twenty-app/` にスターターの `application-config.ts`、デフォルトロール、CI/CD ワークフロー、および統合テストを含む TypeScript プロジェクトが生成されます。
**このフェーズ後:** マシン上にアプリのソースコードがあります。 まだ実行はされていません — それはフェーズ 2 です。
---
## フェーズ 2 — ローカルの Twenty サーバーを起動する
アプリは同期先としての Twenty サーバーを必要とします。 サーバーは、UI、GraphQL API、PostgreSQL を備えた完全な Twenty インスタンスで、Docker 上でローカルに実行されます。 ローカルのコードは定義をそのサーバーにアップロードし、UI に反映されます。
スキャフォルダーが環境を自動的に起動します。Docker が動作している状態で、`twentycrm/twenty-app-dev` イメージを取得し、ポート `2020` で起動して、事前にデモデータが投入されたワークスペース(`tim@apple.dev`)に対して CLI を認証します — サインインは不要です。
既存の Twenty サーバーに接続する場合は、代わりに `--url \<your-server-url>` を指定してください。 リモートサーバーは OAuth で認証されます。ブラウザーが開き、サインインして **Authorize** をクリックすると、CLI にワークスペースへのアクセス権が付与されます。 (ローカルでも `--authentication-method oauth` を指定して OAuth を利用できます。その場合は `tim@apple.dev` / `tim@apple.dev` でサインインします。)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty のログイン画面" />
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI の承認画面" />
</div>
ターミナルにセットアップ完了のメッセージが表示されます。
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="アプリのスキャフォルドに成功しました。" />
</div>
**このフェーズ後:** あなたの CLI が同期を許可された Twenty サーバーが [http://localhost:2020](http://localhost:2020) で稼働しています。
<Note>
Docker がインストールされていない、または起動していない場合、スキャフォルダーが OS に合った開始コマンドを案内します。 Docker が起動したら、`yarn twenty docker:start` で再開できます — 再スキャフォールドは不要です。
</Note>
---
## フェーズ 3 — 変更を同期する
ここが、最も多くの時間を費やす内側のループです。
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
これは `src/` を監視し、変更のたびに再ビルドして、その結果をサーバーに同期します。 ファイルを編集して保存すると、数秒以内にサーバーに変更が反映されます。 ターミナルにライブステータスパネルが表示されます。
より詳細な出力(ビルドログ、同期リクエスト、エラートレース)が必要な場合は、`--verbose` フラグを使用します。
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="開発モードのターミナル出力" />
</div>
ブラウザーで [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) を開きます。 **Your Apps** にアプリが表示されるはずです。
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps リストに My twenty app が表示されている様子" />
</div>
**My twenty app** をクリックすると、その **application registration**(アプリを記述するサーバーレベルのレコード。名前、識別子、OAuth 認証情報、ソース)が表示されます。 同一サーバー上では、1 つの登録を複数のワークスペースにインストールできます。
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="アプリケーション登録の詳細" />
</div>
ワークスペースへのインストールを確認するには **View installed app** をクリックします。 **About** タブには現在のバージョンと管理オプションが表示されます。
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="インストール済みのアプリ" />
</div>
**このフェーズ後:** ライブな開発ループが確立しています。 `src/` 内の任意のファイルを編集すると、UI に反映されます。
### CI やスクリプト向けの一回限りの同期
ウォッチャーなしで同じパイプラインを 1 回だけ実行するには、`plan` と `apply` を使用します。
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| コマンド | 動作 | 使用する場面 |
| ------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `yarn twenty dev` | ソースファイルを監視し、変更のたびに再同期します。 停止するまで実行し続けます。 | 対話的なローカル開発。 |
| `yarn twenty apply` | ビルドと同期を一度だけ実行し、成功時はコード `0`、失敗時は `1` で終了します。 破壊的な変更がある場合に確認を求めます(スキップするには `--force` を指定します)。 | CI、pre-commit フック、AI エージェント、スクリプト化されたワークフロー。 |
| `yarn twenty plan` | メタデータの変更をビルドして出力しますが、**実際には適用しません**。 | 同期によってどのような変更が行われるかを、実行を確定する前に確認します。 |
すべてのモードで、認証済みのリモートが必要です。 `plan` の詳細については、[Syncing & recovery](/l/ja/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) を参照してください。
<Note>
`yarn twenty dev --once` および `yarn twenty dev --once --dry-run` は非推奨であり、それぞれ `yarn twenty apply` および `yarn twenty plan` のエイリアスです。
</Note>
### Dev モードのオプション
| フラグ | 説明 |
| ------------------------------------- | ----------------------------------------- |
| `--force` | 確認なしで破壊的な変更(削除)を適用します。 |
| `--debounceMs \<ms>` | ファイル変更のデバウンス遅延をミリ秒単位で設定します (既定値: `1000`)。 |
| `--verbose` / `--debug` | 詳細なビルドログ、同期リクエスト、およびエラートレースを表示します。 |
## 構築できるもの
アプリは**エンティティ**で構成されており、それぞれが単一の `export default` を持つ TypeScript ファイルとして定義されます:
| エンティティ | 機能 |
| ---------------- | ---------------------------------------------------------------- |
| **オブジェクトとフィールド** | カスタムデータモデル(ポストカード、請求書など) 型付きフィールド |
| **ロジック関数** | HTTP ルート、cron スケジュール、またはデータベースイベントによってトリガーされるサーバーサイドの TypeScript |
| **フロントコンポーネント** | Twenty の UI(サイドパネル、ウィジェット、コマンドメニュー)内でレンダリングされる React コンポーネント |
| **スキルとエージェント** | AI 機能 — 再利用可能な指示と自律型アシスタント |
| **ビューとナビゲーション** | 事前設定済みのリストビューとサイドバーのメニュー項目 |
| **ページレイアウト** | タブとウィジェットを備えたカスタムのレコード詳細ページ |
完全なリファレンス: [Concepts](/l/ja/developers/extend/apps/getting-started/concepts)。
## 次のステップ
<CardGroup cols={2}>
<Card title="設定" icon="screwdriver-wrench" href="/l/ja/developers/extend/apps/config/overview">
アプリケーション ID、デフォルトロール、インストールおよびアンインストールフック、公開アセット。
</Card>
<Card title="データ" icon="database" href="/l/ja/developers/extend/apps/data/overview">
オブジェクト、フィールド、および双方向リレーション。
</Card>
<Card title="ロジック" icon="bolt" href="/l/ja/developers/extend/apps/logic/overview">
ロジック関数、スキル、エージェント、および OAuth 接続。
</Card>
<Card title="レイアウト" icon="table-columns" href="/l/ja/developers/extend/apps/layout/overview">
ビュー、ナビゲーション、ページレイアウト、フロントコンポーネント。
</Card>
<Card title="オペレーション" icon="rocket" href="/l/ja/developers/extend/apps/operations/overview">
CLI、テスト、リモート、CI、およびアプリの公開。
</Card>
</CardGroup>

View File

@@ -0,0 +1,63 @@
---
title: スキャフォルディング
description: yarn twenty dev:add を使用して対話的にエンティティファイルを生成します — オブジェクト、フィールド、ビュー、ロジック関数などを作成できます。
icon: wand-magic-sparkles
---
エンティティファイルを手作業で作成する代わりに、対話型スキャフォルダーを使用します:
```bash filename="Terminal"
yarn twenty dev:add
```
これによりエンティティタイプの選択を促され、必要なフィールドの設定を順を追って案内されたあと、安定した `universalIdentifier` と正しい `defineEntity()` 呼び出しを含む、すぐに利用できるファイルが作成されます。
最初のプロンプトをスキップするために、エンティティタイプを直接渡すこともできます:
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## 利用可能なエンティティタイプ
| エンティティタイプ | コマンド | 生成されたファイル |
| ------------- | ------------------------------------------ | --------------------------------------------------------- |
| オブジェクト | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| フィールド | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| ロジック関数 | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| フロントコンポーネント | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| ロール | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| スキル | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| エージェント | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| ビュー | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| ナビゲーションメニュー項目 | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| ページレイアウト | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
| ページレイアウトタブ | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| コマンドメニュー項目 | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| ビューフィールド | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| 接続プロバイダー | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| タイムライン活動タイプ | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<name>.ts` |
## スキャフォルダーが生成するもの
各エンティティタイプには固有のテンプレートがあります。 例えば、`yarn twenty dev:add object` は次の項目の入力を求めます:
1. **名前(単数)** — 例:`invoice`
2. **名前(複数)** — 例:`invoices`
3. **ラベル(単数)** — 名前から自動入力(例:`Invoice`)
4. **ラベル(複数)** — 自動入力(例:`Invoices`)
5. **ビューとナビゲーション項目を作成しますか?** — はいと回答すると、スキャフォルダーは新しいオブジェクトに対応するビューとサイドバーリンクも生成します。
他のエンティティタイプはより簡単なプロンプトで、ほとんどは名前のみを尋ねます。
`field` エンティティタイプはより詳細で、フィールド名、ラベル、タイプ(`TEXT`、`NUMBER`、`SELECT`、`RELATION` など、利用可能なすべてのフィールドタイプのリストから選択)、および対象オブジェクトの `universalIdentifier` を尋ねます。
## 出力パスのカスタマイズ
`--path` フラグを使用して、生成ファイルをカスタムの場所に配置します:
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```

View File

@@ -0,0 +1,14 @@
---
title: トラブルシューティング
description: 初回実行時によく発生する問題 — Docker、Node のバージョン、Yarn、依存関係。
icon: wrench
---
* **Docker のエラー** — `yarn twenty docker:start` の前に Docker Desktop(またはデーモン)が起動していることを確認してください。 エラーメッセージに、OS に適した起動コマンドが表示されます。
* **誤った Node のバージョン** — 24.5 以上が必要です(`engines.node: ^24.5.0`)。 `node -v` で確認してください。
* **Yarn 4 が見つからない** — `corepack enable` を実行してください。
* **依存関係の破損** — `rm -rf node_modules && yarn install`。
* **`twenty-sdk` が v2.8.0 へのアップグレード後にエラーになる** — v2.8.0 で `dependencies` から `devDependencies` に移動しました。 [Project Structure → Dependencies](/l/ja/developers/extend/apps/getting-started/project-structure#dependencies) を参照してください。
* **`twenty dev:build` は `dependencies` 配下の `twenty-client-sdk` について警告します** — これは Twenty によって実行時に提供されるため、`twenty-sdk` と同様に `devDependencies` へ移動する必要があります。 [Project Structure → Dependencies](/l/ja/developers/extend/apps/getting-started/project-structure#dependencies) を参照してください。
行き詰まりましたか? [Twenty の Discord](https://discord.com/channels/1130383047699738754/1130386664812982322)でヘルプを依頼してください。

View File

@@ -0,0 +1,118 @@
---
title: コマンドメニュー項目
description: defineCommandMenuItem を使用して、フロントコンポーネントをクイックアクションやコマンドメニュー (Cmd+K) のエントリとして表示します。
icon: terminal
---
**コマンドメニュー項目**は、ユーザーと[フロントコンポーネント](/l/ja/developers/extend/apps/layout/front-components)をつなぐブリッジです。 これは Twenty のコマンドメニュー (Cmd+K) にコンポーネントを登録し、必要に応じて、ページ右上のピン留めされたクイックアクションボタンとしても登録します。
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## 設定フィールド
| フィールド | 必須 | 説明 |
| --------------------------------------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | はい | コマンドの安定した一意の ID |
| `label` | はい | コマンドメニュー(Cmd+K)に表示されるフルラベル |
| `frontComponentUniversalIdentifier` | はい | このコマンドが開くフロントコンポーネントの `universalIdentifier` |
| `shortLabel` | いいえ | ピン留めされたクイックアクションボタンに表示される短いラベル |
| `icon` | いいえ | **非推奨** — アプリケーションアイコンが優先されるため無視されます。設定されている場合は、ビルド時に警告が出力されます。 |
| `isPinned` | いいえ | `true` の場合、ページ右上にクイックアクションボタンとして表示します |
| `availabilityType` | いいえ | コマンドの表示場所を制御します:`'GLOBAL'`(常に利用可能)、`'GLOBAL_OBJECT_CONTEXT'`(オブジェクトコンテキストを持つページ上のみ ― インデックスページおよびレコードページ)、`'RECORD_SELECTION'`(レコード選択時のみ)、または`'FALLBACK'`(他のコマンドが一致しないときに表示) |
| `availabilityObjectUniversalIdentifier` | いいえ | コマンドを特定のオブジェクトタイプのページに制限します(例:Company レコードのみ) |
| `conditionalAvailabilityExpression` | いいえ | 表示可否を動的に制御するブール式(下記参照) |
## ヘッドレスコマンド
[ヘッドレスフロントコンポーネント](/l/ja/developers/extend/apps/layout/front-components#headless-vs-non-headless)とペアになったコマンドメニュー項目は、ワンクリックアクション(コードの実行、ナビゲーション、確認と実行)を提供するための一般的な方法です。 Front Components のページでは、アクション実行後にアンマウントするパターンを処理する [SDK Command components](/l/ja/developers/extend/apps/layout/front-components#sdk-command-components)(`Command`、`CommandLink`、`CommandModal`、`CommandOpenSidePanelPage`)について説明しています。
一般的なフロー:ヘッドレスコンポーネントが `<Command execute={...} />` をレンダーし([完全なサンプル](/l/ja/developers/extend/apps/layout/front-components#sdk-command-components)を参照)、コマンドメニュー項目がそれを指すようにします:
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## 条件付き可用性式
`conditionalAvailabilityExpression` フィールドにより、現在のページコンテキストに基づいてコマンドの表示タイミングを制御できます。 式を構築するために、`twenty-sdk` から型付きの変数と演算子をインポートします:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` は、すでに空ではない選択状態を意味します。特定の件数を示す場合にのみ `numberOfSelectedRecords` を使用してください(例: `>= 2`)。
</Note>
### コンテキスト変数
これらは現在のページの状態を表します:
| 変数 | タイプ | 説明 |
| ------------------------------ | --------- | ----------------------------------------------- |
| `pageType` | `string` | 現在のページタイプ(例:'RecordIndexPage'、'RecordShowPage') |
| `isInSidePanel` | `boolean` | コンポーネントがサイドパネル内でレンダリングされているかどうか |
| `numberOfSelectedRecords` | `number` | 現在選択されているレコード数 |
| `isSelectAll` | `boolean` | 「すべて選択」が有効かどうか |
| `selectedRecords` | `array` | 選択されたレコードオブジェクト |
| `favoriteRecordIds` | `array` | お気に入り登録されたレコードの ID |
| `objectPermissions` | `object` | 現在のオブジェクトタイプの権限 |
| `targetObjectReadPermissions` | `object` | 対象オブジェクトの読み取り権限 |
| `targetObjectWritePermissions` | `object` | 対象オブジェクトの書き込み権限 |
| `featureFlags` | `object` | 有効な機能フラグ |
| `objectMetadataItem` | `object` | 現在のオブジェクトタイプのメタデータ |
| `hasAnySoftDeleteFilterOnView` | `boolean` | 現在のビューにソフトデリートフィルターがあるかどうか |
### 演算子
変数を組み合わせてブール式を作成します:
| 演算子 | 説明 |
| ----------------------------------- | ----------------------------------------- |
| `isDefined(value)` | 値が null/undefined でない場合は `true` |
| `isNonEmptyString(value)` | 値が空でない文字列の場合は `true` |
| `includes(array, value)` | 配列に値が含まれている場合は `true` |
| `includesEvery(array, prop, value)` | すべての要素のプロパティに値が含まれている場合は `true` |
| `every(array, prop)` | すべての要素でそのプロパティが truthy の場合は `true` |
| `everyDefined(array, prop)` | すべての要素でそのプロパティが定義されている場合は `true` |
| `everyEquals(array, prop, value)` | すべての要素でそのプロパティが値に等しい場合は `true` |
| `some(array, prop)` | 少なくとも 1 つの要素でそのプロパティが truthy の場合は `true` |
| `someDefined(array, prop)` | 少なくとも 1 つの要素でそのプロパティが定義されている場合は `true` |
| `someEquals(array, prop, value)` | 少なくとも 1 つの要素でそのプロパティが値に等しい場合は `true` |
| `someNonEmptyString(array, prop)` | 少なくとも 1 つの要素でそのプロパティが空でない文字列である場合は `true` |
| `none(array, prop)` | すべての要素でそのプロパティが falsy の場合は `true` |
| `noneDefined(array, prop)` | すべての要素でそのプロパティが未定義の場合は `true` |
| `noneEquals(array, prop, value)` | どの要素でもそのプロパティが値に等しくない場合は `true` |

View File

@@ -0,0 +1,849 @@
---
title: フロントコンポーネント
description: Twenty の UI 内でレンダリングされる、サンドボックスで分離された React コンポーネントを構築します。
icon: window-maximize
---
フロントコンポーネントは、Twenty の UI 内で直接レンダリングされる React コンポーネントです。 フロントコンポーネントは Remote DOM を使用する**分離された Web Worker**内で実行されます。コードはサンドボックス化され、不透明なオリジンの iframe 内で動作しますが、その UI はその iframe 内に制限されるのではなく、ページ内でネイティブにレンダリングされます。
<Warning>
Front components は現在も積極的に開発が進められています。 あなたのコードは実際のブラウザページではなく不完全な DOM に対して実行されるため、高度な使い方では、しばしば何の表示もなく失敗することがあります。 [現在の制限](#current-limitations) を参照してください。
</Warning>
## フロントコンポーネントを使用できる場所
フロントコンポーネントは、Twenty 内の3つの場所でレンダリングできます:
* **サイドパネル** — ヘッドレスでないフロントコンポーネントは、右側のサイドパネルで開きます。 フロントコンポーネントがコマンドメニューからトリガーされた場合のデフォルトの動作です。
* **ウィジェット(ダッシュボードとレコードページ)** — フロントコンポーネントは、[ページレイアウト](/l/ja/developers/extend/apps/layout/page-layouts)内にウィジェットとして埋め込めます。 ダッシュボードやレコードページのレイアウトを設定する際、ユーザーはフロントコンポーネントのウィジェットを追加できます。
* **App settings** — [`defineSettingsFrontComponent()`](#custom-settings-component) で定義されたフロントコンポーネントは、アプリの **Settings** タブ内のセクションとして、デフォルトの変数設定 UI の代わりにレンダリングされます。
フロントコンポーネント単体では UI から直接アクセスできないため、それを*表示*する必要があります。 それを行う方法は次の3つです。
* **[コマンドメニュー項目](/l/ja/developers/extend/apps/layout/command-menu-items)とペアにする** — コマンドメニュー(Cmd+K)に登録し、必要に応じてピン留めされたクイックアクションとして登録します。
* **[ページレイアウト](/l/ja/developers/extend/apps/layout/page-layouts)内のウィジェットとして埋め込む** — レコードの詳細ページまたはダッシュボード上に配置します。
* **[`defineSettingsFrontComponent()`](#custom-settings-component) で定義する** — アプリの **Settings** タブ内のセクションとして、デフォルトの変数設定 UI の代わりにレンダリングされます。
## 基本的な例
フロントコンポーネントの動作を手早く確認するには、[`defineCommandMenuItem`](/l/ja/developers/extend/apps/layout/command-menu-items)とペアにして、ページ右上隅にクイックアクションボタンとして表示させるのが最も簡単です。
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello from my app!</h1>
<p>This component renders inside Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
`yarn twenty dev` で同期するか(または 1 回限りで `yarn twenty apply` を実行すると)、ページ右上にクイックアクションが表示されます:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="右上のクイックアクションボタン" />
</div>
クリックすると、コンポーネントがインラインでレンダリングされます。
## 設定フィールド
| フィールド | 必須 | 説明 |
| --------------------- | --- | ----------------------------------------- |
| `universalIdentifier` | はい | このコンポーネントの安定した一意の ID |
| `component` | はい | React コンポーネント関数 |
| `name` | いいえ | 表示名 |
| `description` | いいえ | コンポーネントの機能の説明 |
| `isHeadless` | いいえ | コンポーネントに可視の UI がない場合は `true` を設定します(下記参照) |
## フロントコンポーネントをページに配置する
コマンド以外にも、**ページレイアウト**でウィジェットとして追加することで、フロントコンポーネントをレコードページに直接埋め込めます。 詳しくは[ページレイアウト](/l/ja/developers/extend/apps/layout/page-layouts)を参照してください。
## カスタム設定コンポーネント
アプリの **Settings** タブ内の自動生成された変数設定 UI を独自のコンポーネントに置き換えるには、`defineFrontComponent` ではなく `defineSettingsFrontComponent` で定義します。 このコンポーネントは、同じ[configuration fields](#configuration-fields)(ただし、設定コンポーネントは常に可視の UI をレンダーするため、受け付けられない `isHeadless` を除く)を受け取り、さらにこのコンポーネントをアプリの設定 UI としてマークします。
このコンポーネントは、Settings タブ全体を置き換えるのではなく、そのタブの内部のセクションとしてレンダリングされます。 Twenty のシステム管理セクション(自動アップグレード、App URL、接続)は常にその上にレンダーされ、アプリ側で上書きすることはできません。
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
1 つのアプリにつき許可される settings front コンポーネントは 1 つだけであり、2 つ以上を宣言するとビルドは失敗します。 存在する場合、アプリの **Settings** タブはデフォルトの変数設定 UI の代わりにこのコンポーネントをレンダーします。
## ヘッドレスと非ヘッドレス
フロントコンポーネントには、`isHeadless` オプションで制御される2つのレンダリングモードがあります:
**非ヘッドレス(デフォルト)** — コンポーネントは可視のUIをレンダリングします。 コマンドメニューからトリガーされた場合、サイドパネルで開きます。 `isHeadless` が `false` または省略された場合のデフォルトの動作です。
**ヘッドレス (`isHeadless: true`)** — コンポーネントはバックグラウンドで不可視のままマウントされます。 サイドパネルは開きません。 ヘッドレスコンポーネントは、ロジックを実行して自動的にアンマウントするアクション向けに設計されています。例えば、非同期タスクの実行、ページへのナビゲーション、確認モーダルの表示などです。 以下で説明する SDK の Command コンポーネントと自然に組み合わせて使用できます。
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
useEffect(() => {
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-tracker',
description: 'Tracks record views silently',
isHeadless: true,
component: SyncTracker,
});
```
このコンポーネントが `null` を返すため、Twenty はそのためのコンテナのレンダリングをスキップします—レイアウトに空白は発生しません。 コンポーネントは引き続き、すべてのフックとホスト通信 API にアクセスできます。
## SDK の Command コンポーネント
`twenty-sdk` パッケージは、ヘッドレスのフロントコンポーネント向けに設計された4つの Command ヘルパーコンポーネントを提供します。 各コンポーネントは、マウント時にアクションを実行し、エラーをスナックバー通知で処理し、完了時にフロントコンポーネントを自動的にアンマウントします。
`twenty-sdk/front-component` からインポートします:
* **`Command`** — `execute` プロップ経由で非同期コールバックを実行します。
* **`CommandLink`** — アプリのパスにナビゲートします。 Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — 確認モーダルを開きます。 ユーザーが確認すると、`execute` コールバックを実行します。 Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — サイドパネルページを開きます。 Props は `page` に依存します。たとえば、`ViewRecord` は `recordId` と `objectNameSingular`(さらに任意で、特定のタブでレコードを開くための `tab` id)を受け取り、他のページは `pageTitle` と `pageIcon` を受け取ります。
`Command` を使用してコマンドメニューからアクションを実行するヘッドレスのフロントコンポーネントの完全な例です:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
さらに、実行前に確認を求めるために `CommandModal` を使用する例です:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/front-component';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
});
```
そして、`CommandOpenSidePanelPage` を使用して、現在のレコードを特定のタブのサイドパネルで開く例です。 `tab` はページレイアウトのタブ id です(デフォルトのレイアウトでは `company-tab-emails` や `company-tab-timeline` のような id を使用し、カスタムレイアウトではタブ自身の id を使用します)。 その id がレコードのレイアウト内に存在しない場合は、代わりにデフォルトのタブが開きます。
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## ロジック関数の呼び出し
フロントコンポーネントは不透明なオリジンの iframe 内にサンドボックス化された Web Worker 内でブラウザーサイドで実行され、一方で[ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions)はサーバーサイドで実行されます。 両者の間にプロセス内での直接呼び出しはありません。その代わり、フロントコンポーネントは HTTP 経由でロジック関数にアクセスします。
`httpRouteTriggerSettings` で宣言されたロジック関数は、そのルートパスで HTTP 経由でアクセスできます。 `RestApiClient` は、`/s/` で始まるパスをアプリのルートとして扱い、それらをあなたの関数が提供されている URL に解決し、`TWENTY_APP_ACCESS_TOKEN` で認証します。
> **Twenty Cloud では、HTTP トリガーのロジック関数はワークスペースごとの専用ドメインで提供されます**。`https://\<your-workspace-subdomain>.withtwenty.com\<path>` で提供されます。 外部から呼び出す場合は、関数の **HTTP trigger** 設定、もしくはアプリケーションの **Settings** タブから、正確な URL をコピーしてください。
ヘッドレスフロントコンポーネントは、`Command` コンポーネント経由でマウント時に呼び出しを実行し、その後自動的にアンマウントできます。
```tsx src/front-components/sync-prs.tsx
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
`RestApiClient` に渡されるパスは、ロジック関数の `httpRouteTriggerSettings.path` に `/s` を付加したものです。 `isAuthRequired: true` のままにしておいてください。コンポーネント用に Twenty が発行する `TWENTY_APP_ACCESS_TOKEN` によってリクエストが認証されます。
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_APP_ACCESS_TOKEN` は自動的に挿入されます。詳しくは [Application variables](#application-variables) を参照してください。 秘匿アプリケーション変数はフロントコンポーネントに公開されることがないため、API キーやその他の機密性の高いロジックはフロントコンポーネントではなく、ロジック関数側に保持してください。
</Note>
### Twenty REST API の呼び出し
アプリの HTTP ルートを呼び出したり、フロントコンポーネントから Twenty のレコードを読み書きしたりするには、`twenty-client-sdk/rest` の `RestApiClient` を使用します。 これは、`/s/...` のパスをワークスペースの関数のベース URL に送り、`/rest/...` を含むそれ以外のすべてのパスを `TWENTY_API_URL` に送信します。
常にページを見ている人として動作します。 `runAs: 'application'` はロジック関数専用のオプションです。コンポーネントにはアプリケーション自身のトークンが渡されないため、ここでそれを要求するとエラーになります。 アプリ自身のアクセスが必要な処理はロジック関数内に置き、代わりにその関数を呼び出してください。
| メソッド | 説明 |
| --------------------------------- | ------------------------------------ |
| `get(path, options?)` | `GET` リクエストを送信します |
| `post(path, body?, options?)` | `POST` リクエストを送信します |
| `put(path, body?, options?)` | `PUT` リクエストを送信します |
| `patch(path, body?, options?)` | `PATCH` リクエストを送信します |
| `delete(path, options?)` | `DELETE` リクエストを送信します |
| `request(method, path, options?)` | 任意の HTTP メソッドによる汎用的なリクエスト |
| `resolveUrl(path, options?)` | リクエストを送信せずに、パスを完全な URL に解決します(リンク向け) |
`options` には、`headers`、`query`(クエリ文字列パラメーターのレコード。null 相当の値はスキップされます)、および `signal` 経由の `AbortSignal` を指定できます。 `FormData` ではないオブジェクト `body` は、自動的に JSON シリアル化されます。 `401` が発生した場合、クライアントはホスト経由で一度だけアクセス トークンを更新し、そのリクエストを再試行します。
ベース URL とトークンは、デフォルトで環境から解決されます。 必要に応じてコンストラクターに上書き設定を渡します — たとえばテスト時などです:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
失敗したリクエストは `RestApiClientError` をスローし、`status`、`statusText`、`url`、および解析済みの `body` を公開します:
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const people = await client.get('/rest/people', {
query: { limit: 10 },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## ランタイムコンテキストへのアクセス
コンポーネント内で、SDK のフックを使用して現在のユーザー、レコード、コンポーネントインスタンスにアクセスします:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
const componentId = useFrontComponentId();
return (
<div>
<p>User: {userId}</p>
<p>Record: {recordId ?? 'No record context'}</p>
<p>Component: {componentId}</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
name: 'record-info',
component: RecordInfo,
});
```
利用可能なフック:
| フック | 戻り値 | 説明 |
| --------------------------------------------- | ---------------------- | ------------------------------------------------ |
| `useUserId()` | `string` または `null` | 現在のユーザーの ID |
| `useSelectedRecordIds()` | `string[]` | 選択されたレコードIDの配列(未選択の場合は空配列) |
| `useRecordId()` | `string` または `null` | **非推奨。** 代わりに `useSelectedRecordIds()` を使用してください |
| `useFrontComponentId()` | `string` | このコンポーネントインスタンスの ID |
| `useTimelineActivityId()` | `string` または `null` | カスタムタイムライン行をレンダリングする際の現在のタイムラインアクティビティ ID |
| `useColorScheme()` | `'light'` または `'dark'` | ホスト UI のアクティブなカラースキーム(`System` はすでに解決済み) |
| `useFrontComponentExecutionContext(selector)` | 項目により異なる | セレクター関数で実行コンテキスト全体にアクセス |
## アプリケーション変数
`isSecret: false` が設定された [`defineApplication()`](/l/ja/developers/extend/apps/config/application) 内で定義されたアプリケーション変数は、`getApplicationVariable` ユーティリティを通じてフロントコンポーネント内で利用できます。
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
シークレット変数(`isSecret: true`)はフロントコンポーネントには公開**されません**。 それらは、サーバーサイドで実行される[ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions)でのみ利用できます。 これにより、API キーなどの機密値がブラウザーに送信されるのを防ぎます。
</Warning>
`getApplicationVariable` は、変数に宣言されている `type` に関係なく、常に **string**(または `undefined`)を返します。 文字列は型に応じて一貫した方法でシリアライズされます(boolean は `"true"` / `"false"`、number は 10 進数の文字列、配列 / オブジェクトは JSON)。これはロジック関数の `process.env` で使用されているのと同じ形式です。各自でパースしてください(`Number(...)`、`JSON.parse(...)`、`=== 'true'` など)。 [Variable types](/l/ja/developers/extend/apps/config/application#variable-types) を参照してください。
次のシステム変数は、常に `process.env` 経由で利用できます。
| 変数 | 説明 |
| ------------------------- | -------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | Twenty コア API のベース URL |
| `TWENTY_APP_ACCESS_TOKEN` | サインインしている人のロールとアプリのロールの共通部分にスコープ設定された短期間有効なトークン。そのため、コンポーネントが閲覧者以上の権限を持つことはありません |
### `TWENTY_FUNCTIONS_URL`
Twenty はまた、`TWENTY_FUNCTIONS_URL` をフロントコンポーネントとロジック関数に挿入します。これは、アプリの HTTP トリガーのロジック関数が提供されるベース URL です。
この変数が存在するのは、その URL が必ずしも Twenty サーバー自体とは限らないためです。 Twenty Cloud では、アプリのルートはワークスペースごとの専用ドメイン(`https://\<your-workspace-subdomain>.withtwenty.com`、または設定されている場合はアプリケーションのプライマリ公開ドメイン)で提供されます。これにより、アプリで作成されたレスポンスが Twenty アプリのオリジンではなく分離されたオリジン上で実行されます。 セルフホストおよびローカルインスタンスでは、アプリのルートはサーバー自体の `/s` プレフィックスの下で提供され、この変数がまったく設定されない場合もあります。 ベース URL はワークスペースやインスタンスごとに異なるため、コードでハードコードすることはできません。サーバーが実行時に正しい値を挿入します。
この変数を直接読む必要があることはほとんどありません。 `/s/` プレフィックス付きのパスで `RestApiClient` を通じてルートを呼び出すと、クライアントが URL を解決します。`/s` プレフィックスを取り除き、`TWENTY_FUNCTIONS_URL` をターゲットにし、この変数が設定されていない場合は `\<TWENTY_API_URL>/s` をフォールバックとして使用します。 リクエストを送信せずに絶対 URL を取得するには、`resolveUrl('/s/\<path>')` を使用します(リンクなどの用途)。 URL を手作業で組み立てる場合にのみ、この変数を直接読み取ってください。
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## ホスト通信 API
フロントコンポーネントは、`twenty-sdk` の関数を使用してナビゲーション、モーダル、通知をトリガーできます:
| 機能 | 説明 |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | アプリ内のページに移動 |
| `openSidePanelPage(params)` | サイドパネルを開く |
| `closeSidePanel()` | サイドパネルを閉じる |
| `openCommandConfirmationModal(params)` | 確認ダイアログを表示 |
| `enqueueSnackbar(params)` | トースト通知を表示 |
| `unmountFrontComponent()` | コンポーネントをアンマウント |
| `updateProgress(progress)` | 進行状況インジケーターを更新 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | `Blob` を FILES フィールドにアップロードします。返り値は `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` または `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` です。 |
アクション完了後にホスト API を使用してスナックバーを表示し、サイドパネルを閉じる例です:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### ストレージ
`localStorage` と `sessionStorage` は、通常のページと同様に、標準的な同期 API として動作します。 キーはアプリのインストールとサインインしているユーザーにスコープされており、他のアプリがそれらを読み取ることはできません。また、同じブラウザに別のユーザーがサインインした場合は、空のストアから開始します。 `localStorage` に書き込まれた値は、リロードをまたいでもデバイス上に保持されます。一方、`sessionStorage` はブラウザセッションの間だけ有効です。
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty はアプリに代わって値を保存するため、書き込みはローカルに即座に反映され、その後バックグラウンドで保存されます。 読み取り処理がホストで待機することはありません。 何も同期されません。値は別のブラウザやマシンには引き継がれないため、デバイス変更後も保持する必要があるものには、ロジック関数の[key-value store](/l/ja/developers/extend/apps/logic/key-value-store)を使用してください。
書き込みには上限があり、すべての制限はバイト数ではなく文字数でカウントされます。キーは最大 512 文字、1 つの値は最大 262,144 文字、各ストレージはアプリおよびユーザーごとに最大 1,048,576 文字です。 制限を超える書き込みは、ブラウザ API と同様に `QuotaExceededError` をスローします。
### 複数のレコードを扱う
複数の選択されたレコードを処理するには `useSelectedRecordIds()` を使用してください。 これは一括操作に役立ちます:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
});
```
レコードの選択に制限された[コマンドメニューアイテム](/l/ja/developers/extend/apps/layout/command-menu-items)として表示します:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## 公開アセット
フロントコンポーネントは、`getPublicAssetUrl` を使用してアプリの `public/` ディレクトリ内のファイルにアクセスできます:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
詳細は[公開アセットのセクション](/l/ja/developers/extend/apps/config/public-assets)を参照してください。
## フロントコンポーネント間で依存関係を共有することについて
デフォルトでは、各フロントコンポーネントはインポートするライブラリのコピーをそれぞれ同梱するため、5つのコンポーネントを持つアプリでは、React が5回配信されます。 共有する依存関係をアプリの `package.json` に宣言すると、それらのライブラリを一度だけビルドし、アプリ内のすべてのコンポーネントが単一のキャッシュ済みファイルから読み込むようにできます:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
その後、各コンポーネントはこれまでどおり依存関係をインポートするだけで済み、コンポーネントコード側で変更する必要はありません:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
知っておくべきこと:
* **アプリごとに1つの共有依存関係バンドル。** バンドルはアプリ自身の依存関係からビルドされるため、配信するバージョンを完全にコントロールできます。
* **インポートする正確な指定子を列挙してください。** `twenty-ui/input` と `twenty-ui/display` は 2 つのエントリです。パッケージ名だけでは、そのサブパスは対象になりません。 `react` を列挙すると、自動的に `react/jsx-runtime` も対象になります。
* **`react` と一緒に `react-dom/client` を共有してください。** すべてのコンポーネントは `createRoot` を通じてレンダーされるため、これを含めない場合、各コンポーネントは引き続き React DOM を同梱することになります。
* **バンドルはキャッシュされます。** バンドルは長期間有効な不変キャッシュ付きのコンテンツハッシュ URL で提供されるため、一度ダウンロードされると、その依存関係のいずれかが変更されるまでアプリのすべてのコンポーネントで再利用されます。
* **共有パッケージを一切インポートしないコンポーネントは、バンドルをダウンロードすることはありません。**
## スタイリング
フロントコンポーネントは複数のスタイリング手法をサポートしています。 次のものを使用できます:
* **インラインスタイル** — `style={{ color: 'red' }}`
* **Twenty UI コンポーネント** — Twenty 独自のコンポーネントライブラリです。以下の [Using Twenty UI components](#using-twenty-ui-components) を参照してください
* **Emotion** — `@emotion/react` による CSS-in-JS
* **styled-components** — `styled.div` パターン
* **Tailwind CSS** — ユーティリティクラス
* React と互換性のある**任意の CSS-in-JS ライブラリ**
## Twenty UI コンポーネントの使用
Twenty はコンポーネントライブラリを [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1) パッケージとして提供しています。 フロントエンドコンポーネントでは、ボタン、タグ、ステータスピル、チップ、アバター、アイコン、タイポグラフィ、そしてワークスペースのライト/ダークテーマに自動で合わせてくれるテーマトークンなどに利用できます。
### インストール
Twenty インスタンスに同梱されているバージョンに固定して、そのパッケージをアプリに追加します。
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` はビルド時にフロントエンドコンポーネントへバンドルされるため、アプリの依存関係に追加するだけで済み、実行時に設定することは何もありません。
### コンポーネントのインポート
パッケージのルートではなく、対応するサブパスからインポートすることで、使用しているコンポーネントだけがバンドルに含まれるようにします。
| サブパス | エクスポート内容 |
| --------------------------- | ---------------------------------------- |
| `twenty-ui/input` | `Button` とフォーム入力 |
| `twenty-ui/data-display` | `Tag`、`Status`、`Chip`、`Avatar` など |
| `twenty-ui/feedback` | `Callout`、`Banner`、`Info` など |
| `twenty-ui/typography` | `H1Title`、`H2Title`、`H3Title`、`Label` など |
| `twenty-ui/icon` | `Icon*` コンポーネント(例: `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`、`themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
const StyledWidget = () => {
return (
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
<Button title="Click me" onClick={() => alert('Clicked!')} />
<Tag text="Active" color="green" />
<Status color="green" text="Online" />
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
name: 'styled-widget',
component: StyledWidget,
});
```
### アイコン
`twenty-ui/icon` から個別のアイコンをインポートします:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
それぞれの名前付きアイコンはツリーシェイクされるため、少数をインポートしてもバンドルサイズへの影響はわずかです。 `IconsProvider`、`useIcons`、`iconsState` の使用は避けてください。これらは Tabler アイコンセット全体(数 MB)を読み込みます。
### テーマ設定とテーマトークン
Twenty UI コンポーネントはワークスペースのライト/ダークテーマに自動的に追従します。レンダラーがホスト上のアクティブなカラースキームを適用し、コンポーネントはそれに基づいて色を決定します。
独自のインラインスタイルでも同じデザイントークンを使うには、`useTheme()` フックを呼び出します。 これにより、アクティブなテーマに紐づいた Twenty のテーマトークン(スペーシング、カラー、角丸、フォント)が返されます。コンポーネント側で `ThemeProvider` をセットアップする必要はありません。
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
`useTheme()` はフックなので、コンポーネント本体の中でトークンを読み取り、値が常に現在のテーマを反映するようにできます。 同じトークンマップは `themeCssVariables` 定数としてもエクスポートされていますが、フロントエンドコンポーネントでは `useTheme()` を優先してください。アプリのマニフェストを抽出している間は、`themeCssVariables` を参照するモジュールレベルの定数が未定義になる可能性があります。
アクティブなスキームを明示的に分岐させるには、`twenty-sdk/front-component` の `useColorScheme()` を使って取得します。このフックは `'light'` または `'dark'` を返します。
## 現在の制限
Front components は現在も積極的に開発が進められています。 レンダリング、スタイリング、イベント処理、要素の計測、およびブラウザー ストレージの操作は問題なく動作します。 それらを*越えた*処理(ref に対する DOM メソッドの呼び出し、要素リサイズの監視、ツリーの外へのポータル)については、現在は未実装または不完全であり、そのほとんどは例外も出さずに黙って失敗します。スキャフォールドがフルなブラウザー DOM を前提に型付けされているため、TypeScript エラーも発生しません。
これらのいずれかが原因でブロックされている場合は、[issue を作成](https://github.com/twentyhq/twenty/issues/new/choose)して、優先度を上げてもらってください。
### レイアウトと計測
要素は自分自身を計測できます。ホストがジオメトリをサンドボックス側にミラーリングするため、読み取りはローカルで応答されますが、最大 1 フレーム分までは古い値になる可能性があり、まだ一度も計測されていない要素の最初の読み取りは 0 を返します。 書き込み後は、`requestAnimationFrame` コールバックまたはエフェクト内で再度読み取ってください。
| API | 何が起きるか |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | ミラーからは動作しますが、`scrollTop` / `scrollLeft` を代入しても何も起こりません (no-op) |
| `getClientRects()`, `window.matchMedia()` | 例外を投げます |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | **ブラウザーのビューポート** を報告する点では動作します。ウィジェット自体のサイズは `document.body.clientWidth` / `clientHeight` です |
| `window.getComputedStyle()` | 要素のインラインスタイルのみを返し、ホストで計算されたカスケードは一切返しません |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError`(`typeof` ガードは機能します) |
| `MutationObserver` | `subtree`、`attributeFilter`、旧値、および `takeRecords()` を含めて動作します |
`getBoundingClientRect` による位置決めは現在動作しますが、`ResizeObserver` を介してサイズ変更を監視しているもの(recharts の `ResponsiveContainer`、Floating UI の `autoUpdate`)は依然として動作しません。 レイアウトにはいずれにせよ CSS を優先してください。スタイルシートは実際のページに適用されるので、flexbox、grid、`aspect-ratio`、`clamp()`、`@container` はフレーム遅延なしで通常どおり動作します。
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout`, `queueMicrotask` は、`window.` プレフィックスなしで動作します。 `window.requestAnimationFrame(...)` などを使った場合だけ例外が投げられます。
</Note>
### DOM アクセス
`ref` が返すのは `HTMLElement` ではなくサンドボックス要素です。
| 記述するコード | 何が起きるか | 代わりに使うもの |
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | 例外を投げます | 制御されたコンポーネントを使い、値は `event.target` から読み取ります |
| `element.classList.add(...)` | 例外を投げます(`classList` は `undefined` です) | `className` 文字列を自分で組み立ててください |
| `document.createTreeWalker()` | 例外を投げます | `querySelector()` / `querySelectorAll()` と `getElementById()` は動作します。`getElementsByClassName()` も動作しますが、ライブな `HTMLCollection` ではなく静的なコレクションを返します |
| `document.activeElement` | 常に `undefined` です | `onFocus` / `onBlur` でフォーカスを追跡してください |
| `canvas` | 何もレンダリングされず、エラーも出ません | SVG、またはオフスクリーンで描画して、その結果を `img` 要素で表示します |
| `createPortal(node, document.body)` | `isConnected` は成功を報告しますが、何もレンダリングされません | `position: absolute` によるインラインのオーバーレイを使うか、ライブラリに自前のコンテナー要素を渡してください |
このポータルのギャップが原因で、Radix、Headless UI、MUI、react-select のポップオーバーはデフォルトでは何もレンダリングしません。 ほとんどのライブラリは container プロップを受け付けるので、自分がレンダリングした要素を指定してください。
### イベント
マウス、ポインター、タッチ、ドラッグ、キーボード、フォーカス、`input`/`change`/`submit`、`scroll`/`wheel`/`contextmenu`、そして `animationend`/`transitionend` はホストへ渡されます。さらに、要素ごとにいくつかのイベントも渡されます:`img` の `load`/`error`、`input`/`textarea` のクリップボードおよびコンポジション、`video`/`audio` のメディア、`details`/`dialog` の `toggle` などです。 それ以外(`onAuxClick`、`onSelect`、`onInvalid`、`onReset`、`onAnimationStart`、pointer capture、`img` の `onLoad`)は、警告なしに破棄されます。
`document.addEventListener()` と `window.addEventListener()` はエラーなく登録されますが、一度も発火しません。そのため、ドラッグは開始した要素からポインターが離れた瞬間に止まります。 `event.preventDefault()` も渡されません。フォーム送信、`dragover`/`drop`、およびリンクのクリックは、すでに保護されています。
### 属性とスタイリング
各要素は自分自身のプロパティをホスト DOM にフォワードします(`a` の `href`、`img` の `src`/`alt`、`input` の `value`/`placeholder`/`disabled` など)。さらに、すべての要素に共通のプロパティセットもフォワードされます:`id`、`className`、`style`、`title`、`tabIndex`、`role`、`draggable`、および任意の `aria-*` / `data-*` 属性(ハイフン区切りのため、`ariaLabel` は破棄されます)。 それ以外のものはすべて暗黙的に破棄されるため、カスタム状態は `data-*` として表現してください。
コンポーネントの CSS(`import './styles.css'`、CSS-in-JS、または `style` 要素のいずれであっても)は、ホストページの `head` に **スコープなし(unscoped)** で挿入されます。 そのため、クラス名が Twenty 独自のクラス名と衝突します(プレフィックスを付け、決して素の `div { ... }` セレクタは書かないでください)。また、`@media` はウィジェットではなくブラウザウィンドウに対してマッチします(独自の `container-type` を指定した `@container` を使用してください)。 インラインの `style` props には影響しません。
### ストレージとネットワーク
`localStorage` と `sessionStorage` はブラウザではなく Twenty によって提供されます。コンポーネントは不透明なオリジンのワーカー内で実行されるため、ホストがアプリに代わって値を保存します。 スコープと制限については、[storage](#storage) を参照してください。 IndexedDB、Cookie、Cache API、および `BroadcastChannel` は引き続き利用できません。 デバイス間で状態を永続化するには、[logic function](/l/ja/developers/extend/apps/logic/logic-functions)を呼び出し、その[key-value store](/l/ja/developers/extend/apps/logic/key-value-store)を使用してください。
`fetch` は動作しますが、いくつか注意点があります。
* Twenty API への呼び出しやアプリのルートへの呼び出しはホストによってプロキシされるため、[`RestApiClient`](#calling-the-twenty-rest-api) を優先して使用してください。 プロキシされた呼び出しでは、`AbortSignal` などの `RequestInit` オプションは破棄され、`string` と `URLSearchParams` のボディのみがサポートされます。
* その他のオリジンからのリクエストは、`Origin: null` を付けてサンドボックスの外へ送信されるため、サードパーティ API は `Access-Control-Allow-Origin: *` を返す場合にのみ応答します。 代わりにロジック関数から呼び出してください。
* `fetch('/rest/people')` は、サンドボックスに相対パスを解決するためのページ URL がないため、Twenty API には決してマッチしません。
### メディアキャプチャ
`navigator.mediaDevices.getUserMedia()` と `MediaRecorder` は、サンドボックス polyfill を通じて front コンポーネント内で動作するため、標準的な録画コードは変更なしで実行でき、一般的なコンテナ/コーデックの組み合わせに対しては `MediaRecorder.isTypeSupported` が応答します。 詳細な `getUserMedia` 制約オブジェクトは受け付けられますが転送はされません。ホストは、要求された種類に対してデフォルト設定でキャプチャを行い、アプリケーション全体を通して同時にライブにできるキャプチャは 1 つだけです。 記録された `Blob` を `uploadFile` ホスト関数で保存します。
### その他のギャップ
* **ファイル内容。** type が `file` の `input` は、ハンドラーにファイルのメタデータのみを渡し、バイト列は渡さないため、`FileReader` は利用できません。 `MediaRecorder` で生成されたものなど、コードですでに保持している `Blob` をアップロードするには、`uploadFile` ホスト関数を使用します。
* **ドラッグ&ドロップのペイロード。** ドラッグイベントは発火しますが、`event.dataTransfer` は `undefined` です。
* **Node 組み込みモジュール。** `fs`、`path`、`node:crypto` はビルドに失敗するため、その処理は [ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions) に移動してください。 Web Crypto、`fetch`、`TextEncoder`、および `URL` は利用可能です。
* **`iframe`** は常に `allow-same-origin` なしで再サンドボックス化されるため、独自のセッションに依存する埋め込みは、ログアウト状態としてレンダリングされます。 また、`onLoad` もありません。

View File

@@ -0,0 +1,46 @@
---
title: ナビゲーションメニュー項目
description: ワークスペースのサイドバーにカスタムエントリを追加します。保存済みビューや外部 URL へのリンクを設定できます。
icon: bars
---
**ナビゲーションメニュー項目**は、左サイドバー内の 1 つのエントリです。 カスタムのサイドバーリンクを提供するには `defineNavigationMenuItem()` を使用します。通常は、提供する各[ビュー](/l/ja/developers/extend/apps/layout/views)ごとに 1 つずつ、または外部 URL を指すリンクとして使用します。
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
## 主なポイント
* `type` は、そのメニュー項目がどこにリンクするかを決定します。 各 `type` は、特定の識別子フィールドと組み合わされます。
| タイプ | 機能 | 必須フィールド |
| ------------------------------------ | --------------------------- | ------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | 保存済みビューを開きます | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | 外部 URL を開きます | `link` |
| `NavigationMenuItemType.FOLDER` | ネストされた項目をラベルの下にグループ化します | `name`(子項目は `folderUniversalIdentifier` を通じてフォルダを参照します) |
| `NavigationMenuItemType.OBJECT` | オブジェクトのデフォルトのインデックスページを開きます | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | スタンドアロンのページレイアウトを開きます | `pageLayoutUniversalIdentifier` |
* `position` はサイドバーでの表示順を制御します。
* enum には、ユーザーが作成したレコードのお気に入りを内部的に扱うために使用される `NavigationMenuItemType.RECORD` も含まれています。これはアプリのマニフェストからは使用できません(レコードを参照するフィールドが存在しません)。
* `icon` と `color` は任意で、エントリの見た目をカスタマイズします。
* `folderUniversalIdentifier` は、任意の項目で利用でき、その項目を `FOLDER` タイプの親の内側にネストするために使用します。
<Note>
**よくある落とし穴:** ナビゲーションメニュー項目が関連付けられていないオブジェクトを作成すると、そのオブジェクトはサイドバーからアクセスできなくなります。 技術的/内部的なオブジェクトでない限り、すべてのカスタムオブジェクトには、それを指すサイドバーエントリを用意する必要があります。
</Note>

View File

@@ -0,0 +1,59 @@
---
title: 概要
description: Twenty の UI 内にアプリを配置します — サイドバー項目、保存済みビュー、レコードページのタブ、およびサンドボックス化された React コンポーネント。
icon: table-columns
---
Twenty アプリの **レイアウトレイヤー** とは、ユーザーに見えるすべての要素を指します。アプリがサイドバーのどこに表示されるか、どのリストビューを提供するか、レコード詳細ページがどのように構成されるか、そしてそれらのページ内でどのカスタム React コンポーネントがレンダリングされるかなどです。
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## このセクションについて
<CardGroup cols={2}>
<Card title="ビュー" icon="list" href="/l/ja/developers/extend/apps/layout/views">
`defineView` — 表示カラム、フィルター、グループなどを含む、保存済みリスト構成。
</Card>
<Card title="ナビゲーションメニュー項目" icon="bars" href="/l/ja/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — ビューまたは外部 URL を指すサイドバー項目。
</Card>
<Card title="ページレイアウト" icon="table-columns" href="/l/ja/developers/extend/apps/layout/page-layouts">
`definePageLayout` および `definePageLayoutTab` — レコード詳細ページ上のタブとウィジェット。
</Card>
<Card title="フロントコンポーネント" icon="window-maximize" href="/l/ja/developers/extend/apps/layout/front-components">
`defineFrontComponent` — Twenty 内でレンダリングされる、サンドボックス化された React コンポーネント。
</Card>
<Card title="コマンドメニュー項目" icon="terminal" href="/l/ja/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — フロントコンポーネントを Cmd+K の項目およびクイックアクションとして登録します。
</Card>
</CardGroup>
## アプリが表示される場所
| 表示場所 | 制御内容 | エンティティ |
| -------------------- | ------------------------------------------------ | ----------------------------------------- |
| **サイドバー** | 保存済みビューまたは外部 URL へリンクするカスタム項目 | `defineNavigationMenuItem` |
| **レコードリスト** | オブジェクト向けの保存済み構成 — 表示カラム、順序、フィルター、グループ | `defineView` |
| **レコード詳細ページ** | レコードページ上のタブおよびウィジェット(独自オブジェクトのもの、または標準オブジェクトのもの) | `definePageLayout`, `definePageLayoutTab` |
| **上記いずれかの内部** | カスタム React ウィジェット — ボタン、フォーム、ダッシュボード、連携機能 | `defineFrontComponent` |
| **アプリ設定** | アプリの「設定」タブ内にある、デフォルトの変数 UI に代わるカスタム設定セクション | `defineSettingsFrontComponent` |
| **コマンドメニュー (Cmd+K)** | ピン留めされたクイックアクションまたは非表示コマンド | `defineCommandMenuItem` |
フロントコンポーネントは Remote DOM を使用して、分離された Web Worker 内で実行されます。ページ内に *ネイティブに*(iframe 内ではなく)レンダリングされますが、ホストページや DOM へ直接アクセスすることはできません。 Twenty との通信は、メッセージパッシング型のホスト API を通じて行われます。
これらは依然として積極的に開発中です。サンドボックスは DOM を部分的にのみ実装しているため、高度な利用方法では失敗する場合があります。 [現在の制限](/l/ja/developers/extend/apps/layout/front-components#current-limitations) を参照してください。

View File

@@ -0,0 +1,158 @@
---
title: ページレイアウト
description: definePageLayout と definePageLayoutTab を使用して、レコードの詳細ページのカスタマイズ(タブ、ウィジェット、front components のレンダー位置)を行います。
icon: table-columns
---
**ページレイアウト**は、レコードの詳細ページがどのように構成されるかを制御します。どのタブを表示し、そのタブにどのウィジェットを含めるかを決定します。 自分が所有するオブジェクトに対してレイアウトを宣言するには `definePageLayout()` を使用し、既に存在するレイアウト(自分のもの、または標準の Twenty のレイアウト)に単一のタブを追加するには `definePageLayoutTab()` を使用します。
| ユースケース | エンティティ |
| ----------------------------------------- | --------------------- |
| 自分が所有するオブジェクト上のレコードページ全体のレイアウトを定義する | `definePageLayout` |
| 既存のレイアウト(自分のオブジェクト、または標準オブジェクト)にタブを1つ追加する | `definePageLayoutTab` |
## definePageLayout
自分が詳細ページ全体を所有している場合に使用します。通常は、自分で定義したカスタムオブジェクトに対して使用します。
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
### 主なポイント
* `type` は `'RECORD_INDEX'`、`'RECORD_PAGE'`、`'DASHBOARD'`、`'STANDALONE_PAGE'` のいずれかです。 特定のオブジェクトの詳細ビューをカスタマイズするには `'RECORD_PAGE'` を使用します。
* `objectUniversalIdentifier` は、このレイアウトを適用するオブジェクトを指定します。
* 各 `tab` は、`title`、`position`、`layoutMode` を持つページのセクションを定義します。レコードページおよびスタンドアロンページには `VERTICAL_LIST`、ダッシュボードには `GRID`、タブのビューポートを埋める単一のウィジェットには `CANVAS` を使用します。 `VERTICAL_LIST` タブでは、ウィジェットが縦方向に積み重ねられます。 タイムライン、ファイル、メモ、タスク、ワークフローなど、独自のスクロールを管理する組み込みウィジェットは 1 つのビューポートを埋めます。フィールド、フロントコンポーネント、グラフ、その他のコンテンツに合わせたウィジェットは、そのコンテンツの高さまたは設定された高さでレンダリングされます。 `GRID` タブでは、ウィジェットは常に 12 列グリッド上のカードとして配置されます。 `CANVAS` ウィジェットには明示的な位置がありません。キャンバスタブに複数のウィジェットが含まれる場合、それらはビューポート全体を埋めるのではなく、コンテンツの高さでレンダリングされます。
* `layoutMode` を明示的に設定します。 `layoutMode` を省略すると、`STANDALONE_PAGE` では `VERTICAL_LIST`、それ以外では `GRID` が適用されますが、これはレコードページで望ましい動作であることはほとんどありません。
* タブ内の各 `widget` は、[front component](/l/ja/developers/extend/apps/layout/front-components)、リレーションリスト、その他のビルトインウィジェットタイプをレンダリングできます。
* `FRONT_COMPONENT` ウィジェットでは、同じアプリのコマンドメニュー項目ユニバーサル識別子を順序付き配列として `headerCommandMenuItemUniversalIdentifiers` に設定できます。 これらのアクションはウィジェットカードヘッダーにアイコンボタンとして表示され、コマンドレベルの可用性チェックと権限チェックが維持されます。 識別子は一意である必要があり、アプリのインストール時に解決される必要があります。
* タブの `position` は表示順を制御します。 組み込みタブの後にカスタムタブを配置するには、より大きな値(例:50)を使用します。
### フィールド ウィジェット
`FIELD` ウィジェットは、レコードの 1 つのフィールドをレンダリングします。 リレーションフィールドの場合、関連レコードの一覧を埋め込むこともできます。
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` は、レイアウトのオブジェクト上のフィールドのユニバーサル識別子を受け取ります。
* `fieldDisplayMode` は `'FIELD'`、`'CARD'`、`'EDITOR'`、`'VIEW'`、`'TABLE'` のいずれかです。 `TABLE` は、1 対多のリレーションフィールドのレコードを一覧表示するビューを埋め込みます。
* `nestedRelationFieldMetadataId` は任意で、リレーション先オブジェクト上の 1 対多リレーションフィールドのユニバーサル識別子を受け取り、2 ホップ先のリレーションにあるレコードを一覧表示するために使用されます(例: 会社ページでその会社の担当者が持つ商談を一覧表示する、または人物ページでその人物の所属する会社の商談を一覧表示する、など)。 最初のホップは 1 対多または多対 1 のリレーションフィールドのどちらでもかまいませんが、2 つ目は必ず 1 対多でなければなりません(ジャンクションリレーションはサポートされません)。また、`fieldDisplayMode: 'TABLE'` が必須であり、ネストされたウィジェットは常に埋め込みビューとしてレンダリングされるため、これを他の表示モードと組み合わせると検証エラーになります。
## definePageLayoutTab
既存のレイアウトにタブを**追加**したい場合にのみ使用します。たとえば、標準の Company ページにアナリティクスタブを追加する場合や、自分のオブジェクトのレイアウトに AI サマリータブを追加する場合などです。
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### 主なポイント
* `pageLayoutUniversalIdentifier` は**必須**であり、インストール時点ですでに存在するページレイアウトを指している必要があります。標準の Twenty のレイアウト、または自分のアプリで定義したレイアウトのいずれかです。 別のインストール済みアプリが所有するレイアウトへのアプリ間参照は、現時点ではサポートされていません。 親レイアウトが存在しない場合、インストールは明確な検証エラーとともに失敗します。
* 標準の Twenty レイアウトの場合は、`twenty-sdk/define` から識別子を import します:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
各レイアウトエントリは、その `tabs` とその `widgets` も公開しているため、任意の階層を参照できます:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
短いエイリアス `STANDARD_PAGE_LAYOUT` も利用できます:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` はこのタブのみにスコープされます。`definePageLayout` 内でインライン定義されたウィジェットとまったく同様に、[front components](/l/ja/developers/extend/apps/layout/front-components)、views などを参照します。
* `position` は、対象のレイアウト上にある既存のタブとの順序を制御します。 組み込みタブとの相対位置が希望どおりになるような値を選択してください。
* 既存のレイアウトに追加だけを行いたい場合は、`definePageLayout` の代わりにこちらを使用してください。 レイアウト全体を自分が所有している場合は、`definePageLayout` を使用してください。

View File

@@ -0,0 +1,113 @@
---
title: ビュー
description: アプリのオブジェクト向けに、列の順序、フィルター、グループが事前設定済みの保存ビューを提供します。
icon: list
---
**ビュー**とは、オブジェクトのレコードをどのように表示するかについての保存された設定です。どのフィールドを表示するか、その順序、表示・非表示、および適用されるフィルターやグループなどを含みます。 `defineView()` を使用して、事前設定済みのビューをアプリに同梱します。
すべてのオブジェクトには、サーバーによってプロビジョニングされるメインのリストビュー(`OBJECT` ナビゲーション項目で開かれる「All ...」テーブル)が自動的に用意されます。これには、アプリが作成するオブジェクトも含まれ、アプリが宣言する各フィールドごとに 1 列が用意されます。 `defineView()` で定義したビューは、そのデフォルトビューに加えて常に**追加の**ビューとして扱われます。
```ts src/views/example-view.ts
import { defineView } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## 主なポイント
* `objectUniversalIdentifier` は、このビューを適用するオブジェクトを指定します。 定義したカスタムオブジェクトでも、Twenty の標準オブジェクトでも可能です。
* オブジェクトのメインのリストビューはサーバー所有です。`key` は非推奨で無視されるため、マニフェストビューがそれを取得することはできません。 サイドバーから 1 クリックでビューにアクセスできるようにしたい場合は、`VIEW` ナビゲーション項目を追加してください。
* `fields` は、どの列をどの順序で表示するかを制御します。 各フィールドは `fieldMetadataUniversalIdentifier` を参照します。 `createdAt` などの自動作成システムフィールドの参照方法については、[システムフィールドのターゲット指定](/l/ja/developers/extend/apps/data/system-fields)を参照してください。
* さらに高度な構成のために、`filters`、`filterGroups`、`sorts`、`groups`、`fieldGroups` も定義できます。
* 同じオブジェクトに複数のビューがある場合、`position` が表示順を制御します。
## オプションのプロパティ
| プロパティ | 値 | 説明 |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (デフォルト), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | レコードのレイアウト方法。 (`FIELDS_WIDGET`、`TABLE_WIDGET`、`KANBAN_WIDGET`、`LIST_WIDGET`、`CALENDAR_WIDGET` も存在しますが、ページレイアウトウィジェットによって内部的に使用されます。) |
| `visibility` | `ViewVisibility.WORKSPACE` (デフォルト), `ViewVisibility.UNLISTED` | ビューがワークスペース全体で一覧表示されるか、ピッカーから非表示にされるか。 |
| `openRecordIn` | 非推奨 | 参照されなくなりました: レコードをどこで開くかは、現在では[object](/l/ja/developers/extend/apps/data/objects)のプロパティ(`defineObject()` の `openRecordIn`)となっており、設定されていない場合は各メンバーの設定が使用されます。 |
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | デフォルトのソート順。 |
| `isCompact` | `boolean` | 行のコンパクト表示。 |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | レコードをフィールドでグループ化します(例: かんばんのカラム)。 |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | かんばんカラムの集計とサイズ設定。 |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | カレンダービュー: レイアウト、レコードの位置を決める日付フィールド、オプションの終了日フィールド。 |
上記のすべての enum は `twenty-sdk/define` からエクスポートされています。
## フィルター
ビューには、あらかじめフィルターを適用した状態で提供できます。 各フィルターには 3 つの要素があります: フィルタリング対象の**フィールド**、**オペランド**(どのように比較するか)、**値**(何と比較するか)。 この 3 つがすべてそろっている必要があります — フィールドの型に適用できないオペランドを使用すると、同期時に拒否されます。
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### フィールドタイプごとにサポートされているオペランド
| フィールドタイプ | サポートされるオペランド |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> 名前が似ているフィールドタイプでも、まったく異なるオペランドを使用する場合があります。一般的な例としては `SELECT` と `MULTI_SELECT` があります。
### オペランドごとの値の形式
`value` フィールドは常に JSON シリアライズ可能な値ですが、想定される形式はオペランドによって異なります。
| オペランドの種類 | 値の形式 | 例 |
| ------------------------------------------------------- | ---------------------- | ------------------------ |
| `SELECT` に対する `IS`, `IS_NOT` | オプションキー(文字列)の配列 | `['ACTIVE', 'PENDING']` |
| `MULTI_SELECT` に対する `CONTAINS`, `DOES_NOT_CONTAIN` | オプションキー(文字列)の配列 | `['TAG_A']` |
| `RELATION` に対する `IS`, `IS_NOT` | レコード ID(UUID)の配列 | `['c5a1...']` |
| テキスト系フィールドに対する `CONTAINS`, `DOES_NOT_CONTAIN` | 文字列 | `'acme'` |
| `NUMBER` に対する `IS`, `IS_NOT` | 文字列(値) | `'5'` |
| `RATING` / `UUID` に対する `IS` | 文字列(値) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | 文字列(境界値) | `'10'` |
| `DATE` / `DATE_TIME` に対する `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 文字列 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | 空の文字列 | `''` |
| `BOOLEAN` に対する `IS` | `'true'` または `'false'` | `'true'` |
## ビューが UI にどのように表示されるか
ビュー単体では、サイドバーから直接アクセスすることはできません。 それをサイドバーに表示するには、ビューの `universalIdentifier` を指す、種類が `VIEW` の[ナビゲーションメニュー項目](/l/ja/developers/extend/apps/layout/navigation-menu-items)と組み合わせます。 それが標準的なパターンです。各カスタムオブジェクトには、作成時にデフォルトのインデックスビューが自動生成されるため、それを開くサイドバーエントリだけを用意しておけば十分です。

View File

@@ -0,0 +1,197 @@
---
title: バックグラウンドジョブ
description: 長時間かかる処理やレート制限される処理は、すべてをインラインで実行するのではなく、別のロジック関数の実行をキューに追加して Twenty のワーカーに渡してください。
icon: layer-group
---
ロジック関数の実行は、その `timeoutSeconds`(最大 900 秒)によって制限されます。 その時間内に完了できない処理 — フル再同期、レコードごとのファンアウト、レート制限を課してくるサードパーティ API など — は、より小さい実行単位に分割する必要があります。
`enqueueJobs` はまさにそれを行います。Twenty のワーカーに、アプリのロジック関数のいずれかを後で実行するよう依頼します。ペイロードごとに 1 回、各実行は独自のプロセスと独自のタイムアウト枠で行われます。 呼び出し元はすぐに戻ります。
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## 実行をエンキューする
`twenty-sdk/logic-function` から `enqueueJobs` をインポートし、実行するロジック関数の `universalIdentifier` を指定して、エンキューする実行ごとに 1 つのペイロードを渡します。
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
各実行は、他のトリガーとまったく同様に、ハンドラーの引数としてペイロードを受け取ります。 対象は呼び出し元と**同じアプリケーション**に属している必要があります。別のアプリの関数をエンキューしようとすると、`Logic function not found` で拒否され、何もエンキューされません。 1 回の呼び出しで最大 `200` 個のペイロードを受け付けます。
<Note>
`enqueueJobs` は、ジョブが受け付けられた時点ですぐに戻り、ジョブが実行された時点では戻りません。 対象の結果は返しません。結果を後から読み戻す必要がある場合は、各対象で生成したものを [key-value store](/l/ja/developers/extend/apps/logic/key-value-store) またはワークスペースレコードに書き込ませてください。
</Note>
<Note>
呼び出しごとに単一のジョブをエンキューする古い `enqueueJob` ヘルパーは非推奨です。 代わりに、要素が 1 つの `payloads` リストで `enqueueJobs` を使用してください。
</Note>
## ジョブオプション
オプションはバッチ内のすべての実行に適用されます。
| オプション | デフォルト | 範囲 | 機能 |
| ------------ | ----- | ------------------------ | --------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | 全体の追加キュー試行。 アプリケーションが要求する再試行回数は `3` に制限されます。 2 回実行しても安全なハンドラーに対してのみ、この値を増やしてください。 |
| `delayMs` | `0` | `0`–`604800000` (7 days) | 実行が実行可能になるまで、この時間待機します。 |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**優先度はまだ設定できません。** エンキューされたジョブは常に最も低い優先度で実行されるため、プラットフォームの処理がアプリケーションジョブによって遅延することはありません。 優先度を制御できる機能は近日中に提供予定です。
</Note>
キューに入れられた実行は、それをエンキューした関数の実行ユーザーを引き継ぐため、同じ権限で動作します。
## 一時的な障害を再試行する
Twenty は、アプリケーションコードからのすべての例外を再試行するわけではありません。 通常のスローされたエラーは、永続的な障害として扱われます。 一時的な障害の場合、キューに入れられたロジック関数は `RetryableLogicFunctionError` をスローすることで、最大 3 回の再試行をリクエストできます。
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
可能な場合は、`RetryableLogicFunctionError` を直接スローしてください。 これを拡張する場合は、その `name` を置き換えないでください。Twenty は、実行ランタイム間でシリアル化された名前 `RetryableLogicFunctionError` を認識します。
`retryCount` は初回実行時には `0` であり、アプリケーションコードが再試行をリクエストした場合にのみ増加します。 `maxRetries` は最大で `3` であり、キューに入れられたジョブの全体的な再試行上限がより小さい場合は、これより低くなることがあります。 プラットフォーム障害は、キューの全体的な安全性バジェットを消費しますが、`retryCount` を増加させません。
キューは、指数バックオフとジッターを使用して再試行を遅延させます。 正確な遅延時間は意図的に保証されていないため、アプリケーションコードは再試行が正確な時刻に行われることに依存しないでください。 `maxRetries` に達すると、別の実行を行わずに、次の `RetryableLogicFunctionError` が最終的なアプリケーション障害として記録されます。
<Warning>
再試行ではハンドラー全体が再実行され、一部の副作用が成功した後に実行される場合があります。 再試行をリクエストする前に、ハンドラーを冪等にしてください。
</Warning>
## 使いどころ: 長い同期処理をページングする
典型的なパターンは、次のカーソルを指定して*自分自身*をエンキューする関数です。 各実行は、自身のタイムアウト内に十分収まる 1 ページ分の処理だけを行い、処理対象がなくなったところでチェーンが停止します。
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## レコードごとにファンアウトする
処理が本質的にアイテムごとの場合は、1 回の呼び出しでアイテムごとに 1 つのジョブをエンキューし、インラインでループするのではなく、ワーカーに並列で処理させます。
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## 長時間実行タスクのためのベストプラクティス
ほとんどの長時間ジョブには、次の 2 つのルールで対応できます。**ループの代わりに再帰させること**、そして **1 回の実行で処理するチャンクを制限すること**。
すべてを 1 回の実行で片付けようとするのは失敗パターンです。タイムアウトに達し、リトライがかかると処理全体をまた最初からやり直すことになります。 代わりに、1 つのチャンクを `timeoutSeconds` 内に余裕をもって完了するサイズにし、現在位置を永続化してから、次の実行をエンキューします。
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
これが成立する理由:
* **チャンクのサイズは平均ではなく最も遅いアイテムに合わせましょう。** `CHUNK_SIZE × 最悪ケースのアイテム処理時間` が、余裕をもって `timeoutSeconds` に収まる必要があります。そうでないと、実行が打ち切られた際にチャンクの末尾が失われます。
* **終了条件を明示的にします。** チャンクがフルで返ってきた間だけ再帰させます。 「結果が 0 件」のみで停止するチェーンは、途中で短いページが返されることがあるソースに対しては、永久に動き続けてしまいます。
* **次の実行をエンキューする前に進捗を永続化**しておきます。そうすることで、どこかのリンクが失敗しても、最初からではなく最後に完了したチャンクから再開できます。
* **各チャンクはべき等に保ちます。** リトライ後に 1 つのチャンクを再処理しても二重書き込みにならないように、処理対象のレコードや外部 ID をキーにして書き込みを行ってください。
* **レート制限のあるサードパーティを相手にする場合は、巨大な 1 回のファンアウトよりもチャンク化したチェーンを優先**します。`delayMs` を設定したチェーンは自分でペース配分しますが、何千ものジョブを一度にエンキューすると、すべてが即座にキュー対象になってしまいます。

View File

@@ -0,0 +1,269 @@
---
title: 接続
description: OAuth を介して、アプリがユーザーに代わってサードパーティのサービスで操作を行えるようにします。
icon: plug
---
接続(Connection)は、ユーザーが外部サービス(Linear、GitHub、Slack など)のために保持する認証情報です。 あなたのアプリは、それらの認証情報を**どのように**取得するか — **接続プロバイダー** — を宣言し、実行時にそれらを利用してサードパーティの API へ認証付きの呼び出しを行います。
現在は OAuth 2.0 のみサポートされています。 将来的な認証情報タイプ(パーソナルアクセストークン、API キー、Basic 認証)も同じインターフェースに差し込まれます — すでに `defineConnectionProvider({ type: 'oauth', ... })` を使用しているアプリは移行の必要がありません。
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="アプリの接続をどのように取得するかを宣言する">
接続プロバイダーは、アプリに必要な OAuth のハンドシェイクを記述します。 ユーザーがアプリの設定で"接続を追加"をクリックし、プロバイダーの同意画面を完了すると、そのワークスペースに `ConnectedAccount` の行が作成されます。
動作するセットアップには、**2 つのファイル**が必要です — 接続プロバイダーと、OAuth クライアント認証情報を保持する `defineApplication` 上の対応する `serverVariables` 宣言です。
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
主なポイント:
* `listConnections({ providerName })` で使用される一意の識別子文字列が `name` です(kebab-case、`^[a-z][a-z0-9-]*$` に一致する必要があります)。
* `displayName` はアプリごとの設定タブおよび AI ツール一覧に表示されます。
* `clientIdVariable` / `clientSecretVariable` は値ではなく**名前**です — `defineApplication.serverVariables` で宣言されたキーと一致している必要があります。 実際の `client_id` と `client_secret` は、アプリ登録 UI を通じてサーバー管理者が入力し、あなたのリポジトリにコミットされることはありません。
* `applicationVariables` ではなく `serverVariables` を使用してください — OAuth の認証情報はサーバー全体で共有され、Twenty サーバーごとに 1 つの OAuth アプリとなります。
* 両方の `serverVariables` が入力されるまで、アプリごとの設定タブには"サーバー管理者が必要"というヒントが表示され、"接続を追加"ボタンは無効になります。
* 現在サポートされる値は `type: 'oauth'` のみです。 この判別子は前方互換です。将来のタイプ(`'pat'`、`'api-key'`、...) は、`oauth` に並ぶ新しいサブ設定ブロックを追加します。
プロバイダーで許可リストに登録する必要がある OAuth のコールバック URL は次のとおりです:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="接続時にロジック関数を実行する" description="接続が確立された瞬間に反応する">
一部のプロバイダは、接続時に、接続が実際に利用可能になる前に永続化しておく必要があるデータを渡してきます。典型的な例は Slack で、OAuth レスポンスに、受信イベントのキーとして使用されるワークスペースの `team_id` が含まれます。 `onConnectLogicFunction` を、同じアプリ内のロジック関数を `universalIdentifier` で参照するように設定すると、`ConnectedAccount` が作成された直後にその関数が実行されます。
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
このフックは**接続中のワークスペース内で非同期に**実行されます(待機されずにキューに入れられるため)、フックの実行が遅かったり失敗したりしても OAuth コールバックをブロックしたり壊したりすることは決してありません。冪等性を持たせ、自身でリトライ処理を行うようにしてください。 ハンドラーは次の内容を受け取ります:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
そこから `getConnection(connectedAccountId)` を使用して最新のアクセストークンを読み取り、プロバイダーの API(例: Slack `auth.test`)を呼び出すか、[key-value store](/l/ja/developers/extend/apps/logic/key-value-store) を使ってマッピングを永続化します。
</Accordion>
<Accordion title="切断時にロジック関数を実行する" description="接続が削除されたときにクリーンアップを行う">
アプリが接続時に確保したものは、接続が解除されたときに必ず解放しなければなりません。 例えば、接続時に `team_id` を確保する Slack 連携は、その確保を解放して、別のワークスペースが同じ Slack チームを接続できるようにしなければなりません。 同じアプリ内のロジック関数を参照するように `onDisconnectLogicFunction` を設定すると、`ConnectedAccount` が削除された直後にその関数が実行されます。
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
on-connect フックと同様に、**切断処理中のワークスペースで非同期に実行され**、切断をブロックすることはありません。 ハンドラーは同じペイロード構造を受け取ります:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
フックが実行される時点では `ConnectedAccount` はすでに削除されているため、`getConnection(connectedAccountId)` はもう解決されません。 クリーンアップに必要なもの(`team_id` や外部サブスクリプション ID など)は、接続時に `connectedAccountId` をキーとして [key-value store](/l/ja/developers/extend/apps/logic/key-value-store) に書き込まれている必要があります。
このフックは、接続が単独で削除されたときに発火します。 アプリをアンインストールすると、代わりにデータベースのカスケードによって接続が削除されるため、その場合はこのフックは実行されません。 その経路用に `defineApplication` 上で `uninstallLogicFunction` を宣言します。これはアプリのメタデータが削除される前に実行されるため、まだ `listConnections` を呼び出して残っているものをクリーンアップできます。
</Accordion>
<Accordion title="listConnections / getConnection" description="ロジック関数から接続を使用する">
ロジック関数ハンドラー内では、`listConnections({ providerName })` が指定したプロバイダーに対するこのアプリの `ConnectedAccount` 行を、更新済みのアクセストークン付きで返します。
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
各接続には次の項目があります:
| フィールド | 説明 |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| `id` | 一意の行 ID。単一の接続を再取得するには `getConnection(id)` に渡します。 |
| `visibility` | `'user'`(ワークスペースの 1 メンバー専用)または `'workspace'`(全メンバーと共有) |
| `scopes` | 連携先プロバイダーによって付与された OAuth 権限(`visibility` とは別で、無関係です) |
| `userWorkspaceId` | 所有者の userWorkspace ID — HTTP ルートトリガーで"要求ユーザーの接続"を選ぶのに役立ちます |
| `accessToken` | 新しい OAuth アクセストークン(期限切れの場合は自動更新) |
| `name` | 接続の表示名(OAuth コールバック時に自動取得。ユーザーが名称変更可能) |
| `handle` | 接続された上流アカウントのメールアドレスは、再接続ごとに更新されます。 トークン交換時に返されたOIDC `id_token`を解決し、プロバイダが1つを返さないときに接続するTwentyユーザーの電子メールに戻ります |
| `authFailedAt` | 直近の更新に失敗した場合に設定されます。ユーザーは再接続する必要があります。 |
主なポイント:
* プロバイダーで絞り込むには `{ providerName }` を渡します。省略すると、このアプリが保有するすべてのプロバイダーの接続を取得します。
* サーバーは返却前にアクセストークンを透過的に更新します。 ハンドラーは常に利用可能なトークン(または `authFailedAt` が設定された状態)を受け取ります。
* `getConnection(id)` は単一行の取得に相当します。
</Accordion>
<Accordion title="ユーザー単位 vs ワークスペース共有の可視性" description="ユーザーがプライベートと共有の認証情報を選ぶ方法">
ユーザーが"接続を追加"をクリックすると、可視性の選択を求められます:
* **自分だけ** — 認証情報は接続したユーザー専用です。 そのユーザーの権限で呼び出される任意のロジック関数(`isAuthRequired: true` の HTTP ルートトリガー)は参照できますが、cron トリガーやデータベースイベントは参照できません。
* **ワークスペースで共有** — どのワークスペースメンバーでもその認証情報を使用できます。 リクエストユーザーを持たないため、Cron/データベースのトリガーからも参照されます。
各ハンドラーで適切な方を使用してください:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
(ユーザー、プロバイダー)ごとに複数の接続が許可されているため、同一ユーザーが "Personal Linear" と "Work Linear" を並行して保持できます。
</Accordion>
<Accordion title="一度きりのプロバイダー設定" description="サードパーティサービスに OAuth アプリを登録する">
各接続プロバイダーごとに、まずサーバー管理者がサードパーティ側で OAuth アプリを登録する必要があります。
1. プロバイダーの開発者設定に移動します(例: https://linear.app/settings/api/applications/new)。
2. **Redirect URI** を `\<SERVER_URL>/auth/apps/callback` に設定します。
3. 生成された **Client ID** と **Client Secret** をコピーします。
4. サーバー管理者として Twenty でインストール済みアプリを開き、対応する `serverVariables` に値を設定します。
5. その後、ワークスペースメンバーはアプリごとの **Connections** セクションから接続を追加できます。
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,110 @@
---
title: キーバリューストア
description: 組み込みのアプリケーション用キー・バリュー ストアを使って、中間結果を永続化し、データをキャッシュし、ロジック関数の実行間で状態を共有します。
icon: database
---
ロジック関数は短命な Node.js プロセス内でサンドボックス実行されます。1 回の実行が終了すると、メモリ上に保持されていたものは何も残りません。 実行間で**何かを記憶しておく必要がある**場合(高コストな API レスポンスのキャッシュ、増分同期のためのカーソルの保存、処理のデバウンス、ある関数から別の関数への状態の受け渡しなど)、組み込みのキー・バリュー ストアに永続化します。
各アプリケーションには独自の分離された名前空間が割り当てられます。エントリは認証済みアプリ単位でキー付けされるため、別のアプリケーションとキーが衝突したり、読み取られたりすることは決してありません。
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## 取得、設定、削除
`kv` を `twenty-sdk/logic-function` からインポートします。 値には、任意の JSON シリアライズ可能なペイロードを指定できます。
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## スコープ
各エントリにはスコープがあり、すべての呼び出しでオプションとして渡されます。 デフォルトは `WORKSPACE` です。
* **`WORKSPACE`**(デフォルト) — エントリは、アプリの現在のワークスペース インストールに対してのみプライベートです。 アプリをインストールした各ワークスペースは、独立したキーセットをそれぞれ取得します。 これは、キャッシュやカーソル、ワークスペース単位の状態に適したスコープです。
* **`SERVER`** — エントリは、サーバー上の**すべてのインストール**間で共有されます。 サーバーエントリは **クレーム** のように動作します。保存される値は常にキーをクレームした workspaceId であり(現在のワークスペースでキーをクレームするには、`set` 時に `value` を省略します)、そのワークスペースだけが上書きや削除を行えます。 どのインストールからでも、そのエントリを読み取ることができます。
サーバークレームは、ワークスペース間ルーティングのために存在します。 [server-route resolver](/l/ja/developers/extend/apps/logic/logic-functions#server-route-trigger) はアプリケーション登録オーナーのワークスペース内で実行されますが、受信 Webhook は通常、外部アカウント ID のみを保持しており、Twenty の workspaceId は持っていません。 各ワークスペースが接続時に自分の外部 ID をクレームし、その後ルート内で解決します。
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
サーバーキーは呼び出し元自身のワークスペースに対してのみクレームでき、他のワークスペースによって上書きされることはないため、あるワークスペースが他人のマッピングを乗っ取ることはできません。 キーがすでに別のワークスペースによってクレームされている場合、`kv.set` は例外をスローします。
## 使ってみる:高コストな呼び出しをキャッシュする
典型的な用途としては、遅い、またはレート制限されたサードパーティのレスポンスをキャッシュしておき、毎回コストを支払うのではなく、繰り返しの実行で再利用することが挙げられます。
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## パターン & ヒント
* **ネームスペース化。** さまざまな用途を分離するため、また `sync-cursor:linear`、`cache:exchange-rate:USD:EUR`、`lock:nightly-report` のように、キーにプレフィックスを付けておくとまとめてルックアップしやすくなります。
* **期限 (TTL)。** ストアには組み込みの有効期限はありません。 値の中にタイムスタンプを保存して(キャッシュの例のように)読み取り時にチェックするか、[cron-triggered function](/l/ja/developers/extend/apps/logic/logic-functions) から古くなったキーを定期的にクリアします。
* **何を保存するか。** 数値、文字列、配列、オブジェクトなど、任意の JSON シリアライズ可能な値を保存できます。 エントリは小さく保ってください。これは調整やキャッシュのためのものであり、大きな BLOB やファイル用ではありません。 ファイルには `FILES` フィールドと [`uploadFile`](/l/ja/developers/extend/apps/logic/logic-functions#uploading-files) を使用してください。
* **可視性。** エントリはインスタンス データベースに保存され、ワークスペースレコードとしては存在しません。そのためワークスペースの UI に表示されることはなく、アプリのデータモデルの一部にもならず、ロールやオブジェクトの権限設定も不要です。
## 代替案:クエリ可能なストアオブジェクト
組み込みストアは、あえて中身が見えないように設計されています。エントリはレコードではないため、UI で閲覧したり、他のオブジェクトと関連付けたり、レコードクエリでフィルタしたりすることはできません。 そうしたものが必要な場合、たとえば可視化された同期ログやレコード単位の状態などには、代わりに一意な `key` フィールドと `RAW_JSON` の `value` フィールドを持つ小さな**テクニカルオブジェクト**を定義し、[typed API client](/l/ja/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) を通じてクエリします。 `defineObject` のリファレンスについては [Objects](/l/ja/developers/extend/apps/data/objects) を、キーの一意性を強制する方法については [Data → Unique indexes](/l/ja/developers/extend/apps/data/overview#unique-indexes) を参照してください。
* **レコードへのスコープ設定。** id をキーにエンコードするのではなく、ストアオブジェクトから対象オブジェクトへの [relation](/l/ja/developers/extend/apps/data/relations) を追加して関連付けます。
* **可視性と権限。** 行は他のレコードと同様にワークスペースのデータベース内に存在するため、API を通じてクエリでき、アプリの [role](/l/ja/developers/extend/apps/config/roles) に従った権限が適用されます。 ストアをメイン UI から隠しておきたい場合は、[navigation menu](/l/ja/developers/extend/apps/layout/navigation-menu-items) に追加しないでください。
<Note>
組み込みストアとは異なり、カスタムオブジェクトは常に 1 つのワークスペースにスコープされます。`SERVER` キーのように、インストール間でエントリを共有することはできません。
</Note>

View File

@@ -0,0 +1,909 @@
---
title: ロジック関数
description: HTTP、cron、およびデータベースのイベントトリガーを備えたサーバーサイドの TypeScript 関数を定義します。
icon: bolt
---
ロジック関数は、Twenty プラットフォーム上で実行されるサーバーサイドの TypeScript 関数です。 HTTPリクエスト、cronスケジュール、またはデータベースイベントでトリガーでき、AIエージェント向けのツールとして公開することもできます。
<AccordionGroup>
<Accordion title="defineLogicFunction" description="ロジック関数とそのトリガーを定義">
各関数ファイルは、ハンドラーと任意のトリガーを含む設定を `defineLogicFunction()` でエクスポートします。
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'POST',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
利用可能なトリガーの種類:
* **httpRoute**:HTTP のパスとメソッドで関数を公開します。 アプリのコードでは、`RestApiClient` を使用する際、ルートパスの先頭に `/s/` を付けてください。デプロイ後の URL では、`TWENTY_FUNCTIONS_URL` で注入されたベース URL が使用され、未設定の場合は `\<server-url>/s` が使用されます。
<Note>
(ヘッドレスの)フロントコンポーネントからルートトリガー型ロジック関数を呼び出す方法については、[ロジック関数を呼び出す](/l/ja/developers/extend/apps/layout/front-components#calling-a-logic-function)を参照してください。
</Note>
* **cron**: CRON 式を使用してスケジュールで関数を実行します。
* **databaseEvent**: ワークスペースのオブジェクトのライフサイクルイベントで実行されます。 イベント操作が `updated` の場合、監視する特定のフィールドを `updatedFields` 配列で指定できます。 未定義または空のままにすると、任意の更新でも関数がトリガーされます。
> 例:`person.updated`、`*.created`、`company.*`
* **serverRoute**: 1 つの登録スコープの HTTP ルートを公開します。 **resolver** 関数(`serverRouteTriggerSettings` で宣言)は、オーナーワークスペースで実行され、同期的な `Response` を返すか、ターゲットワークスペースとエンキューする対象のロジック関数を返します。エンキュールートでは、プラットフォームは `202` で ACK し、その **target** をワーカーキュー上で実行します。 [サーバールートトリガー](#server-route-trigger) を参照してください。
<Note>
CLI を使用して、関数を手動で実行することもできます:
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
ログは次のコマンドで監視できます:
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### ルートトリガーのペイロード
ルートトリガーがロジック関数を呼び出すと、
[AWS HTTP API v2 形式](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) に従う `RoutePayload` オブジェクトを受け取ります。
`RoutePayload` 型を `twenty-sdk/logic-function` からインポートします:
```ts
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
`RoutePayload` 型は次の構造になっています:
| プロパティ | タイプ | 説明 | 例 |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP ヘッダー (`forwardedRequestHeaders` に列挙されたもののみ) | 下記のセクションを参照 |
| `queryStringParameters` | `Record\<string, string \| undefined>` | クエリ文字列パラメーター (複数の値はカンマで連結) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | ルートパターンから抽出されたパスパラメーター | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | 解析済みのリクエストボディ (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | JSON 解析前の元の UTF-8 リクエストボディ。 HMAC 方式の Webhook 署名を検証する際に役立ちます(例: GitHub の `X-Hub-Signature-256`、Stripe)。 ランタイムがそれを保持しなかった場合は `undefined` です。 | |
| `isBase64Encoded` | `boolean` | body が base64 エンコードされているかどうか | |
| `requestContext.http.method` | `string` | HTTP メソッド (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | 生のリクエストパス | |
#### forwardedRequestHeaders
デフォルトでは、セキュリティ上の理由から、受信リクエストの HTTP ヘッダーはロジック関数に**渡されません**。
特定のヘッダーにアクセスするには、`forwardedRequestHeaders` 配列に明示的に列挙してください:
```ts
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
```
ハンドラー内では、転送されたヘッダーに次のようにアクセスします:
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
ヘッダー名は小文字に正規化されます。 小文字のキーを使用してアクセスしてください(例:`event.headers['content-type']`)。
</Note>
#### カスタム HTTP レスポンス
デフォルトでは、ハンドラーからプレーンな値を返すと、それが `200` レスポンスとして送信されます(オブジェクトの場合は JSON、文字列の場合は `text/plain`)。 ステータスコードとレスポンスヘッダーを制御するには、`twenty-sdk/logic-function` から `Response` を返します。
```ts
import { Response } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
return new Response('<h1>Hello</h1>', {
status: 201,
headers: { 'content-type': 'text/html' },
});
};
```
セキュリティ上の理由から、レスポンスヘッダーは許可リストに限定されています。 リストに含まれていないヘッダー(例: `Set-Cookie`、`Access-Control-Allow-Origin` などの CORS ヘッダー、カスタム `X-*` ヘッダー)は、レスポンスが送信される前に暗黙的に削除されます。 許可されているレスポンスヘッダーは次のとおりです。
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
ステータスコードは、有効な HTTP ステータスコード(100 から 599 の間)でなければなりません。 レスポンスヘッダー名は、大文字と小文字を区別せずに照合されます。
</Note>
#### プラットフォームのエラー応答
ハンドラー自身のレスポンス以外に、いくつかの状況ではプラットフォームがルート呼び出しに直接応答します。ルートまたは関数が存在しない場合は `404`、アプリケーションが停止している場合は `403`、実行レート制限に達した場合は `429`、アプリケーションの本番用 `dependencies` が大きすぎてインストールできない場合は `422` が返されます。詳しくは [dependencies size limits](/l/ja/developers/extend/apps/getting-started/project-structure#dependencies) を参照してください。
#### サーバールートトリガー
`httpRouteTriggerSettings` は `/s/` 配下に関数を公開し、リクエストホストからワークスペースを特定します。これは、各ワークスペースが独自ドメインを持つ場合に機能します。 しかし、サードパーティプロバイダーは、すべてのテナントのイベントを **1 つの** URL に配信します。 その場合は、`serverRouteTriggerSettings` を使用します。
トリガーには 2 つの構成要素があります:
1. **resolver** ロジック関数 — `serverRouteTriggerSettings` で宣言される — は、**オーナーワークスペース**(アプリケーション登録を所有するワークスペース)で実行されます。 この関数は受信リクエストを検査し、次のいずれかを返します。
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — プラットフォームは解決済みワークスペースでその target をキューに入れ、`202 { queued: true }` で ACK します、または
* `twenty-sdk/logic-function` からの `Response` — プラットフォームはその HTTP レスポンスを**同期的に**エコーし、ターゲットをキューに入れることは**ありません**(Slack の `url_verification` のようなチャレンジハンドシェイクに使用します)。
resolver は単一の認可ポイントであり、URL には resolver の識別子のみが含まれます。 **ここがリクエスト署名を検証するための推奨箇所です**。resolver は副作用が発生する前に実行され、元の `rawBody` と転送されたヘッダーにアクセスでき、ターゲットに一切触れずにリクエストを拒否できます。
2. **target** ロジック関数 — 通常のワークスペース単位のロジック関数 — は、その後、resolver によって解決されたワークスペースで、resolver が返したペイロード(resolver が変換しなかった場合は元のリクエストペイロード)を使って実行されます。 resolver が enqueue パスを選択した場合、その戻り値は HTTP 呼び出し元からは**観測されません**。
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
エンドポイントには次の URL でアクセスできます:
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
識別子は、マニフェストに記載されている resolver の `universalIdentifier` です。 その URL をプロバイダーに登録します。
**検証用 GET への応答。** 一部のプロバイダーは、後でイベントを `POST` する同じ URL にチャレンジを含む `GET` を送信して、配信先のエンドポイントを事前に検証します。Meta の WhatsApp Cloud API はその一例です。 特に指定しない限り、サーバールートは `POST` にのみ応答するため、両方のメソッドを宣言します。
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
チャレンジは `event.queryStringParameters` で受信され、`Response` を返すと、同じリクエストでプロバイダーにチャレンジが返されます。 文字列のボディは `text/plain` として送信され、これはこれらのプロバイダーが想定する形式です。
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` はデフォルトに追加するのではなく置き換えるため、`['GET']` のみを指定すると、ルートは `POST` を拒否します。 サポートされているのは `GET` と `POST` のみです。 プロバイダーが2つ目の動詞を必要とする場合を除き、未設定のままにしてください。`GET` を宣言するルートでは、認証されていないすべての呼び出し元(予告なく `GET` を送信するクローラーやリンク展開ツールを含む)によってリゾルバーが実行されます。 プラットフォームにメソッドが存在しないものには、リゾルバーが実行されることなく `405` が返されます。
<Note>
**アプリケーションは所有者ワークスペースでクレームされ、インストールされている必要があります。** リゾルバーは **所有者ワークスペース**(アプリケーション登録を所有しているワークスペース)上で実行されるため、サーバールートトリガーが動作するのは、アプリケーションが*クレーム*されている、つまり所有者ワークスペースを持っていること **かつ** そのアプリケーションが **所有者ワークスペースにインストールされている** 場合のみです。 この2つの条件がどちらも満たされるまでは、リゾルバーを実行する場所が存在しないため、ルートをディスパッチできません。 したがって、`serverRouteTriggerSettings` ロジック関数を公開するアプリケーションは、所有者ワークスペースでクレームされインストールされるまで、マーケットプレイスに掲載することはできません。
</Note>
**Resolver の契約。** SDK の `LogicFunctionConfig` 型はコンパイル時にこれを強制します。`serverRouteTriggerSettings` を設定するとすぐに、ハンドラーは `Response` か、`{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }`(またはそのいずれかの `Promise`)を返すように制約されます。 dispatch パスでは、`workspaceId` は target 関数がインストールされているワークスペースである必要があります。そうでない場合、リクエストは `404` で拒否されます。 どちらの形にも一致しない結果(識別子が UUID でないものを含む)は、`502` で拒否されます。
| フィールド | タイプ | ノート |
| ---------------------------------------- | --------------- | -------------------------------------------- |
| `workspaceId` | `string` | ターゲットが実行されるワークスペースの UUID。 |
| `targetLogicFunctionUniversalIdentifier` | `string` | そのワークスペースで呼び出すロジック関数の `universalIdentifier`。 |
| `payload` | `object`(オプション) | 設定された場合、ターゲットに送信されるリクエストボディを置き換えます。 |
<Warning>
**署名の検証はあなたの責任です — resolver 内で検証してください。** プラットフォームはリクエスト署名を検証しません。 resolver はそれを行う推奨箇所です。最初に実行され、`event.rawBody` と、`forwardedRequestHeaders` に指定したヘッダーにアクセスできます。また、エラーをスローする(または一致しない `workspaceId` を返す)ことで、ターゲットが呼び出される前にディスパッチを停止できます。 代わりに検証処理を target 側に押し下げる場合、target は `rawBody` とヘッダーを失わないよう注意する必要があります。つまり、resolver は `payload` を返してはいけません。 副作用を伴う処理の**前に**必ず検証を行い、コンスタントタイム比較を使用してください。
</Warning>
リクエスト署名については、ほとんどのプロバイダーが HMAC-SHA256 で署名します。異なるのはヘッダー名、ダイジェストのエンコーディング、および署名対象となるペイロード文字列です。 いくつかの例を示します。
| プロバイダー | 転送するヘッダー | 署名対象文字列 | ダイジェスト |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ---------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64(シークレットは `whsec_` を取り除いた後の文字列を base64 として扱います) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex(先頭に `sha256=` が付与されます) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex(先頭に `v0=` が付与されます) |
上記の resolver の例では、すでに GitHub の HMAC-SHA256 フローを示しています。連携するプロバイダーに合わせて、ヘッダー名、ダイジェストのエンコーディング、および署名対象となるペイロード文字列を調整してください。
<Note>
resolver が dispatch オブジェクトを返すと、ルートは `202 { queued: true }` を返し、target はワーカーキュー上で実行されます。呼び出し元は target のレイテンシ、結果、失敗を一切観測しません(それらは実行ログに記録されます)。 これにより、送信側の再配信によって処理の遅延が増幅されるのを防げます。これは、Webhook 取り込みにおいて望ましい動作です。
呼び出し元が同じリクエストでレスポンスボディを読み取る必要がある場合(チャレンジハンドシェイク、対話的な受領確認など)、代わりに **resolver** から `Response` を返してください。 プラットフォームはそれを同期的にエコーし、キューをスキップします。そのヘッダーは HTTP ルートレスポンスと同じ許可リストを通過します。 resolver は高速に保ってください。Slack など一部のプロバイダーは数秒でタイムアウトします。 resolver はパブリックエンドポイントとして到達可能であるため、エッジでレート制限をかけて保護してください。
</Note>
#### データベースイベントトリガーのペイロード
データベースイベントトリガーがロジック関数を呼び出すと、変更されたレコードごとに 1 つの `DatabaseEventPayload` を受け取ります。 このペイロードは、ソースワークスペースとオブジェクトに関するメタデータを、レコードレベルのイベントと組み合わせたものです。
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
ペイロードには次のものが含まれます:
| プロパティ | 説明 |
| ------------------------------------------------ | ------------------------------------------------------------------- |
| `name` | `person.updated` などのイベント名。 |
| `workspaceId` | イベントが発生したワークスペース。 |
| `objectMetadata` | 変更されたオブジェクトのメタデータ。 |
| `recordId` | 変更されたレコードの ID。 |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | イベントがワークスペースユーザーによって引き起こされた場合のアクターに関するフィールド。 |
| `properties` | イベントのレコードデータ。操作に応じて `before`、`after`、`diff`、`updatedFields` が含まれます。 |
| イベント | レコードデータ |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
ソフトデリートの場合、レコードの `deletedAt` フィールドが変化するため、`.deleted` はアップデート時と同じ形式になります。
完全な削除の場合は `.destroyed` を使用します。
<Note>
`databaseEventTriggerSettings.updatedFields` は、どの更新イベントで関数をトリガーするかをフィルタリングします。
`event.properties.updatedFields` は、現在のイベントで実際にどのフィールドが変更されたかを示します。
</Note>
作成イベントの例:
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
更新イベントの例:
```ts
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
return {
personId: event.recordId,
updatedFields,
previousEmail: before.emails?.primaryEmail,
currentEmail: after.emails?.primaryEmail,
emailDiff: diff.emails,
};
};
```
メール更新時のみトリガーする例:
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
削除イベントの例:
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### 実行コンテキスト
すべてのハンドラーは、何によってトリガーされたかにかかわらず、実行自体を説明する2番目の引数を受け取ります。 最初の引数はトリガーごとに形式が変わりますが、こちらは変わりません:
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| フィールド | タイプ | 説明 |
| ------------------- | ---------------- | ----------------------------------------------------------------------------------- |
| `workspaceId` | `string` | 実行が属するワークスペース。 |
| `userWorkspaceId` | `string \| null` | このワークスペースにおけるメンバーシップとしての、実行をトリガーした人物。 |
| `workspaceMemberId` | `string \| null` | ワークスペースメンバーのレコードとしての同じ人物であり、レコードをリンクする対象です。 |
| `retryCount` | `number` | 最初の試行では `0`。 [バックグラウンドジョブ](/l/ja/developers/extend/apps/logic/background-jobs)を参照してください。 |
| `maxRetries` | `number` | アプリケーションが要求する再試行回数の上限。 |
誰も実行をトリガーしていない場合、`userWorkspaceId` と `workspaceMemberId` は `null` です。cron スケジュール、インストールフック、および認証されていない Webhook には、それに対応する人物がいません。 その人物にワークスペースメンバーのレコードがない場合、またはそのレコードが削除されている場合も、これらは `null` です。
<Note>
**コンテキストは、誰が実行をトリガーしたかを示します。** 実行で*できること*は、以下で説明する別の宣言です。
</Note>
#### 呼び出しで使用されるアクセス権
すべてのクライアント(`CoreApiClient`、`MetadataApiClient`、`RestApiClient`)は、実行をトリガーした人物として動作します。その人物のロールとアプリケーションのロールの共通部分が適用されるため、呼び出しでできることは、どちらか一方ができることを超えません。 これがデフォルトであり、人物がアプリを使用して自身の権限を超えることは決してできないことを意味します。
誰も実行をトリガーしていない場合、動作する人物がいないため、同じクライアントはアプリケーション自身のアクセス権にフォールバックします。cron スケジュール、インストールフック、および認証されていない Webhook には特別な処理は必要ありません。
人物が実行の背後に*いる*場合でも、アプリケーション自身のアクセス権を正当に必要とする呼び出しがあります。たとえば、アプリの設定レコードの読み取りや、その人物自身では実行できない作業です。 そのために2つ目のクライアントを作成します:
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
両方をモジュールスコープで一度だけ作成し、各呼び出し箇所では、呼び出すクライアントによって使用するアクセス権を指定します。 `RestApiClient` も同じオプションを受け取ります:
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**誰にもトリガーされていない実行は、アプリケーションとして動作します。** cron スケジュール、インストールフック、および認証されていない Webhook には、それに対応する人物がいないため、デフォルトのクライアントはアプリケーション自身のアクセス権にフォールバックして動作し続けます。 `runAs: 'application'` が必要なのは、人物が実行をトリガーした場合にそのアクセス権を使用したいときだけです。
たとえばレコードの帰属先を指定するためなど、誰かが背後にいるかどうかによって関数の動作が異なる場合は、`context.workspaceMemberId` を確認してください。
</Note>
アプリケーション自身のリソースにアクセスする SDK ヘルパーは、常にそのアクセス権を使用し、`runAs` を無視します。[キー値ストア](/l/ja/developers/extend/apps/logic/key-value-store)、[接続](/l/ja/developers/extend/apps/logic/connections)、`runAgent`、`getPublicAssetUrl`、およびクレジットの請求が該当します。
#### 関数を AI ツールまたはワークフロー アクションとして公開する
ロジック関数は 2 つのサーフェス上で公開でき、それぞれに固有のトリガーがあります:
* **`toolTriggerSettings`** — Twenty の AI 機能(チャット、MCP、関数呼び出し)からその関数を見つけられるようにします。 標準的な JSON Schema を使用します。これは LLM がネイティブに理解する形式です。
* **`workflowActionTriggerSettings`** — ビジュアル ワークフロー ビルダー内のステップとして関数を表示します。 ビルダーが適切なフィールドエディタ、変数ピッカー、ラベルをレンダリングできるよう、Twenty の充実した `InputSchema` を使用します。
関数は一方、もう一方、またはその両方を選択できます。 これらは `cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings` と並列に存在します — 同じパターン、同じ形です。
<Note>
**ワークフローの Code アクションとの関係。** ワークフロービルダーに組み込まれている **Code** アクション自体もロジック関数です。Twenty は Code ステップごとに 1 つ作成し、そのエディタをインラインで表示します。 `workflowActionTriggerSettings` を使うことで、その使い捨てのインラインコードを**再利用可能な**アクションに変換できます。アプリ内でその関数を 1 度定義すれば、各 Code ステップにコードをコピーペーストする代わりに、任意のワークフローで選択できるようになります。 エンドユーザー側の表示については、ユーザーガイドの [Code action](/l/ja/user-guide/workflows/capabilities/workflow-actions#code) を参照してください。
</Note>
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
主なポイント:
* 関数はサーフェスを混在させることができます — `toolTriggerSettings` と `workflowActionTriggerSettings` の両方を宣言して、チャットおよびワークフロー ビルダーの両方に公開します。
* `toolTriggerSettings.inputSchema` と `workflowActionTriggerSettings.inputSchema` はいずれも任意です。 省略された場合、マニフェストビルダーはハンドラーのソースコードからそれらを推論します(AI ツールには JSON Schema、ワークフロー アクションには Twenty の `InputSchema`)。 より豊富な型付けが必要な場合は、明示的に指定してください — たとえば、ワークフロー ビルダー向けに `CURRENCY` や `RELATION` といった `FieldMetadataType` に対応したフィールド、または AI エージェントが読み取れる `description` フィールドを使用する場合など:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
```
パラメーターを**一度だけ**宣言して両方のサーフェスで利用できるようにするには、単一の JSON Schema(`InputJsonSchema`)を定義し、`twenty-sdk/logic-function` の `jsonSchemaToInputSchema` を使ってワークフローアクション用に変換します。 `toolTriggerSettings.inputSchema` は JSON Schema を直接受け取りますが、`workflowActionTriggerSettings.inputSchema` には Twenty の `InputSchema` が必要です。
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### ワークフローアクションの完全な例
`workflowActionTriggerSettings` は 4 つのフィールドを受け取ります:
| フィールド | 目的 |
| -------------- | -------------------------------------------------------------------------------------------------- |
| `label` | ワークフロービルダーのステップピッカーに表示されるアクション名。 指定しない場合は、関数の `name` がデフォルトになります。 |
| `icon` | アクションの横に表示されるアイコン(`tabler-icons` の名前。例: `IconBuilding`)。 |
| `inputSchema` | Twenty のリッチな `InputSchema` — ビルダーが設定可能なフィールド(変数ピッカー付き)としてレンダリングするものです。 省略可能。省略された場合はハンドラーから推論されます。 |
| `outputSchema` | ハンドラーが返すデータの形を宣言し、**後続のステップがその出力フィールドへマッピングできるようにします**。 省略可能。指定しない場合、出力は単一の不透明な値として公開されます。 |
これらを組み合わせると、ワークフローアクションとして公開される関数になり、`taskId` を後続ステップから参照できるように出力を宣言できます。
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
アプリがインストールされると、ワークフロービルダーのアクションピッカーに **Enrich Company** が表示されます。 ビルダーは `companyName` と `domain` を入力フィールドとしてレンダリングします(それぞれが前のステップから値を取得可能)、さらに下流のステップでは、そのステップの `taskId` と `enriched` の出力を参照できます。
<Note>
**良い `description` を記述してください。** AI エージェントは、ツールをいつ使用するかを判断するために関数の `description` フィールドに依存します。 ツールが何を行い、いつ呼び出すべきかを具体的に記述してください。
</Note>
</Accordion>
</AccordionGroup>
<Note>
**ランタイムヘルパー。** `twenty-sdk/utils` は、小さなランタイムヘルパーを再エクスポートすることで、ハンドラーが直接 `twenty-shared` からインポートする必要がないようにします。 たとえば、`isDefined(value)` は `null` と `undefined` の両方に対して `false` を返します。これを使うと、オプショナルなハンドラー入力を安全に絞り込めます。こうした入力は、型としては `T | undefined` であっても、実行時には `null` として渡される場合があります。
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**インストールフック** — pre-install、post-install、uninstall の各ハンドラー — はこのランタイムを共有しますが、それぞれ独自の define 関数で宣言され、トリガー設定は受け取りません。 `definePreInstallLogicFunction`、`definePostInstallLogicFunction`、`defineUninstallLogicFunction` については、[インストールフック](/l/ja/developers/extend/apps/config/install-hooks) を参照してください。
</Note>
## タイムラインアクティビティを作成する
`createTimelineActivity()` を使用して、ロジック関数から明示的なドメインイベントを公開します。 まずイベントを[タイムラインアクティビティタイプ](/l/ja/developers/extend/apps/data/timeline-activity-types)として定義し、次に安定したユニバーサル識別子でタイプとオブジェクトを指定します:
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
Twenty はユニバーサル識別子をインストール固有のメタデータ ID に解決し、タイムラインアクティビティタイプが呼び出し元のアプリに属していることを検証して、新しいアクティビティにその表示メタデータのスナップショットを保存します。
必須の入力は `timelineActivityTypeUniversalIdentifier`、`targetObjectUniversalIdentifier`、および `targetRecordId` です。 `happensAt`、`properties`、および `workspaceMemberId` も指定できます。 `happensAt` はタイムライン内のイベントの位置と表示時刻を制御します。デフォルトは作成時刻です。
別のレコードをイベントに関連付けるには、`linkedRecordId` と `linkedObjectMetadataUniversalIdentifier` を一緒に指定します。 履歴表示用のフォールバックとして、`linkedRecordCachedName` を追加で指定することもできます:
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
ロジック関数のロールには、標準の `timelineActivity` オブジェクトに対する書き込み権限が必要です。 明示的なイベントタイプは `action` にバインドしないでください。アクションにバインドされたタイプはすでに自動監査イベントを受け取るため、そうしない場合は重複した行が生成されます。
## 型付き API クライアント(`twenty-client-sdk`)
`twenty-client-sdk` パッケージは、ロジック関数やフロントコンポーネントから Twenty API とやり取りするための、型付き GraphQL クライアントを 2 つ提供します。
| クライアント | インポート | エンドポイント | 自動生成? |
| ------------------- | ---------------------------- | ------------------------------------ | ----------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — ワークスペースデータ(レコード、オブジェクト) | はい、開発/ビルド時 |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — ワークスペース設定、ファイルアップロード | いいえ、あらかじめビルド済みで提供 |
<AccordionGroup>
<Accordion title="CoreApiClient" description="ワークスペースデータ(レコード、オブジェクト)をクエリおよび変更">
`CoreApiClient` は、ワークスペースデータのクエリと変更のための主要なクライアントです。 `yarn twenty dev` または `yarn twenty dev:build` の実行時に**ワークスペースのスキーマから生成される**ため、オブジェクトやフィールドに一致する完全な型付けが行われます。
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
このクライアントは selection-set 構文を使用します。フィールドを含めるには `true` を渡し、引数には `__args` を使い、リレーションにはオブジェクトをネストします。 ワークスペースのスキーマに基づく完全な補完と型チェックが得られます。
<Note>
**CoreApiClient は開発/ビルド時に生成されます。** 先に `yarn twenty dev` または `yarn twenty dev:build` を実行せずに使用すると、エラーが発生します。 生成は自動で行われます—CLI がワークスペースの GraphQL スキーマをイントロスペクトし、`@genql/cli` を使用して型付きクライアントを生成します。
</Note>
#### CoreSchema を型注釈に使用する
`CoreSchema` は、ワークスペースのオブジェクトに対応する TypeScript の型を提供し、コンポーネントの状態や関数パラメーターの型付けに役立ちます:
```ts
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
</Accordion>
<Accordion title="MetadataApiClient" description="ワークスペースの設定、アプリケーション、ファイルアップロード">
`MetadataApiClient` は SDK に同梱されており、あらかじめビルド済みです(生成は不要)。 ワークスペースの設定、アプリケーション、ファイルアップロードのために `/metadata` エンドポイントに対してクエリを実行します。 これは `CoreApiClient` と同じ `runAs` オプションを受け取ります。詳細は[呼び出しで使用されるアクセス権](#whose-access-a-call-uses)を参照してください。
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### ファイルのアップロード
`MetadataApiClient` には、ファイル型フィールドにファイルを添付するための `uploadFile` メソッドが含まれています:
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| パラメーター | タイプ | 説明 |
| ---------------------------------- | -------- | ------------------------------------------------ |
| `fileBuffer` | `Buffer` | ファイルの生データ |
| `filename` | `string` | 保存および表示に使用されるファイル名 |
| `contentType` | `string` | MIME タイプ(省略時は `application/octet-stream` がデフォルト) |
| `fieldMetadataUniversalIdentifier` | `string` | オブジェクト上のファイルタイプのフィールドの `universalIdentifier` |
主なポイント:
* フィールドの `universalIdentifier`(ワークスペース固有の ID ではありません)を使用するため、アップロードコードはアプリがインストールされている任意のワークスペースで動作します。
* 返される `url` は、アップロード済みファイルにアクセスするために使用できる署名付き URL です。
</Accordion>
</AccordionGroup>
<Note>
コードが Twenty 上で実行される際(ロジック関数やフロントコンポーネント)、プラットフォームは認証情報を環境変数として注入します:
* `TWENTY_API_URL` — Twenty API のベース URL
* `TWENTY_APP_ACCESS_TOKEN` — デフォルトのアクセス権用の短命のキーです。誰かが実行の背後にいる場合はその人物のロールとアプリケーションのロールの共通部分、誰もいない場合はアプリケーション自身のロールが適用されます。 ロジック関数では実行をトリガーした人、フロントコンポーネントではページを見ている人が、その人物です。
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — アプリケーション自身のロールのみにスコープされた短命のキーです。 ロジック関数でのみ使用され、常にそこに注入されます。また、`runAs: 'application'` が使用するものです。
これらをクライアントに渡す必要は**ありません**。クライアントは `process.env` から自動的に読み取り、どちらを選択するかについては[呼び出しで使用されるアクセス権](#whose-access-a-call-uses)で説明しています。 アプリケーション自身の権限は、`defineApplicationRole()` で宣言されたロール(または `application-config.ts` の `defaultRoleUniversalIdentifier` で参照されるロール)によって決まります。人物として動作する実行は、そのロールもその人物のロールも超えることはできません。
</Note>

View File

@@ -0,0 +1,61 @@
---
title: 概要
description: HTTP ルート、cron スケジュール、データベースイベント、AI ツール、またはワークフローアクションによってトリガーされ、Twenty 内部で実行されるサーバーサイドの TypeScript。
icon: bolt
---
Twenty アプリの **ロジックレイヤー** は、*実行される* コードです。HTTP リクエスト、cron スケジュール、レコードの変更に反応するサーバーサイドの TypeScript ハンドラー、ワークスペース内で動作する AI スキルとエージェント、そして関数がサードパーティサービス上でユーザーに代わって動作できるようにする OAuth 接続が含まれます。
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
▼
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## このセクションについて
<CardGroup cols={2}>
<Card title="ロジック関数" icon="bolt" href="/l/ja/developers/extend/apps/logic/logic-functions">
コアとなる構成要素 — トリガーの種類、ペイロード、および型付き API クライアント。
</Card>
<Card title="スキルとエージェント" icon="robot" href="/l/ja/developers/extend/apps/logic/skills-and-agents">
再利用可能な AI エージェント向けの指示や、カスタムのシステムプロンプトを備えたアシスタント。
</Card>
<Card title="接続" icon="plug" href="/l/ja/developers/extend/apps/logic/connections">
Linear、GitHub、Slack など、サードパーティサービス向けにアプリが保持する OAuth 資格情報。
</Card>
<Card title="キーバリューストア" icon="database" href="/l/ja/developers/extend/apps/logic/key-value-store">
ロジック関数の実行間で状態を永続化します ― キャッシュ、カーソル、ワークスペース間のクレーム。
</Card>
<Card title="バックグラウンドジョブ" icon="layer-group" href="/l/ja/developers/extend/apps/logic/background-jobs">
ワーカー上でロジック関数の実行をキューに入れて、実行ごとのタイムアウト制限を回避します。
</Card>
</CardGroup>
## トリガータイプの概要
ロジック関数は 1 つ以上のトリガーを選択します。以下の各項目は、`defineLogicFunction()` 上の個別のフィールドです。
| トリガー | 実行タイミング | 設定 |
| --------------- | --------------------------------------------------- | ------------------------------- |
| **HTTP ルート** | リクエストが `/s/\<path>` エンドポイントに到達したとき | `httpRouteTriggerSettings` |
| **クロン** | CRON 式が一致したとき | `cronTriggerSettings` |
| **データベースイベント** | ワークスペースのレコードが作成、更新、または削除されたとき | `databaseEventTriggerSettings` |
| **AI ツール** | Twenty の AI 機能が関数を呼び出すことを決定したとき | `toolTriggerSettings` |
| **ワークフローアクション** | ワークフローステップが関数を呼び出したとき | `workflowActionTriggerSettings` |
関数は分離された Node.js プロセス内でサンドボックス実行され、[`defineApplication()`](/l/ja/developers/extend/apps/config/application) で宣言されたロールにスコープされた型付き API クライアントを通じてワークスペースにアクセスします。
<Note>
**インストール時フック** — インストールの前後に実行されるコード — はこのランタイムを共有しますが、独自の define 関数を使用し、[Config → Install Hooks](/l/ja/developers/extend/apps/config/install-hooks) の下に配置されます。
</Note>

View File

@@ -0,0 +1,181 @@
---
title: スキルとエージェント
description: アプリ向けの AI のスキルとエージェントを定義します。
icon: robot
---
<Warning>
スキルとエージェントは現在アルファ版です。 この機能は動作しますが、まだ進化の途上です。
</Warning>
アプリは、ワークスペース内で動作する AI 機能 — 再利用可能なスキルの指示やカスタムのシステムプロンプトを持つエージェントを定義できます。
<AccordionGroup>
<Accordion title="defineSkill" description="AI エージェントのスキルを定義">
スキルは、ワークスペース内で AI エージェントが利用できる再利用可能な指示と機能を定義します。 組み込み検証付きでスキルを定義するには `defineSkill()` を使用します:
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
主なポイント:
* `name` はスキルの一意の識別子文字列です(kebab-case を推奨)。
* `label` は、UI に表示される人間が読める表示名です。
* `content` にはスキルの指示が含まれます — これは AI エージェントが使用するテキストです。
* `icon`(任意)は、UI に表示されるアイコンを設定します。
* `description`(任意)は、スキルの目的に関する追加のコンテキストを提供します。
</Accordion>
<Accordion title="defineAgent" description="カスタムプロンプトで AI エージェントを定義">
エージェントは、ワークスペース内で動作する AI アシスタントです。 カスタムのシステムプロンプトでエージェントを作成するには `defineAgent()` を使用します:
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
主なポイント:
* `name` はエージェントの一意の識別子文字列です(kebab-case を推奨)。
* `label` は UI に表示される表示名です。
* `prompt` はエージェントの動作を定義するシステムプロンプトです。
* `description`(任意)は、エージェントが何を行うかのコンテキストを提供します。
* `icon`(任意)は、UI に表示されるアイコンを設定します。
* `modelId`(任意)は、エージェントが使用するデフォルトの AI モデルを上書きします。
* `responseFormat`(オプション)は、エージェントの出力の形式を制御します。 自由形式テキストの場合、デフォルトは `{ type: 'text' }` です。 構造化された JSON 出力を強制するには、`{ type: 'json', schema }` を使用します。
デフォルトでは、エージェントは自由形式テキストを返します。 構造化された出力を取得するには、`responseFormat` を `{ type: 'json' }` に設定し、`schema` を指定します:
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
スキーマに関する補足:
* スキーマはフラットなオブジェクトであり、各プロパティの `type` はプリミティブ型(`string`、`number`、`boolean` のいずれか)でなければなりません。 ネストされたオブジェクトおよび配列はサポートされていません。
* 各プロパティの `description`(オプション)は、そのプロパティに何を入れるべきかをモデルに示します。
* `required`(オプション)は、モデルが常に返さなければならないプロパティを列挙します。
* `additionalProperties: false`(オプション)は、`properties` に宣言されていないプロパティを禁止します。
</Accordion>
<Accordion title="runAgent" description="ロジック関数からエージェントを実行する">
`runAgent()` を使用すると、ロジック関数からアプリのエージェント(そのスキルやツールを含む)を実行できます。 `defineAgent()` に渡した `universalIdentifier` でエージェントを識別します。 `prompt` 文字列または会話履歴である `messages` のいずれか一方のみを渡してください。両方は渡さないでください。
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
マルチターンのボット (Slack、Discord、Teams など) の場合は、単一の `prompt` ではなく、スレッド履歴を `messages` として渡してください。
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
主なポイント:
* `prompt` (string) または `messages`(1~100 個の `{ role: 'user' | 'assistant', content: string }` エントリ)のうち、**ちょうど一つだけ**を指定してください。
* エージェントは**同期的に**実行され、自身のツールを通じてレコードの読み取り/更新ができます。`runAgent()` は、実行が完了すると解決されます。
* アプリは自分自身のエージェントのみを実行できます。
* アプリの[デフォルトロール](/l/ja/developers/extend/apps/config/roles)には `AI` パーミッションフラグが付与されている必要があります — その `permissionFlagUniversalIdentifiers` に `SystemPermissionFlag.AI` を追加するか(または `canAccessAllTools: true` を設定します)。
これがない場合、`runAgent()` はパーミッションエラーで失敗します。
* ロジック関数には十分に長い `timeoutSeconds` を設定してください — エージェントの実行には数秒かかることがあります。
* 実行が完了すると `success` は `true` になり、`result` は null 以外になります。失敗時には `success` は `false`、`result` は `null` で、`error` に理由が入ります(たとえば、実行の途中でワークスペースの AI クレジットが不足した場合など)。
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**ループを避けてください:** `*.updated` データベースイベントトリガーから `runAgent()` を呼び出し、そのエージェントが同じレコードを更新する場合、`updatedFields` を使ってエージェントが決して書き込まないフィールド(例: ソース URL)にスコープするか、`runAgent()` を呼び出す前に、対象フィールドのいずれかがまだ空であるかどうかを判定してガードしてください。
</Warning>
### ワークスペースメンバーを代理して実行する場合
実行が人によってトリガーされた場合 — たとえばメッセージに返信するチャットボットなど — には、`runAsWorkspaceMemberId` を渡して、そのエージェントがアプリとしてではなく、そのメンバーとして動作するようにします。
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
この実行はその後、**メンバー自身のロール**で動作します。そのメンバーができることだけを実行でき、それ以上のことはできません。作成されたレコードはそのメンバーに帰属し、そのメンバーの行レベル権限が適用されます。 エージェントのロールは関与しません。これは、背後に誰もいない実行に対するアプリのデフォルトです。 自律的な実行(スケジュールされたジョブ、データベースイベントトリガー)の場合は、そのフィールドを省略します。この場合、エージェント自身のロールが維持されます。 動作中のアプリケーションは、メンバーの権限を狭めることなく、プロビナンスのために実行のコンテキストに紐づいたままになります。
実行をトリガーした人物を、どのワークスペースメンバーに対応付けるかは、あなたのアプリ側の責任です。 メンバーを指定するにはアプリアクセストークンが必要であり、トークンで指定できるメンバーは、そのトークンにユーザーが紐づいているかどうかに依存します。
* ユーザーが紐づいていないトークンは、任意のメンバーを指定できます。 ロジック関数はそのようなトークンで実行され、`client_credentials` を通じて、または API
キーから発行されたトークンも同様です。
* フロントコンポーネントが受け取るような、ユーザーを代表して発行されたトークンは、そのユーザー自身のメンバーのみを指定できます。
それ以外、つまり通常のユーザーセッションやアプリアクセストークンを伴わない API キーは、メンバーをまったく指定できません。
<Warning>
`runAgent()` は、ワークスペースメンバーを解決できない場合(不明または削除されたメンバー、あるいはロールを持たないメンバー)に例外をスローします。 呼び出し元が要求した以上のアクセス権を与えてしまうことになるため、
エージェント自身のロールにフォールバックすることは決してありません。
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: 関数の実行、ログのストリーミング、アプリのインストール管理、リモートの切り替えを行う yarn twenty のコマンド。
icon: terminal
---
`yarn twenty` CLI は、アプリ関連のすべてを操作するためのインターフェースです。 コマンド一覧:
| コマンド | 機能 | 参照ドキュメント |
| ----------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `dev` | ソースファイルを監視し、変更をライブ同期します | [クイックスタート](/l/ja/developers/extend/apps/getting-started/quick-start) |
| `plan` | メタデータの変更を適用せずにプレビュー | [Syncing & recovery](/l/ja/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `apply` | プランを表示した後にメタデータの変更を適用 | [Syncing & recovery](/l/ja/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | アプリをコンパイルし、API クライアントを生成します(`.tgz` にパッケージするには `--tarball` を使用) | [公開](/l/ja/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | TypeScript の型チェックを実行 | [テスト](/l/ja/developers/extend/apps/operations/testing) |
| `dev:add` | 新しいエンティティのスキャフォールドを作成 | [スキャフォールディング](/l/ja/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | 型付き API クライアントを再生成 | このページ |
| `dev:function:exec` / `dev:function:logs` | 関数を実行し、そのログをストリーミング | このページ |
| `dev:translations-extract` | 翻訳可能な文字列を `locales/` カタログに抽出 | [翻訳](/l/ja/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | マーケットプレイスのカタログ同期をトリガー | [公開](/l/ja/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | リリースライフサイクル | [公開](/l/ja/developers/extend/apps/operations/publishing) とこのページ |
| `docker:*` | ローカルの Twenty サーバーコンテナを管理 | [ローカルサーバー](/l/ja/developers/extend/apps/getting-started/local-server) |
| `remote:*` | サーバー接続を管理 | このページ |
すべてのコマンドは、デフォルトではない特定のリモートを対象にするために `-r, --remote \<name>` を受け付けます。
## 関数の実行(`yarn twenty dev:function:exec`)
HTTP、cron、データベースイベントを介さずに、ロジック関数を手動で実行します:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## 関数ログの表示(`yarn twenty dev:function:logs`)
アプリのロジック関数の実行ログをストリーミング表示します:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
これは Docker コンテナのログを表示する `yarn twenty docker:logs` とは異なります。 `yarn twenty dev:function:logs` は、Twenty サーバーからアプリの関数実行ログを表示します。
</Note>
## 型付きクライアントを生成する(`yarn twenty dev:generate-client`)
アプリをビルドしたり同期したりすることなく、アクティブなリモートのスキーマから型付き API クライアント(`twenty-client-sdk`)を再生成します。 これを使用すると、別リポジトリにあるバックエンドサービスのような、Twenty インスタンスと通信する任意のプロジェクトで型付きクライアントを取得できます:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
次に、クライアントをコードでインポートします:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
データモデルが変更されるたびにコマンドを再実行して、生成された型を更新してください。
<Note>
クライアントは `node_modules` 内に生成されるため、コードと一緒にはコミットされません。 インストールのたびに(たとえば `postinstall` スクリプトや CI で)`yarn twenty dev:generate-client` を実行してください。
</Note>
## アプリのアンインストール(`yarn twenty app:uninstall`)
アクティブなワークスペースからアプリを削除します:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## リモートの管理
**remote** とは、アプリが接続する Twenty サーバーのことです。 セットアップ中に、スキャフォルダーが自動的にリモートを作成します。 リモートはいつでも追加や切り替えができます。
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
認証情報は `~/.twenty/config.json` に保存されます。

View File

@@ -0,0 +1,32 @@
---
title: 概要
description: アプリのビルド、テスト、リリース — CLI コマンド、統合テスト、CI、サーバーまたは npm への公開。
icon: rocket
---
**オペレーションレイヤー** とは、アプリを使って *何かをする* のではなく、アプリに対して *行う* あらゆる操作を指します。具体的には、CLI コマンドの実行、実際の Twenty サーバーに対する統合テストの実行、CI の設定、そしてリリースの提供です — 単一サーバーにデプロイする tarball として、またはマーケットプレイスに掲載される npm パッケージとして行われます。
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## このセクションについて
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/ja/developers/extend/apps/operations/cli">
`yarn twenty` リファレンス — exec、logs、uninstall、remotes。
</Card>
<Card title="同期と復旧" icon="compass" href="/l/ja/developers/extend/apps/operations/sync-and-recovery">
どのコマンドをいつ使うか、同期の差分の読み方、そして復旧の手順。
</Card>
<Card title="テスト" icon="flask" href="/l/ja/developers/extend/apps/operations/testing">
Vitest のセットアップ、統合テスト、型チェック、CI ワークフロー。
</Card>
<Card title="公開" icon="upload" href="/l/ja/developers/extend/apps/operations/publishing">
ビルド、tarball のデプロイ、npm への公開、インストール。
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: 公開
icon: upload
description: Twenty アプリをマーケットプレイスに公開するか、社内向けにデプロイします。
---
## 概要
[ローカルでビルドとテストが完了](/l/ja/developers/extend/apps/getting-started/concepts)したら、配布方法は次の2通りです:
* **tarball をデプロイ** — 内部またはプライベート用途のために、特定の Twenty サーバーにアプリを直接アップロードします。
* **npm に公開** — Twenty マーケットプレイスにアプリを掲載し、任意のワークスペースが見つけてインストールできるようにします。
どちらの方法も同じ**ビルド**手順から始まります。
## アプリのビルド
build コマンドを実行してアプリをコンパイルし、配布可能な `manifest.json` を生成します:
```bash filename="Terminal"
yarn twenty dev:build
```
これにより、TypeScript ソースがコンパイルされ、ロジック関数とフロントエンドコンポーネントがトランスパイルされ、すべてが `.twenty/output/` に書き込まれます。 手動配布または公開コマンド用の `.tgz` パッケージも生成するには、`--tarball` を追加します。
## サーバーへのデプロイ(tarball)
一般公開したくないアプリ(プロプライエタリなツール、エンタープライズ専用の連携、実験的ビルドなど)の場合は、tarball を Twenty サーバーに直接デプロイできます。
### 前提条件
デプロイ前に、対象サーバーを指すリモートを設定しておく必要があります。 リモートは、サーバーの URL と認証情報をローカルの `~/.twenty/config.json` に保存します。
リモートを追加:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### デプロイ
アプリをビルドしてサーバーにアップロードするまでを1ステップで実行します:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### デプロイ済みアプリの共有
tarball アプリは公開マーケットプレイスには掲載されないため、同じサーバー上の他のワークスペースは、ブラウズしてもそれらを見つけられません。 デプロイ済みアプリを共有するには:
1. **設定 > アプリケーション > 登録** に移動して、あなたのアプリを開きます
2. **Distribution** タブで、**Copy share link** をクリックします
3. このリンクを他のワークスペースのユーザーと共有すると、アプリのインストールページに直接移動できます。
共有リンクはサーバーのベース URL(ワークスペースのサブドメインなし)を使用するため、サーバー上のどのワークスペースでも機能します。
### バージョン管理
既にデプロイ済みの tarball アプリを更新する場合、サーバーは、`package.json` の `version` が、現在デプロイされているバージョンよりも **厳密に大きい** ([semver](https://semver.org) の順序に従って) ことを要求します。 同じバージョンを再デプロイする、またはそれより低いバージョンをプッシュする操作は、tarball が保存される前に拒否されます—CLI から `VERSION_ALREADY_EXISTS` エラーが表示されます。
アップデートをリリースするには:
1. `package.json` の `version` フィールドを更新してください(例:`1.2.3` → `1.2.4`、`1.3.0`、または `2.0.0`)
2. `yarn twenty app:publish --private`(または `yarn twenty app:publish --private --remote production`)を実行します。
3. アプリがインストールされ、「Settings」タブで自動アップグレードが有効になっているワークスペースは、バックグラウンドで自動的にアップグレードされます。それ以外のワークスペースでは、設定内で利用可能なアップグレードが表示されます。
<Note>
プレリリースタグは期待どおりに動作します。`1.0.0-rc.1` → `1.0.0-rc.2` への引き上げは許可され、`1.0.0` のような最終リリースは `1.0.0-rc.5` よりも高いバージョンとして正しく認識されます。 `package.json` のバージョンは、それ自体が有効な semver 文字列でなければなりません。
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### サーバーバージョンの互換性
特定の Twenty サーバーバージョンで導入された機能(例: v2.3.0 で追加された OAuth プロバイダー)をアプリで使用している場合、`package.json` の `engines.twenty` フィールドを使用して、アプリが必要とする最小サーバーバージョンを宣言してください:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
値は標準的な [SemVer の範囲](https://github.com/npm/node-semver#ranges) です。 よくあるパターン:
| 範囲 | 意味 |
| ---------------------------------- | -------------------------------------- |
| `>=2.3.0` | 2.3.0 以降のどのサーバーでも |
| `>=2.3.0 \<3.0.0` | 2.3.0 以上(次のメジャーバージョン未満) |
| `^2.3.0` | `>=2.3.0 \<3.0.0` と同じ |
**デプロイ時とインストール時に起こること:**
* `engines.twenty` が設定されていて、ターゲットサーバーのバージョンがその範囲を満たさない場合、デプロイ(tarball のアップロード)またはインストールは `SERVER_VERSION_INCOMPATIBLE` エラーと、要求される範囲と実際のサーバーバージョンの双方を示すメッセージとともに拒否されます。
* `engines.twenty` が**未設定**の場合、アプリはどのサーバーバージョンでも受け入れられます(既存のアプリとの後方互換性)。
* サーバーで `APP_VERSION` が設定されていない場合、このチェックはスキップされます。
<Note>
サーバーが最終的な判定を行い、tarball のアップロードとワークスペースへのインストールの両方で `engines.twenty` を検証します。 tarball を別経路でデプロイする場合やマーケットプレイスからインストールする場合でも、サーバーは互換性を引き続き強制します。
</Note>
## 自動化された CI/CD(スキャフォールド済みのワークフロー)
`create-twenty-app` で生成されたアプリには、`.github/workflows/` 配下に GitHub Actions のワークフローが 3 つ、最初から同梱されています。 CI はセットアップ不要で実行でき、CD には 1 つのシークレットを設定するだけでよく、npm への公開には 1 回だけの npm trusted-publisher セットアップが必要です。
### CI — `ci.yml`
`main` へのプッシュやプルリクエストのたびに、統合テストを自動実行します。
**機能:**
1. アプリのソースをチェックアウトします。
2. `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` 複合アクションを使って、分離された Twenty のテスト用インスタンスを起動します(`yarn twenty docker:start --test` の CI 版)。
3. Corepack を有効化し、`.nvmrc` に基づいて Node.js をセットアップし、`yarn install --immutable` で依存関係をインストールします。
4. `yarn test` を実行し、起動したインスタンスから `TWENTY_API_URL` と `TWENTY_API_KEY` を渡すことで、テストが実サーバーと通信できるようにします。
**設定項目:**
* `TWENTY_VERSION`(env、デフォルトは `latest`)— `ci.yml` でこれを編集して、CI で使用する Twenty サーバーのバージョンを固定します。
* 同時実行は `github.ref` 単位でグループ化され、新しいプッシュ時に進行中の実行をキャンセルします。
シークレットは不要です。テスト用インスタンスは一時的で、ジョブの実行中のみ存続します。
### CD — `cd.yml`
`main` へのプッシュのたびに、設定済みの Twenty サーバーへアプリをデプロイします。また、`deploy` ラベルが付与されたプルリクエストからのデプロイにも対応します(任意)。
**機能:**
1. (ラベル付き PR の場合は)PR のヘッド、またはプッシュされたコミットをチェックアウトします。
2. `twentyhq/twenty/.github/actions/deploy-twenty-app@main` を実行します(`yarn twenty app:publish --private` の CI 版)。
3. `twentyhq/twenty/.github/actions/install-twenty-app@main` を実行し、新しくデプロイしたバージョンを対象ワークスペースにインストールします。
**必須の設定:**
| 設定 | 場所 | 目的 |
| ----------------------- | ------------------------------------------------------------ | -------------------------------------------------- |
| `TWENTY_DEPLOY_URL` | `cd.yml` の `env`(デフォルトは `http://localhost:3000`) | デプロイ先の Twenty サーバー。 初回利用前に、実際のサーバーの URL に変更してください。 |
| `TWENTY_DEPLOY_API_KEY` | GitHub リポジトリの **Settings → Secrets and variables → Actions** | 対象サーバーでデプロイ権限を持つ API キー。 |
<Note>
デフォルトの `TWENTY_DEPLOY_URL`(`http://localhost:3000`)はプレースホルダーであり、GitHub ホストランナーからは到達できません。 CD を有効化する前に、サーバーの公開 URL に更新するか、ネットワークアクセス可能なセルフホストランナーを使用してください。
</Note>
**PR からプレビューデプロイをトリガーする:**
プルリクエストに `deploy` ラベルを追加します。 `cd.yml` の `if:` ガードにより、その PR のヘッドコミットを使ってジョブが実行され、マージ前に対象サーバー上で変更を検証できます。
### 公開 — `publish.yml`
バージョンタグ(例: `v1.0.0`)をプッシュしたとき、または Actions タブからワークフローを手動実行したときに、プロビナンス付きでアプリを npm に公開します。
**機能:**
1. あなたのアプリをチェックアウトし、Node.js をセットアップして npm を更新します(信頼できる公開には npm 11.5.1 以降が必要です)。
2. `yarn twenty app:publish` を実行します。これによりアプリがビルドされ、`.twenty/output` が npm に公開されます。 CI 環境では自動的に `--provenance` と `--access public` が追加されるため、ワークフロー内でフラグを指定する必要はありません。
**一度きりのセットアップ:**
npmjs.com で自分のパッケージを開き、**Settings → Trusted Publisher** から、このリポジトリを `publish.yml` ワークフローで登録します(詳しくは [npm trusted publishing docs](https://docs.npmjs.com/trusted-publishers) を参照してください)。 provenance 付きで公開すると、どの GitHub リポジトリがそのパッケージをビルドしたかが証明されます。これは、Twenty マーケットプレイスでアプリの所有権を主張する方法にもなります。
<Note>
npm は **public** なソースリポジトリからの provenance しか受け付けません。 private リポジトリから publish しようとすると、npm は OIDC provenance バンドルを `E422 ...` エラーで拒否します。 「Unsupported GitHub Actions source repository visibility: "private"」というエラー。 private リポジトリから publish するには、publish ステップの `env` で `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` を設定して provenance を無効化します(コメントアウトされたヒントが scaffold された `publish.yml` に含まれています)。
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### 再利用可能なアクションのバージョン固定
`ci.yml` と `cd.yml` のワークフローは、`@main` の再利用可能なアクションを参照しているため、`twentyhq/twenty` リポジトリでのアクションの更新が自動的に取り込まれます。 ビルドを決定的にしたい場合は、各 `uses:` 行の `@main` をコミット SHA またはリリースタグに置き換えてください。
## npm への公開
npm に公開すると、Twenty マーケットプレイスでアプリが見つけられるようになります。 任意の Twenty ワークスペースは、UI からマーケットプレイスのアプリを参照、インストール、およびアップグレードできます。
### 要件
* [npm](https://www.npmjs.com) アカウント
* `package.json` の `keywords` 配列にある `twenty-app` キーワード(手動で追加してください — `create-twenty-app` テンプレートにはデフォルトで含まれていません)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### マーケットプレイスのメタデータ
`defineApplication()` の設定は、マーケットプレイスでのアプリの表示方法を制御する任意のフィールドをサポートします。 `public/` フォルダー内の画像を参照するには、`logo` と `galleryImages` を使用します:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
マーケットプレイスのフィールド(`author`、`category`、`aboutDescription`、`websiteUrl`、`termsUrl` など)の完全な一覧は、Building Apps ページの[defineApplication アコーディオン](/l/ja/developers/extend/apps/config/application#marketplace-metadata)を参照してください。
#### 推奨されるギャラリー画像のサイズ
マーケットプレイスでは、`galleryImages` は固定の `8:5` コンテナ内で表示されます(例:`1600×1000 px`)。
<Note>
どのようなアスペクト比でもギャラリー画像は全体が表示され、トリミングされることはありません。ただし、`8:5` よりも著しく縦長の場合は、左右に空白の帯が表示されます。
</Note>
#### 画像サイズの上限
`logo` と各 `galleryImages` ファイルは **10 MB** を超えてはいけません。 マーケットプレイスが公開済みアセットを再ホスティングする際、サイズが大きすぎるファイルはスキップされるため、表示されません。
### 公開
```bash filename="Terminal"
yarn twenty app:publish
```
特定の dist-tag(例: `beta` や `next`)で公開するには:
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### マーケットプレイスの検出の仕組み
Twenty サーバーは、npm レジストリからマーケットプレイスのカタログを**毎時**同期します。
待たずにすぐに同期をトリガーできます:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
マーケットプレイスに表示されるメタデータは、`defineApplication()` 設定から取得されます。上記の [Marketplace metadata](#marketplace-metadata) を参照してください。
<Note>
アプリで`defineApplication()`内に`aboutDescription`が定義されていない場合、マーケットプレイスはnpm上のパッケージの`README.md`を概要ページのコンテンツとして自動的に使用します。 つまり、npm と Twenty のマーケットプレイスの両方に対して、1 つの README を維持できます。 マーケットプレイスで異なる説明文を使用したい場合は、`aboutDescription` を明示的に設定してください。
</Note>
### CI での公開
上で説明したスキャフォールド済みの `publish.yml` ワークフローは、バージョンタグが付けられたときに provenance 情報付きで自動的に npm へ公開します。 `yarn twenty app:publish` は CI で実行される際に `--provenance` と `--access public` を自動的に追加するため、ワークフローで npm フラグを指定する必要はありません。一度きりの trusted-publisher セットアップだけが必要です。
その他の CI システム(GitLab CI、CircleCI など)の場合は、`yarn install` を実行してから `yarn twenty app:publish` を実行します。 環境が OIDC トークンを発行できる場合は provenance が生成され、それ以外の場合は自動的にスキップされます。
<Note>
**npm provenance** により npm のリスティングにトラストバッジが追加され、公開 CI パイプライン上の特定のコミットからビルドされたパッケージであることをユーザーが検証できるようになります。 また、これにより Twenty マーケットプレイスでアプリの所有権を主張できるようになります。 詳細は [npm provenance のドキュメント](https://docs.npmjs.com/generating-provenance-statements)を参照してください。
</Note>
## アプリのインストール
アプリが公開(npm)またはデプロイ(tarball)されると、ワークスペースは UI からインストールできます。
Twenty の **設定 > アプリケーション** ページに移動すると、マーケットプレイスと tarball でデプロイされたアプリの両方を参照してインストールできます。
{/* TODO: add screenshot of the UI when the app is registered */}
コマンドラインからアプリをインストールすることもできます:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
サーバーはインストール時に semver によるバージョニングを強制し、デプロイ時の規則を反映しています:
* ワークスペースに既にインストールされているものと同じバージョンをインストールしようとすると、`APP_ALREADY_INSTALLED` エラーで拒否されます。
* 現在インストールされているものより低いバージョンをインストールしようとすると、`CANNOT_DOWNGRADE_APPLICATION` エラーで拒否されます。
より新しいバージョンをインストールするには、まずデプロイまたは公開してから、`yarn twenty app:install` を再実行してください。
</Note>

View File

@@ -0,0 +1,126 @@
---
title: 同期と復旧
description: どのコマンドをいつ使うか、同期の出力の読み方、そしてローカルのメタデータがずれたときに完全リセットへ到達する前に行うべき復旧手順の段階的な手引きについて説明します。
icon: compass
---
ローカルでのアプリ開発は**同期**を中心に回ります。CLI がマニフェストを再ビルドし、サーバーはそれと、すでにワークスペースに存在するメタデータとの差分だけを適用します。 このページでは、どのコマンドを使うべきか、同期で何が変更されたのかをどう読み取るか、そしてローカルの状態が一貫していないように見えるときに取るべき手順を(順番に)説明します。
## どのコマンドをいつ使うか
<Note>
日々のローカルでの反復開発では、ほとんどの場合 `yarn twenty dev` を使うことになります。 デプロイと公開はリリースのためのものであり、ローカルでの開発ループのためのもの**ではありません**。
</Note>
| やりたいこと… | コマンド | ノート |
| ------------------------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| ライブ同期でローカルに反復開発する | `yarn twenty dev` | ファイルを監視し、変更のたびに同期します。 |
| 1 回だけ同期して終了(CI、スクリプト、フック向け) | `yarn twenty apply` | 1 回ビルドして同期し、その後終了します。 破壊的変更の確認をスキップするには、`--force` を追加します。 |
| 変更を**適用せずに**プレビュー | `yarn twenty plan` | 差分を計算して表示しますが、何も書き込みません。 |
| ワークスペースからアプリを削除する | `yarn twenty app:uninstall` | プロンプトをスキップするには、`--yes` を追加します。 |
| サーバーに tarball をアップロードする | `yarn twenty app:publish --private` | `package.json` のバージョンが**厳密により高い**必要があります — [Publishing](/l/ja/developers/extend/apps/operations/publishing) を参照してください。 |
| マーケットプレイス(npm)に公開する | `yarn twenty app:publish` | — |
| デプロイ済みバージョンをインストール / アップグレードする | `yarn twenty app:install` | 現在デプロイされているバージョンをインストールします。 |
| ローカルサーバーを消去してクリーンに開始する | `yarn twenty docker:reset` | ローカルデータを**すべて**削除します — 最終手段です。 |
<Note>
`yarn twenty dev --once` および `yarn twenty dev --once --dry-run` は、`yarn twenty apply` と `yarn twenty plan` の非推奨エイリアスとして依然として動作します。
</Note>
### ローカル同期ではバージョンの更新は不要
厳密に増加する `version` のルール(デプロイ時の `VERSION_ALREADY_EXISTS`、インストール時の `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION`)は、リリースパスである **`app:publish` / `app:install`** に適用されます。 `yarn twenty dev` はマニフェストをその場で同期し、バージョン変更を要求することはないため、反復するのに `package.json` を触る必要はありません。 ローカルの変更をテストするためにバージョンを上げている場合は、開発ループが必要なところでリリース経路を使ってしまっています。
## 同期出力の読み方
各同期では、適用した(`plan` の場合は適用される)メタデータの変更が出力されます。Terraform と同様に、エンティティごとにその属性を含むブロックが 1 つずつ表示され、その後にサマリー行が続きます。
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
これは最初の診断手段です。どのオブジェクト、フィールド、レイアウトが変更されたかを正確に示すので、UI を確認する前に、同期が想定どおりに動作したかを確認できます。
破壊的変更(`to destroy`)は、何を削除するかとあわせて一覧表示され(例: `objectMetadata "auditNote" — drops the table and all its rows`)、対話的な確認、またはスクリプト内での `--force` が必要です。
同期が単一のエンティティで失敗した場合、エラーには問題のエンティティとその `universalIdentifier` が、次のように示されます。
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
その識別子を使って、推測で衝突元を探すのではなく、マニフェスト内(必要であればワークスペース内)のエンティティを特定してください。
## 変更内容のプレビュー(プラン)
`yarn twenty plan` はマニフェストをビルドし、サーバーにマイグレーションプランを問い合わせ、その内容を **何も適用せずに** 表示します。 コミットする前に「この同期は何を変更するか?」という問いに安全に答える方法です。
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
プランの例:
* **何も書き込みません** — メタデータマイグレーション、アプリケーションレコードの更新、デフォルトのロール / タブの変更、API クライアントの生成は一切行いません。
* 実際の同期が適用するのと**同じ差分**を返すため、作成 / 更新 / 削除されるエンティティを事前に確認できます。
* リスクの高い変更の前や、AI 生成の変更をレビューするとき、または予期せぬ変更が行われそうな場合にスクリプトを失敗させたいときなどに有用です。
<Note>
プランでは **メタデータ** の変更のみがプレビューされます。また、アプリが少なくとも一度は同期されている(ワークスペース側がその存在を知っている)必要があります。 一度も同期されていないアプリに対して実行すると、サーバーはそのアプリがインストールされていないと報告します — まず一度 `yarn twenty dev` を実行してください。
</Note>
## リカバリーラダー
ローカルのメタデータが正しくないように見える場合は、次の順番でエスカレートし、問題が解消したところで止めてください。 各ステップは前のものよりも影響が大きくなります。
1. **再同期。** `yarn twenty apply` を再度実行します。 同期はべき等であり、クリーンなマニフェストを再実行しても安全で、多くの場合は一時的な不具合が解消されます。
2. **プランをプレビュー。** `yarn twenty plan` を実行して、次の同期が何を変更しようとしているのかを、適用せずに正確に確認します。
3. **名前付きエラーを読む。** 同期が失敗した場合は、メッセージ内のメタデータタイプと `universalIdentifier`(上記参照)を確認し、そのエンティティをマニフェスト内で特定します。 コンフリクトは、重複または再利用された識別子を指していることがほとんどです。
4. **アンインストールして再インストール。** `yarn twenty app:uninstall` を実行し、その後再度同期します(`yarn twenty dev`)。 これにより、ワークスペースの残りを維持したまま、アプリのメタデータをクリーンな状態から再構築します。
5. **フルリセット(最後の手段)。** `yarn twenty docker:reset` を実行し、その後再シードと再同期を行います。
<Warning>
`yarn twenty docker:reset` はローカルインスタンス内のデータを**すべて**削除します — すべてのワークスペース、レコード、アプリが消去されます。 それまでの手順がすべて失敗した場合にのみ使用してください。
</Warning>
<Note>
メタデータエラーが発生しましたか? [issue を作成](https://github.com/twentyhq/twenty/issues/new/choose)し、失敗したマイグレーションメッセージ(メタデータタイプと `universalIdentifier` を含む)、同期時の `Metadata changes` の出力、および実行したコマンドを記載してください。
</Note>
## 1 つのワークスペースでの同時同期を避ける
同期ではメタデータマイグレーションが適用されます。 **同じワークスペースに対して同時に**複数の同期、デプロイ、インストール操作を実行すると(たとえば、複数のターミナルや AI エージェントが並行して反復している場合)、それらのマイグレーションが入り混じり、メタデータが一部だけ適用された状態のままになってしまう可能性があります。
サーバーはワークスペースごとに同期を直列化してこれを防いでいますが、それでも、センシティブなメタデータ操作は同時に実行するのではなく、**単一の**プロセスに集約するべきです。 複数のエージェントで開発をオーケストレーションしている場合は、それらの sync / deploy / install 呼び出しを 1 つのキューに流し込み、同時ではなく一度に 1 つだけ実行されるようにしてください。
## 失敗の切り分け
問題が発生したときは、メタデータの差分と名前付きエラーによって、どこで失敗したかを特定できます。
* **マニフェストビルドエラー** — CLI が同期前に失敗します(`MANIFEST_BUILD_FAILED`、`TYPECHECK_FAILED`)。アプリのソースを修正してください。
* **同期 / マイグレーションエラー** — ビルドは成功しますが、差分の適用が失敗し、エンティティと `universalIdentifier` が示されます。そのコンフリクトしているメタデータを修正してください。
* **依存関係サイズエラー** — 同期またはインストールが失敗しました。アプリの本番用 `dependencies` がランタイムレイヤーとしてインストールするには大きすぎるためです(`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`)。ランタイムでロジック関数がインポートしないパッケージ(UI ライブラリ、開発ツール類)は `devDependencies` に移動してください。
* **アプリコードの実行時エラー** — 同期自体は成功しているものの、ロジック関数やコンポーネントが実行時に正しく動作しません。[function logs](/l/ja/developers/extend/apps/operations/cli) を確認してください。
* **ローカルインスタンスの状態** — 上記のいずれにも当てはまらないのにワークスペースの見た目が依然としておかしい場合は、リカバリーラダーに従って下の段階へ進んでください。

View File

@@ -0,0 +1,264 @@
---
title: テスト
description: Vitest のセットアップ、実際の Twenty サーバーに対する統合テスト、型チェック、および GitHub Actions を使った CI。
icon: flask
---
SDK は、テストコードからアプリのビルド、デプロイ、インストール、アンインストールを可能にするプログラム用 API を提供します。 [Vitest](https://vitest.dev/) と型付き API クライアントを組み合わせることで、実際の Twenty サーバーに対してエンドツーエンドで動作を検証する統合テストを作成できます。
## npm パッケージの使用
アプリで任意の npm パッケージをインストールして使用できます。 ロジック関数とフロントコンポーネントはどちらも [esbuild](https://esbuild.github.io/) でバンドルされ、依存関係は出力にインライン化されます—実行時に `node_modules` は不要です。
### パッケージのインストール
```bash filename="Terminal"
yarn add axios
```
次に、コードでインポートします:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
フロントコンポーネントでも同様に機能します:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### バンドルの仕組み
ビルドステップでは esbuild を使用して、各ロジック関数および各フロントコンポーネントごとに自己完結した単一ファイルを生成します。 インポートされたパッケージは、以下に一覧表示されているものを除き、バンドル内にインライン化されます。
**ロジック関数**は Node.js 環境で実行されます。 Node の組み込みモジュール(`fs`、`path`、`crypto`、`http` など) は利用可能で、インストールは不要です。
**フロントコンポーネント**は Web Worker で実行されます。 Node の組み込みモジュールは利用できません—ブラウザー環境で動作する npm パッケージのみが使用できます。 サンドボックスは *一部のみ* の DOM を実装しているため、パッケージはクリーンにビルドできても実行時に失敗する可能性があります。詳しくは [Current limitations](/l/ja/developers/extend/apps/layout/front-components#current-limitations) を参照してください。
どちらの環境でも、`twenty-client-sdk/core` および `twenty-client-sdk/metadata` が事前提供モジュールとして利用可能です—これらはバンドルされず、実行時にサーバーによって解決されます。
フロントコンポーネントは、[共有依存関係バンドル](/l/ja/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) を通じて依存関係を共有することもできます。 アプリの package.json の `frontComponentSharedDependencies` フィールドに記載されたパッケージは、1 回だけビルドされて別ファイルにまとめられ、各コンポーネントにインライン化される代わりに実行時にそのファイルから読み込まれます。そのため、共有パッケージのみをインポートするコンポーネントは、それ自身の依存コードをほとんど持ちません。
## セットアップ
スキャフォルドされたアプリにはすでに Vitest が含まれています。 手動で設定する場合は、依存関係をインストールしてください:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
アプリのルートに `vitest.config.ts` を作成します:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
サーバーに到達可能であることを検証し、SDK 用のテスト設定(`~/.twenty/config.test.json`)を書き込み、テストの実行前にアプリを同期するグローバルセットアップファイルを作成します。
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## プログラム用 SDK API
`twenty-sdk/cli` サブパスは、テストコードから直接呼び出せる関数をエクスポートします:
| 関数 | 説明 |
| -------------- | -------------------------------------------- |
| `appBuild` | アプリをビルドし、必要に応じて tarball にパッケージ化 |
| `appDeploy` | tarball をサーバーにアップロード |
| `appDevOnce` | アプリを 1 回ビルドして同期します(`yarn twenty apply` と同じ)。 |
| `appInstall` | アクティブなワークスペースにアプリをインストール |
| `appUninstall` | アクティブなワークスペースからアプリをアンインストール |
各関数は、`success: boolean` と `data` または `error` のいずれかを含む結果オブジェクトを返します。
## 統合テストの作成
アプリをビルド、デプロイ、インストールし、その後ワークスペースに表示されることを検証する完全な例を次に示します:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## テストの実行
ローカルの Twenty サーバーが起動していることを確認し、次を実行します:
```bash filename="Terminal"
yarn test
```
また、開発中はウォッチモードでも実行できます:
```bash filename="Terminal"
yarn test:watch
```
## 型チェック
テストを実行せずに、アプリの型チェックのみを実行することもできます:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
これは、あなたのアプリの `tsconfig.json` に対して `tsc --noEmit` を実行し、型エラーを報告します。 スキャフォルドされたアプリには、テストファイル(`tsconfig.spec.json`)も対象とする `yarn typecheck` スクリプトも同梱されています。
## GitHub Actions による CI
スキャフォルダーは、すぐに使えるワークフローを `.github/workflows/ci.yml` に生成します。 `main` へのすべてのプッシュおよびすべてのプルリクエスト時に、ランナー内で一時的な Twenty サーバーを起動(`twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test` アクション経由)し、そのサーバーを指すように `TWENTY_API_URL` / `TWENTY_API_KEY` を設定した上で、`yarn lint`、`yarn typecheck`、`yarn test:unit`、`yarn test` を実行します。 シークレットは一切不要で、ワークフローの先頭にある `TWENTY_VERSION` 環境変数を通じてサーバーバージョンを固定できます。
スキャフォルドされた 3 つのワークフロー(`ci.yml`、`cd.yml` デプロイパイプライン、npm パブリッシング用の `publish.yml`)の詳細な手順については、[Publishing → Automated CI/CD](/l/ja/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) を参照してください。

View File

@@ -0,0 +1,126 @@
---
title: 翻訳
description: 複数の言語でアプリを提供できます — 単一の locales カタログで、マニフェストのラベルと UI 文字列を翻訳します。
icon: language
---
Twenty のアプリは **英語** で作成されています。ソースやマニフェスト内の文字列は英語のソーステキストであり、`en` は翻訳の元となるソースロケールです。未翻訳のロケールは `en` にフォールバックします。
アプリには 2 種類の翻訳可能なテキストがあり、どちらも同じ `locales/` カタログを通じて処理されます。
* **Manifest labels** — オブジェクト名とフィールド名、ビュータイトル、メニュー項目、その他アプリのメタデータで宣言される文字列。
* **Front-component strings** — React のフロントコンポーネントがレンダリングするUIテキスト。
翻訳可能な文字列にマークを付けてロケールごとのカタログに抽出し、それらのカタログを翻訳すれば、ビルドが現在のユーザーに適切な言語を提供します — 追加の設定は不要です。
## フロントコンポーネント文字列のマーク
`20-sdk/front-component` から翻訳ヘルパーをインポートします。
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### When to use which
* **`Trans`** — JSX 内の静的テキストです。 補間には `message` と `values` の props を使用します。
例: `Trans message="Hi {name}" values={{ name }}` のようにします。
子要素内で直接補間すると、静的に抽出できません。
* **`useTranslate().t`** — コンポーネント内の動的文字列。
ユーザーが言語を切り替えたときに再レンダリングします。 これをレンダリング内で好みます。
* **`t(...)`** (imported directly) — eager translation used **anywhere** ,
including event handlers, helpers, module scope — だけでなく、render内だけでなく。
* **`msg(...)`** — dataとして宣言された文字列の遅延記述子 (定数、
config)。 後で`t(descriptor)`で解決します。
### コンテキスト
異なる変換元文字列を曖昧にするために `context` を渡します。
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
コンテキスト付きの文字列は、ロケールカタログ内ではそのコンテキストをキーとしたグループとして表示されます:
`"\<context>": { "\<message>": "\<translation>" }`。 マニフェストのラベルは、設計上コンテキスト付きになっています — それぞれが果たすメタデータ上の役割(`objectMetadata.labelSingular`、`fieldMetadata.label`、`view.name` など)を保持しているため、同じ語でもオブジェクト名として訳す場合とビュー名として訳す場合で、異なる訳語になる可能性があります。
## 抽出と翻訳
アプリディレクトリから抽出コマンドを実行します。
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
抽出処理では、マニフェストのラベルとフロントコンポーネントのソース内にある `t()`/`msg()`/`Trans` の文字列の両方が収集され、ソース文字列をキーとして `locales/\<locale>.json` にまとめられます。 翻訳を入力:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
コンテキストに依存しない文字列はフラットなエントリです。コンテキスト付きの文字列(すべての
マニフェストラベルを含む)は、そのコンテキストキーの下にネストされます。
`{name}`のようなプレースホルダは、実行時に置き換えられます —
翻訳でそれらを維持します。 空の文字列はすべて元のテキストに戻ります。
## 動作方法
`twenty dev:build` はカタログをコンパイルし、現在のユーザーに対して適切な言語を提供します。マニフェストのラベルはサーバー側で解決され、フロントコンポーネントのカタログは各コンポーネントバンドルに組み込まれます。 実行時には、コンポーネントが実行コンテキスト(ホストの現在の言語)からロケールを読み取り、そのカタログに対して各文字列を解決し、翻訳が存在しない場合はソースにフォールバックします。 ホスト側で言語を切り替えると、`Trans` と
`useTranslate().t` の文字列が即時に再レンダリングされます。
マニフェストラベルは、すべての同期時にコンパイルされます。`twenty dev:build`、`twenty apply`
および継続的な `twenty dev` の監視も同様です。そのため、ロケールファイルを編集して
同期するだけで、新しいテキストを確認できます。 フロントコンポーネントカタログはビルド時に
各コンポーネントバンドルに組み込まれるため、それらのいずれかを変更した場合も
`twenty dev:build` の再実行(および再デプロイ)が必要です。
`Trans` のテキスト子要素は複数行にまたがってもかまいません — 空白は JSX と同じように折りたたまれるため、どちらもキー `Welcome back` として抽出されます:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -0,0 +1,79 @@
---
title: 5. An AI agent
icon: robot
description: エージェントに、あなたのツールを使用して、チャットからドキュメントを生成させます。
---
`generate-document` は **tool** として公開されているため、AI エージェントはそれを呼び出すことができます。
ユーザーが \*"
Jeffery Griffinの提案を生成する"\*と言えるように、エージェントとスキルを追加しましょう。
## スキル
[skill](/l/ja/developers/extend/apps/logic/skills-and-agents) は
命令を再利用できます — エージェントに付与する知識です。
ツールの使い方をモデルに教えます。
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## エージェント
[agent](/l/ja/developers/extend/apps/logic/skills-and-agents) はプロンプトを
モデルとペアリングします。 ビルド警告を避けるため、明示的に `responseFormat` を設定してください。
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
エージェントは、そのロールが許可している場合にのみツールを呼び出すことができます。 すでにアプリのロールに対して
`canAccessAllTools: true` と `canBeAssignedToAgents: true` を設定済みです。
詳しくは、[第2章](/l/ja/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access) を参照してください。
</Note>
## お試しください
**ドキュメントアシスタント** とのチャットを開き、CRM内の
人のためのドキュメントを下書きするように依頼します。 レコードを見つけて `generate-document` を呼び出し、作成したドキュメントを返します — そのドキュメントは、コマンドメニューやワークフロー経由の場合とまったく同様に、**Documents** ビューに表示されます。
これは、ロジックをツールとして公開するための報酬です: **1つの関数、多くのフロントドア** —
コマンドメニュー、HTTP、ワークフローステップ、そして現在の自然言語。
**このステップの後:** アプリは機能が完了し、本当に便利です。
に出荷しましょう。
<Card title="次へ: 公開 →" icon="rocket" href="/l/ja/developers/extend/apps/tutorials/document-generator/publishing">
マーケットプレイスのメタデータを追加して公開します。
</Card>

View File

@@ -0,0 +1,294 @@
---
title: 4. UI の構築
icon: table-columns
description: ビュー、サイドバーナビゲーション、コマンド、フロントコンポーネント。
---
現在、オブジェクトは設定を通じてのみアクセス可能です。 アプリに本物の存在感を持たせて、UI にしっかり組み込みましょう。リストビュー、サイドバーエントリ、ワンクリックで実行できる **Generate document** コマンド、レコードページのフロントコンポーネントによるドキュメントの**プレビュー**、そしてテンプレート用のネイティブなリッチテキスト**エディター**タブを追加します。
## ビューとナビゲーション
[view](/l/ja/developers/extend/apps/layout/views) は、指定されたオブジェクトのリストです。
[navigation menu item](/l/ja/developers/extend/apps/layout/navigation-menu-items)
はそのビューをサイドバーに配置します。 サーバーはすでに各オブジェクトのデフォルトの「All ...」ビューを自動で用意しているため、アプリが定義するビューは追加の厳選されたビューとなります。
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
テンプレートに同じペアを追加します。 両方ともサイドバーに表示されます:
<Frame caption="サイドバーにあるドキュメントとテンプレート。生成されたドキュメントが一覧表示されます。">
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="生成されたドキュメントを持つドキュメントビュー" />
</Frame>
## フロントコンポーネント
[front component](/l/ja/developers/extend/apps/layout/front-components) は Twenty 内でサンドボックス化された React
コンポーネントです。 選択したレコードを読み込み、`CoreApiClient` 経由で
person テンプレートを読み込み、最後の
チャプターからルートに POST を読み込みます。
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
インラインCSS変数(`var(--t-color-blue)`)のスタイルで、
`21ui`からインポートされた値ではありません。 ビルド中にSDKがパッケージをモックするので、
テーマ定数のモジュールレベルのインポートは`undefined`になります。
[full component](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx)を参照してください。
</Warning>
## 開くためのコマンド
`availabilityType: 'RECORD_SELECTION'` を指定した [command menu item](/l/ja/developers/extend/apps/layout/command-menu-items) は、Person が選択されているときに表示され、サイドパネルでコンポーネントを開きます。
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## フロー全体を試してみてください
**People**を開き、 <kbd>⌘K</kbd> / <kbd>Ctrl K</kbd> を押します。
アプリでタグ付けされた「ドキュメントの生成」が表示されます:
<Frame caption="Person が選択されるとコマンドが表示されます。">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="ドキュメントを生成するコマンドメニュー" />
</Frame>
実行: コンポーネントがサイドパネルで開きます。 テンプレートを選択し、
**生成**をクリックし、**ドキュメント**に新しいレコードの土地を追加します。
<Frame caption="フロントコンポーネント、テンプレートをロードし、クリック時に生成します。">
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="ドキュメントサイドパネルを生成する" />
</Frame>
生成されたすべてのドキュメントはアプリを作成者として記録します:
<Frame caption="作成された文書ジェネレータ、ステータスが生成されました。">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="生成されたドキュメント レコード" />
</Frame>
## レコードページにドキュメントをプレビュー
フロントコンポーネントはコマンドメニューだけではありません —
レコードページに**タブ**としてマウントできます。
Markdown 本文を洗練された印刷可能なページとしてレンダリングするドキュメントレコードに *Preview* タブを追加しましょう。
コンポーネントは現在のレコード ID を実行コンテキストから読み込み、
ドキュメントを読み込み、レンダリングします。 フロントコンポーネントは、HTMLタグの
ホワイトリストのみを許可する**サンドボックス**で実行されます。生のHTMLインジェクション (`dangerouslySetInnerHTML`) と
`\<style>`はブロックされているため、小さな[`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
ヘルパーを介して、マークダウンをインライン
スタイルでレンダリングします。
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
query: { id: recordId },
});
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
[page layout](/l/ja/developers/extend/apps/layout/page-layouts) でマウントします。 `RECORD_PAGE` レイアウトは、オブジェクトのレコードビューにタブを追加します。単一の
`FRONT_COMPONENT` ウィジェットを含むタブは、コンポーネントをフル幅で表示します:
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
**プレビュー**タブを開くと、
共有可能なウェブページとPDFへのリンクが美しくレンダリングされます。
<Frame caption="format@@0 タブでは、インラインスタイルとクイックリンクを使用してドキュメントをレンダリングします。">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="レコードページタブ内のドキュメントビューアフロントコンポーネント" />
</Frame>
## リッチテキストエディタでテンプレートを編集
テンプレートにはカスタムコンポーネントは一切必要ありません。 `body` は `RICH_TEXT` フィールドなので、Twenty にはすでにフル機能のリッチテキストエディターが用意されています。標準の Note および Task オブジェクトが使用しているものと同じエディターです。
テンプレートのレコードページでサーフェスを作成します。
`EDITOR`ディスプレイモードで`FIELD`ウィジェットを持つタブを追加します。`fieldMetadataId`を介して`body`
フィールドを指しています:
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
`RICH_TEXT` フィールドには、エディタのブロック JSON と Markdown
プロジェクションの両方が格納されます。 生成パイプラインはその Markdown プロジェクションを読み取るため、プレースホルダー、PDF、共有可能なウェブページはすべてそのまま問題なく動作します。詳しくは、[`template-record.page-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts) を参照してください。
`FIELD` ウィジェットは、他にもさまざまな表示モードやオプションをサポートしています。たとえば、2 ホップ先の入れ子になったリレーションのレコードを一覧表示することもできます。詳しくは、[フィールドウィジェットリファレンス](/l/ja/developers/extend/apps/layout/page-layouts#field-widgets)を参照してください。
編集者は適切なリッチテキストエディタでテンプレートを書き込むようになりました:
<Frame caption="format@@0タブ:本文フィールドにバインドされている、Twentyのネイティブリッチテキストエディタ。">
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="ネイティブリッチテキストエディタタブのテンプレートレコード" />
</Frame>
**このステップの後:** ドキュメントは美しくプレビューでき、テンプレートはアプリ内で編集可能になります。 次に、AIエージェントがチャットからそれらを生成します。
<Card title="次へ: AI エージェント →" icon="robot" href="/l/ja/developers/extend/apps/tutorials/document-generator/ai-agent">
エージェントとツールと呼ぶスキルを追加します。
</Card>

View File

@@ -0,0 +1,134 @@
---
title: 1. データモデル
icon: database
description: オブジェクト、フィールド、および関連を持つモデルドキュメントとテンプレート。
---
アプリには2つのカスタムオブジェクトが必要です: **ドキュメントテンプレート** (書き込み内容) と
**ドキュメント** (生成された結果). 定義してみましょう
CLI で各エンティティファイルをScaffold — 有効な UUID と適切な
フォルダーを生成します。
```bash filename="Terminal"
yarn twenty dev:add object
```
以下に完成したファイルを示します。
<Note>
すべての `*_UNIVERSAL_IDENTIFIER` 定数は
`src/constants/universal-identifiers.ts` にあり、使用される場所で import されます。 下のスニペット
は、簡潔にインポートするためにそれらを省略します。あなた自身のファイルに保存します。
</Note>
## テンプレートオブジェクト
テンプレートには `name`、`{{placeholders}}` を含む `body`、そして Person 向けか Company 向けかを示す `target` があります。 `body` は
`RICH_TEXT` フィールドです。したがって、20 はリッチテキストエディタを提供します。
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
`SELECT` オプション **values** は `UPPER_CASE` (`Person`ではなく`PERSON`) でなければなりません。そして
`defaultValue` は余分な引用符で包まれています: \`\`'PERSON'` ``` 。 `label\`は
ユーザーが見るものです。
</Warning>
## ドキュメントオブジェクト
生成されたドキュメントには、レンダリングされた `content` と `status` が保存されます。 `DRAFT` / `ジェネレータ`を`status`で選択し、同じ方法で
を定義します。 フルファイル:
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## リレーションとリンクする
各ドキュメントは、それがから来たテンプレートを指す必要があります。 リレーションは
**双方向** で、それぞれ独自のフィールドファイルで両側を定義します。
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
反対側(`template-documents-relation.field.ts`)は、逆方向を指す「ドキュメント」という名前の
`RelationType.ONE_TO_MANY` フィールドです。
フルパターンについては [Relations](/l/ja/developers/extend/apps/data/relations) を参照してください。
## 二十日で見る
`yarn tin dev` が実行されている場合は、 **Settings → Data model** を開きます。 両方のオブジェクト
がアプリでタグ付けされて表示されます。
<Frame caption="ドキュメントジェネレータアプリが所有するカスタムオブジェクトの両方。">
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="ドキュメントとドキュメントテンプレートを表示するデータモデルの設定" />
</Frame>
1つのテンプレートを作成してテストするには、*セールスプロポーザル*という名前を付けてください。**ターゲット**を
*Person*に設定し、いくつかのプレースホルダーを持つ本体を貼り付けてください:
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="テンプレートレコード。 本文は、ドキュメントが生成されるまでプレースホルダを保持します。">
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="プレースホルダー ボディを持つ販売提案テンプレートの記録" />
</Frame>
**このステップの後:** `documentTemplate` と `document` オブジェクト、
リレーションによってリンクされているオブジェクト、 そして生成するテンプレートが 1 つあります。 次に、それを埋めるロジック。
<Card title="次へ: ドキュメントの生成 →" icon="bolt" href="/l/ja/developers/extend/apps/tutorials/document-generator/generating-documents">
テンプレートを満たすロジック機能を記述します。
</Card>

View File

@@ -0,0 +1,233 @@
---
title: 2. ドキュメントの生成
icon: bolt
description: AIツールとワークフローアクションとして公開されるロジック機能の1つ。
---
コア: [ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions)
テンプレートとレコードをロードし、プレースホルダを埋め、新しい
ドキュメントを保存します。
**ハンドラ**としてビジネスロジックを記述し、
いくつかのトリガーを通してそれを公開します。 この章では、**AI ツール** と
**ワークフローアクション** の 2 つを配線します。
## レンダリングヘルパー
単体テストを簡単に行えるように、純粋なロジックを独自のファイルに保存します。 これにより、レコード
を `{{dot.path}}` トークンに平坦化し、それらを置き換えます。
```ts filename="src/logic-functions/utils/render-template.ts"
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
export const renderTemplate = (body: string, values: Record<string, string>) => {
const missingTokens = new Set<string>();
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
const value = values[token];
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
return value;
});
return { content, missingTokens: [...missingTokens] };
};
```
<Tip>
副作用がないため、高速ユニットテスト
(`yarn test:unit`) でカバーできます。 [Testing](/l/ja/developers/extend/apps/operations/testing) を参照してください。
</Tip>
## The handler
ハンドラは生成された [`CoreApiClient`](/l/ja/developers/extend/apps/logic/logic-functions)
を使用してCRMデータを読み書きします。 テンプレートをロードし、ターゲットレコードをロードし、
本文を埋め、`document`を作成します。
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';
export const generateDocumentHandler = async (
input: { templateId: string; recordId: string },
) => {
const client = new CoreApiClient();
// Use a filtered list query, not the singular lookup: the singular query
// throws when nothing matches, which would become a 500 instead of a 404.
const { documentTemplates } = await client.query({
documentTemplates: {
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
edges: { node: { id: true, name: true, body: true, target: true } },
},
});
const documentTemplate = documentTemplates?.edges?.[0]?.node;
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
const { createDocument } = await client.mutation({
createDocument: {
__args: { data: {
name: `${documentTemplate.name} — ${record.displayName}`,
content, status: 'GENERATED', templateId: documentTemplate.id,
} },
id: true, name: true,
},
});
return { success: true, documentId: createDocument.id, content, missingTokens };
};
```
`loadRecordValues`はPerson vs.a Company に対して異なるクエリを実行し、
結果を平坦化します。
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts)を参照してください。
## ツールとワークフローアクションとして公開する
一つの `defineLogicFunction` は複数のトリガーを持つことができます。 ここでは、`toolTriggerSettings`
をAIエージェントから呼び出すことができ、`workflowActionTriggerSettings` をビジュアルワークフロービルダーの
ステップに変換します。 どちらもJSONスキーマを使用して入力を記述します。
```ts filename="src/logic-functions/generate-document.ts"
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
name: 'generate-document',
description: 'Generate a document from a template and a CRM record.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: generateDocumentInputSchema,
},
workflowActionTriggerSettings: {
label: 'Generate Document',
icon: 'IconFileText',
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
outputSchema: [{ type: 'object', properties: {
success: { type: 'boolean' }, documentId: { type: 'string' },
} }],
},
handler: generateDocumentHandler,
});
```
入力スキーマは `templateId` と `recordId` を説明するプレーンな JSON スキーマです —
は [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts) を参照してください。
## アクセスを許可
ロジック関数はアプリのロールとして実行されます。 テンプレートとレコード
を読み込み、ドキュメントを作成する必要があります。これは `src/roles/default-role.ts` で許可します。
```ts
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Document Generator default role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canAccessAllTools: true,
canBeAssignedToAgents: true,
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```
`UPLOAD_FILE`では、次のセクションで生成されたPDFをアップロードできます。
詳細な権限については、 [Roles](/l/ja/developers/extend/apps/config/roles) を参照してください。
## 実際の PDF ファイルを添付する
レンダリングされたテキストフィールドは役に立ちますが、ユーザは実際のドキュメントを望んでいます。
**PDF**を生成して、ダウンロード可能なファイルとしてレコードに保存しましょう。
まず、`document` オブジェクトに `FILES` フィールドを指定し、PDFを保持します。 アプリはファイルフィールドに
をアップロードするので、このフィールドはアップロードのルートとなります。
```ts filename="src/objects/document.object.ts"
{
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.FILES,
name: 'file',
label: 'File',
icon: 'IconFileTypePdf',
universalSettings: { maxNumberOfValues: 1 },
}
```
今度はそのPDFをレンダリングします。 アプリは本物の Node プロジェクトなので、必要な npm パッケージを自由に追加し、他の場所と同じようにインポートできます。 私たちは **[pdf-lib](https://pdf-lib.js.org/)**
を使って PDF と **[marked](https://marked.js.org/)** を Markdown
本文を解析します。 — CLI はそれらを関数のランタイムにインストールします。
```bash filename="Terminal"
yarn add pdf-lib marked
```
フルヘルパーは
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts)。
これは、Markdown を `marked.lexer` でトークンにパースし、その後 pdf-lib を使ってレイアウトします。実際の見出し、**太字**/*斜体* のラン、箇条書きおよび番号付きリスト、引用ブロックや罫線などを備え、単なるテキストの羅列ではなく、テンプレート自体を洗練された複数ページの A4 レイアウトとしてレンダリングします。
<Frame caption="生成されたPDF:実際の組版とマークダウン書式、テンプレート本文をレンダリングします。">
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="洗練された、市場性のある生成されたPDF" />
</Frame>
<Note>
pdf-lib に組み込まれているフォントは WinAnsi エンコーディングを使用しているため、西ヨーロッパ言語のアクセント付き文字はそのままレンダリングされます。ヘルパーはスマートクォートやダッシュをマッピングし、エンコードできない文字は削除します。 ラテン語以外の文字 (中国語、アラビア語、キリル文字) をレンダリングすることは、
が Unicode フォントを埋め込むことを意味します。
</Note>
次に、それをアップロードし、参照をレコードに保存します。 `uploadFile` は、あなたのアプリが所有するファイルフィールドにバイト
をルートします。返された`id`はあなたが保存したものです。
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';
const uploaded = await new MetadataApiClient().uploadFile(
Buffer.from(bytes),
fileName,
'application/pdf',
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);
await client.mutation({
updateDocument: {
__args: {
id: documentId,
data: { file: [{ fileId: uploaded.id, label: fileName }] },
},
id: true,
},
});
```
生成されたドキュメントには、ダウンロード可能な PDF が含まれています。
<Frame caption="ドキュメントのformat@@0フィールドに保存されている生成されたPDF。">
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="生成された PDF ファイルを持つドキュメント レコード" />
</Frame>
<Note>
`uploadFile` は **app-owned** ファイル項目だけを対象にしています (アップロードには常にフィールドを所有する
アプリと`UPLOAD_FILE` ロールフラグが必要です)。 そのため、PDF
は `file` フィールド上に配置されています。録音には
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
が使用するパターンと同じです。
</Note>
**このステップの後:** それぞれの生成されたドキュメントには、実際のダウンロード可能なPDFがあります。 しかし、
はまだ UI からジェネレーターを呼び出すことはできません。そのために HTTP ルートが必要です。
<Card title="次へ: HTTP ルート →" icon="グローブ" href="/l/ja/developers/extend/apps/tutorials/document-generator/http-routes">
HTTP 経由で関数を提供し、ドキュメントを Web ページとして表示します。
</Card>

View File

@@ -0,0 +1,140 @@
---
title: 3. HTTPルート
icon: globe
description: HTTP 経由で関数をトリガーし、ドキュメントを Web ページとしてレンダリングします。
---
同じハンドラは HTTP リクエストに応答することもできます。 2つのルートを追加します:
* ドキュメントを生成するUI呼び出しの **POST** エンドポイントと
* ドキュメントを印刷可能なウェブページとしてレンダリングするパブリック**GET** エンドポイント。
どちらも `httpRouteTriggerSettings` を使用します。 アプリのルートは `/s/` パスで指定されます。ローカル環境やセルフホスト環境では、そのパスは Twenty サーバーによって提供されます(例: `http://localhost:2020/s/documents/generate`)。 Twenty Cloud では、`RestApiClient` はワークスペースの functions ドメインを通じてパスを解決します。
## POST route — オンデマンドで生成
ここでは `generateDocumentHandler` を再利用しているため、ロジックを繰り返す必要はありません。リクエストボディを読み取るだけの薄いアダプターになっています。
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
共有ハンドラーは失敗時に提案された `status` を返すので、ルートは適切な `4xx`/`5xx` コードで
応答できます。 `isAuthRequired: true` は、呼び出し元が有効なトークンを提示する必要があることを意味します。次の章のフロントエンドのコンポーネントは、ユーザーのアクセストークンを自動的に渡します。
## GET route — ウェブページとしてレンダリング
JSON の代わりに HTML を返すには、本文を
`Content-Type` ヘッダーで囲みます。 このルートはパブリックなので(`isAuthRequired: false`) 、
生成されたドキュメントをリンクとして共有できます。
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
`documentHtmlPage` は、Markdown 本文を HTML にレンダリングし([marked](https://marked.js.org/) を使用し、サニタイズ済み)、テンプレートコンテンツだけが表示される、きれいで印刷可能なページに差し込みます。これは PDF やアプリ内プレビューと同じ見た目です。
[ヘルパーを参照](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts)。
## お試しください
テンプレートとワークスペース内の人を使用して、ルートを呼び出します(トークンを
**設定 → APIs & Webhook**から取得します)
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
返されたドキュメントをブラウザで開きます:
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="公開 GET ルートは、ドキュメントを印刷可能なページとしてレンダリングします。">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="レンダリングされたドキュメントの Web ページ" />
</Frame>
<Tip>
`yarn tindev:function:logs` でテスト中に関数のログをストリーミングしたり、
`yarn tindev:function:exec` で直接呼び出したりすることもできます。
</Tip>
**このステップの後:** アプリは HTTP 経由でドキュメントを生成し、Web ページとして配信できるようになります。 `curl`なしで使えるようにしましょう。
<Card title="次へ: UIの構築 →" icon="table-columns" href="/l/ja/developers/extend/apps/tutorials/document-generator/building-the-ui">
表示、ナビゲーション、コマンド、およびフロントコンポーネント。
</Card>

View File

@@ -0,0 +1,63 @@
---
title: "チュートリアル: Document Generator"
icon: wand-magic-sparkles
description: 実際の Twenty アプリを構築し、CRM データからパーソナライズされたドキュメントを生成しましょう。
---
このチュートリアルでは、再利用可能なテンプレートを、既に CRM にあるデータを使ってパーソナライズされたドキュメントに変換するアプリ **Document Generator** を作成します。
一度テンプレートを作成し、`{{placeholders}}` を記述しておけば、コマンドメニュー、AI エージェント、またはワークフローからワンクリックで、任意の Person または Company 用の入力済みドキュメントを生成できます。
<Frame caption="特定の人物向けに生成され、印刷可能なページとして開かれる 1 つのテンプレート。">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="生成された営業提案書" />
</Frame>
## 学べること
各章で 1 つずつ機能を追加していきます。 最後には、SDK のほとんどの部分に触れ終えていることになります。
| 章 | 機能 | リファレンス |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- |
| [1. データモデル](/l/ja/developers/extend/apps/tutorials/document-generator/data-model) | オブジェクト、フィールド、およびリレーション | [Data](/l/ja/developers/extend/apps/data/overview) |
| [2. ドキュメント生成](/l/ja/developers/extend/apps/tutorials/document-generator/generating-documents) | Markdown テンプレートを埋めて、整えられた PDF を添付するロジック関数(AI ツール + ワークフローアクション) | [ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions) |
| [3. HTTP ルート](/l/ja/developers/extend/apps/tutorials/document-generator/http-routes) | ルートから JSON と共有可能な HTML ページを配信する | [ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions) |
| [4. UI 構築](/l/ja/developers/extend/apps/tutorials/document-generator/building-the-ui) | ビュー、ナビゲーション、コマンドメニュー、ドキュメントのプレビューとテンプレート編集用のフロントエンドコンポーネント | [レイアウト](/l/ja/developers/extend/apps/layout/overview) |
| [5. AI エージェント](/l/ja/developers/extend/apps/tutorials/document-generator/ai-agent) | エージェント + スキル | [スキルとエージェント](/l/ja/developers/extend/apps/logic/skills-and-agents) |
| [6. 公開](/l/ja/developers/extend/apps/tutorials/document-generator/publishing) | マーケットプレイスに公開する | [公開](/l/ja/developers/extend/apps/operations/publishing) |
## 前提条件
[クイックスタート](/l/ja/developers/extend/apps/getting-started/quick-start)を完了している必要があります。
ローカルの Twenty サーバーがポート `2020` で動作しており、CLI がそのサーバーに対して認証済みになっている状態です。
まだであれば、今すぐスキャフォールドして起動してください:
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
完成版のコードを読みたいですか? 完成したアプリは
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator) にあります。
以下のすべてのスニペットは、そこからコピーしたものです。
</Note>
## アプリ全体の構成
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="プレースホルダー付きテンプレートが、コマンドメニュー、AI エージェント、ワークフロー、または共有可能なリンクからのトリガーによって、PDF 付きの整えられたドキュメントに生成されます" />
</Frame>
リッチテキストエディタで `{{placeholders}}` を使って **テンプレート** を一度作成します。 テンプレートと CRM レコードを選ぶと、プレースホルダーが埋められ、整えられた
**ドキュメント**(PDF ファイル付き)が保存されます。 その他のもの — コマンドメニュー、AI エージェント、
ワークフローステップ、共有可能なリンク — はすべて、その 1 つのジェネレーターをトリガーする別々の手段にすぎません。
## このループを動かし続ける
チュートリアル全体を通して、ターミナルで `yarn twenty dev` を実行したままにしておいてください。 `src/` 配下のファイルを追加または編集するたびに、数秒以内にサーバーへ再同期されるため、ビルドしながら各機能が UI に現れていく様子を確認できます。
<Card title="構築を開始する →" icon="database" href="/l/ja/developers/extend/apps/tutorials/document-generator/data-model">
第 1 章: ドキュメントとテンプレートをモデリングする。
</Card>

View File

@@ -0,0 +1,136 @@
---
title: 6. 公開
icon: rocket
description: マーケットプレイスのメタデータを追加し、アプリを公開します。
---
アプリは動作します。 最後のステップは、マーケットプレイスのためにそれを説明し、公開することです。
## マーケットプレイスのメタデータを追加
[アプリケーション config](/l/ja/developers/extend/apps/config/application) は、マーケットプレイスに表示される
アイデンティティを持ちます。これは、作成者、カテゴリ、ロゴ、サポート
リンクです。 `public/`にロゴを入れて、`logo`で参照してください。
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logo: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/ja/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
既定のロールは、専用のファイル内で `defineApplicationRole()` を使って宣言します。ここでは `defaultRoleUniversalIdentifier` を渡す必要はもうありません。
</Tip>
`package.json`に`20app`キーワードを追加すると、アプリが見つかります。
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## ギャラリースクリーンショットを追加
マーケットプレイスのリストはスクリーンショットで自分自身を販売します。
`public/gallery/`にPNGをいくつかドロップし、`galleryImages`で参照します。リストページの
ギャラリーとして表示されます。 行列の順序で入力します。
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
ペイオフでリード: 最初のスクリーンショットを完成結果(生成された
ドキュメント)にし、それがどのようにトリガーされ、作成されたかを表示します。 鮮明で高解像度の
キャプチャを使用する — これはユーザーが最初に目にするものです。
</Tip>
`README.md` にも同じ処理を与えます。npm と GitHub のトップページです。
値のプロポジションとスクリーンショットで開き、見出しの機能
を一覧表示してから、ビルドの詳細を折り目以下に保ちます。
## 出荷前に確認
CI と同じゲートを実行します。
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty plan # preview the metadata diff
```
このプランは、適用せずにサーバー上で何が変わるかを正確に出力します。
最終確認として有用です。
[Testing](/l/ja/developers/extend/apps/operations/testing) と
[Syncing & recovery](/l/ja/developers/extend/apps/operations/sync-and-recovery) を参照してください。
## 公開
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
`app:publish` はデフォルトで npm にビルドおよび公開します。`--private` は代わりに
tarball を Twenty サーバーのプライベートレジストリにアップロードします。 公開されたアプリ
をマーケットプレイスで表示するには、カタログ同期をトリガーします。
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
詳細とリリースのチェックリスト:
[Publishing](/l/ja/developers/extend/apps/operations/publishing).
## あなたはアプリを構築しました 🎉
6つのチャプターでは、SDKのほとんどを使用しました。
* **データをモデル化するためのオブジェクト、フィールド、リレーション**
* **AIツール**、**ワークフローアクション**、**HTTPルート**として公開される**ロジック関数**
* **UIの表示、ナビゲーション、コマンド、フロントコンポーネント**
* 自然言語生成のための **エージェント + スキル**
* **マーケットプレースのメタデータ** とパブリッシュフロー
完成したアプリは
[`packages/20apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator)にあります。
## 次の場所へ
<CardGroup cols={2}>
<Card title="データ参照" icon="database" href="/l/ja/developers/extend/apps/data/overview">
すべてのフィールドタイプ、リレーション、インデックスオプション。
</Card>
<Card title="ロジックリファレンス" icon="bolt" href="/l/ja/developers/extend/apps/logic/overview">
Cronとdatabase-event トリガー、キー値ストア、OAuth 接続。
</Card>
<Card title="レイアウト参照" icon="table-columns" href="/l/ja/developers/extend/apps/layout/overview">
ページレイアウト、ダッシュボードウィジェット、およびより多くのUIサーフェス。
</Card>
<Card title="オペレーション" icon="rocket" href="/l/ja/developers/extend/apps/operations/overview">
CLI、テスト、リモコン、CI。
</Card>
</CardGroup>

View File

@@ -0,0 +1,147 @@
---
title: API
description: REST または GraphQL を使用して、プログラムから CRM データをクエリおよび変更します。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twentyは開発者に優しい設計で、強力なAPIを提供し、カスタムデータモデルに適応します。 異なる統合ニーズに対応する4つの異なるAPIタイプを提供します。 異なる統合ニーズに対応する4つの異なるAPIタイプを提供します。 異なる統合ニーズに対応する4つの異なるAPIタイプを提供します。
## 開発者優先アプローチ
Twenty は、お使いのデータモデルに特化した API を生成します:
* **長いIDが不要**:オブジェクトとフィールド名をエンドポイントに直接使用
* **標準とカスタムオブジェクトを同等に扱う**:カスタムオブジェクトも組み込みのものと同じAPI処理を受ける
* **専用エンドポイント**:各オブジェクトとフィールドに固有のAPIエンドポイントが設けられる
* **カスタムドキュメント**:ワークスペースのデータモデルに特化して生成
<Note>
API キー作成後、**Settings → API & Webhooks** でパーソナライズされた API ドキュメントを利用できます。 Twenty はカスタムデータモデルに合致する API を生成するため、ドキュメントはお使いのワークスペース専用です。
</Note>
## 2 つの API タイプ
### コアAPI
`/rest/` または `/graphql/` でアクセスできます。
実際の **レコード**(データ)を扱います:
* People、Companies、Opportunities などを作成・読み取り・更新・削除。
* データのクエリとフィルター
* レコードの関係管理
### メタデータAPI
`/rest/metadata/` または `/metadata/` でアクセスできます。
**ワークスペースとデータモデル**を管理:
* オブジェクトやフィールドの作成、変更、削除
* ワークスペース設定の構成
* オブジェクト間のリレーションシップを定義
## REST と GraphQL の比較
Core と Metadata の両 API は、REST と GraphQL の形式で利用可能です:
| 形式 | 利用可能な操作 |
| ----------- | ---------------------------------------- |
| **REST** | CRUD、バッチ操作、アップサート |
| **GraphQL** | 同様に、**バッチアップサート**、1 回の呼び出しでのリレーションシップクエリ |
ニーズに合わせて選択してください—どちらの形式でも同じデータにアクセスできます。
## APIエンドポイント
| 環境 | ベース URL |
| ---------- | ------------------------- |
| **クラウド** | `https://api.twenty.com/` |
| **セルフホスト** | `https://{your-domain}/` |
## 認証
すべての API リクエストにはヘッダーに API キーが必要です:
```
Authorization: Bearer YOUR_API_KEY
```
### APIキーを作成
1. **Settings → APIs & Webhooks**に移動
2. **+ Create key** をクリック
3. 設定:
* **Name**: キーのわかりやすい名前
* **Expiration Date**: キーの有効期限
4. **保存**をクリック
5. **すぐにコピー** — キーは一度しか表示されません
<VimeoEmbed videoId="928786722" title="API キーの作成" />
<Warning>
API キーは機密データへのアクセスを許可します。 信頼できないサービスと共有しないでください。 漏洩した場合は、直ちに無効化して新しいものを生成してください。
</Warning>
### API キーにロールを割り当てる
セキュリティを高めるため、アクセスを制限する特定のロールを割り当ててください:
1. **設定 → メンバー → 役割** に移動します
2. 割り当てるロールをクリック
3. **割り当て** タブを開く
4. **API Keys** の下で、**+ Assign to API key** をクリック
5. API キーを選択
キーはそのロールの権限を継承します。 詳細は[権限](/l/ja/user-guide/permissions-access/capabilities/permissions)を参照してください。
### APIキーの管理
**Regenerate**: Settings → APIs & Webhooks → キーをクリック → **Regenerate**
**Delete**: Settings → APIs & Webhooks → キーをクリック → **Delete**
## API プレイグラウンド
組み込みのプレイグラウンドでブラウザから直接 API をテストできます—**REST** と **GraphQL** の両方で利用可能です。
### プレイグラウンドにアクセス
1. **Settings → APIs & Webhooks**に移動
2. API キーを作成(必須)
3. プレイグラウンドを開くには **REST API** または **GraphQL API** をクリック
### 提供内容
* **インタラクティブなドキュメント**: お使いの特定のデータモデル向けに生成
* **ライブテスト**: ワークスペースに対して実際の API 呼び出しを実行
* **スキーマエクスプローラー**: 利用可能なオブジェクト、フィールド、リレーションシップを閲覧
* **リクエストビルダー**: オートコンプリートでクエリを構築
プレイグラウンドはカスタムのオブジェクトとフィールドを反映するため、ドキュメントは常にお使いのワークスペースに対して正確です。
## バッチ操作
REST と GraphQL の両方がバッチ操作をサポートしています:
* **バッチサイズ**:リクエストあたり最大60記録
* **操作**: 複数のレコードの作成、更新、削除
**GraphQL のみの機能:**
* **バッチアップサート**: 1 回の呼び出しで作成または更新
* 複数形のオブジェクト名を使用(例:`CreateCompany` ではなく `CreateCompanies`)
## API レートリミット
プラットフォームの安定性を確保するため、API リクエストはレート制限されています:
| 制限 | 値 |
| ---------- | ------------------- |
| **リクエスト** | 1 分あたり 100 回の呼び出し |
| **バッチサイズ** | 1 回の呼び出しあたり 60 レコード |
<Tip>
バッチ操作を使用してスループットを最大化しましょう—個別のリクエストではなく、1 回の API 呼び出しで最大 60 レコードを処理できます。
</Tip>

View File

@@ -0,0 +1,112 @@
---
title: Webhooks
description: CRM でイベントが発生したときにリアルタイム通知を受信します。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
ウェブフックは、Twenty でイベントが発生したときに、ポーリング不要でリアルタイムにお使いのシステムへデータをプッシュします。 外部システムとの同期を保ち、オートメーションをトリガーしたり、アラートを送信したりするために使用できます。
## Webhookを作成
1. **Settings → APIs & Webhooks → Webhooks**に移動
2. **+ Webhookを作成**をクリック
3. ウェブフックの URL を入力(外部からアクセス可能である必要があります)
4. **保存**をクリック
ウェブフックは直ちに有効化され、通知の送信を開始します。
<VimeoEmbed videoId="928786708" title="ウェブフックの作成" />
### Webhookを管理
**編集**: ウェブフックをクリック → URL を更新 → **保存**
**削除**: ウェブフックをクリック → **削除** → 確認
## イベント
Twenty は次のイベントタイプに対してウェブフックを送信します。
| イベント | 例 |
| ----------- | ---------------------------------------------------------- |
| **レコードの作成** | `person.created`, `company.created`, `note.created` |
| **レコードの更新** | `person.updated`, `company.updated`, `opportunity.updated` |
| **レコードの削除** | `person.deleted`, `company.deleted` |
すべてのイベントタイプはウェブフックの URL に送信されます。 イベントのフィルタリングは将来のリリースで追加される可能性があります。
## ペイロード形式
各ウェブフックは JSON ボディを含む HTTP POST を送信します。
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| フィールド | 説明 |
| --------- | --------------------------- |
| `イベント` | 何が起きたか(例: `person.created`) |
| `データ` | 作成/更新/削除された完全なレコード |
| `タイムスタンプ` | イベントが発生した時刻(UTC) |
<Note>
受信を確認するために、**2xx HTTP ステータス**(200~299)で応答してください。 2xx 以外の応答は配信失敗として記録されます。
</Note>
## ウェブフックの検証
Twenty はセキュリティのために各ウェブフックリクエストに署名します。 リクエストが正当であることを確認するために署名を検証してください。
### ヘッダー
| ヘッダー | 説明 |
| ---------------------------- | -------------- |
| `X-Twenty-Webhook-Signature` | HMAC SHA256 署名 |
| `X-Twenty-Webhook-Timestamp` | リクエストのタイムスタンプ |
### 検証手順
1. `X-Twenty-Webhook-Timestamp` からタイムスタンプを取得
2. 次の文字列を作成: `{timestamp}:{JSON payload}`
3. ウェブフックシークレットを使用して HMAC SHA256 を計算
4. `X-Twenty-Webhook-Signature` と比較
### 例(Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const isValid = expectedSignature === req.headers["x-twenty-webhook-signature"];
```
## ウェブフック vs ワークフロー
| メソッド | 方向 | ユースケース |
| ----------------------- | --- | --------------------------- |
| **ウェブフック** | OUT | あらゆるレコード変更を外部システムへ自動通知 |
| **ワークフロー + HTTP リクエスト** | OUT | カスタムロジック(フィルター、変換)でデータを外部送信 |
| **ワークフローのウェブフックトリガー** | IN | 外部システムから Twenty にデータを受信 |
外部データの受信については、[Webhook トリガーを設定](/l/ja/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)を参照してください。

View File

@@ -0,0 +1,32 @@
---
title: 拡張
description: API、Webhook、カスタムアプリで Twenty の機能を拡張できます。
---
<Frame>
<img src="/images/user-guide/integrations/plug.png" alt="AI" />
</Frame>
## 概要
Twenty は拡張性を念頭に設計されています。 当社の API、Webhook、アプリフレームワークを使用して既存のツールと統合し、カスタム機能を構築できます。
## 現在できること
* **API**: REST または GraphQL を使用して、プログラムから CRM データをクエリおよび変更します。
* **ウェブフック**: Twenty でイベントが発生したときにリアルタイム通知を受信します。
* **アプリ**: Twenty の機能を拡張するカスタムアプリケーションを構築する
## 始めに
<CardGroup cols={2}>
<Card title="API" icon="code" href="/l/ja/developers/extend/api">
プログラムから Twenty に接続
</Card>
<Card title="ウェブフック" icon="bell" href="/l/ja/developers/extend/webhooks">
イベントの通知をリアルタイムで受け取る
</Card>
<Card title="アプリ" icon="puzzle-piece" href="/l/ja/developers/extend/apps/getting-started/quick-start">
カスタマイズをコードとして構築する
</Card>
</CardGroup>

View File

@@ -0,0 +1,189 @@
---
title: OAuth
icon: key
description: PKCE を用いた認可コードフロー、およびサーバー間アクセス向けのクライアントクレデンシャル。
---
Twenty は、ユーザー向けアプリでは認可コード + PKCE、サーバー間アクセスではクライアントクレデンシャルを用いた OAuth 2.0 を実装しています。 クライアントは [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) に従って動的に登録され、ダッシュボードでの手動セットアップは不要です。
## OAuth を使用するタイミング
| シナリオ | 認証方法 |
| ---------------------- | ---------------------------------------------------------------------------- |
| 社内向けスクリプト、自動化 | [API キー](/l/ja/developers/extend/api#authentication) |
| ユーザーの代理として動作する外部アプリ | **OAuth — 認可コード** |
| サーバー間、ユーザーコンテキストなし | **OAuth — クライアントクレデンシャル** |
| UI 拡張機能を備えた Twenty アプリ | [アプリ](/l/ja/developers/extend/apps/getting-started/quick-start)(OAuth は自動で処理されます) |
## クライアントを登録
Twenty は [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) に準拠した**動的クライアント登録**をサポートします。 手動セットアップは不要—プログラムで登録します:
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**レスポンス:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
`client_secret` は安全に保管してください—後から再取得することはできません。
</Warning>
## スコープ
| スコープ | アクセス |
| --------- | ---------------------------------------- |
| `api` | Core および Metadata API への読み取り/書き込みのフルアクセス |
| `profile` | 認証済みユーザーのプロフィール情報を読み取る |
スコープはスペース区切りの文字列で指定します: `scope=api profile`
## 認可コードフロー
アプリが Twenty ユーザーの代理として動作する場合にこのフローを使用します。
### 1. ユーザーを認可にリダイレクト
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| パラメーター | 必須 | 説明 |
| ----------------------- | --- | ------------------------------------------------- |
| `client_id` | はい | 登録済みのクライアント ID |
| `response_type` | はい | `code` である必要があります |
| `redirect_uri` | はい | 登録済みのリダイレクト URI と一致している必要があります |
| `scope` | いいえ | スペース区切りのスコープ(既定は `api`) |
| `state` | 推奨 | CSRF 攻撃を防ぐためのランダムな文字列 |
| `code_challenge` | 推奨 | PKCE チャレンジ(ベリファイアの SHA-256 ハッシュを base64url エンコード) |
| `code_challenge_method` | 推奨 | PKCE 使用時は `S256` である必要があります |
ユーザーに同意画面が表示され、アクセスを許可または拒否します。
### 2. コールバックを処理
認可後、Twenty はあなたの `redirect_uri` にリダイレクトします:
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
`state` が送信した値と一致することを確認します。
### 3. コードをトークンに交換
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**レスポンス:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. アクセストークンを使用
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. 期限切れ時にリフレッシュ
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## クライアントクレデンシャルフロー
ユーザー操作のないサーバー間統合向け:
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
返されるトークンは特定のユーザーに紐づかないワークスペースレベルのアクセス権を持ちます。
## サーバーディスカバリー
Twenty は標準のディスカバリーエンドポイントで OAuth 設定を公開しています:
```
GET /.well-known/oauth-authorization-server
```
これにより、すべてのエンドポイント、対応するグラントタイプ、スコープ、および機能が返されます—汎用的な OAuth クライアントを構築するのに役立ちます。
## API エンドポイント概要
| エンドポイント | 目的 |
| ----------------------------------------- | ----------------- |
| `/.well-known/oauth-authorization-server` | サーバーメタデータのディスカバリー |
| `/oauth/register` | 動的クライアント登録 |
| `/oauth/authorize` | ユーザー認可 |
| `/oauth/token` | トークンの交換とリフレッシュ |
| 環境 | ベース URL |
| ---------- | ------------------------ |
| **クラウド** | `https://api.twenty.com` |
| **セルフホスト** | `https://{your-domain}` |
## OAuth と API キー
| | APIキー | OAuth |
| --------------- | -------------- | ----------------- |
| **セットアップ** | 設定で生成 | クライアントを登録し、フローを実装 |
| **ユーザーコンテキスト** | なし(ワークスペースレベル) | 特定ユーザーの権限 |
| **最適な用途** | スクリプト、社内ツール | 外部アプリ、マルチユーザー統合 |
| **トークンローテーション** | 手動 | リフレッシュトークンにより自動 |
| **スコープによるアクセス** | API へのフルアクセス | スコープによりきめ細かな制御 |

View File

@@ -0,0 +1,117 @@
---
title: ウェブフック
icon: satellite-dish
description: レコードが変更されたときに通知を受け取ります — 作成・更新・削除のたびに、HTTP POST をお使いのエンドポイントに送信します。
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
レコードが作成・更新・削除されるたびに、Twenty はお使いの URL に HTTP POST を送信します。 カスタムオブジェクトを含む、すべてのオブジェクトタイプが対象です。
## Webhookを作成
1. **Settings → APIs & Webhooks → Webhooks**に移動
2. **+ Webhookを作成**をクリック
3. ウェブフックの URL を入力(外部からアクセス可能である必要があります)
4. **保存**をクリック
ウェブフックは直ちに有効化され、通知の送信を開始します。
<VimeoEmbed videoId="928786708" title="ウェブフックの作成" />
### Webhookを管理
**編集**: ウェブフックをクリック → URL を更新 → **保存**
**削除**: ウェブフックをクリック → **削除** → 確認
## イベント
Twenty は次のイベントタイプに対してウェブフックを送信します。
| イベント | 例 |
| ----------- | ---------------------------------------------------------- |
| **レコードの作成** | `person.created`, `company.created`, `note.created` |
| **レコードの更新** | `person.updated`, `company.updated`, `opportunity.updated` |
| **レコードの削除** | `person.deleted`, `company.deleted` |
すべてのイベントタイプはウェブフックの URL に送信されます。 イベントのフィルタリングは将来のリリースで追加される可能性があります。
## ペイロード形式
各ウェブフックは JSON ボディを含む HTTP POST を送信します。
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| フィールド | 説明 |
| ----------- | --------------------------- |
| `event` | 何が起きたか(例: `person.created`) |
| `data` | 作成/更新/削除された完全なレコード |
| `timestamp` | イベントが発生した時刻(UTC) |
<Note>
受信を確認するために、**2xx HTTP ステータス**(200~299)で応答してください。 2xx 以外の応答は配信失敗として記録されます。
</Note>
## ウェブフックの検証
Twenty はセキュリティのために各ウェブフックリクエストに署名します。 リクエストが正当であることを確認するために署名を検証してください。
### ヘッダー
| ヘッダー | 説明 |
| ---------------------------- | -------------- |
| `X-Twenty-Webhook-Signature` | HMAC SHA256 署名 |
| `X-Twenty-Webhook-Timestamp` | リクエストのタイムスタンプ |
### 検証手順
1. `X-Twenty-Webhook-Timestamp` からタイムスタンプを取得
2. 次の文字列を作成: `{timestamp}:{JSON payload}`
3. ウェブフックシークレットを使用して HMAC SHA256 を計算
4. `X-Twenty-Webhook-Signature` と比較
### 例(Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## ウェブフック vs ワークフロー
| メソッド | 方向 | ユースケース |
| ----------------------- | --- | --------------------------- |
| **ウェブフック** | OUT | あらゆるレコード変更を外部システムへ自動通知 |
| **ワークフロー + HTTP リクエスト** | OUT | カスタムロジック(フィルター、変換)でデータを外部送信 |
| **ワークフローのウェブフックトリガー** | IN | 外部システムから Twenty にデータを受信 |
外部データの受信については、[Webhook トリガーを設定](/l/ja/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)を参照してください。