2026年10月8日時点のDify公式資料をもとに、Webアプリへ下書き生成を組み込むときのAPI境界を整理します。画面から呼べるようにするだけでは、利用者のなりすまし、予算超過、通信切断後の二重実行を防げません。
この記事では、ブラウザとDifyの間にアプリ側バックエンドを置き、認証・認可・利用上限と実行結果の状態を管理する設計を扱います。DifyのWorkflowは生成に集中させ、生成物の承認と業務上の確定を分けます。
対象は、DifyのWorkflowをWebアプリへ組み込むエンジニアと、その運用を判断するテックリードです。題材は架空の「申請文の下書き生成」です。実績の範囲は、公式仕様の確認と、後掲するJavaScriptのローカルモック検証です。Dify実機でのAPI呼び出し、性能測定、本番導入、工数削減の測定は行っていません。
APIキーを隠すだけでは、利用者の権限は守れない
Dify公式のAPI導入ガイドでは、アプリのAPIキーはそのアプリに対応し、バックエンドから呼び出すよう説明されています。ブラウザへ埋め込んだキーは取り出せるため、フロントエンドへ配布しません。
もう一つの境界がuserです。公式のEnd User Identityによれば、これは呼び出し元が指定する利用者識別子であり、Difyはその値を本人認証しません。
したがって、画面から受け取ったuserをそのまま転送すると、APIキーを隠しても別利用者の識別子を名乗る余地が残ります。次の責任分担は、仕様を踏まえた設計案です。
| 境界 | 管理するもの | 信頼してはいけないもの |
|---|---|---|
| ブラウザ → アプリAPI | セッション認証、操作権限、入力形式、CSRF対策 | リクエスト本文の利用者ID・組織ID |
| アプリAPI → Dify | サーバー側APIキー、認証結果から導出したuser、入力の許可リスト |
ブラウザが指定した接続先・任意の入力変数 |
| Dify → アプリAPI | 実行状態、出力の形式、業務上の承認 | HTTP 200だけを根拠にした完了判定 |
テナントをまたぐ利用者IDの衝突も避けます。例えば、認証済みの組織とアカウントに対応する不透明なIDをサーバー側で管理し、その値をuserへ渡します。メールアドレスを識別子として送る必要はありません。
直接呼び出し・バックエンド・非同期ジョブを比較する
| 方式 | 適する条件 | 残る責任・コスト |
|---|---|---|
| ブラウザからDify APIを直接呼ぶ | アプリAPIキーを持つブラウザには採用しない | キー流出と利用者識別子の偽装を防げない |
| アプリ側バックエンドから同期実行 | 短時間の下書き生成、完了を待てる画面 | 認証・認可、同時実行数、タイムアウト、結果の保存 |
| バックエンド+永続ジョブ | 長時間処理、画面を閉じても追跡したい処理 | ジョブの状態管理、所有者確認、再開・照合、キュー運用 |
今回の設計案では、外部への送信や業務データ更新を含めない、短い下書き生成から始めます。そのため同期バックエンドを選びます。ただし、結果不明のまま再実行するUIにはしません。処理が長くなる、切断が頻発する、確定操作を含む場合は、永続ジョブへ移す判断が必要です。
DifyのWorkflow API概要では、Workflowは呼び出しごとに独立して実行されます。API化したことだけを根拠に、重複要求が自動でまとめられると考えないようにします。
公開する前に、入力と出力の契約を決める
ここでいう公開は、Dify内でWorkflowの実行対象を確定する操作です。業務システムから誰が利用できるかは、アプリ側で別に制御します。Run Workflowは公開済みWorkflowを実行するAPIです。
架空のWorkflowでは、次の契約を先に決めます。
| 項目 | この例の契約 |
|---|---|
| 入力 |
inputs.request_textに申請の素材となる文字列 |
| 出力 |
data.outputs.draftに下書き文字列 |
| 認証 | アプリ側セッションで確認し、バックエンドだけがDifyキーを使う |
| 認可 | 認証済み利用者が下書き生成機能を使えるか確認する |
| 完了条件 | Workflow成功+出力形式が妥当で、下書きが確認待ちとして保存されたこと |
| 確定条件 | 利用者が内容を確認して承認すること。生成成功だけでは申請しない |
request_textとdraftはこの例で決めた変数名で、Difyが自動で用意する名前ではありません。実際のアプリ設定と合わせます。空入力、長すぎる入力、想定外の出力を公開前の評価データに入れ、出力名を変える変更もAPIの契約変更として扱います。
通常の/workflows/runは現在公開中のWorkflowを使います。公開版を固定したい場合は、公式のRun Workflow by IDで特定の公開済みworkflow_idを指定できます。Dify Cloudでは、特定の公開版を実行する機能は有料プラン向けです。Sandboxプランでは最新の公開版が実行されます(2026-10-10確認:Version Control)。これは公開版の選択であり、実行ごとのworkflow_run_idとは用途が異なります。
APIキーはサーバーの秘密管理へ保存します。検証用と本番用ではアプリとキーを分け、キーの失効、更新、漏えい時の停止手順を用意します。キー、Authorizationヘッダー、入力全文、出力全文を通常ログへ残しません。
HTTPアダプターでは、生成成功を「確認待ち」に変換する
以下はNode.js用のサーバー側モジュールdify-draft.mjsです。呼び出し側で認証・認可と利用上限を確認し、認証結果から得た識別子を渡す前提です。この関数自身は利用者を認証しません。画面が送ったIDをそのまま渡してはいけません。
APIキーはDIFY_API_KEYから読み、接続先はコード側に固定します。入力上限2,000文字、出力上限10,000文字、待機時間15秒は例として置いた値です。性能実測に基づく設定でも、Difyの上限値でもありません。独自のsignalを渡す場合は、呼び出し側で待機期限も組み込みます。
// dify-draft.mjs: サーバー側のみ。自動再試行は行わない。
export async function generateDraft(
text,
authenticatedUserId,
signal = AbortSignal.timeout(15_000),
) {
if (
typeof text !== "string" || !text.trim() || text.length > 2_000 ||
typeof authenticatedUserId !== "string" ||
!/^[A-Za-z0-9_-]{1,128}$/.test(authenticatedUserId)
) {
return { state: "not_started", reason: "invalid_input" };
}
if (signal.aborted) {
return { state: "not_started", reason: "aborted_before_request" };
}
const apiKey = process.env.DIFY_API_KEY?.trim();
if (!apiKey) {
return { state: "not_started", reason: "missing_server_config" };
}
let response;
try {
response = await fetch("https://api.dify.ai/v1/workflows/run", {
method: "POST",
redirect: "error",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: { request_text: text.trim() },
response_mode: "blocking",
user: authenticatedUserId,
}),
signal,
});
} catch {
// 通信切断・中断は、Dify側の未実行を証明しない。
return { state: "unknown", reason: "transport_interrupted" };
}
if (!response.ok) {
if ([400, 401, 403, 404, 413, 415, 429].includes(response.status)) {
return { state: "rejected", httpStatus: response.status };
}
return { state: "unknown", reason: "upstream_http_error" };
}
let body;
try {
body = await response.json();
} catch {
return { state: "unknown", reason: "unreadable_response" };
}
const runId = body?.workflow_run_id;
if (
typeof runId !== "string" ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(runId)
) {
return { state: "unknown", reason: "missing_run_id" };
}
const draft = body?.data?.outputs?.draft;
if (body?.data?.status !== "succeeded") {
return { state: "reconcile_required", runId, reason: "run_not_succeeded" };
}
if (typeof draft !== "string" || !draft.trim() || draft.length > 10_000) {
return { state: "reconcile_required", runId, reason: "invalid_output" };
}
return { state: "review_pending", runId, draft };
}
このモジュールはHTTP境界の例であり、DBへの保存、Webのルート、レート制限、業務内容の正しさの評価は含みません。review_pendingを返しただけでは、保存済み・承認済みとは扱えません。
呼び出し側では、受け付けた操作のIDと所有者を保存し、上の結果で状態を更新します。review_pendingなら下書きを保存してから確認画面を出します。unknownなら再実行ボタンを有効にする前に照合へ進めます。保存に失敗した場合も、生成自体の失敗と区別します。
出力の文字列検証は内容の正しさを保証しません。事実、申請条件、誤った断定などを別の品質評価で確認し、必要ならHuman Reviewへ送ります。画面への表示もテキストとして行い、生成文を未検証のHTMLとして挿入しません。
タイムアウトと失敗を、同じ再試行ボタンへ押し込まない
| アダプターの結果 | 残る状態 | 次の動作 |
|---|---|---|
not_started |
この関数から外部呼び出ししていない | 入力・設定・中断理由を直して、新しい操作として開始 |
rejected |
HTTPエラーを受信した。途中の副作用がないことまでは断定しない | 原因を分類。自動再試行はしない |
unknown |
実行の有無・結果を確定できない | 操作を保留し、記録を照合。必要なら人が判断 |
reconcile_required |
実行IDはあるが、成功または出力契約を満たさない | 実行詳細を確認し、復旧または再実行を判断 |
review_pending |
生成結果を受信した | 下書きを保存し、品質評価・利用者確認へ進む |
Difyのエラーガイドでは、429でもtoo_many_requestsは同時実行数、rate_limit_errorは利用枠の問題として区別されます。前者はバックオフの候補ですが、後者は待ち時間を少し増やしても解決しません。401や入力不正も、そのまま繰り返す理由がありません。
上の例はHTTP状態だけを返す最小構成です。本番では、許可リストにあるエラーcodeを内部記録へ追加し、同時実行数、利用枠、設定不備を分類します。エラー本文は利用者へ丸ごと返しません。また、Dify側の401をアプリ利用者のログイン切れとして表示すると、再ログインしても直らない障害になります。
通信失敗の再試行については、APIの一般的な再試行指針に加えて、Workflowが外部へ与える副作用を確認します。特に送信・登録を含む処理では、タイムアウトを「未実行」と決めつけられません。今回の下書き生成でも、自動再実行すれば追加の推論コストが発生し得るため、アダプターでは再試行しません。
workflow_run_idが保存できていれば、Get Workflow Run Detailで状態と出力を照合できます。ただし、切断前に実行IDを受け取れない場合、この方法だけでは解決しません。自前の操作IDがあることと、Difyが同じ操作を重複排除することも別です。
長時間化したときは、接続と実行を分けて管理する
blockingからstreamingへ変えるだけで、耐障害性が完成するわけではありません。画面の接続が切れても、開始した実行を追跡できる保存先が必要です。
長時間処理へ拡張する場合の設計案は、次のようになります。
同じ操作IDの再送は、所有者と入力の一致を確認し、既存の状態を返します。異なる入力での使い回しは拒否します。DBの一意制約と状態更新で同時実行を制御し、dispatchingのままプロセスが落ちた操作は、未実行と断定せず照合対象にします。このジョブ設計自体は未実装です。
公式資料では、task_idは実行中の制御、workflow_run_idは実行記録の参照に使います。停止はstreamingの実行を対象にします。Workflow API概要
ブラウザのAbortControllerで待機を中断しても、Dify側の処理や、既に成功した外部操作が取り消されたことにはなりません。停止APIを設けるなら、バックエンドで操作の所有者を確認してから呼び、停止後にも結果を照合します。特にWorkflowの停止APIはuserを検証しないと公式に説明されているため、所有者確認を省けません。End User Identity
ローカルで何を確認し、何が残っているか
掲載したアダプターを記事から抽出し、Node.js v24.11.1で外部通信をモックした検証を行いました。検証対象はHTTP境界の分岐であり、Dify実機の動作ではありません。
ローカル検証結果の抜粋です。構文確認と19件のテストが通りました。
tests 19
pass 19
fail 0
| 検証対象 | 期待する動作 |
|---|---|
| 正常な実行・妥当な下書き |
review_pending。送信・登録を実行しない |
| 空入力、長すぎる入力、利用者識別子の不正 |
not_started。外部呼び出しゼロ |
| APIキー未設定、呼び出し前に中断済み |
not_started。外部呼び出しゼロ |
| 401・429・500 | 401・429はrejected、500はunknown。自動再試行ゼロ |
| 通信切断・要求開始後の中断 |
unknown。取消済みと断定しない |
| 読めないJSON、実行ID欠落 |
unknown。成功として画面へ返さない |
| Workflow失敗、出力形式不正 |
reconcile_required。実行IDを保持 |
この検証では、認証・認可の実装、保存失敗、同時要求の重複排除、画面のCSRF対策、実環境の切断・停止挙動は未確認です。Dify実機とWebアプリを組み合わせた検証が、本番採用前に別途必要です。
本番へ進めるときの品質ゲートと費用の見方
-
認証結果から
userを導出し、リクエスト本文の利用者IDを採用しない - 生成、実行詳細の参照、停止の各操作で所有者と操作権限を確認する
- Cookie認証ではCSRF対策を用意し、ブラウザへAPIキーを配布しない
- 利用者・組織ごとの要求数と同時実行数を制限する
- 入力・出力契約、禁止入力、応答サイズの上限、内容評価を確認する
- 正常・失敗・中断・結果不明・保存失敗を別の状態として残す
- APIキー更新と緊急停止の手順を、検証環境で確認する
- 操作ID、許可された実行ID、処理段階、所要時間、状態を記録し、全文や秘密情報を通常ログへ出さない
- 保存・再送・切断・停止の結合検証を行い、実行済み操作を重複実行しないか確認する
費用は、推論、Difyの実行枠、バックエンド、ログ、運用対応を分けて見ます。固定の料金や削減率はここでは示しません。まず要求数、トークン使用量、結果不明件数、人による修正時間を実測します。
業務上の効果も「生成できた件数」だけでは判断できません。承認された下書き1件に必要な確認時間と、差し戻し率を計測します。再試行で生成数だけ増えても、推論費用と確認作業が増えれば改善とは言えません。工数効果を出す場合は、同じ難易度の作業について導入前後を比較する必要があります。
次に改善するのは、操作状態の永続化と結果不明時の照合です。その後、実測した待機時間と中断率を見て、同期のまま運用できるか、streamingと永続ジョブが必要かを判断します。
参考リンク
- Dify: Get Started with the Dify API
- Dify: End User Identity
- Dify: Workflow App API
- Dify: Run Workflow
- Dify: Run Workflow by ID
- Dify: Get Workflow Run Detail
- Dify: Handle Errors and Rate Limits
同様の仕組みの設計・構築・運用については相談可能。
この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。
