本記事はQiita Advent Calender 2025 わたなべの4日目です!
4日目は、私渡邊が作成します!
今回は界隈を騒がせた Vercel Workflowについて書いてみます!
ただ解説するだけでは面白くないので、workflowが活かせそうなアプリとして絵日記のアプリも実装してみます!
![]()
夏休みの絵日記クソだるいわー
今回はアプリ上でのworkflow部分について記述し、フロントへの反映や困った点については別の記事で再度記述します。
記事に入る前のご注意
アプリは実装しているのですが、直近セキュリティ脆弱性で騒がれているRSC(React Server ComponentとNext.js)をがっつり使っている上に、自分のOpenAI API keyもアプリ内で利用しているため、当面アプリの公開は控えたいと思います。
アプリ側のReact, Nextはアップデートしましたが念の為
一応ここにバージョン修正の案内がきており、尚且つ該当するプロジェクトはvercel上にアラート出ているので対応しておいた方が良いかと思います。
Vercel Workflowについて
こちらのURLに説明のあるVercel Workflow。2025年10月の頭くらいに発表されたと思いますが、当時のXを騒がせていました。Workflowという名前の通り、一連の動作を簡単に実装できるものとなります。
1. 代表的な機能
-
ワークフローを可視化
- それぞれのステップがどのように進んでいるか、vercelのダッシュボードなどから確認できます
- 実際に作成したワークフローがどのように進行しているかを、直感的に理解できるのでサクッと実装しやすいです
-
自動リトライ
- 並列に実行していく処理の中でネットワークエラーなどで失敗しても、その部分を自動で再度実行してくれます
- 画像生成に失敗して処理が止まる、といった不慮の事故を大きく減らせます
-
長時間処理・待機
- Next.jsなどをEdge runtimeで動かしていると長時間の処理が苦手ですが、workflowはその欠点を補うように、時間のかかる処理も実行可能です
- 具体的な時間はすみません🙇ちゃんと調べないとなんともいえないのですが、日を跨ぐレベルも大丈夫らしいです
- また、ユーザーの処理を受け取ってから次のフローに進む処理も簡単に実装できます
- 例えば、LLMがドラフトを書いて、それをユーザーが修正して、次に進むなど
![]()
今回のアプリのワークフロー。順次実行したり、待ったり、並列処理したり。
これDifyでの実装はめんどくさいやろ
昨今のAIを利用したアプリケーションの隆盛にフィットした機能といえますが、シンプルにdbとのやり取りなども安定して実施できますので、async / awaitやpromise.allにさらに付加価値がついた感じですね。
やり方はとっても簡単!関数の頭に"use workflow", "use step"と書くだけ!
しかも、workflowはVercelのQueuesの上で動くから、開発者は難しいことなんも考えなくていいぞ!
2. 当然物議を醸したDirectiveとベンダーロックイン
ここでいつものVercel感がしてきました。嫌いな人は嫌うやつですね。
use workflow, use stepなどNext.jsでも見たことある"use client", "use server"の再来ですね。
decoratorとかwrapperとか色々あるだろ!という反応が結構あったように思います。
また、ベンダーロックインも気になるところです。
現在workflow, Queuesともにベータとのことで、無料でも使えますが将来的にはわかりません。workflow自体はオープンソースですが、とはいえ頼りすぎると今後が困るのではという意見も見られました。
次のような記事を発見しました。私の拙い説明より詳しいので、よりわかりやすいかもしれません。
上記、リスクを加味した上で利用する(本番には使わないのかもしれませんが)のが良さそうですね。
一方で、これらvercelの動きを違う視点で分析した記事も発見しました。
まあプロトタイピング的な視点では作って即捨てるので別に構わないんですどね
とりあえず試してみたいので絵日記を作る
このように、界隈で話題になっていたsdkなので試してみたいと思い、活用できそうなアプリケーションを実装してみることにしました。
- 複数フローを繋げる
- ユーザーのインプットを待機する
- 時間のかかる処理を入れる
という視点で考えた結果、絵日記を自動生成するアプリにしてみました。
1. フロー
フローとしては、以下の通り。
1) 箇条書きを送信
ユーザー → 「今日あったこと」を箇条書きで送る
2) AIが文章を下書き
アプリ → AIが絵日記の文章を自動で考え、少しずつ画面に表示
3) ユーザーが文章を直す
ユーザー → 気になるところを手直しして送信
ここでワークフローは入力待ち
4) AIが画像とタイトルを生成
アプリ → 直した文章をもとにAIが絵を作り、タイトルも自動生成
絵の生成とタイトルの生成を並列で実行
5) 完成して保存
アプリ → 文章・画像・タイトルを保存し、一覧や詳細画面で見られるようにする
もう少し厳密に書くと、折りたたみのような感じです
絵日記生成ワークフロー(テキスト図)
ユーザー → 箇条書き入力 → /api/diary/create で Diary 下書き作成+workflow起動
[ユーザー UI (/creation)]
|
| 箇条書きを入力して送信
v
[API /api/diary/create]
|
| Diary下書き保存 + workflowIdでワークフロー開始
v
[workflow: diaryWorkflow]
|
| state=GENERATING に更新
|---------------------------------------------+
| |
| SSEで初稿ストリーム開始 | -> UIが文章を逐次表示
v |
[stepStartStream -> OpenAI(テキスト)] |
| |
| 初稿 text |
| |
v |
[stepUpdateDiary: state=WAITING_USER, content=初稿]
|
| ユーザー修正を待つ (hook)
v
[ユーザー修正送信 -> /api/diary/revise]
|
| revisedBullets を渡して再開
v
[workflow再開: state=DRAWING, content=修正版]
|
| 並列処理:
| A) stepGenerateDiaryImage (OpenAI画像 -> ImageKitアップロード)
| B) stepGenerateDiaryTitle (OpenAIでタイトル生成)
v
[stepUpdateDiary: state=COMPLETED, imageUrl/hasImage, title, content]
|
| UIが /api/workflow/status をポーリング
v
[ダッシュボード/詳細で完成した日記 (画像+タイトル付き) を表示]
これはDifyで実装はめんどくさいやろ
2. 見た目
- 入力欄
![]()
- 左の箇条書きにざっくりと入力して日記を作成ボタンを押す
- AIがドラフトを真ん中の欄に記述するので適宜修正。修正を投稿ボタンが現れるので押す
- 修正した文章を元に画像とタイトルを生成
- 確認欄
![]()
- 文章と生成された絵が見られる(UI見づらいのは諸事情によるのでごめんなさい)
ポイントとなるのは、更新ボタン押下やブラウザシャットダウンなどで落ちてしまっても、ワークフローは継続的に動いており続きから処理を開始できるところですね。
ワークフロー開始時に払い出されるRun IDを管理できていれば、ユーザー入力待ちの状態からでも続きを実行できます
3. 構成
GitHubがこちら。
package.jsonがつらつらと書いてあって長いので、GitHubを貼り付けました。バージョン等気になる方はどうぞ。昨今のRSC騒動にも対応しました
ざっくりのファイル構成は次のような感じです
app/ # Next.js App Router pages & API
page.tsx # ダッシュボード(マイ日記一覧)
creation/ # 日記作成フローUI
diary/[id]/ # 日記詳細
login/ # ログイン画面
api/ # API ルート(diary/create, revise, update, workflow/status など)
components/ # 再利用UIコンポーネント
db/ # Drizzle スキーマ (schema.ts)
lib/ # auth/db/OpenAI/ImageKit/workflow クライアント
workflows/ # ワークフロー定義 (diary-creation など)
steps/ # ワークフロー各ステップ (diarySteps.ts)
public/ # 静的アセット
docs/ # 仕様・タスクなどのドキュメント
このworkflowsフォルダと、stepsフォルダ、そして後述するHooksがポイントですね。
これで先ほどの画像にあったようなフローを実装しています。
![]()
再掲
3.1. workflows
- "use workflow"; が書かれた関数を実行するとworkflowとしてスタートします
- このworkflowが長時間でも生き続ける(deployしなおしても生き続けるらしい)ので、処理を継続しやすいのですね
- 関数名が
diaryWorkFlowとなっています。画像中の長いバーの文言と一致していることがわかります - 今回のアプリでは、下記APIの
await start(diaryWorkflow, [ { workflowId: token, bullets: bullet, userId: user.id }, ]);で起動しています
3.2. steps
- "use step"; と書かれた関数をworkflowの中で実行すると、stepとして記録されます
- 緑色のブロック1つ1つがstepですね(
step関数というらしいです) - 長いブロックの中にある
stepGenerateDiaryImageがコード中の記述と一致していることがわかるかと思います - この
step関数はデフォルトでリトライしてくれるので耐久性に優れているのですね
鋭い方は、それぞれのstep関数はworkflowClientの関数をwrapした形式になっていることに気づかれるかと思います。"use step";は関数のトップレベルの記述する必要があり、類似の処理をする際にこの書き方の方が都合良かったのですね。
- こちらが
workflowClient - workflowの中では通常の
fetchは使えないという制約のため、API Clientとは別に実装しています
3.3. Hooks
workflowで全体が流れるのはわかった。stepでそれぞれを管理できるのもわかった。どうやってユーザーの入力を待つんだ?という疑問に答えるのがHooksです。
![]()
再再掲
画像のところに記述がありますが、hookという外部の入力を受け付ける受け皿のようなものを準備して、そこに値が入ったら先に進むという感じにすることで、ユーザーの入力を待つことができます。
受け皿はこれだけです。これをworkflowとAPIの中で使います。
3.3.1. workflowの中で準備して待つ
24行目、const userEditedPromise = userEditedHookClient.create({ token: workflowId }); これが受け皿の準備です。
49行目、const { revisedBullets } = await userEditedPromise;ここで、受け皿のuserEditedPromiseが値を受け取ると、受け取った値(今回は再編集した日記の文章)をそれ以降のworkflowで利用できます。
3.3.2. 受け皿に渡す
受け皿にはAPI経由でフロントから値を渡します。
34-36行目await userEditedHookClient.resume(workflowId, { revisedBullets, });。先ほどのHookをresumeし、workflow開始時に発行されたIDと、渡したい値(今回は修正した日記の文章)を渡すことで、ワークフローが再開されます。
全体の流れとしては以上のようになります。
やっぱりこれは、Difyでは厳しい感じするやろ
残り
書いていたら記事がめちゃくちゃ長くなってきたので一旦区切ります!
1週間後の記事で
- ワークフローの結果をフロントにどのように反映しているのか
- ワークフローを記述する際の注意点
- ワークフロー以外の技術選定の理由
についても記事を出したいと思います!
また、お読みいただけると嬉しいです!
余談
ReactとNextアップデートしたら全く生成されなくなった・・・
終わったと思ったところ、
こういうこともあるので、なんでも人のせいにするのは良くないですね。
余談2
Q. 筆者はDify嫌いなの?
A. そんなことはないですし、ハッカソンではよく使ってます。ただ、読み方を変えたことは許してないです。




