- 動作確認日:2026-10-01
- n8nのバージョン:2.41.3(Docker、セルフホスト)
① 結論:このワークフローでできること
自分のTelegramボットに送ったメモを、n8nが取りに行き、「受信日時・本文・メッセージID」をGoogleスプレッドシートに1通1行で記録します。
- n8nを
localhostで動かしていても使えます。Telegramからn8nを呼んでもらうのではなく、n8nのほうからメッセージを取りに行く(getUpdates)作りにしました。 - どこまで読んだか(
offset)を覚えているので、何回実行しても、同じメモは1回しか記録しません。 - 読んだ位置は、シートへの記録が済んでから進めます。途中で失敗しても、メモがなくなりません。
/startのようなコマンドや、自分以外からのメッセージは記録しません。- 動作確認のため、起動にはWebhookを使っています。実際に使うときは、Schedule Triggerで数分ごとに実行する想定です。
② 完成イメージ

ボットに、テスト用のメモを3通送ってから、WebhookのURLにGETでアクセスしました。次の文章が返ってきます(実際の出力です)。
3件のメモをシートに記録しました
記録されたシートは、次のとおりです(messageId の列は省略しています)。
| receivedAt | text |
|---|---|
| 2026-10-01 11:30 | 「買い物:牛乳」 |
| 2026-10-01 11:30 | 「アイデア:天気とLINEを組み合わせる」 |
| 2026-10-01 11:30 | 「読みたい本:n8n入門」 |
続けてもう一度実行すると、次の文章が返り、シートの行は増えませんでした。
| 実行 | 返ってきた文章 | シート |
|---|---|---|
| 1回目 | 3件のメモをシートに記録しました | 3行追記 |
| 2回目 | 新しいメモはありませんでした | 変わらない |
実行したときの件数の流れは、次のとおりです。
| ノード | 1回目 | 2回目 |
|---|---|---|
| Webhook | 1 | 1 |
| Get Offset | 1 | 1 |
| Get Updates(HTTP Request) | 1 | 1 |
| Pick Memos | 3 | 1(空) |
| Has Memo | 3(true側) | 1(false側) |
| Google Sheets | 3 | 動かない |
| Save Offset / Save Offset (none) | Save Offset:3 | Save Offset (none):1 |
| Respond Saved / Respond None | Respond Saved:3 | Respond None:1 |
③ 使うノードと必要なもの
必要なもの
- n8n(今回はバージョン2.41.3をDockerで動かしました)
- Telegramのアカウントと、自分で作ったボットの「トークン」
- 自分の「チャットID」
- Googleスプレッドシートと、タブ(今回は
telegram-logという名前のタブ。中身は空で大丈夫です) - 認証情報:Google Sheets OAuth2 API
ボットの作り方とチャットIDの調べ方は、「n8nでブログの新着記事をTelegramに通知するワークフロー」の記事で説明しています。
なぜTelegram Triggerを使わないのか
n8nには「Telegram Trigger」があり、ボットにメッセージが届いた瞬間にワークフローを動かせます。ただし、Telegram Triggerは、Telegramのサーバーからn8nを呼んでもらうしくみです。そのため、インターネットから見えるURL(https)でn8nを動かしている必要があります。
今回のn8nは localhost で動かしているので、Telegramからは届きません。そこで、n8nのほうから、TelegramのAPI(getUpdates)で新しいメッセージを取りに行く作りにしました。
| 方法 | 外から見えるURL | 届いてから記録するまで |
|---|---|---|
| Telegram Trigger | 必要 | すぐ |
| getUpdatesで取りに行く(今回) | 不要 | 次に実行したとき(Schedule Triggerの間隔しだい) |
使うノード
| ノード | 役割 | 設定したこと |
|---|---|---|
| Webhook | 動作確認用の起動ボタン | Path:test-telegram-sheets、Respond:Using 'Respond to Webhook' Node |
| Code(Get Offset) | 前回どこまで読んだか(offset)を取り出す | $getWorkflowStaticData('global').offset を返す |
| HTTP Request(Get Updates) | 新しいメッセージを取る | URL:https://api.telegram.org/botトークン/getUpdates?offset={{ $json.offset }}&allowed_updates=["message"] |
| Code(Pick Memos) | 自分とのチャットのテキストだけを取り出す | 下のコード |
| If(Has Memo) | メモがあるか確かめる | text が空でない |
| Google Sheets | メモを記録する | Operation:Append or Update Row、Sheet:By Name telegram-log、Column to match on:messageId |
| Code(Save Offset / Save Offset (none)) | 次に読む位置を覚える | 下のコード |
| Respond to Webhook(Respond Saved / Respond None) | 結果を返す | 記録したとき:○件のメモをシートに記録しました、なかったとき:新しいメモはありませんでした |
TelegramのAPIは、URLの中にトークンを入れるしくみです。そのため、Get UpdatesのURLにはトークンが入ります。このワークフローのJSONを人に渡すときは、トークンの部分を必ず消してください(この記事の④のJSONでは YOUR_BOT_TOKEN に置き換えています)。

Pick Memosでは、次の処理をしています(画像では、チャットIDを隠しています)。
// 自分とのチャットのテキストだけを取り出す
const MY_CHAT_ID = YOUR_CHAT_ID;
const updates = $json.result || [];
const rows = updates
.map(u => u.message)
.filter(m => m && m.chat.id === MY_CHAT_ID && m.text && !m.text.startsWith('/'))
.map(m => ({
receivedAt: DateTime.fromSeconds(m.date).setZone('Asia/Tokyo').toFormat('yyyy-MM-dd HH:mm'),
text: m.text,
messageId: m.message_id,
}));
return rows.length ? rows.map(r => ({ json: r })) : [{ json: { text: '' } }];
m.chat.id === MY_CHAT_IDで、自分とのチャット以外のメッセージを除いています。ボットのユーザー名を知っている人は、誰でもボットに話しかけられるからです。!m.text.startsWith('/')で、/startのようなコマンドを除いています。- Telegramの日時(
date)は、1970年からの秒数で返ってくるので、日本時間の日時に直しています。 - メモがないときは、空の
textを1つだけ返して、次のIfノードでfalse側に流しています。

Save Offsetでは、次の処理をしています。Save Offset (none) も、コメント以外は同じです。
// シートに記録できたあとで、次に読む位置を覚える
const state = $getWorkflowStaticData('global');
const updates = $('Get Updates').first().json.result || [];
if (updates.length) state.offset = Math.max(...updates.map(u => u.update_id)) + 1;
return $input.all();
getUpdatesにoffset(最後に読んだupdate_id+ 1)を付けて呼ぶと、Telegramは、それより前のメッセージを「読んだ」とみなして、もう返さなくなります。- そのため、offsetを覚えるのは、シートへの記録が済んだあとにしています(理由は⑤に書きました)。

Google Sheetsノードでは、照合する列(Column to match on)を messageId にしました。万一、同じメモをもう一度受け取っても、行が増えずに上書きされます。
④ JSON
n8nの公開API(POST /api/v1/workflows)でこのJSONからワークフローを作成し、有効化してから、上の結果を確認しました。
使うときは、次の箇所を自分の環境に合わせて書き換えてください。
YOUR_BOT_TOKEN:ボットのトークン(Get UpdatesのURLの中)YOUR_CHAT_ID:自分のチャットID(Pick Memosのコードの中。数字をそのまま書きます)YOUR_SPREADSHEET_ID:記録先のスプレッドシートのIDYOUR_CREDENTIAL_ID:n8nに登録したGoogle Sheetsの認証情報のID
{
"name": "test-telegram-sheets",
"nodes": [
{
"id": "b3300000-0001-4000-8000-000000000001",
"name": "Webhook",
"webhookId": "b3300000-aaaa-4000-8000-000000000001",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"parameters": {
"path": "test-telegram-sheets",
"responseMode": "responseNode",
"options": {
}
}
},
{
"id": "b3300000-0002-4000-8000-000000000002",
"name": "Get Offset",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
0
],
"parameters": {
"jsCode": "// 前回どこまで読んだか(offset)を取り出す\nconst state = $getWorkflowStaticData(\u0027global\u0027);\nreturn [{ json: { offset: state.offset || 0 } }];"
}
},
{
"id": "b3300000-0003-4000-8000-000000000003",
"name": "Get Updates",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
440,
0
],
"parameters": {
"url": "=https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates?offset={{ $json.offset }}\u0026allowed_updates=[\"message\"]",
"options": {
}
}
},
{
"id": "b3300000-0004-4000-8000-000000000004",
"name": "Pick Memos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
0
],
"parameters": {
"jsCode": "// 自分とのチャットのテキストだけを取り出す\nconst MY_CHAT_ID = YOUR_CHAT_ID;\nconst updates = $json.result || [];\n\nconst rows = updates\n .map(u =\u003e u.message)\n .filter(m =\u003e m \u0026\u0026 m.chat.id === MY_CHAT_ID \u0026\u0026 m.text \u0026\u0026 !m.text.startsWith(\u0027/\u0027))\n .map(m =\u003e ({\n receivedAt: DateTime.fromSeconds(m.date).setZone(\u0027Asia/Tokyo\u0027).toFormat(\u0027yyyy-MM-dd HH:mm\u0027),\n text: m.text,\n messageId: m.message_id,\n }));\nreturn rows.length ? rows.map(r =\u003e ({ json: r })) : [{ json: { text: \u0027\u0027 } }];"
}
},
{
"id": "b3300000-0005-4000-8000-000000000005",
"name": "Has Memo",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
880,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "e1",
"leftValue": "={{ $json.text }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {
}
}
},
{
"id": "b3300000-0006-4000-8000-000000000006",
"name": "Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1100,
-100
],
"parameters": {
"operation": "appendOrUpdate",
"documentId": {
"__rl": true,
"value": "YOUR_SPREADSHEET_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "telegram-log",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {
},
"matchingColumns": [
"messageId"
],
"schema": [
]
},
"options": {
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "YOUR_CREDENTIAL_ID",
"name": "Google Sheets account"
}
}
},
{
"id": "b3300000-0009-4000-8000-000000000009",
"name": "Save Offset",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
-100
],
"parameters": {
"jsCode": "// シートに記録できたあとで、次に読む位置を覚える\nconst state = $getWorkflowStaticData(\u0027global\u0027);\nconst updates = $(\u0027Get Updates\u0027).first().json.result || [];\nif (updates.length) state.offset = Math.max(...updates.map(u =\u003e u.update_id)) + 1;\nreturn $input.all();"
}
},
{
"id": "b3300000-0010-4000-8000-000000000010",
"name": "Save Offset (none)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
120
],
"parameters": {
"jsCode": "// メモがなくても、読んだ位置(/start などのコマンド)は進めておく\nconst state = $getWorkflowStaticData(\u0027global\u0027);\nconst updates = $(\u0027Get Updates\u0027).first().json.result || [];\nif (updates.length) state.offset = Math.max(...updates.map(u =\u003e u.update_id)) + 1;\nreturn $input.all();"
}
},
{
"id": "b3300000-0007-4000-8000-000000000007",
"name": "Respond Saved",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1540,
-100
],
"parameters": {
"respondWith": "text",
"responseBody": "={{ $input.all().length }}件のメモをシートに記録しました",
"options": {
}
}
},
{
"id": "b3300000-0008-4000-8000-000000000008",
"name": "Respond None",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1320,
120
],
"parameters": {
"respondWith": "text",
"responseBody": "新しいメモはありませんでした",
"options": {
}
}
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Get Offset",
"type": "main",
"index": 0
}
]
]
},
"Get Offset": {
"main": [
[
{
"node": "Get Updates",
"type": "main",
"index": 0
}
]
]
},
"Get Updates": {
"main": [
[
{
"node": "Pick Memos",
"type": "main",
"index": 0
}
]
]
},
"Pick Memos": {
"main": [
[
{
"node": "Has Memo",
"type": "main",
"index": 0
}
]
]
},
"Has Memo": {
"main": [
[
{
"node": "Google Sheets",
"type": "main",
"index": 0
}
],
[
{
"node": "Save Offset (none)",
"type": "main",
"index": 0
}
]
]
},
"Save Offset (none)": {
"main": [
[
{
"node": "Respond None",
"type": "main",
"index": 0
}
]
]
},
"Save Offset": {
"main": [
[
{
"node": "Respond Saved",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets": {
"main": [
[
{
"node": "Save Offset",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
⑤ つまずきポイントと対処
このワークフローは、2回作り直してから動きました。どちらも、実際に起きたことです。
1回目:メモが「なし」と判定され、しかも取り直せなくなった
- 起きたこと:4通のメモを受け取れていたのに、「メモがあるか」を判定するIfノードで、4通ともfalse側(メモなし)に流れました。シートには何も書かれませんでした。
- 原因1:Ifノードの条件を、「
emptyが true ではない」という書き方にしていて、メモの行では期待どおりに判定されませんでした。 - 原因2:最初は、メッセージを受け取った時点で、読んだ位置(offset)を先に進めていました。そのため、Telegramはこの4通を「読んだ」とみなし、もう返さなくなりました。メモがなくなってしまったわけです。
- 対処:Ifノードの条件を「
textが空でない」に変えました。さらに、offsetを覚えるのを、シートへの記録が済んだあと(Save Offset)に移しました。
2回目:同じメモが2回記録された
- 起きたこと:作り直したあとに2回実行すると、2回目にも同じ3通が記録され、シートが6行になりました。
- 原因:Save Offsetが、次に読む位置を受け取れておらず、offsetが保存されていませんでした。そのため、2回目もTelegramから同じメッセージが返ってきました。
- 対処:次に読む位置を、Get Updatesの結果から直接計算するようにしました。また、Google Sheetsノードを「Append or Update Row」(
messageIdで照合)に変えて、同じメモを受け取っても行が増えないようにしました。テストで重複した6行は消してから、テストし直しました。
⑥ 応用アイデア
どれも、まだ試していないアイデアです。
- Webhookを「Schedule Trigger」に替えて、5分ごとにメモを取りに行く
- 「買い物:」「アイデア:」のような頭の言葉で、シートのタブを分ける
- 記録できたら、ボットから「記録しました」と返事をする(Telegramノードの「Send Message」)
- n8nをインターネットから見えるURLで動かしているなら、Telegram Triggerに替えて、届いた瞬間に記録する
