日曜の朝、Chatworkの通知が来なかった。
毎朝9時に走るはずのスケジュール通知。月曜から土曜まで問題なく動いていた。原因は30秒でわかった。Macがスリープしていた。日曜は朝寝坊したからMacを開いていなかった。launchdで組んだバッチ処理は、Macが起きていなければ動かない。当たり前のことだが、それに気づくのは実際に落ちたときだ。
この一件で、自動化の設計を根本から見直した。「何をlaunchdで動かし、何をn8nで動かすか」——この判断基準が固まるまでに3ヶ月かかった。
前提:自分の環境
- Mac(M2 MacBook Air): メインの開発・作業マシン。Claude Codeはここで動く
- VPS(4コア6GB・Docker): n8nを含む15コンテナが24時間稼働
- 自動化の総数: launchd 8本 + n8n 25本 + GASトリガー 12本
3つの実行基盤を使い分けている。この記事ではlaunchdとn8nに絞る。GASは守備範囲が違うので別の話だ。
launchdの得意なこと
ローカルファイルの操作
launchdが唯一無二の強みを持つのは、Macのローカルファイルを直接触れること。
<!-- ~/Library/LaunchAgents/com.team.auto-backup.plist -->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.team.auto-backup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/me/tools/auto-backup.sh</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key><integer>12</integer>
<key>Minute</key><integer>0</integer>
</dict>
<dict>
<key>Hour</key><integer>23</integer>
<key>Minute</key><integer>0</integer>
</dict>
</array>
</dict>
</plist>
たとえばローカルのgitリポジトリを1日2回自動コミットする処理。ファイルの差分を検出して、変更があればコミットする。これはMac上でしか動かない。VPSにリポジトリのクローンを置く方法もあるが、リアルタイムの作業ファイルを扱うならローカルに限る。
Obsidianのvaultをバックアップする処理も同じだ。ローカルのmdファイルを読んで、加工して、gitに入れる。この手の処理はlaunchdの独壇場。
Claude Codeとの連携
Claude Codeのセッション外で、ローカル環境を整備する処理もlaunchd向きだ。
たとえば、深夜にGemini CLIを使ってその日のメモを統合分析するスクリプト。ローカルのファイルを読んで、分析結果をローカルに書き出す。Macが起きてさえいれば、3時に勝手に動いて朝には結果が置いてある。
launchdでやるべきこと(まとめ)
- ローカルファイルの読み書きが必要な処理
- gitの自動コミット・push
- ローカルの静的サイト生成
- Claude Codeのセッション外で動くローカルスクリプト
launchdの不得意なこと
Macがスリープしたら終わり
冒頭の話に戻る。launchdはmacOSのデーモン管理システムだ。Macが起きていなければ動かない。
しかもlaunchdの StartCalendarInterval はスリープ中に予定時刻を過ぎた場合、復帰後にリカバリ実行されることもある。Apple公式ドキュメントには「コンピュータがスリープ中だった場合、起動後に実行される」と書いてある。だが実際には、スリープが長時間続いた場合やmacOSのバージョンによって挙動が安定しない。リカバリされないケースを何度も経験した。
[期待] 毎朝9:00に実行
[現実] 9:00にMacスリープ → 10:30に開く → 実行されない
StartInterval(秒数指定の繰り返し)の場合は、スリープ復帰後に1回だけリカバリ実行される——らしいのだが、これも環境によって挙動が違う。信じないことにしている。
外部APIへの依存
launchdからChatwork APIを叩くスクリプトを動かしていた時期がある。問題は2つ。
- Macがスリープしていたら叩けない(上述)
- Wi-Fiが切れていたらタイムアウトする
移動中にMacのWi-Fiが不安定になると、API呼び出しが中途半端に失敗する。レスポンスを受け取れずにプロセスが残る。ログに curl: (28) Operation timed out が並ぶ。
外部APIを定期的に叩く処理は、常時接続が保証されたサーバーで動かすべきだ。当たり前のことだが、最初は「Macでいいじゃん」と思っていた。甘かった。
エラー通知が面倒
launchdのジョブが失敗したとき、通知する仕組みは自分で作る必要がある。標準出力・標準エラーはログファイルに落ちるが、能動的に見に行かないと気づかない。
n8nなら、Error WFに飛ばしてChatworkに通知する仕組みが10分で組める。launchdでこれをやろうとすると、シェルスクリプト内にエラーハンドリングとcurl通知を自前で書くことになる。できるけど、毎回書くのか? という話だ。
n8nの得意なこと
24時間365日、止まらない
VPS上のDockerで動いているn8nは、Macのスリープに関係なく動き続ける。これが最大のメリット。
[launchd] Macが寝てたら → 止まる
[n8n] VPSが生きてれば → 動く
夜間バッチ、休日の定期処理、5分おきのポーリング——「いつ動いても確実に動く」必要がある処理は全部n8n。
外部API連携が楽
n8nには主要サービスのノードが標準で用意されている。Chatwork、Slack、Gmail、Google Sheets、HTTP Request——ノードを置いて認証情報を入れるだけ。curlを手書きする必要がない。
しかもリトライロジックが組み込まれている。HTTP Requestノードには「失敗時にN回リトライ」の設定がある。launchdのシェルスクリプトでこれをやろうとすると:
MAX_RETRY=3
for i in $(seq 1 $MAX_RETRY); do
response=$(curl -s -w "%{http_code}" ...)
http_code="${response: -3}"
if [ "$http_code" = "200" ]; then
break
fi
sleep $((i * 5))
done
毎回これを書くのか。書かないだろう。n8nならチェックボックスを入れるだけだ。
エラーハンドリングが構造化されている
n8nには「Error Trigger」というノードがある。任意のWFがエラーで停止したとき、別のWFを起動できる。
うちでは全WF共通の「Error WF」を1本作っていて、どのWFが落ちてもChatworkに通知が飛ぶようにしている。WF名・エラー内容・発生時刻が整形されて届く。
{
"workflow": "API監視Bot v2",
"error": "HTTP 429 Too Many Requests",
"timestamp": "2026-04-03T14:22:31Z"
}
これを一元化できるのが大きい。launchdだと、8本のスクリプトそれぞれにエラー通知ロジックを書く必要がある。1箇所直し忘れて、あるスクリプトだけ通知が来ないまま3日落ちてた、ということが実際にあった。
WFの視覚化とデバッグ
n8nのWebUIでは、実行結果がノードごとに色分けされる。成功=緑、失敗=赤、スキップ=グレー。どこで詰まったかが一目でわかる。
ターミナルのログを grep ERROR で漁る作業とは雲泥の差だ。
判断基準:3つの問い
「この処理、launchdとn8nのどっちで動かす?」
3ヶ月の運用で、判断基準が3つに収束した。
問い① ローカルファイルを触るか?
Yesならlaunchd。ローカルのgitリポジトリ、Obsidianのvault、Claude Codeのプロジェクトファイル——これらはMac上にしかない。VPSにはない。
ただし、「ファイルを読むだけ」ならrsyncやSCPでVPSに転送してn8nで処理する方法もある。判断基準は「リアルタイムのファイルが必要か」だ。5分前のファイルでよければVPS側で処理できる。
問い② 24時間動く必要があるか?
Yesならn8n。Macは寝る。人間も寝る。夜間バッチ、休日処理、5分おきのポーリングは全部n8n。
「平日の勤務時間だけでいい」なら、launchdでもいける。ただし「本当に勤務時間だけでいいか?」は慎重に考えるべきだ。最初は「平日だけ」と思っていたのに、あとから「やっぱり休日も」になるパターンを3回経験した。最初からn8nで組んでおけばよかった。
問い③ 外部APIを叩くか?
Yesならn8n。理由は前述の通り、常時接続・リトライ・エラー通知の3点セットがn8nには標準装備されている。launchdでcurlを手書きして同じ信頼性を出すのは、不可能ではないが割に合わない。
フローチャート
ローカルファイルを触る?
→ Yes → launchd
→ No ↓
24時間動く必要がある?
→ Yes → n8n
→ No ↓
外部APIを叩く?
→ Yes → n8n
→ No → どっちでもいい(n8n寄り)
「どっちでもいい」場合はn8nを推す。理由はエラー通知の一元化。launchdを増やすと監視の穴が増える。
実際の配置
参考までに、自分の環境での配置を書いておく。
launchd(8本)
| ジョブ | 頻度 | 理由 |
|---|---|---|
| gitリポジトリの自動コミット | 12:00 / 23:00 | ローカルファイル操作 |
| Obsidian vault バックアップ | 23:30 | ローカルファイル操作 |
| ローカルログのローテーション | 毎日4:00 | ローカルファイル操作 |
| 深夜統合分析(Gemini CLI) | 毎日3:00 | ローカルファイル読み書き |
| その他4本 | 各種 | ローカル依存 |
n8n(25本+)
| WFカテゴリ | 本数 | 理由 |
|---|---|---|
| 外部API監視・通知 | 8本 | 24時間 + 外部API |
| 定期レポート生成 | 5本 | 24時間 |
| Webhook受信・処理 | 4本 | 常時待受 |
| エラーハンドリング | 2本 | 全WF共通基盤 |
| インフラ監視 | 3本 | 24時間 |
| その他 | 3本+ | 各種 |
見ての通り、本数はn8nが3倍以上。「迷ったらn8n」の結果、自然とこうなった。
失敗から学んだこと3つ
①「Macでいいじゃん」は甘い
最初はlaunchdで全部やろうとした。curlでAPIを叩き、jqでパースし、結果をファイルに書き出す。動く。動くのだが、Macをスリープした瞬間に全部止まる。金曜の夜にMacを閉じて月曜に開いたら、土日の処理が全部飛んでいた。
② launchdのログは能動的に見ないと気づかない
/tmp/com.team.*.log にログを吐くようにしていたが、3週間見ていなかった。見に行ったら、あるジョブが2週間前から毎回失敗していた。/tmp は再起動でクリアされるので、古いログは消えていた。ログの場所を /var/log/ に変えて、ログローテーションを入れた。
③ 2つの基盤の監視も自動化する
launchdのジョブが失敗してもn8nは知らない。n8nが落ちてもlaunchdは知らない。2つの基盤が独立しているので、片方が死んでいても気づかない。
解決策として、n8n側にインフラ全体のヘルスチェックWFを作った。15分おきにn8nの全WF + VPSの全コンテナの状態を確認し、異常があればChatworkに通知する。launchd側は日次でVPSに curl を飛ばして疎通確認する。お互いを監視する構造にした。
cronという第3の選択肢
VPS上にはcronもある。n8nとcronの使い分けも簡単に書いておく。
| 観点 | cron | n8n |
|---|---|---|
| 設定方法 | crontab -e |
WebUI or MCP |
| ロジック分岐 | シェルで書く | IFノードで |
| エラー通知 | 自前で書く | Error WFに集約 |
| 視覚的デバッグ | なし | あり |
| APIリトライ | 自前で書く | チェックボックス |
5分おきにCSVにデータを蓄積するだけの処理、APIを叩かないファイル整理——こういう「ロジックがないバッチ」はcronの方がシンプルだ。n8nのノードを組むまでもない。
ただ、cronが増えると「何がどこで動いているか」が見えなくなる。crontab -l の出力を定期的にバックアップしておくことを勧める。
設計パターンの結論
| 処理の性質 | 基盤 | 理由 |
|---|---|---|
| ローカルファイル操作 | launchd | Mac上にしかないファイル |
| 外部API定期実行 | n8n | 24時間 + リトライ + 通知 |
| Webhook受信 | n8n | 常時待受 |
| 単純なファイルバッチ(VPS) | cron | ノードを組むまでもない |
| 判断に迷ったら | n8n | 監視の一元化 |
3ヶ月前、日曜の朝に通知が来なかった。あのとき「launchdでいいじゃん」をやめた。正しい判断だったと思っている。