i18n - docs translations (#22617)

Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22617?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-07-07 11:54:54 +02:00
committed by GitHub
parent 07a921f8ca
commit 18ca89bcdd
97 changed files with 13247 additions and 0 deletions
@@ -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>
@@ -0,0 +1,299 @@
---
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)
はそのビューをサイドバーに配置します。
```ts filename="src/views/documents.view.ts"
import { defineView, ViewKey } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
key: ViewKey.INDEX,
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 { 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 apiBaseUrl = process.env.TWENTY_API_URL;
const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ templateId, recordId }),
}).then((r) => r.json());
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>
## 開くためのコマンド
People が選択されているときに表示され、サイドパネルでコンポーネントを開く
`availabilityType: 'RECORD_SELECTION'` を指定した [command menu item](/l/ja/developers/extend/apps/layout/command-menu-items) を追加します。
```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 { 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 = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?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` レイアウトは、オブジェクトのレコードビューにタブを追加します。`CANVAS` タブの `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.CANVAS,
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',
gridPosition: { 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) を参照してください。
編集者は適切なリッチテキストエディタでテンプレートを書き込むようになりました:
<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>
@@ -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>
@@ -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>
@@ -0,0 +1,141 @@
---
title: 3. HTTPルート
icon: globe
description: HTTP 経由で関数をトリガーし、ドキュメントを Web ページとしてレンダリングします。
---
同じハンドラは HTTP リクエストに応答することもできます。 2つのルートを追加します:
* ドキュメントを生成するUI呼び出しの **POST** エンドポイントと
* ドキュメントを印刷可能なウェブページとしてレンダリングするパブリック**GET** エンドポイント。
どちらも `httpRouteTriggerSettings` を使用します。 アプリのルートはあなたの
20のサーバーの`/s`の下で提供されます(例:`http://localhost:2020/s/documents/generate`)。
## 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>
@@ -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) | Views、ナビゲーション、コマンドメニュー、およびドキュメントをプレビューしてテンプレートを編集するフロントコンポーネント | [レイアウト](/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>
@@ -0,0 +1,136 @@
---
title: 6. 公開
icon: rocket
description: マーケットプレイスのメタデータを追加し、アプリを公開します。
---
アプリは動作します。 最後のステップは、マーケットプレイスのためにそれを説明し、公開することです。
## マーケットプレイスのメタデータを追加
[アプリケーション config](/l/ja/developers/extend/apps/config/application) は、マーケットプレイスに表示される
アイデンティティを持ちます。これは、作成者、カテゴリ、ロゴ、サポート
リンクです。 `public/`にロゴを入れ、`logoUrl`で参照してください。
```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.',
logoUrl: '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をいくつかドロップして、`screenshots` を使って参照します。リストページでギャラリー
としてレンダリングします。
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
screenshots: [
'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 dev --once --dry-run # 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>