皆さんこんにちわ、横浜で清掃業を営みながら、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側のbody | body形式をJSONに、Content-Type付与 |
| Failed to parse JSON | bodyの記号ミス | 余計な } や先頭の { 欠けを直す |
| 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では、この日報アプリに音声入力を組み合わせて、移動中でも片手で日報を残せる形に発展させる予定です。最後まで読んでいただき、ありがとうございました。


コメント