マーケ運用の自動化に n8n は必要か ― API × Claude Code で組み直す
問い合わせの受信から分類・CRM登録・通知までのマーケ運用の配管を、ワークフローSaaSを使わずに素のAPIとClaude Code(Agent SDK)で組む実装を示します。トリガー・冪等性・監視・コストの設計と、それでもn8nが正解になる条件まで、運用の実測レンジで書きます。
by Shin
「マーケのオペレーションを自動化するなら、n8n のようなワークフローツールを入れるべきなのか?」——結論から言うと、一人〜少人数の運用なら、多くの場合は不要です。ワークフローSaaSが売っているもの(トリガー・コネクタ・リトライ・可視化)は、cron と自前のAPIルートと数行のコードで置き換えられます。自動化の本体は配管ではなく判断の層の設計——どこまでを決定的なコードで固め、どこだけをLLMに渡すか——であり、そこはツールを何にしても変わりません。この記事では、問い合わせの受信→分類→CRM登録→通知という同じユースケースを、ワークフローSaaSを使わずに素のAPIとClaude Code(Agent SDK)で組む実装と、それでも n8n が正解になる条件を書きます。
この記事は実装に寄せた [lab] 記事です。「なぜ一人のマーケターでも実装で戦えるのか」というビジネス背景は なぜ今、マーケチーム1人でも「実装」で戦えるのか を、自動化の全体設計と環境構築は Claude Codeでマーケティング業務を自動化する実践ガイド を参照してください。本記事はそのうち**イベント駆動の配管をどう持つか(ツール選定込み)**に範囲を絞ります。定期実行ジョブの足回り(GitHub Actions の cron)は Claude Code × GA4 で週次レポートを自動生成する が実装例です。
設計の線引き ― 決定的な層と確率的な層
最初に決めるべきは、ツール選定ではありません。「どこを決定的(deterministic)に動かし、どこを確率的(probabilistic)に任せるか」の線引きです。ここを曖昧にしたまま全部をLLMに投げると、安いはずの自動化が高く・脆くなります。
線引きはシンプルで、100%同じ挙動をしてほしいものはコードで、判断やゆらぎを許せるものだけをモデルに渡す。具体的には次のように分担します。
| 層 | 担当 | 理由 |
|---|---|---|
| トリガー・受信(フォームPOST・定期実行) | 自前のAPIルート/cron(GitHub Actions) | 取りこぼさない・冪等にしたい=決定的であるべき |
| 分岐・ルーティング | コードの if | 条件は明示的でテスト可能であるべき |
| 外部API呼び出し(CRM・Slack・DB) | fetch 数行 | 認証・リトライ・ログを自分の手元に置く |
| 判断(分類・優先度付け・言語判定) | LLM(素のMessages API) | ルールで書ききれない「ゆらぎ」がある |
| 文脈つき生成(返信ドラフト等) | Claude Code / Agent SDK | リポジトリのブランドガイドや過去事例を読ませたい=ツールと文脈が必要 |
この表は、配管を n8n のノードにしても、コードにしても、まったく同じ形になります。つまり「n8n を使うか」は自動化の本質ではなく、配管の持ち方という従属的な選択です。だからこそ、配管に何を選ぶかは「そのツールが売っているものを、自分が本当に必要としているか」で決めるべきです。
ここで一つ、流行に逆らう本音も書いておきます。判断のすべてをClaude Code(Agent SDK)にやらせる必要はありません。
- 入力テキストを4区分に分類するようなステートレスな判断は、素のMessages APIで十分で、その方が速く安い。
- Claude Code=Agent SDK が本当に効くのは、エージェントループとツールが必要な仕事です。たとえば「リポジトリ内のブランドボイス定義(
CLAUDE.md)と匿名化した過去の返信テンプレを読み込み、その文体で初回返信ドラフトを書く」「データを取りに行って小さなスクリプトを走らせてから判断する」といった、ファイル参照・コード実行・複数ステップを伴うタスクです。
つまり本記事の構成では、分類は安価なAPI、文脈つきドラフト生成だけAgent SDKという二段構えにします。全ステップを claude -p に流す実装をよく見かけますが、コストと遅延の両面で割に合わないことが多い、というのが運用しての結論です。
ワークフローSaaSが売っているものと、その置き換え
n8n(や Zapier・Make)が提供している価値を分解すると、実は4つしかありません。一つずつ、素の道具で置き換えられるかを見ます。
① トリガー(Webhook受信・スケジュール)。 フォームのPOSTを受けるのは、Next.js なら Route Handler が1ファイルで済みます。しかも自前のアプリでフォームを持っているなら、フォームとAPIが同一オリジンになるので、n8n 構成で必要だった「自前API→n8nへ転送する区間を共有シークレットで守る」という配管が丸ごと消えます。守る境界が1つ減るのは、行数の話ではなくセキュリティ設計の簡素化です。定期実行(夜間の再スコアリング・未対応リードのリマインド)は GitHub Actions の cron が最も安く、GA4週次レポートで使ったものと同じ足回りがそのまま流用できます。
② コネクタ(数百のSaaS接続)。 ここは冷静に数えるべき所です。このユースケースで接続する外部サービスは Supabase(CRM)と Slack(通知)と Claude API の3つで、どれも fetch 数行で叩けます。コネクタの価値が本当に効くのは、接続先が数十本あり、しかもOAuthトークンの取得・更新・失効をSaaS側に肩代わりさせたいときです(自前で持つとどうなるかは マーケ自動化は「1週間後」に静かに止まる ― OAuthトークン失効の運用 に書いたとおり、それ自体が運用対象になります)。
③ リトライ・エラーハンドリング。 ワークフローSaaSのリトライ設定は便利ですが、中身は「確率的なステップにだけ指数バックオフを付け、決定的なステップは冪等キーで二重実行を吸収する」というコードで数行の原則です。仕組みは後述の実装に含めます。
④ 可視化(実行ログのGUI・フロー図)。 これは正直に、コードでは置き換えられません。フロー図が仕様書を兼ね、非エンジニアが実行ログを目視で追えることには固有の価値があります。ここが必要な組織では n8n が正解になります(最終節で条件を整理します)。
まとめると、一人〜少人数でリポジトリ中心に運用しているなら、①〜③は素の道具で足り、④は(一人なら)自分がコードを読めるので不要——というのが本記事の判断です。
実装:問い合わせ→分類→CRM登録→通知(コード)
全体の流れはこうです。決定的な配管はAPIルートとコードが担い、確率的な2ステップ(分類・ドラフト)だけを外に切り出します。n8n 版と比べると、箱がノードから関数に変わっただけで、形は同じです。
[フォームPOST → Route Handler]
→ 即時に 200 を返す(after() で残りを非同期化)
→ [分類: Messages API] ← ステートレス判断(安価)
→ leadScore で分岐(コードの if)
├─ qualified → [ドラフト生成: Agent SDK サービス] ← 文脈つき生成
│ → [CRM登録: Supabase REST] → [Slack通知(要承認)]
└─ low / spam → [CRM登録のみ] → [日次サマリへ集計]1. 受信(Route Handler・即時応答と非同期処理)
受信で最初に効くのは**「即座に応答を返して、本処理は非同期にする」**設計です。Agent SDK経由のドラフト生成は数秒〜十数秒かかることがあり、その間フォーム送信者を待たせるとUXもタイムアウトも悪化します。n8n では Respond to Webhook ノードでやっていたことを、Next.js では after()(レスポンス送信後に残りの処理を続行するAPI)で書けます。
// app/api/intake/route.ts — 受信・冪等キー発番・非同期処理への切り出し
import { after } from "next/server";
import { classify } from "@/lib/ops/classify";
import { draftReply } from "@/lib/ops/draft";
import { upsertInquiry } from "@/lib/ops/crm";
import { notifySlack } from "@/lib/ops/notify";
export async function POST(req: Request) {
const body = await req.json();
// 冪等キー:同一送信の二重処理を後段で弾くために発番
const intake = {
...body,
intake_id: crypto.randomUUID(),
received_at: new Date().toISOString(),
};
// まず「受け取った」事実だけをDBに残す(この時点で取りこぼしはなくなる)
await upsertInquiry({ ...intake, status: "received" });
// 分類・ドラフト・通知はレスポンス送信後に続行(送信者を待たせない)
after(async () => {
const cls = await classify(intake);
const draft =
cls.lead_score >= 70 ? await draftReply(intake, cls) : undefined;
await upsertInquiry({ ...intake, classify: cls, draft, status: "pending_review" });
await notifySlack(intake, cls, draft);
});
return Response.json({ ok: true }, { status: 200 });
}ポイントは2つです。第一に、分類より先に「受信した」行をDBに入れる。後段のどこで壊れても、問い合わせ自体は received のまま残るので、取りこぼしが構造的に起きません。第二に、フォームと同一オリジンなので、n8n 版で必要だったHMAC署名の検証コード(転送区間の防御)が不要になっています。スパム対策(honeypot・レート制限)は受信側の関心事として別に持ちます——最小構成は 問い合わせフォームの静かな失敗を塞ぐ に書きました。
2. 分類(ステートレス・安価なAPI)
分類は素のMessages APIで十分です。JSONのみを返させるプロンプトにして、受け取った側でスキーマ検証します。高頻度・低単価で回したいので、ここは軽量モデルを指定します(モデルIDはハードコードせず環境変数に)。2026-07-07時点の単価で言えば、この分類は Haiku 4.5(5 per MTok・入力/出力)で十分です。Sonnet 5 世代の登場で上位モデルの単価も下がりましたが(→ §コスト管理)、「分類は安価なモデルで十分」という設計判断そのものは、モデルが安く賢くなるほどむしろ強まります。
// lib/ops/classify.ts — 分類(Messages API 直叩き)+スキーマ検証
const CATEGORIES = ["growth", "ai_impl", "analytics", "advisory", "other"];
export async function classify(intake) {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: process.env.CLASSIFY_MODEL, // 例: 軽量モデル。退役IDを直書きしない
max_tokens: 300,
system:
"あなたはB2Bマーケの問い合わせ仕分け担当です。必ず指定のJSONのみを出力し、前置き・コードフェンスを付けないこと。",
messages: [{
role: "user",
content:
`次の問い合わせを分類せよ。\n本文: ${intake.message}\n会社: ${intake.company ?? "不明"}\n\n` +
`出力スキーマ:\n{"category":"growth|ai_impl|analytics|advisory|other","lead_score":0-100,"language":"ja|en","urgency":"low|mid|high","reason":"50字以内"}`,
}],
}),
});
// LLM出力を信じてそのまま分岐させると、ある日壊れる。必ず検証してから先へ流す
const text = (await res.json()).content?.[0]?.text ?? "";
let parsed;
try { parsed = JSON.parse(text.trim()); } catch { parsed = null; }
const ok =
parsed &&
CATEGORIES.includes(parsed.category) &&
Number.isFinite(parsed.lead_score) &&
["ja", "en"].includes(parsed.language);
// 信頼できないときは安全側(要人手確認)に倒す
return ok ? parsed : { category: "other", lead_score: 0, language: "ja", needs_review: true };
}n8n 版ではこれが「HTTP Request ノード+Code ノード」の2ノードでした。コードにすると1関数です。どちらでも中身は同じで、スキーマ検証を挟んで安全側に倒すという原則の方が、置き場所より重要です。
3. 文脈つき返信ドラフト(ここでAgent SDKを使う)
ここがClaude Codeの出番です。リポジトリ内のブランドボイス定義と匿名化した過去返信を読ませて、その文体でドラフトを書かせます。Agent SDK を薄いHTTPサービスに包んで、配管から fetch で呼びます——実はこの部分、n8n 版の実装でも元からワークフローの外に立つ独立サービスでした。配管を何にしようが、この設計は変わりません。
// draft-service/server.ts — Agent SDK を包む薄いHTTPサービス
import express from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
const app = express();
app.use(express.json());
app.post("/draft", async (req, res) => {
const { message, company, category, language } = req.body;
// Agent SDK:ファイル読取ツールだけ許可(書込・bashは渡さない=最小権限)
const run = query({
prompt:
`repo内の brand/voice.md と brand/reply-templates/${category}.md を読み、` +
`次の問い合わせへの初回返信ドラフトを${language === "en" ? "英語" : "日本語"}で書け。` +
`誇張・断定的な効果保証はしない。署名は入れない。本文のみ出力。\n\n` +
`会社: ${company}\n本文: ${message}`,
options: {
model: process.env.DRAFT_MODEL, // 退役IDを直書きしない
allowedTools: ["Read", "Grep"], // 最小権限:読むだけ
permissionMode: "bypassPermissions", // サンドボックス内・読取専用なので非対話
cwd: process.env.REPO_PATH, // ブランド資産のあるリポジトリ
},
});
let draft = "";
for await (const m of run) {
if (m.type === "assistant") {
for (const b of m.message.content) if (b.type === "text") draft += b.text;
}
}
res.json({ draft: draft.trim() });
});
app.listen(8787, () => console.log("draft-service on :8787"));呼び出し側は leadScore が閾値以上(qualified)のときにのみ実行し、低スコア・スパムには生成コストをかけません。分岐は n8n の Switch ノードではなく、§実装1 の Route Handler 内の if 一行です。
4. CRM登録(Supabase REST・冪等 upsert)
CRMは自前のSupabaseテーブルに入れます。REST(PostgREST)を直接叩き、intake_id で冪等 upsert にします。サービスロールキーは環境変数(デプロイ先の secrets)で管理し、コードに直書きしません。
// lib/ops/crm.ts — Supabase へ冪等 upsert
export async function upsertInquiry(row) {
const res = await fetch(`${process.env.SUPABASE_URL}/rest/v1/inquiries`, {
method: "POST",
headers: {
apikey: process.env.SUPABASE_SERVICE_KEY,
Authorization: `Bearer ${process.env.SUPABASE_SERVICE_KEY}`,
"content-type": "application/json",
Prefer: "resolution=merge-duplicates", // intake_id で冪等 upsert
},
body: JSON.stringify(row),
});
if (!res.ok) throw new Error(`crm upsert failed: ${res.status}`);
}5. 通知(Slack・人間が承認して送る)
最後にSlackへ通知します。ここでドラフトと分類結果を出し、送信は人間が承認する——これが「判断を自動化しない」線引きの実装です。通知は Incoming Webhook への POST 一発で、Block Kit のボタン(承認/却下)を付けられます。ボタンの受け口は Slack の interactivity 設定でもう1本の Route Handler(/api/slack/actions・署名検証つき)を指し、承認されたときだけ実送信の処理を走らせる形にできます。
// lib/ops/notify.ts — 要承認カードを Slack に投稿
export async function notifySlack(intake, cls, draft) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
blocks: [
{ type: "section", text: { type: "mrkdwn",
text: `*新規問い合わせ* \`${cls.category}\` / score *${cls.lead_score}*\n>${intake.message}` } },
...(draft ? [{ type: "section", text: { type: "mrkdwn", text: `*返信ドラフト:*\n${draft}` } }] : []),
{ type: "actions", elements: [
{ type: "button", style: "primary", text: { type: "plain_text", text: "承認して送信" },
value: intake.intake_id, action_id: "approve_send" },
{ type: "button", text: { type: "plain_text", text: "自分で書く" },
value: intake.intake_id, action_id: "manual" },
] },
],
}),
});
}運用しての実測(匿名・レンジ)
以下は2026年5月時点の運用値です(案件特定を避けるためレンジ表記)。当時この配管はワークフローツール(n8n)で持っていましたが、時間と精度を支配するのは分類とドラフト生成のLLM呼び出しで、配管を本記事の構成に替えてもそこは変わりません。実測は当時のものとして残し、書き換えていません。
- 分類精度:4区分で人手照合との一致はおおむね9割前後。誤りはほぼ「
other/要確認」側に倒れるよう閾値を設計しているので、取りこぼし(高スコアの見落とし)は体感でさらに低い。 - 初動時間:フォーム送信からSlackに「分類+ドラフト」が出るまで、ドラフト生成込みで数十秒〜1分台。手動で気づいて初動していた頃(数時間〜半日)と比べると、初動のばらつきが消えたことの方が効きました。
- 取りこぼし:未対応のまま放置される問い合わせは、導入後ほぼゼロ(全件がCRMに
pending_reviewで残るため)。
精度の数字はカテゴリ設計とプロンプト次第で簡単に上下します。ここは「9割出ます」と言い切る種類の数字ではなく、自分のデータ・自分の区分での参考値として読んでください。
監視・エラーハンドリング・コスト管理
自動化は「動いた」より「壊れたと分かる」が本番です。n8n の Error Trigger(全ワークフローの例外を1本に集約する仕組み)がやっていたことを、自前の配管では次の2つで置き換えます。
エラーハンドリング。 非同期処理の全体を try/catch で包み、失敗したら Slack のアラートチャンネルへ投げて、DBの行を status: "error" に更新します。リトライは確率的なステップ(分類・ドラフト)にだけ指数バックオフで付け(for ループで数行です)、CRM登録のような決定的処理は冪等キー(intake_id)で二重実行を吸収します。LLM出力は前述のスキーマ検証ゲートを必ず通し、壊れたJSONは保留側に倒す。「失敗を握りつぶさず、安全側に倒す」が原則です。
監視。 最低限、(1) 受信件数と処理完了件数の差分(received のまま残っている行があれば処理が止まっている)、(2) needs_review 比率(上がったら分類が劣化したサイン)、(3) 1日のSDK/APIコスト、の3つをダッシュボード化します。Supabaseの集計クエリと日次のcronで足ります。受信の事実を最初にDBへ入れる設計(§実装1)にしたのは、この (1) の差分監視を成立させるためでもあります。
コスト管理。 ここは2026年に明確に変わった点があるので、最新情報として強調します。
これを踏まえたコスト設計はシンプルです。
- 高頻度・低単価のステップ(分類)は素のMessages APIで軽量モデルにする。Agent SDKに流さない。
- Agent SDK(ドラフト生成)は qualified リードだけに絞る。
ifで足切りするだけで生成コストは大きく落ちます。 - モデルは用途で使い分ける:分類は軽量モデル、ドラフトのように品質が効く所だけ上位モデル。
- トークンを絞る:プロンプトに過去事例を丸ごと貼らず、Agentのツール(Read/Grep)で必要箇所だけ読ませる。これが結果的に安い。
- ワークフローSaaSを使わないぶん、SaaSの月額がゼロになります。配管のコストは既存のホスティングと GitHub Actions の無料枠に吸収されます。
自分の運用レンジで言うと、分類は1件あたり概ね1円未満〜数円のオーダー、Agent SDK経由のドラフトはツール往復と文脈量しだいで1件あたり数円〜十数円のオーダー。月の問い合わせが数十件規模なら、合計は月で数ドル〜十数ドルに収まりました(2026年5月時点・当時のモデル単価での実測。モデル・入力長で動くので、必ず自分のワークロードで実測してから固定してください)。
それでも n8n が正解になる条件
ここまで「使わずに済む」側を書いてきましたが、ワークフローSaaSが正解になる状況は明確に存在します。自分の状況が次のどれかに当てはまるなら、素直に n8n(や同種のツール)を選ぶべきです。
- コードを書かない人に運用を引き継ぐ。フロー図がそのまま仕様書になり、非エンジニアが実行ログを目視で追える。この価値はコードでは代替できません。一人で完結しない組織では、これだけで採用理由になります。
- 接続先が数十本あり、OAuthをSaaSに肩代わりさせたい。コネクタの本当の価値は接続の手軽さではなく、トークンの取得・更新・失効という運用の肩代わりです。自前で持つとそれ自体が監視対象になります。
- 人間の承認フローをSlackの外で組みたい。多段承認・差し戻し・担当割り当てのようなワークフローUIが必要なら、自作は割に合いません。
- すでに n8n が社内で動いている。動いている配管を思想の違いだけで壊す理由はありません。線引き(決定的/確率的)さえ正しければ、配管はどちらでも成立します。
逆に、一人〜少人数で、資産がリポジトリに集約されていて、接続先が数本なら——本記事の構成のほうが、月額も、守るべき境界も、覚えるツールも少なくて済みます。
ここまでが、ワークフローSaaSを使わずに、素のAPIとClaude Code(Agent SDK)で問い合わせ対応を「取りこぼさない・初動が一定・全件記録される」状態にする実装でした。鍵は派手なエージェントではなく、確率的な層を最小限に絞り、決定的な層で固める設計の方にあります。その線引きが正しければ、配管が n8n でもコードでも自動化は成立します——だからこそ、配管には「自分が本当に必要とするもの」だけを選んでください。
同じ「問い合わせを分類する」でも、スパム判定のように誤りコストが非対称な問題では、LLM を判断に使わないのが正解になります。その対比は LLM API 無料で回すスパム分類 に書きました。なぜこの規模の実装を一人のマーケターが持てるのか——その事業的な意味は なぜ今、マーケチーム1人でも「実装」で戦えるのか に。自動化の全体設計と環境構築の基礎は Claude Codeでマーケティング業務を自動化する実践ガイド に書いています。