はじめに
「エンジニアなのにSNS運用は手動でやっている」──心当たりのある方は多いのではないでしょうか。
毎日Xを開いて、ネタを考えて、下書きして、投稿する。1回あたり20〜30分、週5回投稿するなら月に10時間近くの工数です。エンジニアの時間としては決して安くありません。
そこで、SNS運用の「半自動化」システムを作りました。完全自動化ではなく、あえて人間の承認ステップを残した設計にしています。この記事ではアーキテクチャと実装のポイントをまとめます。
環境
- Google Apps Script(GAS)
- Cloud Firestore(Firebase)
- Discord Webhook / Bot API
- Node.js 24.x(ローカルスクリプト用)
- X API v2(Free プラン)
アーキテクチャ全体像
[Notion DB] → 同期スクリプト → [Firestore: raw]
↓
AI生成 → [Firestore: posts]
↓
Discord #approval に通知
↓
ユーザー承認(日時リプライ)
↓
GAS Cron(10分毎)→ X投稿
日常のメモや気づきをNotionに書き溜め、それを定期的にFirestoreに同期。AIで投稿案を生成し、Discordの承認フローを経て予約投稿する流れです。
ポイント1:なぜ「半自動化」なのか
最初は完全自動化を考えましたが、2つの理由でやめました。
1. 誤投稿リスク
AIが生成した文章をそのまま投稿すると、意図しない表現や事実誤認が混じるリスクがあります。個人ブランドに直結するSNSでこれは致命的です。
2. 「自分の言葉」の担保
AIのリライトがうまくいっても、自分の体験や意見が薄まると読者に響きません。最終チェックで「自分ならこう言い換える」を挟むことで、投稿の質を保てます。
承認フローにDiscordを採用した理由は、スマホからでも1タップで承認できるからです。Slackでも同じ構成は可能ですが、個人利用ならDiscordの方が無料枠が広いです。
ポイント2:GAS Cronのハマりどころ
GASのトリガーでCron的な定期実行を実現していますが、いくつか注意点があります。
// トリガー設定
function setupTriggers() {
// 既存トリガーを削除(重複防止)
ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t));
// 10分毎に投稿チェック
ScriptApp.newTrigger('cronPostScheduledPosts')
.timeBased()
.everyMinutes(10)
.create();
// 毎日13:00にDiscord通知
ScriptApp.newTrigger('sendPendingPostsToDiscord')
.timeBased()
.atHour(13)
.everyDays(1)
.create();
}
ハマりポイント:
- GASの
everyMinutes()は1, 5, 10, 15, 30のいずれかしか指定できない - タイムゾーンは
appsscript.jsonで明示しないとUTCになる - 1日の実行時間上限(無料:90分)に注意。10分毎×144回=1440回/日だが、1回の実行が数秒なら問題なし
ポイント3:X API Freeプランの制約
// X API v2 での投稿(OAuth 1.0a)
async function postToX(text) {
const oauth = buildOAuthHeader('POST', endpoint, params);
const res = UrlFetchApp.fetch(endpoint, {
method: 'post',
headers: { Authorization: oauth },
contentType: 'application/json',
payload: JSON.stringify({ text }),
});
return JSON.parse(res.getContentText());
}
Freeプランの制約:
- 月間500投稿まで(1日約16本が上限)
- 予約投稿APIは使えない → 自前でCronを組む必要あり
- Rate limit: 17リクエスト/15分
予約投稿APIが使えない点が最大の制約です。これを回避するために、Firestoreに scheduled_post_time を持たせ、GASのCronで「予定時刻を過ぎた投稿」を検出して投稿する方式にしました。
ポイント4:Firestoreのステータス管理
投稿のライフサイクルをFirestoreのフィールドで管理しています。
posts/{docId}
├── status: "サニタイズ済" → "投稿予約済" → "投稿済"
├── generated_post: "投稿テキスト..."
├── target: "[経営者]" | "[エンジニア]"
├── scheduled_post_time: Timestamp
├── posted_at: Timestamp
├── x_url: "https://x.com/..."
└── media_published: boolean
ステータス遷移がシンプルなので、Firestoreのクエリ1本でフィルタリングできます。
// 投稿予約済みかつ予定時刻を過ぎたものを取得
const now = new Date();
const snapshot = await firestore
.collection('posts')
.where('status', '==', '投稿予約済')
.where('scheduled_post_time', '<=', now)
.get();
運用コスト
| 項目 | 月額 |
|---|---|
| GAS | 無料 |
| Firestore | 無料枠内(月5万読取/2万書込) |
| Discord Bot | 無料 |
| X API Free | 無料 |
| 合計 | 0円 |
サーバーレス構成に寄せることで、ランニングコストをゼロに抑えられています。
まとめ
- SNS運用の半自動化は「完全自動」より「人間の承認を残す」方が品質を保てる
- GAS + Firestore + Discord の組み合わせで運用コスト0円
- X API Freeプランの制約(予約投稿API不可)はFirestore + Cronで回避可能
- 月10時間の手動作業が月1.5時間に削減
エンジニアがSNS運用に時間を取られるのはもったいないです。「仕組みを作る」こと自体がエンジニアリングの練習にもなるので、一石二鳥でおすすめです。
この記事を書いた人
BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。
GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。
👉 BENTEN Web Works — お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中