# ワークフローとプロジェクト

> client.workflows を使って、TypeScript から Nodaro のワークフローを作成、更新、共有、エクスポート、インポート、実行し、client.projects でプロジェクトに整理します。

Source: https://nodaro.ai/ja/docs/developers/sdk/workflows

**`client.workflows`** は Nodaro のワークフローの読み取り、書き込み、共有、実行を行い、**`client.projects`** はワークフローを入れるプロジェクトを管理します。ワークフローは接続されたノードからなるキャンバスで、どのワークフローも 1 つのプロジェクトに属します。これらのメソッドは、[ワークフローの REST API](https://nodaro.ai/docs/developers/api/workflows) と同じエンドポイントを呼び出します。概念については、[ワークフローとプロジェクト](https://nodaro.ai/docs/concepts/workflows-and-projects)を参照してください。

## メソッド
| メソッド | 内容 |
| --- | --- |
| [`workflows.list(params)`](#listparams) | プロジェクトのワークフローを、グラフを除いて一覧表示します |
| [`workflows.get(id)`](#getid) | 1 つのワークフローを、ノード、接続、設定とともに読み取ります |
| [`workflows.getPublic(id)`](#getpublicid) | リンクで共有されたワークフローを、トークンなしで読み取ります |
| [`workflows.create(input)`](#createinput) | プロジェクトにワークフローを作成します |
| [`workflows.update(id, input)`](#updateid-input) | ワークフローの任意のフィールドを変更します |
| [`workflows.delete(id)`](#deleteid) | ワークフローを削除します |
| [`workflows.run(id, params?)`](#runid-params) | ワークフローの実行を開始します |
| [`workflows.export(workflowId, opts?)`](#exportworkflowid-opts) | ワークフローを、移植可能な JSON バンドルとしてエクスポートします |
| [`workflows.import(input)`](#importinput) | バンドルをプロジェクトにインポートします |
| [`workflows.setVisibility(id, visibility)`](#setvisibilityid-visibility) | ワークフローを非公開にするか、ワークスペースに公開します |
| [`workflows.move(id, params)`](#moveid-params) | ワークフローを別のプロジェクトに移動します |
| [`workflows.sharedWithMe()`](#sharedwithme) | ほかの人から共有されたワークフローを一覧表示します |
| [`workflows.collaborators.*`](#collaborators) | ワークフローの共有相手を一覧表示、追加、変更、削除します |
| [`projects.list()`](#projectslist) ほか | プロジェクトを一覧表示、読み取り、作成、更新、削除します |

## client.workflows
### list(params)
プロジェクト内のワークフローを一覧表示します。一覧で返されるのはメタデータだけで、`nodes`、`edges`、`settings`、`sourcePrompt` は含まれません。グラフ全体を取得するには、`get()` で 1 つのワークフローを読み取ります。

```ts
list(params: { projectId: string }): Promise<{ data: Workflow[] }>
```

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "ワークフローを一覧表示するプロジェクトです。" },
}}
/>

```ts
const { data: workflows } = await client.workflows.list({ projectId })
for (const wf of workflows) console.log(wf.id, wf.name, wf.updatedAt)
```

あなたから見えないプロジェクトを指定すると、`NotFoundError` をスローします。

### get(id)
1 つのワークフローを、完全な `nodes`、`edges`、`settings` を含めて読み取ります。

```ts
get(id: string): Promise<{ data: Workflow }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークフローの ID です。" },
}}
/>

```ts
const { data: wf } = await client.workflows.get(workflowId)
console.log(`${wf.name}: ${wf.nodes?.length ?? 0} nodes, version ${wf.version}`)
```

ワークフローを更新する予定がある場合は、`version` を保持しておいてください。`version` を使うと、ほかの人の書き込みがあっても、安全に更新できます。

### getPublic(id)
所有者がリンクで共有したワークフローを読み取ります（`GET /v1/public/workflows/:id`）。トークンは不要です。ワークフローが返されるのは、共有がオンになっている間だけです。オフの場合、このメソッドは `NotFoundError` をスローします。

```ts
getPublic(id: string): Promise<{ data: Workflow }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "共有されたワークフローの ID です。" },
}}
/>

```ts
const { data: shared } = await client.workflows.getPublic(workflowId)
```

認証プロバイダーがトークンを返さない場合、リクエストはトークンなしで送られます。

### create(input)
プロジェクトにワークフローを作成し、完全なレコードを返します。必要なのは `projectId` と `name` だけで、ほかのフィールドにはサーバーのデフォルト値が使われます。

```ts
create(input: CreateWorkflowInput): Promise<{ data: Workflow }>
```

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "ワークフローを作成するプロジェクトです。" },
name: { type: 'string', required: true, description: "ワークフローの名前です。" },
description: { type: 'string', description: "説明です。" },
folderId: { type: 'string | null', description: "プロジェクト内のフォルダーです。" },
nodes: { type: 'GenericNode[]', description: "キャンバス上のノードです。" },
edges: { type: 'GenericEdge[]', description: "ノード間の接続です。" },
settings: { type: 'Record<string, unknown>', description: "ワークフローの設定です。" },
sourcePrompt: { type: 'string', description: "ワークフローの生成元のプロンプトです（ある場合）。" },
}}
/>

```ts
const { data: wf } = await client.workflows.create({
projectId,
name: "Product launch video",
nodes: [],
edges: [],
})
```

できあがったグラフを使うには、エディターで作ったワークフローをエクスポートして、その `nodes` と `edges` を読み取るか、[テンプレート](https://nodaro.ai/docs/developers/sdk/apps-and-templates)を複製します。

### update(id, input)
ワークフローのフィールドを任意の組み合わせで変更し、更新後の完全なレコードを返します（`PATCH /v1/workflows/:id`）。指定しなかったフィールドは、そのまま残ります。

```ts
update(id: string, input: UpdateWorkflowInput): Promise<{ data: Workflow }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークフローの ID です。" },
name: { type: 'string', description: "新しい名前です。" },
description: { type: 'string', description: "新しい説明です。" },
folderId: { type: 'string | null', description: "同じプロジェクト内の別のフォルダーに、ワークフローを移動します。" },
nodes: { type: 'GenericNode[]', description: "新しいノードのリスト全体です。" },
edges: { type: 'GenericEdge[]', description: "新しい接続のリスト全体です。" },
settings: { type: 'Record<string, unknown>', description: "新しいワークフローの設定です。" },
sourcePrompt: { type: 'string', description: "ワークフローの生成元のプロンプトです。" },
thumbnailUrl: { type: 'string | null', description: "プレビュー画像です。すでにホストされている画像の URL を指定します。null を指定すると削除されます。" },
expectedVersion: { type: 'number', description: "読み取ったバージョンです。保存されているバージョンと異なる場合、更新は WorkflowConflictError で拒否されます。expectedUpdatedAt よりも、こちらを推奨します。" },
expectedUpdatedAt: { type: 'string', description: "読み取った updatedAt の値です。expectedVersion の代わりに使える、以前からの方法です。" },
}}
/>

```ts
await client.workflows.update(workflowId, { name: "Renamed", expectedVersion: 7 })
```

**安全な更新**：`expectedVersion` も `expectedUpdatedAt` も指定しない場合は、最後の書き込みが優先されます。どちらかを指定すると、読み取った後に誰もワークフローを変更していない場合にだけ、更新が適用されます。変更されていた場合は [`WorkflowConflictError`](https://nodaro.ai/docs/developers/sdk/errors#workflowconflicterror) をスローします。その `currentRecord` には現在のワークフローが入っているので、もう一度読み取らずにマージできます。

```ts

try {
await client.workflows.update(workflowId, { settings, expectedVersion: wf.version })
} catch (err) {
if (err instanceof WorkflowConflictError && err.currentRecord) {
const merged = mergeSettings(err.currentRecord.settings, settings)
await client.workflows.update(workflowId, {
settings: merged,
expectedVersion: err.currentVersion,
})
} else {
throw err
}
}
```

ノードデータにある実行状態の値（実行中のステータスや、現在のジョブ ID など）は、サーバーによって削除され、保存されることはありません。

### delete(id)
ワークフローを削除します。

```ts
delete(id: string): Promise<{ success: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークフローの ID です。" },
}}
/>

```ts
await client.workflows.delete(workflowId)
```

ID が存在しない場合や、あなたのものではない場合は、`NotFoundError` をスローします。そのため、削除が何も知らせずに失敗することはありません。

### run(id, params?)
ワークフローの実行を開始し、すぐに `{ executionId, status }` を返します。実行は、サーバー上で続きます。進行状況と結果は、[`client.executions.get()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) をポーリングして取得します。

```ts
run(id: string, params?: { nodeIds?: string[] }): Promise<{ executionId: string; status: "pending" | "running" }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークフローの ID です。" },
nodeIds: { type: 'string[]', description: "指定したノードだけを実行します。ワークフロー全体を実行するには、省略します。" },
}}
/>

```ts
const { executionId } = await client.workflows.run(workflowId, {
nodeIds: ["text-prompt-1", "image-gen-2"],
})
const { data: execution } = await client.executions.get(executionId)
```

- アカウントの残高が、実行にかかりうる最大の料金に足りない場合は、`InsufficientCreditsError` をスローします。
- OAuth トークンには、`workflows:execute` スコープが必要です。
- ワークスペース内のワークフローを実行するには、[`client.withWorkspace()`](https://nodaro.ai/docs/developers/sdk/client#withworkspaceworkspaceid) で得たクライアントで `run` を呼び出します。

### export(workflowId, opts?)
ワークフローの保存済みのバージョンを、移植可能な JSON バンドルとしてエクスポートします。これは、エディターの**エクスポート**メニューが書き出すのと同じファイル形式です。`assets: true` を指定すると、エディターの**アセットを含める**と同じように、ワークフローが使うアセットもバンドルに含まれます。

```ts
export(workflowId: string, opts?: { assets?: boolean }): Promise<{ data: WorkflowExport }>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "エクスポートするワークフローです。" },
assets: { type: 'boolean', default: 'false', description: "ワークフローが使うキャラクター、オブジェクト、クリーチャー、ロケーションを含めます。インポート時に、それらが作成し直されます。" },
}}
/>

```ts
const { data: bundle } = await client.workflows.export(workflowId, { assets: true })
```

**移植性**：バンドルが参照できるのは、ほかのインスタンスが取得できるメディアだけです。ほかのインスタンスからアクセスできない URL をノードが使っている場合、バンドルはそれらを `portability.unreachableMedia` に列挙します。たとえば、`localhost` 上にあるセルフホスティング環境自身のストレージ、ローカルネットワークのアドレス、`.internal` の名前などです。

```ts
bundle.portability?.unreachableMedia
// [{ nodeId: "n1", nodeLabel: "Video URL", field: "videoUrl", url: "http://localhost:3000/storage/..." }]
```

すべてのメディアの URL が公開されている場合、このフィールドはありません。アクセスできないメディアを含むバンドルもインポートはできますが、該当するノードは、メディアをアップロードし直すまで、インポート先のインスタンスでは実行できません。[インポートとエクスポート](https://nodaro.ai/docs/guides/import-export)を参照してください。

### import(input)
エクスポートしたバンドルを、自分のプロジェクトのいずれかにインポートし、新しいワークフローを返します。

```ts
import(input: WorkflowExport & { projectId: string }): Promise<{
data: Workflow
importReport?: WorkflowImportReport
}>
```

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "インポート先のプロジェクトです。" },
'...bundle': { type: 'WorkflowExport', required: true, description: "エクスポートしたバンドルのフィールドです。入力にスプレッドして渡します。" },
}}
/>

```ts
const { data: wf, importReport } = await client.workflows.import({ ...bundle, projectId })
```

インポートでは、次の処理が行われます。

- **アセットが作成し直されます**：バンドル内のキャラクター、オブジェクト、クリーチャー、ロケーションは、あなたのアカウントの新しい項目になります。ワークフローの参照先も、それらに付け替えられます。対象は、アセットノード、ノードデータ内のすべての `@` メンション、ワークフローの設定です。
- **メディアがコピーされます**：ほかのホストにあるメディアは、アクセスできる場合、このインスタンスのストレージにコピーされます。そのため、ワークフローがほかの人のサーバーに依存することはありません。上限は、ワークフローのメディアで 25 ファイル、同梱のアセットでさらに 25 ファイルです。画像は 20 MB まで、動画とオーディオは 50 MB までです。
- **コピーはストレージの使用量に含まれます**：ストレージが足りなくなっても、ワークフロー自体はインポートされ、収まらなかったアセットはレポートに一覧表示されます。

`importReport` は、何が起きたかを示します。

```ts
importReport
// {
//   rehosted: 3,                                    // files copied to this instance
//   unreachable: [{ nodeId, nodeLabel, field, url }], // private hosts, left as they were
//   skipped: [{ nodeId, field, url, reason: "HTTP 404" }],
//   assetIdMap: { "<bundled asset id>": "<new asset id>" },
//   assetsSkipped: [{ kind: "character", id, name: "Kira", reason: "Storage limit exceeded" }],
// }
```

`assetIdMap` は、バンドルがアセットを含んでいた場合に必ず存在します。ワークフロー内のメンションは、サーバーがすでにすべて更新しています。このマップは、ワークフローの外で保持している参照にだけ使ってください。`assetsSkipped` が含まれるのは、何かが除外された場合だけです。

### setVisibility(id, visibility)
ワークフローを、作成者と追加した共同編集者だけが見られる `"private"` にするか、ワークスペースの全員が見られる `"workspace"` にします。

```ts
setVisibility(id: string, visibility: "private" | "workspace"): Promise<{ data: Workflow }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークフローの ID です。" },
visibility: { type: '"private" | "workspace"', required: true, description: "ワークフローを見られる人です。" },
}}
/>

```ts
await client.workflows.setVisibility(workflowId, "workspace")
```

変更できるのは、作成者かワークスペースの管理者だけです。それ以外の人が変更しようとすると、`ForbiddenError` がスローされます。ワークスペースは、Nodaro Cloud の[組織](https://nodaro.ai/docs/developers/sdk/organizations)にあります。

### move(id, params)
ワークフローを別のプロジェクトに移動します（`POST /v1/workflows/:id/move`）。ワークフローは、元のフォルダーから外れます。

```ts
move(id: string, params: { projectId: string }): Promise<{
data: Workflow
droppedCollaborators: { userId: string; name: string | null }[]
}>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ワークフローの ID です。" },
projectId: { type: 'string', required: true, description: "移動先のプロジェクトです。" },
}}
/>

```ts
const { droppedCollaborators } = await client.workflows.move(workflowId, { projectId: archiveProjectId })
```

移動によってワークフローがワークスペースの外に出る場合、そのワークスペースから得ていたアクセス権はなくなります。アクセス権を失った人は、`droppedCollaborators` で返されます。

### sharedWithMe()
ほかの人があなたに直接共有したワークフローを一覧表示します。所属しているワークスペース内の作品は、そのワークスペース自身の一覧にすでに表示されるため、含まれません。各ワークフローには、あなたがそのワークフローに対して持つ `grantedRole` が付いています。

```ts
sharedWithMe(): Promise<{ data: (Workflow & { grantedRole: "viewer" | "editor" })[] }>
```

```ts
const { data: shared } = await client.workflows.sharedWithMe()
for (const wf of shared) console.log(wf.name, wf.grantedRole)
```

組織のないインスタンスでは、この一覧は空です。

### collaborators
ワークフローの共有相手です。`client.workflows.collaborators` からアクセスします。これらのメソッドは、[組織](https://nodaro.ai/docs/developers/sdk/organizations)のあるインスタンスで使えます。それ以外のインスタンスでは、`NotFoundError` をスローします。

```ts
collaborators.list(workflowId: string): Promise<{ data: Collaborator[] }>
collaborators.add(workflowId: string, input: { userId?: string; email?: string; role: "viewer" | "editor" }): Promise<{ data: { userId: string; role: "viewer" | "editor" } }>
collaborators.update(workflowId: string, userId: string, input: { role: "viewer" | "editor" }): Promise<{ data: { userId: string; role: "viewer" | "editor" } }>
collaborators.remove(workflowId: string, userId: string): Promise<{ success: true }>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "ワークフローの ID です。" },
userId: { type: 'string', description: "相手のユーザー ID です。add() では、userId と email のどちらか一方だけを指定します。" },
email: { type: 'string', description: "任意のメールアドレスです。相手がまだアカウントを持っていなくてもかまいません。" },
role: { type: '"viewer" | "editor"', required: true, description: "相手がワークフローに対してできることです。" },
}}
/>

```ts
await client.workflows.collaborators.add(workflowId, { email: "editor@example.com", role: "editor" })

const { data: people } = await client.workflows.collaborators.list(workflowId)
// [{ userId, name, avatar, role }]: email addresses are never returned
```

共同編集者は、自分のユーザー ID を指定して `remove()` を呼び出すと、ワークフローから抜けられます。

## client.projects
プロジェクトは、ワークフローをまとめるものです。どのワークフローも、ちょうど 1 つのプロジェクトに属します。

### projects.list()
自分のプロジェクトを一覧表示します。

```ts
list(): Promise<{ data: Project[] }>
```

```ts
const { data: projects } = await client.projects.list()
```

### projects.get(id)
1 つのプロジェクトを読み取ります。

```ts
get(id: string): Promise<{ data: Project }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロジェクトの ID です。" },
}}
/>

```ts
const { data: project } = await client.projects.get(projectId)
```

### projects.create(input)
プロジェクトを作成します。

```ts
create(input: { name: string; description?: string; settings?: Record<string, unknown> }): Promise<{ data: Project }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "プロジェクトの名前です。" },
description: { type: 'string', description: "説明です。" },
settings: { type: 'Record<string, unknown>', description: "プロジェクトの設定です。" },
}}
/>

```ts
const { data: project } = await client.projects.create({ name: "Spring campaign" })
```

### projects.update(id, input)
プロジェクトを変更します。少なくとも 1 つのフィールドを渡してください。

```ts
update(id: string, input: { name?: string; description?: string; settings?: Record<string, unknown> }): Promise<{ data: Project }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロジェクトの ID です。" },
name: { type: 'string', description: "新しい名前です。" },
description: { type: 'string', description: "新しい説明です。" },
settings: { type: 'Record<string, unknown>', description: "新しいプロジェクトの設定です。" },
}}
/>

```ts
await client.projects.update(projectId, { description: "Assets for the spring launch" })
```

### projects.delete(id)
プロジェクトを削除します。

```ts
delete(id: string): Promise<{ success: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロジェクトの ID です。" },
}}
/>

```ts
await client.projects.delete(projectId)
```

## 型
| 型 | 構造 |
| --- | --- |
| `Workflow` | `id`、`projectId`、`userId`、`name`、`description?`、`folderId?`、`version?`、`thumbnailUrl?`、`nodes?`、`edges?`、`settings?`、`sourcePrompt?`、`createdAt`、`updatedAt`。グラフのフィールドがあるのは、完全なレコードだけです。 |
| `Project` | `id`、`userId`、`name`、`description?`、`settings?`、`createdAt`、`updatedAt` |
| `Collaborator` | `userId`、`name?`、`avatar?`、`role` |
| `WorkflowExport` | `export()` が返し、`import()` が受け付ける、移植可能なバンドル |
| `GenericNode`, `GenericEdge` | `nodes` と `edges` で使われる、ノードと接続の構造 |

これらの型は、すべて `@nodaro/sdk` からエクスポートされています。[型](https://nodaro.ai/docs/developers/sdk/types)を参照してください。

## Frequently asked questions

### Nodaro のワークフローを、コードから実行するにはどうすればよいですか？

client.workflows.run(workflowId) を呼び出します。すぐに executionId が返されます。ステータスが completed、failed、cancelled、timed_out のいずれかになるまで、client.executions.get(executionId) をポーリングしてください。

### ワークフローの一部のノードだけを実行できますか？

はい。client.workflows.run の第 2 引数に、実行するノード ID のリストである nodeIds を持つオブジェクトを渡します。実行されるのは、指定したノードだけです。

### ほかの人の変更を上書きしないようにするには、どうすればよいですか？

client.workflows.update に、読み取ったバージョンを expectedVersion として渡します。その間にワークフローが変更されていた場合、呼び出しは現在のレコードを含む WorkflowConflictError をスローするので、マージしてから再試行できます。

### ワークフローを別の Nodaro インスタンスにコピーするには、どうすればよいですか？

assets オプションを true にして client.workflows.export でエクスポートし、別のインスタンスで client.workflows.import と projectId を使ってバンドルをインポートします。アクセスできるメディアは、新しいインスタンスのストレージにコピーされます。
