n8nとGoogleスプレッドシート連携:読み書き・更新・重複を防ぐ

公開 書いた人 大林 諒

「n8nの使い方入門:最初のワークフローを作って動かす」で、問い合わせをGoogleスプレッドシート「問い合わせ一覧」に1行足す「問い合わせ受付」を作りました。この記事では、そのシートを本番で使い続ける書き方を固めます。足す・読む・書き換える・消す操作を使い分け、やり直しや再送でも同じ問い合わせが2行入らない形にし、日付や電話番号が化けない設定と、Googleの上限に当たらない組み方まで入れます。前提は「問い合わせ一覧に追加」ノードが動いていること。版は2026年10月時点の安定版2.44.7(Google Sheetsノードは4.7)です。

この記事の内容
  1. 先に決めること
  2. Credentialを作る:OAuth2とサービスアカウント
  3. 操作の一覧と使い分け
  4. 手順:重複しない「問い合わせ受付」にする
  5. 日付・数値・空欄の扱い
  6. APIの上限に当たらない書き方
  7. シートを台帳にしすぎない判断
  8. 応用
  9. できないことと、その代わり
  10. よく詰まるところ
  11. 次にやること

先に決めること

シートを記録に使うか、台帳に使うか

足していくだけの記録と、同じ行を書き換える台帳では組み方が違います。問い合わせ一覧は記録ですが、「対応状況」を書き換えた時点で台帳の性格も持ちます。先に、n8nが書き換えてよい列を決めます。問い合わせ一覧なら「対応状況」だけで、ほかの列は人が直さない約束にします。実際に本番で動かしていると、壊れるのはたいてい人とn8nが同じ列を書き換えているシートです。

行を何で見分けるか

行番号には頼りません。人が並べ替えたり行を挿入・削除したりすると、行番号はすぐ変わります。代わりに、問い合わせごとに決まって変わらない値を「受付ID」として列に持たせます。メールアドレスは使えません。同じ人が2回問い合わせると、2件目が1件目を上書きします。メールアドレスで照合してよいのは、1社1行の顧客台帳のような表だけです。

誰の権限で書くか

GoogleのCredential(認証情報)は、OAuth2(Googleアカウントでログインしてつなぐ)とサービスアカウント(Google Cloudに作るロボット用のアカウント)の2通りです。社員30名ほどでGoogle Workspaceなら、共用アカウント(automation@example.co.jp)のOAuth2で足ります。

値をGoogleに解釈させるか

ノードの「Cell Format」の初期値「Let Google Sheets format」は、手で打ったのと同じように値を解釈します。「Let n8n format」は送った値をそのまま入れます。問い合わせ一覧は後者にします。理由は「日付・数値・空欄の扱い」で書きます。

Credentialを作る:OAuth2とサービスアカウント

項目OAuth2サービスアカウント
誰として書くかログインしたGoogleアカウントサービスアカウント(〜@〜.iam.gserviceaccount.com)
ノードの「Authentication」「OAuth2 (recommended)」「Service Account」
n8nのCredentialの種類Google Sheets OAuth2 APIGoogle Service Account API
n8n Cloudでの作り方「Sign in with Google」でログインするだけGoogle Cloudで鍵を作って貼る
触れるシートそのアカウントが開けるシート全部共有されたシートだけ
ファイルの置き場所マイドライブでも共有ドライブでも共有ドライブ
止まるきっかけアカウントの停止・削除、ExternalかつTestingの状態なら7日で失効鍵の削除、共有の解除
シートの変更履歴に出る名前そのアカウントサービスアカウントのメールアドレス

公式ドキュメントもOAuth2を推奨しています。サービスアカウントを選ぶのは、共用アカウントを作れない社内規程があるときや、n8nが触れるシートを共有で1枚ずつ絞りたいときです。OAuth2の作り方と7日で切れる条件は「n8nの使い方入門:最初のワークフローを作って動かす」、共有の範囲や担当者が辞めたときの付け替えは「n8nの認証情報(Credential):OAuth2・APIキー・共有と権限」に書きます。ここではサービスアカウントだけ作ります。

サービスアカウントで作る

  1. Google Cloud Consoleで、n8n用のプロジェクトが選ばれていることを確かめる
  2. 「APIs & Services」の「Library」で Google Sheets API を探し、「ENABLE」を押す
  3. 同じ画面で Google Drive API も「ENABLE」にする
  4. 「APIs & Services」の「Credentials」で「+ Create credentials」から「Service account」を選ぶ
  5. 「Service account name」に n8n-sheets と入れ、「Create and continue」を押す
  6. ロールは付けずに「Done」を押す
  7. できたサービスアカウントを開き、「Keys」タブで「Add key」から「Create new key」を選ぶ
  8. 「JSON」を選んで「CREATE」を押す。鍵のファイルがダウンロードされる
  9. n8nでGoogle Sheetsノードを開き、「Authentication」を「Service Account」にする
  10. Credentialの欄で「Create new credential」を選ぶ
  11. JSONの client_email の値を「Service Account Email」に貼る
  12. JSONの private_key の値を、前後の引用符を除いて「Private Key」に貼り、「Save」を押す
  13. Googleドライブで、問い合わせ一覧を置いた共有ドライブのメンバー管理を開き、client_email のアドレスを「投稿者」以上で追加する(ファイル単位の共有なら「編集者」)

「Region」などは初期値のままです。n8nの公式ドキュメントによると、2025年4月15日より後に作ったサービスアカウントはマイドライブにアクセスできず、共有ドライブだけを扱えます。ドメイン全体の委任で回避できますが公式も勧めていないので、共有ドライブへ移して共有します。JSONはシートを書き換えられる鍵なので、パスワードと同じ場所に保管します。

n8nのGoogle Service Account APIのCredential画面。Service Account EmailとPrivate Keyの欄
n8nのGoogle Service Account APIのCredential画面。Service Account EmailとPrivate Keyの欄

操作の一覧と使い分け

「Resource」は、シートの中を扱う「Sheet Within Document」と、ファイルそのものを作る・消す「Document」の2つです。普段使うのは前者の8つの操作です。

操作(Operation)ノードパネルでの名前やること問い合わせ受付での使いどころ
Append RowAppend row in sheet末尾に行を足す照合する値を持てない記録
Append or Update RowAppend or update row in sheet照合する列が同じ行があれば書き換え、無ければ足す問い合わせ一覧への書き込み(この記事の形)
Update RowUpdate row in sheet照合する列が同じ行だけ書き換える。無ければ何もしない対応状況の書き換え
Get Row(s)Get row(s) in sheet行を読む。「Filters」で絞れる未対応の件数、会社名で履歴を探す
Delete Rows or ColumnsDelete rows or columns from sheet指定した行番号から行(または列)を消す原則使わない
ClearClear sheet中身を消す。シート(タブ)は残る毎回作り直す作業用シート
CreateCreate sheetシート(タブ)を作る年ごとの記録シート
DeleteDelete sheetシート(タブ)を丸ごと消す使わない

一番間違えやすいのが「Delete」で、行ではなくシートそのものを消します。

「Append Row」は、やり直しや再送のたびに行が増えるので、照合する値を作れないときだけ使います。

ノードパネルで「Google Sheets」を開き、操作の一覧が並んでいるところ
ノードパネルで「Google Sheets」を開き、操作の一覧が並んでいるところ

手順:重複しない「問い合わせ受付」にする

準備:受付IDの列を足す

  1. 「問い合わせ一覧」を開き、H1のセルに「受付ID」と入れる
  2. 見出しの前後に空白が無いことを確かめる(n8nは空白まで区別して読む)

書き込みを Append or Update Row に替える

  1. 「問い合わせ一覧に追加」を開く
  2. 「Operation」を「Append or Update Row」に替える
  3. 「Mapping Column Mode」をいったん「Map Automatically」にしてから「Map Each Column Manually」に戻し、H列の「受付ID」を読み込ませる
  4. 「Column to match on」で「受付ID」を選ぶ
  5. 「Values to Send」の「受付ID」に下の式を入れる
  6. 「受付日時」の式を下のものに替える
  7. ほかの6列に入門の記事と同じ値が入っていることを確かめる
  8. 「Add option」から「Cell Format」を足し、「Let n8n format」を選ぶ
  9. 「Add option」から「Minimise API Calls」を足してオンにする
  10. 問い合わせフォームのノードにピン留めしたデータがある状態で、「Execute step」を2回押す
  11. シートの行が1行だけ増え、2回目で増えていないことを確かめる

受付ID:

{{ $json.submittedAt }}_{{ $json.email }}

受付日時:

{{ DateTime.fromISO($json.submittedAt).toFormat('yyyy-MM-dd HH:mm') }}

受付IDは、ミリ秒まで入った送信日時とメールアドレスをつないだ値で、2026-10-26T14:05:12.345+09:00_yamada@example.com のようになります。同じ送信からは何度作っても同じ値、別の送信なら違う値になるのが条件です。

実行の番号 $execution.id は使いません。失敗した実行をやり直すと番号が変わり、同じ問い合わせが別のIDで2行目に入ります。受付日時を $now から作るのをやめたのも同じ理由です。自社サイトのフォームが送信ごとの番号を送ってくれるなら、その番号を受付IDにするのが一番確実です。照合に使った列は「(using to match)」と表示され、書き換えの対象から外れます。

Append or Update Rowの設定。Column to match onが受付ID、Values to Sendに8列が並んでいる
Append or Update Rowの設定。Column to match onが受付ID、Values to Sendに8列が並んでいる

1回の実行の中の重複を落とす

Append or Update Row は、実行の最初にシートを1回だけ読み、その時点の受付IDと照らし合わせます。同じ実行の中に、シートにまだ無い同じ受付IDが2件あると、どちらも「無い」と判定されて2行足されます。過去の問い合わせをCSVからまとめて入れるときや、Webhookで一度に複数件届くときに起きます。

  1. Append or Update Row の手前の線の + を押し、「Remove Duplicates」を足す
  2. 「Operation」は「Remove Items Repeated Within Current Input」のまま
  3. 「Compare」を「Selected Fields」にする
  4. 「Fields To Compare」に submittedAt, email と入れる

送り元が数秒おきに再送してくるサービスでは、別々の実行がほぼ同時にシートを読むので、シートの照合だけでは防ぎきれません。そのときは「Operation」を「Remove Items Processed in Previous Executions」にし、「Keep Items Where」を「Value Is New」、「Value to Dedupe On」に受付IDの式を入れます。通した値はn8nのデータベースに残り、「History Size」の初期値は10,000件です。

Remove Duplicatesの設定。Selected FieldsとFields To Compareに2項目
Remove Duplicatesの設定。Selected FieldsとFields To Compareに2項目

対応状況を書き換える(Update Row)

受付IDと新しい対応状況が前のノードから届く例です。Slackのボタンで「対応済み」を押す作り方は「n8nとSlack連携:通知・投稿・ボタンで承認・スラッシュコマンド」にあります。

  1. Google Sheetsノードを足し、「Update row in sheet」を選ぶ
  2. 「Document」と「Sheet」は「From list」で問い合わせ一覧とシート1を選ぶ
  3. 「Mapping Column Mode」は「Map Each Column Manually」
  4. 「Column to match on」で「受付ID」を選ぶ
  5. 「Values to Update」の「受付ID」に {{ $json.receptionId }}、「対応状況」に {{ $json.status }} を入れる
  6. ほかの6列は、欄の右のごみ箱のボタンで外す

6は飛ばせません。ノードの版4.7では、空のまま残した欄は空欄として書き込まれ、その行の会社名や内容が消えます。

「Column to match on」には見出しに無い row_number(n8nが付ける行番号)も出ますが、読んでから書くまでに人が並べ替えると別の行を書き換えるので、受付IDがある表では使いません。「Map Each Column Manually」のとき、照合する行が無いとUpdate Rowは何もせず、後ろのノードも動きません。書き換わるのは上から最初の1行だけです。

Update Rowの設定。受付IDと対応状況の2欄だけが残っている
Update Rowの設定。受付IDと対応状況の2欄だけが残っている

読み出す(Get Row(s))

毎朝「未対応」の件数をSlackに送る例です。毎朝決まった時刻に動かす設定は「n8nの定期実行とサブワークフロー:Schedule Triggerと部品化」に書きます。

  1. Google Sheetsノードを足し、「Get row(s) in sheet」を選ぶ
  2. 「Document」と「Sheet」に問い合わせ一覧とシート1を選ぶ
  3. 「Filters」の「Add Filter」を押し、「Column」を「対応状況」、「Value」を「未対応」にする
  4. ノード名を「未対応を読む」に変える
  5. 「Settings」で「Always Output Data」をオンにする
  6. 後ろのSlackノードの「Settings」で「Execute Once」をオンにし、本文に下の式を入れる
未対応の問い合わせ: {{ $('未対応を読む').all().filter(i => i.json.row_number).length }}件

読んだ行は1行が1件のitemになります。row_number はn8nが付ける行番号で、データは2から始まります。

[
  {
    "row_number": 5,
    "受付日時": "2026-10-26 14:05",
    "会社名": "株式会社サンプル",
    "お名前": "山田 太郎",
    "メールアドレス": "yamada@example.com",
    "種類": "お見積もり",
    "内容": "料金表を送ってください",
    "対応状況": "未対応",
    "受付ID": "2026-10-26T14:05:12.345+09:00_yamada@example.com"
  }
]

「Always Output Data」がオフだと、未対応が0件の朝は出力が空になり、Slackが動かずに0件の知らせも届きません。オンにすると空のitemが1件出るので、件数は row_number を持つitemだけ数えています。

Filtersは空白まで区別する完全一致です。条件を2つ以上足すと「Combine Filters」の初期値「AND」で全部満たす行になります。4.7では一致した行を全部返し、先頭の1行だけなら「Return only First Matching Row」をオンにします。「〜を含む」は全部読んで「Filter」ノードで絞ります。

Get Row(s)の設定。FiltersでColumnが対応状況、Valueが未対応
Get Row(s)の設定。FiltersでColumnが対応状況、Valueが未対応
Get Row(s)のOUTPUTをTable表示にし、row_numberの列が先頭に出ているところ
Get Row(s)のOUTPUTをTable表示にし、row_numberの列が先頭に出ているところ

行を消す

行は消さずに、対応状況を「迷惑」「重複」のように書き換えて残すのを基本にします。どうしても消すときは「Delete Rows or Columns」で、「To Delete」を「Rows」、「Start Row Number」に消す行の番号、「Number of Rows to Delete」に行数を入れます。複数の行を1行ずつ消すときは必ず下から消します。上から消すと下の行番号が繰り上がり、別の行を消します。

日付・数値・空欄の扱い

同じ値を送っても、「Cell Format」でシートに入る値が変わります。

送る値Let Google Sheets format(初期値)Let n8n format
2026-10-26 14:05日時の値になり、表示は列の表示形式とシートの地域の設定で決まるその文字のまま
0312345678(電話番号)数値になり、先頭の0が消えるその文字のまま
= で始まる問い合わせ本文数式として計算されるその文字のまま
式の結果が数値の 1000数値数値
空の値空欄空欄

「Let n8n format」を選んだのは2行目と3行目のためです。0が消えた電話番号にはかけ直せず、誰でも書けるフォームの本文が数式として動くのも避けたいからです。受付日時は yyyy-MM-dd HH:mm の文字で入るので、並べ替えれば日時の順になり、2026-10 で始まる行を数えれば月の件数になります。

フォームから届く数値は文字なので、「Let n8n format」では文字のまま入り、SUMで足されません。数値の列は {{ Number($json.employees) }} のように直して送ります。

読む側の初期値は、Get Row(s)の「Output Formatting」で「General Formatting」が「Values (unformatted)」(数値は数値のまま、カンマや通貨記号は外れる)、「Date Formatting」が「Formatted Text」(見えているとおりの文字)です。「Serial Number」にすると、日付は1899年12月30日からの日数(2026年10月26日なら46321)で返ります。

困るのは、人が手で日付を打つ列です。「Formatted Text」はシートの地域の設定と表示形式どおりの文字で返すので、誰かが設定を変えた日からn8n側の読み取りが合わなくなります。その列は「表示形式」→「数字」→「カスタム日時」で yyyy-mm-dd の形に固定します。読んだ文字をLuxonで日時に戻す書き方は「n8nの式(Expression)とデータの扱い:$json・ノードの参照・日付・JSON」にまとめます。

空欄にも癖があります。Get Row(s)では、行の途中の空欄は空の文字 "" で返りますが、行の右端から続く空欄は項目そのものが出てきません。対応状況が右端の列で空の行では、$json['対応状況'] が undefined になり、IFノードの比較が思った方向に倒れません。式では {{ $json['対応状況'] ?? '' }} と書くか、右端に必ず値が入る列(この記事なら受付ID)を置きます。

APIの上限に当たらない書き方

Google Sheets APIの上限は1分あたりの回数で、読み出しと書き込みそれぞれ、プロジェクト全体で1分300回、1ユーザーあたり1分60回です。超えると 429: Too many requests が返り、1分ごとに回数が戻ります。1日の上限はありません。共用アカウント1つでつないでいると、その60回を全ワークフローで分け合います。

Google Sheetsノードは、itemが1件でも100件でも1回の実行でまとめて送ります。2.44.7のソースでは、Append Rowはシートを1回読んで書き込みを2回(「Minimise API Calls」がオンなら1回)、Update Rowは読み1回と書き込み1回です。上限に当たるのは、ほとんどが1件ずつ流す組み方をしたときです。

やり方効く場面設定
まとめて1回で書くCSVからの取り込み、毎朝の一括更新Loop Over Itemsで1件ずつにせず、全部のitemをそのままGoogle Sheetsノードへ渡す
読むのを1回にするGet Row(s)の前のノードから複数件のitemが来るGet Row(s)の「Settings」で「Execute Once」をオン
書き込みの回数を減らす途中に空行・空列が無い記録用のシート「Add option」から「Minimise API Calls」をオン
待ちを入れる小分けにしないと書けない処理「Loop Over Items」の「Batch Size」を50にし、ループの最後に「Wait」で60秒待つ
失敗したらやり直すときどき429が出る「Settings」の「Retry On Fail」をオン、「Max. Tries」を5、「Wait Between Tries (ms)」を5000

Get Row(s)は届いたitemの数だけシートを読むので、10件届けば10回読みます。

「Retry On Fail」は待ち最大5秒・最大5回で、1分単位の上限を待ちきれないことがあります。件数の多い処理は「待ちを入れる」の形にし、それでも落ちた実行は「n8nを止めずに運用する:アップデート・バックアップ・エラー通知・脆弱性」のエラー通知で拾います。

「Minimise API Calls」は上限対策以外にも効きます。オフのとき、n8nはシートを読んで「最後の行の次」の行番号を決めてから書くので、ほぼ同時に2件届くと同じ行を選び、後の1件が前の1件を上書きすることがあります。オンにするとGoogle側の追記の仕組みで末尾に足すので、この取り合いが起きません。途中に空行や空列があると位置がずれるので、記録用のシートは隙間を作らない運用にします。

シートを台帳にしすぎない判断

スプレッドシートは1ファイル2,000万セルまたは100MBまで入りますが、n8nで困るのはずっと手前です。Append RowもAppend or Update Rowも書く前にシート全体を読むので、行が増えるほど実行が重くなります。問い合わせが1日数件なら年に千数百行で、数年はシートのままで困りません。運用しているn8nで移したのは、次のどれかが起きたときです。

起きていること移す先
同じ行を3人以上が書き換え、誰がいつ直したか追えないkintone・Salesforce(変更の履歴と権限が残る)
1件の問い合わせに対応の記録が何件も付くkintone・Salesforce(親子の関係を持てる)
行ごとに見せる人を分けたいkintone・Salesforce
n8nの中だけで使う照合用の値(処理済みのIDなど)n8nのData tables
行が数万になり、1回の実行に時間がかかる年ごとに別のシートへ移す。集計が要るならデータベース

Data tablesはn8nの中にある表で、「Data tables」タブから作ります。Data Tableノードに「Upsert row(s)」や「If row does not exist」があり、照合用に向いています。上限は初期値でインスタンス全体200MiB、セルフホストは N8N_DATA_TABLES_MAX_SIZE_BYTES で変えられます。

顧客管理へ入れる手順は「n8nとkintone・Salesforce・HubSpot連携:問い合わせを顧客管理に入れる」で書きます。

応用

年ごとにシートを分ける

「Sheet」を「By Name」にして {{ $now.toFormat('yyyy') }}年 と書けば、「2026年」のシートに入ります。シートは「Create」で年明けに作っておきます。Sheetの式は最初のitemで1回だけ計算され、残りのitemも同じシートに入るので、年をまたぐ複数件は年ごとに分けてから別々のノードで書きます。

シートの変更をきっかけに動かす

「Google Sheets Trigger」を使い、「Trigger On」で「Row Added」「Row Updated」「Row Added or Updated」から選びます。Credentialは書き込み用とは別の「Google Sheets Trigger OAuth2 API」です。一定の間隔で前回と比べる仕組みなので、編集した瞬間には動きません。日時が数値で届くときは「DateTime Render」を「Formatted String」にします。

ノードに無い操作をする

セルの色付け、並べ替え、行の挿入はノードにありません。HTTP Requestノードで「Authentication」を「Predefined Credential Type」にし、Google Sheets OAuth2 APIのCredentialを選べば、Google Sheets APIを直接呼べます。HTTP Requestの基本は「n8nで外部サービスとつなぐ:Webhook・HTTP Request・API」に書きました。

できないことと、その代わり

できないこと代わりの手
2つ以上の列の組み合わせで照合する(4.7の照合は1列)組み合わせた値を1列(受付IDなど)に入れて照合する
部分一致や大小の比較で読む全部読んで「Filter」ノードで絞る
同じ値の行をまとめて書き換える書き換わるのは最初の1行だけ。同じ値の行を作らない設計にする
書き込み中にほかの実行を待たせる(ロック)同時の書き込みが多い表はkintone・Salesforce・Data tablesへ
人の編集の瞬間に動かすGoogle Apps Scriptのインストール型の編集トリガーからWebhookを呼ぶ
2025年4月15日より後に作ったサービスアカウントでマイドライブのファイルを使う共有ドライブに置く

よく詰まるところ

症状原因直し方
同じ問い合わせが2行入るAppend Rowで、やり直しや再送のたびに足しているAppend or Update Rowにし、受付IDで照合する
Append or Update Rowなのに2行入る同じ実行の中に同じ受付IDが2件ある。またはメールの大文字・小文字や空白で値が揺れている手前にRemove Duplicates。照合する値は .trim().toLowerCase() で揃える
照合する列が数値で、毎回行が増えるAppend or Update Rowはシートに見えている文字(例: 1,234)と送った値(1234)を比べる照合する列は文字の値にし、「Let n8n format」で書く
Update Rowのあと、会社名や内容が消えた4.7では空の欄が空欄として書き込まれる書き換えない列は欄ごと外す
「Column names were updated after the node’s setup」ノードを作ったあとで見出しを変えた「Mapping Column Mode」を選び直して列を読み込み直す
送っていない列(submittedAt・formModeなど)が勝手に増えた「Map Automatically」で、「Handling extra fields in input」が初期値の「Insert in New Column(s)」「Ignore Them」にする
日付が46321のような数値で届く「Date Formatting」が「Serial Number」、またはトリガーの日時の表示が数値「Formatted Text」にする。トリガーは「DateTime Render」を「Formatted String」に
対応状況が空の行で、式が undefined になる右端から続く空欄は項目ごと出てこない?? '' を付けるか、右端に必ず値が入る列を置く
429、または「The service is receiving too many requests from you」1ユーザー1分60回の上限まとめて書く、Get Row(s)を「Execute Once」に、Loop Over ItemsとWaitで待つ
サービスアカウントで「Document」の一覧に出ない、権限エラーになる共有していない、またはマイドライブに置いている共有ドライブに置き、client_email を「投稿者」以上で追加する
「Delete」でシートが丸ごと消えた「Delete」は行ではなくシートを消す操作スプレッドシートの「ファイル」→「変更履歴」から戻す。行は「Delete Rows or Columns」で消す

次にやること

  • n8nの認証情報(Credential):OAuth2・APIキー・共有と権限
  • n8nの式(Expression)とデータの扱い:$json・ノードの参照・日付・JSON
  • n8nとkintone・Salesforce・HubSpot連携:問い合わせを顧客管理に入れる
  • n8nの使い方入門:最初のワークフローを作って動かす

あわせて読む