前回の記事では、LINEに送った現場報告をn8n Cloud経由でkintoneに登録し、受付番号をLINEに返す仕組みを作りました。今回はその続きとして、写真付きの報告に対応します。

現場報告では、「どこが壊れているか」を文章で説明するより、写真を1枚送ってもらった方が早く正確に伝わります。ただし、LINEで写真を扱うには、テキストだけのときにはなかった2つの壁があります。

この記事では、画像を取得してkintoneの添付ファイルフィールドに保存する方法と、「別々に届くテキストと写真を、どうやって同じ報告にまとめるか」の考え方を、n8nのワークフローと合わせて解説します。前回のワークフローを作ってあることが前提です。

■完成形:報告を送ってから写真を送ると、同じレコードに添付される

使い方は次のとおりです。まず前回と同じように、1行目に拠点、2行目以降に報告内容を書いて送ります。

中央営業所
エアコンから異音あり
至急確認お願いします

返信の最後に、写真を送るよう案内を追加しました。

報告を受け付けました。
受付番号 #1
拠点:中央営業所

写真がある場合は、10分以内に続けて送ってください。

続けて写真を送ると、その写真が受付番号 #1 のレコードの「写真」フィールドに添付され、LINEに次の返信が届きます。複数枚をまとめて送った場合も、すべて同じレコードに追加されます。

写真を1枚受け付けました。
受付番号 #1

報告のテキストを送らずに写真だけを送った場合は、拠点「未入力」、報告内容「(写真のみの報告)」のレコードを新しく作って、そこに写真を添付します。写真が行き場を失って消えることはありません。

■LINEの仕様:写真は「IDだけ」が、テキストとは別に届く

まず、LINEから写真がどのように届くかを確認しておきます。ユーザーが写真を送ると、Webhookには次のような画像メッセージのイベントが届きます(必要な部分だけ抜粋)。

{
  "type": "message",
  "replyToken": "7840b71058e24a5d91f9b5726c7512c9",
  "source": { "type": "user", "userId": "U4af49806..." },
  "message": {
    "type": "image",
    "id": "354718705033693859",
    "contentProvider": { "type": "line" },
    "imageSet": {
      "id": "E005D41A7288F41B65593ED38FF6E983...",
      "index": 1,
      "total": 2
    }
  }
}

前回扱ったテキストメッセージ("type": "text")とはmessage.typeが違うだけで、同じ形のイベントです。ここから、写真を扱ううえで押さえておくべきLINEの仕様が4つ見えてきます。

仕様

ワークフローへの影響

Webhookには画像そのものは入っておらず、メッセージID(message.id)だけが届く

メッセージIDを使って「コンテンツを取得する」APIを呼び、画像のバイナリを取りにいく必要がある

テキストと写真は、それぞれ別のメッセージイベントになる

「どの写真がどの報告のものか」は、LINEは教えてくれない。ワークフロー側で決める必要がある

複数の画像を同時に送ると、基本は画像ごとに別のWebhookが届く(2枚が1つのWebhookにまとまることもある)。届く順序は不定

同じ報告への写真の追加が、ほぼ同時に何本も走る。後から届いた処理が、先の処理の結果を上書きしないようにする必要がある

画像の提供元(contentProvider.type)がexternalの場合は、コンテンツを取得するAPIで取得できない

lineのものだけを処理する

imageSetは、複数の画像を同時に送ったときだけ含まれる項目です。indexは何枚目か(1から始まる)、totalは同時に送った枚数を表します。ただし、送信元がAndroid版LINE 11.15以前の場合はindexとtotalが含まれません。また、LINEの公式リファレンスにもあるとおり、Webhookはindexの順に届くとは限りません。

実際に筆者のスマホ(2026年10月15日)から、アルバムで3枚を選んでまとめて送る操作を3回試すと、届き方は毎回少しずつ違いました。

試した内容

Webhookの届き方

n8nに届いたimageSet.indexの順

1回目:写真3枚

3本の別々のリクエスト(各1イベント)。約0.3秒間隔

2→1→3

2回目:報告の後に写真3枚

3本の別々のリクエスト(各1イベント)。約0.4秒間隔

1→2→3

3回目:写真だけ3枚

2本のリクエスト。1本目に1枚、2本目に2枚分のイベント

1→(2と3が同時)

どの回も、3枚は同じimageSet.idとtotal: 3を持っていました。一方、1枚だけ送った写真にはimageSetが付いていませんでした。

つまり「1枚目が先に届くはず」「1枚ずつ別のWebhookで届くはず」という前提は、どちらも置けません。また、写真ごとの処理(画像の取得・アップロード・保存)には数秒かかるので、0.3秒間隔で届けば処理は同時に走ります。この3つを前提に設計します。

もうひとつ、取得できる期間にも注意が必要です。ユーザーが送ったコンテンツは、送信から一定期間が過ぎるとLINEのサーバーから自動的に削除され、保存期間は保証されていません。「あとでまとめて取りにいく」のではなく、Webhookを受けたその場で取得してkintoneに保存します。

■設計:写真は「同じ人の直近10分の報告」に追加する

テキストと写真が別々に届く以上、どこかで両者をひも付けるルールが必要です。今回は、次のルールにしました。

  1. 写真が届いたら、同じLINEユーザーが直近10分以内に登録した、いちばん新しいレコードを探し、その「写真」フィールドに追加する
  2. 見つからなければ、「写真のみの報告」としてレコードを新しく作り、そこに添付する
  3. 同じレコードへの追加が同時に走ったら、kintoneのリビジョンで衝突を検知し、少し待ってからやり直す
  4. 複数枚をまとめて送った写真で、まだレコードがない場合は、1枚目(imageSet.indexが1)がレコードを作り、2枚目以降はそれを待ってから追加する

テキストと写真を1回の処理でまとめて受け取れれば簡単ですが、LINEはそうした形では送ってくれません。n8nの1回の実行の中で「後から来る写真」を待ち続けるのも現実的ではありません。そこで、ひも付けの基準をkintone側のデータ(送信者と登録日時)に置くことにしました。毎回kintoneを検索すればよいので、n8nの側で状態を持つ必要がありません。

「10分」は運用に合わせて変えてかまいません。短すぎると写真が別のレコードになりやすく、長すぎると前の報告に次の報告の写真が紛れ込みやすくなります。返信メッセージで「10分以内に続けて送ってください」と案内しているのは、このルールを現場の人に伝えるためです。

3と4が必要な理由:何もしないと写真が消える

3枚の写真をまとめて送ると、LINEからは多くの場合3本のWebhookがほぼ同時に届き、n8n Cloudでは3つの実行が並行して動きます。kintoneの添付ファイルフィールドを更新するときは、残したい既存のファイルもすべて指定する必要があり、指定しなかったファイルはフィールドから削除されます。そのため、何も対策をしないと、次のようなことが起きます。

順番

処理A(写真1)

処理B(写真2)

kintoneのレコード

1

レコードを読む(写真0枚)

レコードを読む(写真0枚)

写真0枚

2

「既存0枚+写真1」で更新

写真1

3

「既存0枚+写真2」で更新

写真2だけ(写真1が消える)

これを防ぐのがリビジョンです。kintoneのレコードには更新のたびに1ずつ増えるリビジョン番号があり、レコードを更新するAPIでrevisionを指定すると、実際のリビジョンと一致しない場合はエラーになり、レコードは更新されません。上の例では、処理Bが順番3でエラーになるので、処理Bはレコードを読み直し、「写真1+写真2」で更新し直します。

4は、写真だけを複数枚送ったときのための工夫です。まだレコードがない状態で3つの処理がそれぞれ「見つからないので新規登録」を行うと、写真1枚ずつのレコードが3件できてしまいます。そこで、新規登録はimageSet.indexが1の写真だけが行い、2枚目以降は少し待ってから検索し直すようにしました。

■費用への影響:写真1枚ごとに実行回数を1回使う

前回と同じく、LINEへの返信は応答メッセージ(Reply API)なので、LINEの無料メッセージ通数は消費しません。一方、n8n Cloudの実行回数には注意が必要です。複数の画像は基本的に画像ごとに別のWebhookで届くため、テキスト1件+写真3枚の報告は、n8nでは最大4回の実行になります。前回紹介したStarterプラン(月2,500回まで)で運用する場合は、写真の枚数も含めて実行回数を見積もってください。

また、添付した写真はkintoneのディスク容量を使います。LINEから取得できる画像は、内部で縮小などの変換が行われている場合があり、スマホで撮った元のファイルより小さくなることがあります。

■手順1:kintoneアプリに「写真」フィールドを追加する

前回作った「現場報告」アプリに、添付ファイルフィールドを1つ追加します。

フィールド名

フィールドの種類

フィールドコード

入る値

写真

添付ファイル

photos

LINEから送られた写真

直近の報告を探すときは、kintoneの標準の「作成日時」フィールドを使います。新しく作ったアプリでは、作成日時フィールドのフィールドコードは初期値の「作成日時」になっています。フィールド名を変えていても、フィールドコードが「作成日時」のままか、アプリの設定画面で確認しておいてください。

次に、APIトークンの権限を見直します。前回は「レコード追加」だけにしていましたが、今回は既存のレコードを探して写真を追加するため、次の3つが必要です。

権限

使う場面

レコード閲覧

直近の報告を探す

レコード追加

報告や「写真のみの報告」を登録する

レコード編集

既存のレコードに写真を追加する

権限を変えたら[保存]を押し、さらに[アプリを更新]を押すまで反映されない点は前回と同じです。閲覧権限を付けたことで、トークンが漏れた場合はレコードの内容を読まれるおそれが出てきます。トークンはn8nの認証情報だけに保存し、ほかの場所には貼り付けないようにしてください。

なお、ファイルをkintoneにアップロードするAPI自体には、APIトークンの特別な権限は要りません。アップロードしたファイルをレコードに添付するときに、レコード追加・編集の権限(とフィールドの編集権限)が必要になります。

■手順2:ワークフローの全体像

前回のワークフローを複製して改造するのがおすすめです。n8n Cloudのワークフロー一覧で前回のワークフローの[…]→[Duplicate]を選ぶと、同じ内容のコピーが作られます。最初の3つのノード(LINE Webhook/署名を計算/署名は一致?)はそのまま使い、その後ろを次のように組み替えます。

[LINE Webhook] → [署名を計算] → [署名は一致?] → [イベントを振り分ける] → [テキスト?]
                                                                              │
  ┌─ true(テキスト)────────────────────────────────────────────────────────┘
  │   [kintoneに登録] → [LINEに返信]
  │
  └─ false(写真)
      [画像を取得] → [ファイル名を付ける] → [kintoneにアップロード] → [写真をまとめる]
        → [検索の準備] → [直近の報告を探す] → [保存先を決める] → [保存できる?]
             ↑                                                      │
             │                         true → [kintoneに保存] ─成功→ [LINEに写真の受付を返信]
             │                                     │エラー
             │                         false ──────┴──→ [少し待つ] → [再試行の準備]
             └──────────────────────────────────────────────────────────────┘

写真の処理は、大きく3つの段階に分かれます。

段階

ノード

やること

写真をkintoneに預ける

画像を取得/ファイル名を付ける/kintoneにアップロード

LINEから画像を取得し、kintoneの一時保管領域にアップロードしてfileKeyを受け取る

添付先を決める

写真をまとめる/検索の準備/直近の報告を探す/保存先を決める/保存できる?

直近の報告を探し、既存のレコードに追加するか、新規登録するかを決める

保存してやり直す

kintoneに保存/少し待つ/再試行の準備/LINEに写真の受付を返信

レコードに保存する。衝突したら少し待って「検索の準備」からやり直す

認証情報は前回の3つ(LINE Channel Secret/kintone API Token/LINE Channel Access Token)をそのまま使います。新しく作る必要はありません。

■手順3:イベントを振り分ける(Code)

前回の「報告内容を取り出す」ノードを、次のコードに置き換えます(ノード名も「イベントを振り分ける」に変えています)。テキストのイベントは前回と同じく拠点と報告内容を取り出し、画像のイベントはメッセージIDとimageSetの情報を取り出します。それぞれにkind(textまたはimage)を付けておき、次のIFノードで振り分けます。

// 1回のWebhookに複数のイベントが入ることがある。
// テキストと画像は、それぞれ別のメッセージイベントとして届く。
const events = $input.first().json.body?.events ?? [];
const out = [];

for (const ev of events) {
  if (ev.type !== 'message') continue;
  const base = {
    replyToken: ev.replyToken,
    userId: ev.source?.userId ?? '',
    messageId: ev.message.id,
  };

  if (ev.message.type === 'text') {
    const lines = ev.message.text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
    const site = lines.length > 1 ? lines[0] : '未入力';
    const report = lines.length > 1 ? lines.slice(1).join('\n') : lines[0];
    out.push({ json: { ...base, kind: 'text', site, report } });
  }

  // 画像は、LINEのサーバーに保存されたもの(contentProvider.type が line)だけ取得できる
  if (ev.message.type === 'image' && ev.message.contentProvider?.type === 'line') {
    out.push({
      json: {
        ...base,
        kind: 'image',
        // 複数枚をまとめて送ったときだけ付く。index は1始まり(古いAndroid版では付かない)
        imageSetId: ev.message.imageSet?.id ?? '',
        imageIndex: ev.message.imageSet?.index ?? 0,
      },
    });
  }
}

return out;

ここでcontentProvider.typeがlineの画像だけを残しているのは、externalの画像はコンテンツを取得するAPIで取得できないためです。スタンプや動画なども、ここで取り除かれます。

続く「テキスト?」はIFノードで、条件は{{ $json.kind }} is equal to text です。trueの出口にテキストの処理を、falseの出口に写真の処理をつなぎます。

■手順4:テキストの報告(前回とほぼ同じ)

「kintoneに登録」ノードは前回のまま使えます。「LINEに返信」ノードは、返信トークンと拠点の参照先を、削除した「報告内容を取り出す」から「テキスト?」に変え、返信の最後に写真の案内を足します。

{{ JSON.stringify({
  replyToken: $('テキスト?').item.json.replyToken,
  messages: [{
    type: 'text',
    text: '報告を受け付けました。\n受付番号 #' + $json.id + '\n拠点:' + $('テキスト?').item.json.site
      + '\n\n写真がある場合は、10分以内に続けて送ってください。'
  }]
}) }}

参照先のノード名を変え忘れると、存在しないノードを参照するエラーになります。ノード名を変えたときは、そのノードを$('…')で参照している式もあわせて直してください。

■手順5:画像を取得して、kintoneにアップロードする

画像を取得(HTTP Request)

設定項目

値

Method

GET

URL

https://api-data.line.me/v2/bot/message/{{ $json.messageId }}/content

Authentication

Generic Credential Type → Header Auth → LINE Channel Access Token

Options → Response → Response Format

File

Options → Response → Put Output in Field

photo

いちばん間違えやすいのがドメイン名です。返信に使ったapi.line.meではなく、api-data.line.meです。LINEの公式リファレンスでも、大容量データの送受信用に他のエンドポイントとはドメイン名が異なると注意書きがあります。認証は返信と同じチャネルアクセストークンで、Authorization: Bearer …のヘッダーをそのまま使えます。

このAPIは、成功するとステータスコード200と画像のバイナリデータを返します。Response Formatを「File」にすると、n8nはレスポンスをバイナリデータとして受け取り、Put Output in Fieldで指定したphotoという名前で後続のノードに渡します。

ファイル名を付ける(Code)

LINEから取得した画像には、ファイル名が付いていません。このままkintoneにアップロードすると、添付ファイルの名前が分かりにくくなるので、メッセージIDからファイル名を付けておきます。ModeはRun Once for Each Itemにします。

// LINEから取得した画像にはファイル名が付いていないので、メッセージIDから付ける
const bin = $input.item.binary.photo;
const ext = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif' }[bin.mimeType] ?? 'jpg';
bin.fileName = `line_${$json.messageId}.${ext}`;
bin.fileExtension = ext;
return $input.item;

画像の形式はレスポンスのContent-Typeヘッダーで示され、n8nはそれをバイナリデータのmimeTypeに入れています。拡張子はそこから決めています。

kintoneにアップロード(HTTP Request)

設定項目

値

Method

POST

URL

https://(サブドメイン).cybozu.com/k/v1/file.json

Authentication

Generic Credential Type → Header Auth → kintone API Token

Send Body

ON

Body Content Type

Form-Data

Body Parameters

Parameter Type:n8n Binary File/Name:file/Input Data Field Name:photo

kintoneのファイルアップロードAPIは、multipart/form-data形式で、パラメータ名をfileにして送る決まりです。Nameをphotoなどにしてしまうとエラーになります。成功すると、次のようなfileKeyが返ってきます。

{ "fileKey": "c15b3870-7505-4ab6-9d8d-b9bdbc74f5d6" }

この時点では、ファイルはまだどのレコードにも添付されておらず、kintoneの一時保管領域に置かれているだけです。レコードに添付されないまま3日たつと削除されます。このfileKeyを、次の段階でレコードの「写真」フィールドに指定します。

Content-Typeヘッダーを自分で追加する必要はありません。Form-Dataを選ぶと、n8nが区切り文字(boundary)付きのヘッダーを自動で付けます。手でmultipart/form-dataとだけ書いたヘッダーを足すと、区切り文字が欠けてアップロードに失敗する原因になります。

■手順6:添付先のレコードを探す

写真をまとめる(Code)

1回のWebhookに複数の画像イベントが入っていた場合に備えて、同じ実行に届いた写真を送信者ごとに1つにまとめます。前述のとおり、筆者の検証でも2枚分のイベントが1回のWebhookで届くことがありました。画像ごとに別のWebhookで届いた場合は、写真1枚がそのまま1件になります。ModeはRun Once for All Itemsです。

// 同じ実行に届いた写真を、送信者ごとに1件にまとめる
const groups = {};

$input.all().forEach((item, i) => {
  const src = $('ファイル名を付ける').itemMatching(i).json;
  const g = groups[src.userId] ??= {
    userId: src.userId,
    replyToken: src.replyToken,
    fileKeys: [],
    minIndex: Infinity,
    pairedItem: [],
  };
  g.fileKeys.push(item.json.fileKey);
  // imageSetの番号のうち、いちばん小さいもの(imageSetがなければ0)
  g.minIndex = Math.min(g.minIndex, src.imageIndex);
  g.pairedItem.push({ item: i });
});

return Object.values(groups).map(({ pairedItem, ...g }) => ({
  json: { ...g, retry: 0 },
  pairedItem,
}));

minIndexは、まとめた写真のimageSet.indexのうちいちばん小さい値です。imageSetがない写真(1枚だけ送った写真など)は0になります。この値を、後で「新規登録してよいか、1枚目を待つか」の判断に使います。retryはやり直しの回数で、最初は0です。

検索の準備(Code)

直近の報告を探すための、kintoneのクエリを作ります。ModeはRun Once for Each Itemです。

// 直近10分以内に、同じ人が登録したレコードを探す条件を作る
const since = new Date(Date.now() - 10 * 60 * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z');
return {
  json: {
    ...$json,
    query: `line_user_id = "${$json.userId}" and 作成日時 >= "${since}" order by $id desc limit 1`,
  },
};

このノードは、最初の検索と、やり直しのときの検索の両方の入り口になります(後で「再試行の準備」からこのノードに線をつなぎます)。検索のたびに「今から10分前」を計算し直すので、やり直しのときも条件がずれません。

日時はUTCの2026-10-11T01:23:45Zのような形で指定しています。JavaScriptのtoISOString()はミリ秒まで出力するので、ミリ秒の部分は取り除いています。

なお、kintoneの作成日時は分単位で記録されます(筆者のアプリでは2026-10-15T05:39:00Zのように秒が00になっていました)。そのため「10分」は厳密な10分ではなく、1分弱の幅があります。

直近の報告を探す(HTTP Request)

設定項目

値

Method

GET

URL

https://(サブドメイン).cybozu.com/k/v1/records.json

Authentication

Generic Credential Type → Header Auth → kintone API Token

Send Query Parameters

ON

Query Parametersは次の5つです。

Name

Value

app

123(アプリID)

query

{{ $json.query }}

fields[0]

$id

fields[1]

$revision

fields[2]

photos

実際に送られるクエリは、次のようになります。

line_user_id = "U4af49806..." and 作成日時 >= "2026-10-11T01:13:45Z" order by $id desc limit 1

「同じLINEユーザー」かつ「10分前以降に作成」のレコードを、レコードIDの大きい順(新しい順)に並べて、先頭の1件だけを取り出しています。fieldsで、レコードID・リビジョン・写真フィールドの3つだけを取得するように絞っています。

■手順7:既存のレコードに追加するか、新規登録するかを決める

保存先を決める(Code)

このワークフローの中心になるノードです。ModeはRun Once for Each Itemにします。appの123は、自分のアプリIDに置き換えてください。

const req = $('検索の準備').item.json;
const found = $json.records?.[0];
const newFiles = req.fileKeys.map(fileKey => ({ fileKey }));

if (found) {
  // 直近の報告に追加する。既存のファイルも指定しないと消えてしまう
  const existing = found.photos.value.map(f => ({ fileKey: f.fileKey }));
  return {
    json: {
      ...req,
      action: 'save',
      method: 'PUT',
      recordId: found.$id.value,
      body: {
        app: 123,
        id: found.$id.value,
        revision: found.$revision.value, // 他の処理が先に更新していたらエラーになる
        record: { photos: { value: [...existing, ...newFiles] } },
      },
    },
  };
}

// 直近の報告がない。複数枚まとめて送られた2枚目以降は、1枚目がレコードを作るのを待つ
if (req.minIndex > 1 && req.retry < 3) {
  return { json: { ...req, action: 'wait' } };
}

// 写真だけの報告として新規登録する
return {
  json: {
    ...req,
    action: 'save',
    method: 'POST',
    body: {
      app: 123,
      record: {
        site: { value: '未入力' },
        report: { value: '(写真のみの報告)' },
        line_user_id: { value: req.userId },
        photos: { value: newFiles },
      },
    },
  },
};

判断は3通りです。

状況

結果

直近10分の報告が見つかった

そのレコードを更新(PUT)。既存の写真のfileKey+新しい写真のfileKeyを指定し、revisionも付ける

見つからず、複数枚まとめて送られた写真の2枚目以降(minIndexが2以上)

1枚目がレコードを作るのを待つ(action: 'wait')。3回待っても見つからなければ、新規登録に切り替える

見つからず、上記以外

「写真のみの報告」として新規登録(POST)

既存のレコードを更新するときのポイントは2つです。1つ目は、すでに添付されている写真のfileKeyも、必ず値に含めることです。kintoneのレコード更新APIでは、添付済みのファイルがあるフィールドにファイルを追加する場合、添付済みのファイルのfileKeyもフィールドの値に指定する決まりになっています。新しい写真だけを指定すると、既存の写真はフィールドから削除されます。ここでは、検索で取得した既存の写真のfileKeyの後ろに、新しい写真のfileKeyを足しています。

なお、kintoneのドキュメントには「レコード取得のレスポンスに含まれるファイルキーはダウンロード用」という記載もあります。筆者の環境(2026年10月15日)では、レコード取得で得たfileKeyをそのまま更新に指定して、既存の写真が消えずに残ることを確認しました。

2つ目は、検索で取得した$revisionをrevisionに指定することです。検索してから更新するまでの間に、別の処理が同じレコードを更新していれば、リビジョンが一致せずにエラーになります。エラーになったときの扱いは、手順8で作ります。

保存できる?(IF)

条件は{{ $json.action }} is equal to save です。trueの出口を「kintoneに保存」に、falseの出口(1枚目を待つ場合)を「少し待つ」につなぎます。

kintoneに保存(HTTP Request)

PUT(更新)とPOST(新規登録)で、URLは同じ/k/v1/record.jsonです。違うのはメソッドとボディだけなので、どちらも「保存先を決める」で組み立てておき、1つのノードで送ります。

設定項目

値

Method

Expressionに切り替えて {{ $json.method }}

URL

https://(サブドメイン).cybozu.com/k/v1/record.json

Authentication

Generic Credential Type → Header Auth → kintone API Token

Send Body

ON(JSON)

JSON

{{ JSON.stringify($json.body) }}

Settings → On Error

Continue (using error output)

Methodの欄にマウスを合わせると[Fixed/Expression]の切り替えが出るので、Expressionにして式を入れます。

On Errorを「Continue (using error output)」にすると、ノードの出口が「Success」と「Error」の2つに分かれます。リビジョンが一致しないなどでkintoneがエラーを返しても、ワークフローは止まらず、Errorの出口に処理が流れます。Successの出口は「LINEに写真の受付を返信」に、Errorの出口は「少し待つ」につなぎます。

■手順8:衝突したら、少し待ってやり直す

少し待つ(Wait)

設定項目

値

Resume

After Time Interval

Wait Amount

2

Wait Unit

Seconds

すぐにやり直すと、同時に動いている別の処理とまたぶつかりやすいので、2秒待ちます。

再試行の準備(Code)

ModeはRun Once for Each Itemです。

// kintoneの更新が他の処理とぶつかったとき(GAIA_CO02)と、
// 1枚目の写真がレコードを作るのを待つときだけ、やり直す
const err = $json.error?.message ?? '';
if (err && !err.includes('GAIA_CO02')) {
  throw new Error(`kintoneへの保存に失敗しました:${err}`);
}
if ($json.retry >= 5) {
  throw new Error(`写真を保存できませんでした(userId:${$json.userId})`);
}
return {
  json: {
    userId: $json.userId,
    replyToken: $json.replyToken,
    fileKeys: $json.fileKeys,
    minIndex: $json.minIndex,
    retry: $json.retry + 1,
  },
};

Errorの出口から流れてきたデータには、元の入力に加えてerrorという項目が付きます。そのmessageにkintoneのエラーコードGAIA_CO02(指定したリビジョンが最新ではない)が含まれるときだけ、やり直します。実際にリビジョンがぶつかったとき、kintoneは次のエラーを返しました。

409 - {"code":"GAIA_CO02","id":"…","message":"指定したrevisionは最新ではありません。ほかのユーザーがレコードを更新した可能性があります。"}

権限エラーや値の誤りなど、それ以外のエラーは何度やり直しても成功しないので、その場でエラーにして実行を止めます。止まった実行は、n8nの[Executions]に失敗として残ります。

やり直しは最大5回です。retryを1つ増やして「検索の準備」に戻すと、レコードの検索からやり直しになり、最新のリビジョンと写真の一覧を取り直してから、もう一度更新します。「再試行の準備」の出口を、キャンバス上で「検索の準備」の入口にドラッグしてつなげば、ループの完成です。

LINEに写真の受付を返信(HTTP Request)

設定は「LINEに返信」と同じで、ボディだけが違います。

{{ JSON.stringify({
  replyToken: $('保存先を決める').item.json.replyToken,
  messages: [{
    type: 'text',
    text: '写真を' + $('保存先を決める').item.json.fileKeys.length + '枚受け付けました。\n受付番号 #'
      + ($json.id ?? $('保存先を決める').item.json.recordId)
  }]
}) }}

新規登録(POST)のときは、kintoneのレスポンスに登録したレコードのidが入っています。更新(PUT)のレスポンスには新しいrevisionしか入っていないため、「保存先を決める」で控えておいたrecordIdを使います。

返信は、写真を受け取った実行ごとに1回送ります。写真を3枚まとめて送ると、画像ごとに別のWebhookで届いた場合は「写真を1枚受け付けました」が3回届きます。2枚が1回のWebhookにまとまって届いた場合は、「写真を2枚受け付けました」と「写真を1枚受け付けました」の2回です。1回にまとめたい場合は、返信をimageSet.total枚そろったときだけにするなどの工夫が必要ですが、Webhookの届く順序は不定で、古いAndroid版LINEではtotalが付かないため、今回は確実さを優先しています。

■手順9:動作確認は「別々」「まとめて」「写真だけ」の3パターンで

ワークフローを公開したら、次の3パターンを試します。前回のワークフローと同じパス(/webhook/line)を使っているので、前回のワークフローは先に非公開(Unpublish)にしてください。同じパスのWebhookを持つワークフローを2つ同時に公開することはできません。LINE DevelopersのWebhook URLは変える必要がありません。

パターン

送り方

期待する結果

1. 報告→写真

3行の報告を送り、続けて写真を1枚送る

報告のレコードの写真フィールドに1枚添付される

2. 報告→写真をまとめて

3行の報告を送り、続けて写真を3枚選んでまとめて送る

同じレコードに3枚すべて添付される。Executionsに「少し待つ」を通った実行が出ることがある

3. 写真だけ

報告を送らずに、写真を3枚まとめて送る

「(写真のみの報告)」のレコードが1件だけ作られ、3枚すべて添付される

筆者の環境(2026年10月15日、n8n Cloud)で実際に試した結果は次のとおりです。

パターン

kintoneの結果

n8nの実行

2. 報告→写真3枚

報告のレコード(受付番号 #1)に3枚とも添付。リビジョンは1→4

写真の実行3件がほぼ同時に始まり、1件目はそのまま保存。2件目は1回、3件目は2回GAIA_CO02でぶつかり、やり直して保存。最後の実行が終わるまで約15秒

3. 写真だけ3枚

「(写真のみの報告)」のレコード(#2)が1件だけでき、3枚とも添付

1枚目(index 1)の実行が新規登録。2枚目・3枚目は1回のWebhookにまとまって届き、「少し待つ」を1回通ってから2枚まとめて追加

パターン2と3で大事なのは、kintoneのレコードを開いて写真が3枚そろっているかを確認することです。リビジョンの指定を忘れていると、エラーは出ないのに写真が1〜2枚しか残らない、という気付きにくい不具合になります。

■つまずきやすいポイント

症状

原因と対処

画像の取得が404になる

URLのドメインがapi.line.meになっている。api-data.line.meに直す。送信から時間がたち、LINE側でコンテンツが削除された場合も取得できない

画像の取得が401になる

認証情報にkintone API Tokenが選ばれている。LINE Channel Access Tokenに切り替える

kintoneへのアップロードがエラーになる

Body ParametersのNameがfileになっていない、またはParameter Typeが「n8n Binary File」になっていない。Input Data Field Nameが、画像を取得ノードのPut Output in Field(photo)と一致しているかも確認する

写真が添付されず、kintoneの添付ファイルにも出てこない

アップロードだけ成功して、レコードへの保存に失敗している。アップロードしたファイルは添付されないまま3日で削除される。Executionsで「kintoneに保存」のエラー内容を確認する

写真を追加すると、前の写真が消える

「保存先を決める」で既存の写真のfileKeyを値に含めていない。添付ファイルフィールドは、指定しなかったファイルが削除される

まとめて送った写真の一部が残らない

revisionを指定していない、または「kintoneに保存」のOn Errorが「Continue (using error output)」になっておらず、やり直しが動いていない

「直近の報告を探す」がエラーになる

作成日時フィールドのフィールドコードが「作成日時」でない、またはAPIトークンに「レコード閲覧」権限がない。権限を変えた後に[アプリを更新]を押したかも確認する

写真の保存がエラーになる

APIトークンに「レコード編集」権限がない、または「写真」フィールドのフィールドコードがphotosになっていない

ワークフローを公開できない

前回のワークフローが同じパス(/webhook/line)で公開されたまま。前回のものを非公開にする

「Referenced node doesn't exist」のようなエラーが出る

ノード名を変えたのに、$('…')で参照している式を直していない。特に前回の「報告内容を取り出す」を参照している式に注意

■この方式の限界

「同じ人の直近10分の報告に追加する」というルールは、シンプルな分、次のようなケースでは期待どおりになりません。運用で気になるものがあれば、ルールを足して対応してください。

■まとめ:ひも付けはkintoneのデータで、同時更新はリビジョンで守る

LINEの写真をkintoneに保存する処理そのものは、「メッセージIDで画像を取得する」「kintoneにアップロードしてfileKeyを受け取る」「fileKeyをレコードに指定する」の3ステップで、n8nのHTTP Requestノードだけで組めます。押さえるべきは、取得先がapi-data.line.meであることと、アップロードのパラメータ名がfileであることの2点です。

本当の難しさは、テキストと写真が別々に、しかも複数枚の写真が順不同で同時に届くことにあります。今回は、ひも付けの基準を「同じ送信者」と「直近10分の作成日時」というkintone側のデータに置き、同時更新による写真の消失をリビジョンで防ぎました。添付ファイルフィールドは、既存のファイルも指定し直さないと消えるという性質があるため、リビジョンによる確認は省略しないでください。

まずは自分のスマホから、報告→写真、写真3枚まとめて、写真だけ、の3パターンを送り、kintoneのレコードに写真がそろうところまで確認してみてください。

■参考リンク

※本記事の内容は2026年10月時点の情報です。各サービスの仕様・画面・料金は変更される可能性があるため、最新情報は公式ドキュメントをご確認ください。