【Dify実践】作業日報まとめくんを作る|エラー6連発を乗り越えてスプレッドシート自動記録までたどり着いた話

DifyとGASを連携させて作業日報をGoogleスプレッドシートへ自動記録する仕組みのイメージ図。左側に「日報データベース」と書かれたスプレッドシートの画面、中央に「正規表現処理」「JSON変換」「DIFYワークフロー」「GAS自動化」という処理プロセスの光るキューブ、右側にスマートフォンを持つ作業服姿の男性が描かれています。上部には「【Dify実践】作業日報まとめくんを作る|エラー6連発を乗り越えてスプレッドシート自動記録までたどり着いた話」というブログ記事タイトルが入っています。 Difyワークフロー

皆さんこんにちわ、横浜で清掃業を営みながら、IT未経験の状態からDifyを学んでいる49歳のYasuです。このブログでは、私が実際に手を動かしてつまずいたこと、解決できたことを、できるだけ正直に記録しています。

前回はキーワードを入れるだけ!Jina AIで競合を並列収集→差別化記事構成→本文全文生成→WordPress自動投稿→SNS投稿文まで全自動化

のアプリを作りましたね。

今回は趣向を変えて、毎日の業務でそのまま使える「作業日報まとめくん」を作りました。

やることはシンプルです。その日の作業をスマホに箇条書きで放り込むと、AIが整った日報・気づき・明日のToDoに整形し、さらにGoogleスプレッドシートへ1行ずつ自動で記録してくれる。たった4ブロックの小さなアプリです。ところが、この4ブロックを「ok」と言わせるまでに、私は6種類ものエラーを踏みました。今回はその格闘の記録を、エラーメッセージの読み解き方ごと、まるごとお見せします。同じ場所でつまずいた方が、ログのどこを見ればいいか分かるようになれば嬉しいです。

完成イメージ

まず、何ができあがったのかをお見せします。入力するのは、こんな雑なメモです。

今日やったこと

・田中様宅 エアコン2台クリーニング

・午後から山田ビル 床ワックスがけ

・新人の佐藤くん同行 まだ手順あやしい

・洗剤の在庫少なくなってきた

これを入れると、日報・気づき・明日のToDoに整形され、同時にスプレッドシートへ日時つきで1行追加されます。毎日続ければ、立派な業務記録が自動でたまっていく仕組みです。

アプリの全体像(4ブロック構成)

中身はとてもシンプルで、4つのブロックを縦に並べるだけです。前回までの14ブロック構成に比べたら拍子抜けするほど簡単……のはずでした。

ブロック役割
開始メモを貼り付ける入力欄
LLM日報・気づき・ToDoに整形
コード実行JSONを正規表現で個別抽出
HTTPリクエストGAS経由でスプレッドシートへ記録

① 開始ブロック:メモを受け取る入口

入力フィールドを1つ作るだけです。種類は必ず「段落(複数行)」を選びます。1行だけの欄では箇条書きを何個も入れられないからです。変数名は英語の小文字でmemoとしました。

💡 実践のヒント: 変数名は後のブロックで呼び出すときに使います。日本語にすると参照でつまずきやすいので、memoのような半角英字にしておきましょう。

② LLMブロック:メモを日報に整える頭脳

アプリの心臓です。memoを受け取り、日報・気づき・ToDoの3つに整えてJSONで返してもらいます。プロンプトは私の技術ルール(バッククォートなし・1行JSON)に合わせ、役割の具体化や思考ステップの明示など、プロンプトエンジニアリングの定石を盛り込みました。全文は後半に掲載します。

💡 実践のヒント: 最初は欲張らず、日報だけを生成させて動作確認しましょう。動いたら気づき、次にToDoと1つずつ増やすと、エラーの原因が特定しやすくなります。

③ コード実行ブロック:JSONをバラす作業台

②がまとめて返すJSONを、3つの値に切り分けます。json.loads()は文章に記号が混じると壊れやすいので、私のルールどおり正規表現で個別抽出します。

💡 実践のヒント: returnは必ず1行にまとめます。複数行のreturnはコード実行ブロックでエラーになりやすいので、最初から1行で書く癖をつけると安全です。

④ HTTPリクエストブロック:スプレッドシートへ送る郵便屋

Difyから直接スプレッドシートには書けないので、間にGAS(Google Apps Script)を受付窓口として置きます。#30の経費精算アプリと同じ方式です。bodyは完全1行・スペースなし、ヘッダーにContent-Typeを付けるのが鉄則です。

💡 実践のヒント: GASを修正したら必ず「デプロイを更新」で反映します。「新しいデプロイ」でやり直すとURLが変わり、Dify側も貼り替えないと届かなくなります。

ここからが本番:踏んだエラー6連発

ここまで読むと簡単そうですよね。私もそう思っていました。ところが、つなげてテストするたびに違うエラーが出る。結局6種類踏みました。でも、これこそが一番の学びでした。エラーメッセージは敵ではなく、原因を教えてくれる案内人だったのです。順番に振り返ります。

エラー① 出力が {} になる(Output result is missing)

最初の壁です。③コード実行ブロックの出力が空の{}になりました。原因は、コードがnippou・kizuki・todoの3つを返しているのに、Dify側の出力変数がresult1つしか設定されていなかったこと。Difyは『returnした辞書のキー』と『出力変数の名前』が一致したものだけを受け取ります。名前が違えば、当然空っぽです。

解決策は、出力変数を削除して、nippou・kizuki・todoの3つを(すべてString型で)作り直すこと。コードのキーと一字一句そろえるのが肝でした。

💡 実践のヒント: コード実行ブロックで出力が空のときは、まず「returnのキー名」と「出力変数の名前」が完全一致しているか見比べましょう。9割はここです。

エラー② Cannot read properties of undefined (reading ‘contents’)

③が直り、④まで届くようになって出たエラーです。これはGAS側が、送られてきたはずのデータの中身(e.postData.contents)を読もうとしたら空っぽだった、という意味でした。つまりDifyからbody(中身)が届いていない。HTTPリクエストのbody形式がJSONになっていなかったのと、Content-Typeヘッダーが抜けていたのが原因でした。

💡 実践のヒント: undefinedの’contents’が読めないと出たら、送る側(Dify)のbodyが空だと疑いましょう。エラーが出る場所(GAS)と原因の場所(Dify)が違うのが、初心者がハマるポイントです。

エラー③ Failed to parse JSON:本文の末尾に余計な } が混入

次は、bodyテンプレートに直接書いてしまった余計な}が原因でした。変数の直後に}”と打ってしまっていて、送られるJSONの形が壊れていたのです。私は最初これをLLMの出力のせいだと思い込み、ずいぶん遠回りしました。実際は④HTTPリクエストのbody欄の打ち間違いでした。

💡 実践のヒント: HTTPリクエストのbodyは、変数の前後に余計な記号を付けないこと。正しい形は {“key”:”変数”,…} で、各変数の直後に来るのは次のキーへつなぐ “,”next”:” だけです。

エラー④ Failed to parse JSON:今度は先頭の {“nippou”:” が欠けた

③を直そうとbodyを打ち直したら、今度は逆に先頭のひらき{“nippou”:”が抜けてしまいました。JSONは{で始まらないとパースできないので、またエラーです。bodyを手打ちで直すときは、先頭のひらきと末尾の閉じの両方を、毎回必ず確認する必要があると痛感しました。

💡 実践のヒント: body全体は {“nippou”:”…”,”kizuki”:”…”,”todo”:”…”} の形。先頭の { と末尾の } が両方そろっているか、打ち直すたびに目視確認しましょう。

エラー⑤ Cannot read properties of null (reading ‘getSheets’)

bodyが通り、GASのJSON.parseも抜けて、ついにスプレッドシート処理まで到達。しかし今度はgetSheetsがnullで読めないと出ました。これはgetActiveSpreadsheet()がスプレッドシートを見つけられなかったという意味です。

そして真相は……記録先のスプレッドシートを、私はまだ作っていませんでした。開くべきシートが存在しないのだから、nullになるのは当然です。

💡 実践のヒント: nullで’getSheets’が読めないと出たら、スプレッドシート自体の存在と、GASがそのシートに紐づいているかを確認しましょう。シートの「拡張機能→Apps Script」から作ると確実に紐づきます。

エラー⑥(番外)スプレッドシートIDの指定

シートを新規作成し、IDで直接指定するopenById方式に切り替えました。IDはURLの/d/と/editの間の長い文字列です。末尾のスラッシュは含めず、必ずダブルクォートで囲むのがポイントでした。初めてopenByIdを使うときは権限の承認画面が出るので、許可を押す必要があります。

💡 実践のヒント: スプレッドシートIDは https://docs.google.com/spreadsheets/d/【ここがID】/edit の中央部分。末尾の / や ?gid=0 は含めません。

そして {“result”:”ok”} が出た

6つのエラーを一つずつ潰し、最後にテスト実行したら、待ち望んだ{“result”:”ok”}が返ってきました。スプレッドシートを開くと、見出し行の下に、日時つきで日報・気づき・ToDoがきれいに1行入っていました。声が出るほど嬉しい瞬間でした。

エラーメッセージ早見表(保存版)

今回いちばんお伝えしたいのが、この対応表です。エラーメッセージの文言から原因の場所がわかれば、解決はぐっと早くなります。私が今回学んだ対応関係をまとめました。

エラーの文言原因の場所対処
Output result is missing / {}コード実行の出力変数returnのキーと出力変数名を一致させる
undefined (reading ‘contents’)Dify側のbodybody形式をJSONに、Content-Type付与
Failed to parse JSONbodyの記号ミス余計な } や先頭の { 欠けを直す
null (reading ‘getSheets’)スプレッドシートシートを作成しGASを紐づける

💡 実践のヒント: エラーが出る場所と、原因がある場所は別のことが多いです。「どこでエラーが出たか」より「何を読もうとして失敗したか」を読むと、原因の場所が見えてきます。

プロンプト全文(②LLM 日報整形)

コピーしてそのまま使えるよう、②LLMのプロンプトを全文掲載します。

# 役割

あなたは横浜で清掃業を営む会社の現場管理者です。毎日の作業メモを、

経営者やスタッフが後から見返せる「実用的な業務日報」に整える専門家として

振る舞ってください。

# 入力(その日の作業メモ)

{{#開始.memo#}}

# 思考ステップ(この順で考えること)

1. メモから「実施した作業」「関わった人」「使った資材」「気になった点」を仕分ける

2. 実施作業を時系列または重要度順に並べ、日報本文にまとめる

3. メモ内の問題・非効率・リスクを「気づき」として抽出する

4. 気づきや未完了事項から、明日の具体的なToDoを導き出す

# 各項目の作成ルール

・nippou:事実ベースで「だ・である」調、300字以内目安、担当者名と場所を残す

・kizuki:改善点・課題を1〜3点、なぜ問題かを一言添える

・todo:実行可能な具体アクション(「確認する」より「◯◯に発注する」)

# 制約

・メモにない情報を創作しないこと

・各値の中に波カッコ{ }やダブルクォート”を絶対に含めないこと

# 出力形式(厳守)

・下記JSONを「1行」で出力すること

・バッククォート(“`)や前置き・後書きを一切付けないこと

・キーの順序・名称を変えないこと

{“nippou”:”日報本文”,”kizuki”:”気づき”,”todo”:”明日のToDo”}

コード全文(③コード実行)

import re

def main(arg1: str) -> dict:

nippou = re.search(r'”nippou”:”(.*?)”‘, arg1)

kizuki = re.search(r'”kizuki”:”(.*?)”‘, arg1)

todo = re.search(r'”todo”:”(.*?)”‘, arg1)

return {“nippou”: nippou.group(1) if nippou else “”, “kizuki”: kizuki.group(1) if kizuki else “”, “todo”: todo.group(1) if todo else “”}

コード全文(④連携先のGAS)

function doPost(e) {

try {

if (!e || !e.postData || !e.postData.contents) {

return ContentService.createTextOutput(

JSON.stringify({result: “error”, message: “bodyが届いていません”})

).setMimeType(ContentService.MimeType.JSON);

}

const data = JSON.parse(e.postData.contents);

const ss = SpreadsheetApp.openById(“あなたのスプレッドシートID”);

const sheet = ss.getSheets()[0];

if (sheet.getLastRow() === 0) {

sheet.appendRow([“日時”, “日報”, “気づき”, “明日のToDo”]);

}

const now = Utilities.formatDate(new Date(), “Asia/Tokyo”, “yyyy/MM/dd HH:mm”);

sheet.appendRow([now, data.nippou || “”, data.kizuki || “”, data.todo || “”]);

return ContentService.createTextOutput(

JSON.stringify({result: “ok”})

).setMimeType(ContentService.MimeType.JSON);

} catch (error) {

return ContentService.createTextOutput(

JSON.stringify({result: “error”, message: String(error)})

).setMimeType(ContentService.MimeType.JSON);

}

}

テスト結果のサンプル

冒頭のメモを実際に流した結果、スプレッドシートにはこう記録されました(抜粋)。

日時:2026/06/09 20:19

日報:本日は田中様宅にてエアコン2台のクリーニングを実施。

   午後から山田ビルで床ワックスがけを行った。新人の佐藤を同行させ手順を指導した。

気づき:佐藤の作業手順が不安定であるため、さらなる指導が必要。洗剤の在庫が少なく早急に補充が必要。

ToDo:洗剤を発注する、佐藤への手順再指導を計画する、山田ビルの仕上がりを確認する

雑なメモが、後から見返せる業務記録に変わりました。狙いどおりです。

まとめ

今回の最大の学びは、エラーは怖くない、ということでした。6回もつまずきましたが、エラーメッセージは毎回ちゃんと『どこで・何を読もうとして失敗したか』を教えてくれていました。undefinedの’contents’なら送る側の問題、nullの’getSheets’ならシートが無い——文言と原因がつながると、解決のスピードがまるで変わります。

IT未経験から始めた私でも、4ブロックの小さなアプリを、エラーを一つずつ潰しながら完成まで持っていけました。大事なのは、エラーで止まったときに『どこを見ればいいか』を知っていること。今回の早見表が、同じ場所でつまずいた誰かの助けになれば嬉しいです。次回#35では、この日報アプリに音声入力を組み合わせて、移動中でも片手で日報を残せる形に発展させる予定です。最後まで読んでいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました