Chatworkのやり取りをGoogleスプレッドシートに自動で残す方法
「Chatworkのチャット、あとから見返したいな」
「引き継ぎや監査のために、履歴を残しておきたい」
そんなときに便利なのが、
GoogleスプレッドシートとGoogle Driveを使った自動保存です。
少し難しそうに見えるかもしれませんが、
一度設定してしまえば、あとは自動で動き続けます。
この記事では、
専門知識がなくても迷わないように
順番に、やさしく説明していきます。

スクリーンショットを撮って、画像で場所が迷わないようにしてあります。
この設定でできること
設定が完了すると、こんなことが自動でできるようになります。
- Chatworkの
- グループチャット
- 個別チャット(DM)
をまとめて取得
- ルームごとに
- Googleスプレッドシートへ保存
- 添付ファイルも
- 必要なルームだけGoogle Driveに保存
- 毎日0時に自動実行
- 「今日は更新がなかった」日も履歴として残る
一度設定すれば、基本的に放置でOKです。

無料で使っていて、chatworkの履歴が見えなくなる人の一時的な保存方法です。注意として、基本的には他の方とスプレッドシート共有しないこと。すべて出力されるのでセキュリティ的にはあまりよくありません。
決して悪用せず、自己責任でご使用してください。
事前に準備するもの
あらかじめ、次の2つがあれば大丈夫です。
- Googleアカウント
- Chatworkのアカウント
ステップ①:保存先のフォルダを作ります
まずは、
添付ファイルを保存するためのフォルダを作ります。
Googleを開いたときに、9つの点をクリックするとGoogledriveがあります。



→
folders/ の後ろの文字列をコピー
ステップ②:管理用のスプレッドシートを作ります
次に、
チャット履歴を管理するスプレッドシートを作ります。


(例:Chatwork_バックアップ管理)

このスプレッドシートが、
これからの管理画面になります。
ステップ③:Apps Script を開きます
Chatwork_バックアップ管理のスプレッドシート


ステップ④:スクリプトを貼り付けます


下記のスクリプトを貼り付けてください。
/**
* Chatwork → Google Sheets(グループチャット別シート:DM含む) + Drive(添付ファイル)
*
* ✅要件
* - シート一覧:更新(する/しない)+添付取得(する/しない, デフォルトしない)
* - 更新しないはチャット/添付ともに完全スキップ(ログは0件で残す)
* - 添付は file_id マーカー方式で重複保存防止
* - バッチ実行+タイムアウト前に安全停止
* - シート並び順固定:
* 1番左:シート一覧
* 2番目:更新履歴(シート一覧の右隣)
*
* ▼ Script Properties(事前に設定)
* - CHATWORK_TOKEN
* - CW_EXPORT_FOLDER_ID
*/
const CW_BASE = "https://api.chatwork.com/v2";
const BATCH_SIZE = 10;
const MAX_RUNTIME_MS = 5 * 60 * 1000;
const FILE_MARKER_PREFIX = ".saved_";
function runDailyChatworkExport() {
const startMs = Date.now();
const props = PropertiesService.getScriptProperties();
const token = props.getProperty("CHATWORK_TOKEN");
const rootId = props.getProperty("CW_EXPORT_FOLDER_ID");
if (!token) throw new Error("CHATWORK_TOKEN が未設定です");
if (!rootId) throw new Error("CW_EXPORT_FOLDER_ID が未設定です");
const ss = SpreadsheetApp.getActiveSpreadsheet();
// ★一覧設定読み込み(更新/添付)
const indexSettings = getIndexSettings_(ss);
// ★まずシート一覧を更新(この中でシート順も固定)
updateSheetIndex_(ss, indexSettings);
// 日付が変わったらバッチ先頭へ
const today = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM-dd");
if (props.getProperty("CW_BATCH_DATE") !== today) {
props.setProperty("CW_BATCH_DATE", today);
props.setProperty("CW_BATCH_OFFSET", "0");
}
// Drive(添付ファイル用)
const root = DriveApp.getFolderById(rootId);
const filesRoot = getOrCreateFolder_(root, "Files");
// 更新履歴
const logSheet = getOrCreateSheet_(ss, "更新履歴");
ensureLogHeader_(logSheet);
const runAt = new Date();
// グループチャット一覧(DM含む)
const rooms = cwGet_("/rooms", token) || [];
const total = rooms.length;
// バッチ範囲
const offset = Number(props.getProperty("CW_BATCH_OFFSET") || 0);
const end = Math.min(offset + BATCH_SIZE, total);
let nextOffset = offset;
for (let i = offset; i < end; i++) {
if (Date.now() - startMs > MAX_RUNTIME_MS) {
props.setProperty("CW_BATCH_OFFSET", String(i));
return;
}
const r = rooms[i];
const roomId = r.room_id;
const roomName = r.name || String(roomId);
const sheetName = makeSafeSheetName_(roomName, roomId);
// 設定(デフォルト:更新する/添付しない)
const setting = indexSettings[sheetName] || { update: "する", attach: "しない" };
// 更新しない:完全スキップ(ログは0件)
if (setting.update === "しない") {
logSheet.appendRow([runAt, roomId, roomName, 0, 0]);
nextOffset = i + 1;
continue;
}
/* ---------- チャット ---------- */
const roomSheet = getOrCreateSheet_(ss, sheetName);
ensureRoomHeader_(roomSheet);
const initKey = `CW_INIT_DONE_${roomId}`;
const isInitDone = props.getProperty(initKey) === "1";
const force = isInitDone ? 0 : 1;
const msgs = cwGet_(`/rooms/${roomId}/messages?force=${force}`, token) || [];
if (!isInitDone) props.setProperty(initKey, "1");
const lastMsgKey = `CW_LAST_MSGID_${roomId}`;
const lastMsgId = Number(props.getProperty(lastMsgKey) || 0);
let maxMsgId = lastMsgId;
const rows = [];
for (const m of msgs) {
const mid = Number(m.message_id || 0);
if (mid <= lastMsgId) continue;
maxMsgId = Math.max(maxMsgId, mid);
const acc = m.account || {};
rows.push([
m.message_id || "",
m.send_time ? new Date(m.send_time * 1000) : "",
m.update_time ? new Date(m.update_time * 1000) : "",
acc.account_id || "",
acc.name || "",
acc.chatwork_id || "",
m.body || ""
]);
}
if (rows.length) {
roomSheet.getRange(roomSheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
}
if (maxMsgId > lastMsgId) props.setProperty(lastMsgKey, String(maxMsgId));
/* ---------- 添付(設定が「する」の場合のみ) ---------- */
let savedFileCount = 0;
if (setting.attach === "する") {
const roomFolder = getOrCreateFolder_(filesRoot, sanitizeDriveName_(roomName) + "_" + roomId);
const files = cwGet_(`/rooms/${roomId}/files`, token) || [];
for (const f of files) {
if (Date.now() - startMs > MAX_RUNTIME_MS) break;
const marker = FILE_MARKER_PREFIX + String(f.file_id);
if (roomFolder.getFilesByName(marker).hasNext()) continue;
const finfo = cwGet_(`/rooms/${roomId}/files/${f.file_id}?create_download_url=1`, token);
if (!finfo.download_url) continue;
const res = UrlFetchApp.fetch(finfo.download_url, {
headers: { "x-chatworktoken": token },
muteHttpExceptions: true
});
const code = res.getResponseCode();
if (code >= 200 && code < 300) {
roomFolder.createFile(res.getBlob().setName(finfo.filename || `file_${f.file_id}`));
roomFolder.createFile(marker, "", MimeType.PLAIN_TEXT);
savedFileCount++;
}
}
}
// 更新履歴
logSheet.appendRow([runAt, roomId, roomName, rows.length, savedFileCount]);
nextOffset = i + 1;
if (i % 5 === 4) Utilities.sleep(200);
}
props.setProperty("CW_BATCH_OFFSET", String(nextOffset >= total ? 0 : nextOffset));
}
/* ======================= シート一覧(リンク+更新設定+並び順固定) ======================= */
function getIndexSettings_(ss) {
const sheet = ss.getSheetByName("シート一覧");
const map = {};
if (!sheet || sheet.getLastRow() < 2) return map;
// ヘッダー:リンク / シート名 / 更新 / 添付取得 / 更新日時
const values = sheet.getRange(2, 1, sheet.getLastRow() - 1, Math.min(5, sheet.getLastColumn())).getValues();
values.forEach(r => {
const name = r[1];
if (!name) return;
map[String(name)] = {
update: (String(r[2]) === "しない") ? "しない" : "する",
attach: (String(r[3]) === "する") ? "する" : "しない" // デフォルトしない
};
});
return map;
}
function updateSheetIndex_(ss, settings) {
// --- 必要シートを確保
let indexSheet = ss.getSheetByName("シート一覧");
if (!indexSheet) indexSheet = ss.insertSheet("シート一覧");
let logSheet = ss.getSheetByName("更新履歴");
if (!logSheet) logSheet = ss.insertSheet("更新履歴");
// --- ★並び順固定:1番左=シート一覧、2番目=更新履歴
ss.setActiveSheet(indexSheet);
ss.moveActiveSheet(1);
ss.setActiveSheet(logSheet);
ss.moveActiveSheet(2);
// --- 一覧生成(シート一覧/更新履歴は除外)
const exclude = new Set(["シート一覧", "更新履歴"]);
indexSheet.clear();
indexSheet.appendRow(["リンク", "シート名", "更新", "添付取得", "更新日時"]);
indexSheet.setFrozenRows(1);
const names = ss.getSheets()
.map(s => s.getName())
.filter(n => !exclude.has(n))
.sort((a, b) => a.localeCompare(b, "ja"));
const now = new Date();
const rows = names.map(name => {
const gid = ss.getSheetByName(name).getSheetId();
const url = ss.getUrl() + "#gid=" + gid;
const conf = settings[name] || { update: "する", attach: "しない" }; // ★添付はデフォルトしない
return [`=HYPERLINK("${url}","開く")`, name, conf.update, conf.attach, now];
});
if (rows.length) {
indexSheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
const rule = SpreadsheetApp.newDataValidation()
.requireValueInList(["する", "しない"], true)
.setAllowInvalid(false)
.build();
// 更新(C列)+添付取得(D列) にプルダウン
indexSheet.getRange(2, 3, rows.length, 2).setDataValidation(rule);
}
indexSheet.setColumnWidth(1, 80);
indexSheet.setColumnWidth(2, 360);
indexSheet.setColumnWidth(3, 90);
indexSheet.setColumnWidth(4, 110);
indexSheet.setColumnWidth(5, 170);
}
/* ======================= 共通ユーティリティ ======================= */
function cwGet_(path, token) {
const res = UrlFetchApp.fetch(CW_BASE + path, {
method: "get",
headers: { "x-chatworktoken": token },
muteHttpExceptions: true
});
const code = res.getResponseCode();
if (code === 204) return [];
if (code < 200 || code >= 300) {
throw new Error(`Chatwork API error ${code}: ${res.getContentText()}`);
}
return JSON.parse(res.getContentText());
}
function getOrCreateSheet_(ss, name) {
return ss.getSheetByName(name) || ss.insertSheet(name);
}
function getOrCreateFolder_(parent, name) {
const it = parent.getFoldersByName(name);
return it.hasNext() ? it.next() : parent.createFolder(name);
}
function ensureRoomHeader_(sheet) {
if (sheet.getLastRow() > 0) return;
sheet.appendRow(["message_id","send_time","update_time","account_id","account_name","chatwork_id","body"]);
sheet.setFrozenRows(1);
}
function ensureLogHeader_(sheet) {
if (sheet.getLastRow() > 0) return;
sheet.appendRow(["更新日時", "room_id", "ルーム名", "件数", "添付ファイル数"]);
sheet.setFrozenRows(1);
}
function makeSafeSheetName_(roomName, roomId) {
const suffix = "_" + String(roomId);
const base = String(roomName)
.replace(/[\[\]\*\?\/\\:]/g, "_")
.replace(/[\u0000-\u001F]/g, "")
.trim();
const maxBaseLen = Math.max(1, 31 - suffix.length);
return (base || "room").slice(0, maxBaseLen) + suffix;
}
function sanitizeDriveName_(s) {
return String(s).replace(/[\\/:*?"<>|]/g, "_").trim().slice(0, 80);
}
「意味が分からない…」と感じても大丈夫です。
触るのはここまでです。
ステップ⑤:ChatworkのAPIトークンを取得します
記録を取得するアカウントでログインをしてください。


このトークンは
自分専用の鍵のようなものなので、
外に公開しないようにしてください。
ステップ⑥:必要な設定を登録します(ここが大事)
Apps Scriptの画面で、


① Chatworkのトークン
- キー:
CHATWORK_TOKEN - 値:コピーしたAPIトークン
② 保存先フォルダID
- キー:
CW_EXPORT_FOLDER_ID - 値:ステップ①でコピーした文字列

登録したら保存します。
ステップ⑦:最初のテスト実行
- 関数の選択で
runDailyChatworkExportを選ぶ - 実行ボタンをクリック
- 表示される確認画面で
権限を許可する
<>のマークを押すと戻れます。







少し警告っぽい画面が出ることがありますが、
Google Apps Scriptではよくある表示です。
そのまま進んで大丈夫です。

ルーム(グループチャット)・DMが多ければ多いほど、出力に時間がかかります。この次に説明するトリガー設定を行なって、放置しておきましょう。
お急ぎに場合は、手動で実行を行い、実行ログで実行完了になったら→また実行を何回も繰り返し行うと、どんどん反映されます。
ステップ⑧:スプレッドシートを確認します
スプレッドシートに戻ると、
- 一番左に シート一覧
- その右に 更新履歴
- さらに右に 各ルームのシート
が自動で作られていれば成功です。
ステップ⑨:シート一覧で調整します
「シート一覧」では、
ルームごとに動きを調整できます。
ただし、初回起動のみだとまだ表示がされていません。
- 更新
→ チャットを取得するかどうか - 添付取得
→ 添付ファイルを保存するかどうか

おすすめは、
- 普段:
- 更新:する
- 添付取得:しない
- 必要なルームだけ:
- 添付取得:する
添付は重いので、
必要なときだけONが安心です。
ステップ⑩:毎日0時に自動で動かします
時計アイコン(トリガー)をクリック


runDailyChatworkExport・時間主導型 → 毎日 → 0時
実行関数:runDailyChatworkExport・時間主導型 → 毎日 → 0時
これで、
毎日自動でバックアップされます。
よくある不安
同じ添付ファイルが何度も保存されませんか?
保存されません。
一度保存したファイルには「保存済みの印」が付き、
次回以降は自動でスキップされます。
途中で止まっても大丈夫?
大丈夫です。
次回は続きから再開します。
シート一覧と更新履歴のシートがどっかいきました!



少し手順は多いですが、
設定は最初の1回だけです。
- 日々のチャット管理が楽になる
- 引き継ぎや監査にも安心
- 「もしものとき」に履歴が残っている
そんな仕組みを、
無料で作ることができます。
■ 最後に—デザインの相談はお気軽に。
- LP制作・Meta広告運用代行
- ホームページ制作
- バナー制作
- 採用サイト構築
- セミナー・スクールサイト構築
- Elementor / STUDIO 実装
- LINE公式xエルメ構築
など、あなたの事業に合わせて最適な提案ができます。
「まずは小さく試してみたい」という方も大歓迎です。

