SNS自動化というと、「毎朝9時に投稿する」「AIに投稿文を書かせる」くらいの話になりがちだ。
でも本当に難しいのは投稿ボタンではない。
正しい記事だけを配る。二重投稿しない。1サービスが止まっても全停止しない。CAPTCHAを無理やり突破しない。人間にしかできない仕事だけ人間へ渡す。そして、配った後の結果から次の配り方を変える。
ここまでつながって、ようやく「配信が自動化された」と言える。
12言語、複数SNS、メルマガまで扱うなら、配信はもはや予約投稿機能ではない。小さな分散システムである。
1. 完成形は「完全無人」ではない。「人間が例外だけ処理する」
自動化の目標を「人間が一切触らない」にすると、設計がおかしくなる。
現実の外部サービスには、人間であることを確認するCAPTCHA、SMS認証、2FA、本人確認、利用規約への明示同意、開発者アプリの審査などがある。これは故障ではない。サービス側が意図的に「ここは本人を通す」と決めた境界だ。
だから完成形はこうなる。
記事生成 → 品質確認 → 各言語版 → 本番公開 → 本番HTMLの読み戻し → 配信候補 → 各言語の投稿文 → SNS・メルマガ → 実績回収 → 次回の配信判断
途中で人間しか処理できない箇所が来たら、その1件だけ人間へ渡す。
たとえば日本語XだけCAPTCHAなら、日本語Xだけ待つ。英語Bluesky、フランス語Facebook、メルマガ、アクセス分析、次の記事生成まで一緒に正座させる必要はない。
CAPTCHA 1個で12言語全部が喪に服す必要はない。
Cloudflare Workflowsのようなdurable workflow製品も、長時間の状態保持、失敗ステップの再試行、外部イベントや人間承認待ちを想定している。 大事なのは特定製品を使うことではなく、「人間待ちはシステム停止ではなく、状態の一つ」として持つことだ。
2. 「記事ができた」と「配ってよい」を分ける
配信レイヤーを記事生成と直結すると事故りやすい。
Markdownが保存された。翻訳が終わった。ビルドが通った。
どれも「読者が今そのURLを正しく読める」とは限らない。
だから配信可能な単位を、記事名ではなく、
articleId × locale × contentSha
で持つ。
さらにSNSでは、
articleId × locale × contentSha × platform × campaignType
まで細かくする。
ここで重要なのがcontentShaだ。同じURLでも本文が更新されれば別revisionである。古い本文に対して作った投稿文を、新しい本文の告知として流してはいけない。
そして配信へ渡す条件は「GitHubにある」ではなく、そのlocale、そのcontentShaの本番HTMLを実際に読み戻して確認済みであること。
投稿自動化の入口に必要なのは「原稿がある?」ではなく「読者が今開ける完成物がある?」だ。
3. タイムアウト=失敗、ではない。ここを間違えると双子投稿が生まれる
外部APIで怖いのは、明確なエラーより曖昧なエラーだ。
投稿APIへ送信した。こちらはタイムアウトした。返事がない。
このとき、
「失敗したっぽい。もう一回送ろう」
とやると、1回目が実は成功していた場合に同じ投稿が2つ生まれる。
APIが返事しなかったので念のため同じ記事を3回投稿しました、は自動化界の怪談である。
Cloudflare Queuesは既定でat-least-once deliveryを採用しており、まれに同じメッセージが複数回配送され得る。そのため公式ドキュメントも、一意IDやidempotency keyを使った重複排除を勧めている。
そこで配信ごとに決定的なキーを作る。
sha256(articleId + locale + contentSha + platform + campaignType)
受領テーブル側はこのキーをUNIQUEにする。
タイムアウトしたら即再投稿ではなく、
- 受領記録を確認する
- provider側で投稿済みか読み戻せるなら確認する
- 未投稿と確認できた場合だけ再試行する
という順にする。
Queueは「絶対1回だけ実行される魔法」ではない。重複しても結果を1回分に収束させるのが設計だ。
4. 障害は「システム全体」ではなく「最小scope」に閉じ込める
自動化で一番もったいないのは、1件のエラーを全体障害へ昇格させることだ。
最低でもfailure domainを、
platform × locale × account
に分ける。
日本語Xで認証切れなら、そこだけBLOCKED。
英語Blueskyが429なら、そこだけRETRY_WAIT。
あるFacebook Pageが本人確認待ちなら、そこだけHUMAN_ACTION_REQUIRED。
他は動く。
状態も「成功 / 失敗」の2値だけでは足りない。
- READY
- ACTIVE
- DEGRADED_BUT_RUNNING
- HUMAN_ACTION_REQUIRED
- BLOCKED_PROVIDER
- RETRY_WAIT
- DISABLED_BY_POLICY
くらいに分けた方が現実を表現できる。
全15系統のうち14系統が動いて1系統だけ人間待ちなら、overallは「全停止」ではない。DEGRADED_BUT_RUNNINGに近い。
429に根性論は通じない。Retry-Afterがあるなら従う。5xxは上限付き指数バックオフ。401/403は正規のcredential refresh経路があるなら一度修復し、それでも駄目なら対象アカウントだけ止める。
CAPTCHAや人間確認は自動retry loopに入れない。100回叩いて人間に進化するAPIはない。
5. Human Handoff Queueは「助けて」ではなく作業指示書にする
人間へ渡す仕組みが雑だと、自動化の最後に巨大な手作業が残る。
悪い引き渡しはこれだ。
「SNSが止まっています。確認してください。」
何を? どこで? ログインだけ? 設定変更も必要? 終わったらどうする?
正しいhandoffは、1件ごとに最低限、
- platform
- locale
- account
- blockerType
- 検出時刻
- 再試行可能時刻
- 開くURL
- 人間がやること
- やってはいけないこと
- 完了後に自動系が何を再確認するか
- このblockerが止めるscope
- 他作業を継続できるか
を持つ。
たとえば、
「この言語の公式アカウントでログインし、表示中のCAPTCHAだけ完了してください。投稿設定やプロフィールは変更しないでください。完了後は次回巡回で認証状態を読み戻し、canary投稿から再開します。」
なら、一回で分かる。
そして重要なのは、CAPTCHAを解くことを自動化しないことだ。
solverを使う、challengeを偽装する、非公式経路で回避する。これは自動化ではなく、相手が設定した境界を破る方向に行っている。
人間は毎日の投稿担当から外し、機械が正当に越えられない境界だけを引き受ける。
パスワード、OAuth access/refresh token、session cookie、SMS/2FA code、recovery code、private API secretはGitHubや通常ログへ残さない。GitHubへ残すのはpublic handle、状態、sanitized error、receipt ID、public post URLなど、再開に必要な非秘密情報だけにする。
6. 自動投稿とengagement botを混ぜない
公式アカウントが自サイトの記事を自動投稿することと、いいね・フォロー・返信・DMまで自動化することは別問題だ。
Xの2026年4月版Automation Rulesは、ルールに従った有用・情報提供目的の自動投稿を認める一方、APIのrate limit回避、Webサイトをスクリプト操作する非API自動化、スパム的・重複的な投稿を禁じている。また自動いいねは禁止されている。
だから初期値は地味なくらいでよい。
- AUTO_PUBLISH = true
- AUTO_LIKE = false
- AUTO_FOLLOW = false
- AUTO_UNFOLLOW = false
- AUTO_REPLY = false
- AUTO_DM = false
まず「公式記事を正しく届ける」だけに責任範囲を絞る。
Blueskyも公式APIで投稿を作成でき、投稿にはlanguage情報を持たせられる。 つまり、多言語運用では「英語投稿文を全部へ投げる」より、localeごとに本文とlanguage metadataを合わせる設計が自然だ。
ブラウザ自動操作はアカウント初期設定など、公式APIがない・人間操作が許されている局面の補助に限定する。定常配信は、可能な限り公式APIと公式認証へ寄せる。
初期チャネルは、たとえば ja→X、en→X/Bluesky、ko→X、zh-Hans→Weibo、zh-Hant→Facebook/Threads、es・pt-BR・id・th・vi・fr→Facebook、de→Facebook/X のようにlocaleごとに決める。Instagram、TikTok、Reelsのような画像・動画中心の面は、カード生成とmedia QAが安定してから第二段階へ回す。プラットフォームのAPI・Automation Policy・利用条件は実装時点で必ず再確認する。
7. 12言語運用は「日本語投稿の翻訳大会」にしない
多言語サイトでありがちな事故がある。
日本語記事を12言語へ翻訳した。 日本語のSNS文を1個作った。 それも11言語へ翻訳した。 完成。
これでは本文を12言語化した意味が薄い。
投稿文はそのlocaleの実際の公開本文を読んで、その言語のサイト公式アカウントが一言漏らすように作る。
基本形は短い。
これ、地味に一番困るやつ。🫠 URL
あるいは、
そこ自動化できるんだ。👀 URL
長い要約や「必見」「衝撃」「今すぐチェック」は要らない。サイト自身の投稿なのに、第三者が偶然見つけて感動したようなfake testimonialを作るのもおかしい。
ブランド人格は共通にしつつ、言語ごとに自然な文章にする。別人が運営しているように偽装しない。
さらにmedical、legal、investment、大きなお金、災害、犯罪、死亡、自傷、暴力、性被害、未成年、安全、政治・選挙、強い対立はserious modeへ切り替える。
その場合は原則emojiなし、煽りなし、記事以上の断定なし。政治記事ならendorsementを自動で足さない。
「ネタ全開」は万能設定ではない。救急車の記事に🫠を置く必要はない。
8. メルマガは「SNSのメール版」ではなく、同じ配信思想を別adapterへ載せる
メルマガも本文生成から直接sendしてはいけない。
最低限、
- explicit opt-in
- locale
- topics
- consentAt
- status
- unsubscribeAt
- createdAt
を持ち、同じ記事を何度も送らない。
問い合わせ用メールアドレスと大量配信の仕組みも論理分離する。
読者からの返信先は既存窓口を使えても、送信基盤は後から専用providerへ交換できるadapterにしておく。
Gmailは大量送信者に対し、送信認証、迷惑・未承諾メールの回避、解除しやすい仕組みを要求しており、subscription messageではunsubscribeを重要な要件としている。
購読解除は「次回バッチでたぶん止まります」ではなく、即時に配信対象から外す。
メルマガの価値はアドレスを集めることではない。読者が頼んだ情報を、頼んだ頻度で、やめたい時にすぐやめられることだ。
9. KPIを「いいね」にすると、いいねを取りに行く機械が完成する
自動改善を入れるなら、何を目的関数にするかが重要だ。
SNSのいいね数だけを最大化すると、刺激の強いタイトル、極端な言い切り、炎上寄りのネタが勝ちやすくなる。
でも記事サイトで本当に欲しいのは別だ。
優先順位をたとえば、
- site visit
- meaningful reading
- next article
- return visit
- newsletter signup
とする。
「その投稿でサイトへ来たか」「ちゃんと読んだか」「次の記事へ進んだか」「また来たか」を重く見る。
配信campaignも、
- NEW
- UPDATED
- TRENDING
- POPULAR
- EVERGREEN
に分ける。
誤字を1文字直しただけで「大型アップデート!」と再投稿しない。UPDATEDはmaterial updateだけ。
POPULARやTRENDINGも、既存の実アクセス計測があるならそれを再利用する。配信用に別の人気ランキングを作り、数字が二つになって戦争を始める必要はない。
配信時間も「日本語だから20時」のような固定観念ではなく、実際の locale × country × platform × weekday × hour を見て探索する。最初は複数slotを試し、データがたまってから寄せる。
10. 実際の使い方――「投稿する」ではなく、状態を次へ進める
実運用は次の一連の状態遷移として考えると分かりやすい。
- 対象localeの記事が本番公開される。
- exact contentShaの本番HTMLを読み戻し、PRODUCTION_VERIFIEDにする。
- articleId × locale × contentSha × platform × campaignTypeで配信候補を作る。
- そのlocaleの本文を読んで短い投稿文を生成する。
- hype禁止、serious mode、長さ、URL、言語を検査する。
- idempotency key付きでoutboxへ入れる。
- providerの公式APIから送る。
- API receiptだけでなく、可能ならpublic postを読み戻す。
- サイト流入・読了・次記事・再訪を回収する。
- NEW / UPDATED / TRENDING / POPULAR / EVERGREENの次回判断へ使う。
途中でCAPTCHAが出たら、そのscopeだけHUMAN_ACTION_REQUIREDへ。
429ならRETRY_WAITへ。
providerが落ちていればBLOCKED_PROVIDERへ。
他は進む。
最初から15アカウントへ一斉射撃もしない。adapterごとに、
auth readback → dry run → 実記事1件のcanary → receipt → public readback → locale確認 → duplicate suppression確認 → analytics attribution確認
まで通してから広げる。
そして運用者が見る入口は、一つのcurrent-statusにまとめる。
実装上も配信用に第二の記事工場・第二schedulerを生やすより、PRODUCTION_VERIFIED後のpost-publication childとして既存publication ownerへ接続し、既存のdurable runtime/state storeを再利用する方が状態の正本を増やしにくい。
「今どうなってる?」に対して、15個のJSONとログを人間に発掘させたら、その時点で自動化が人間へ仕事を返している。
自動配信で一番大切なのは、「何も失敗しないこと」ではない。
失敗しても、どこが止まったか分かり、止まる範囲が小さく、人間にしかできない部分だけ人間へ渡り、残りは勝手に進み、復旧後は二重実行せず続きから戻れること。
投稿ボタンを消すのは序章だ。
本当の自動化は、失敗した日の運用まで自動化することである。
