バンドルをインポートする。apps/web/public/artifacts/inbound-lead-triage-n8n/inbound-lead-triage-n8n.jsonをn8nのWorkflows → Import from Fileからインポートします。フローには2つのエントリポイントがあります。リアルタイムパス用のwebhookと、バックストップスイープ用の日次cronです。
Apollo — Enrich Domainは8sのタイムアウトとneverError: trueでApolloの組織エンリッチエンドポイントを呼び出します。失敗してもフローは止まりません。enrichmentOk: falseのペイロードが生成されるだけで、スコアリングステップはこれを1ポイントのペナルティとして扱うよう指示されています。Merge Lead + Firmographicsは正規化されたリードとエンリッチメントを結合し、Claudeが参照するバンドルを作成します。
Claude — Score Leadはclaude-haiku-4-5、6sのタイムアウト、score・reasoning・primary_pain_hypothesis・disqualifiersを持つ単一のJSONオブジェクトを強制するシステムプロンプトでhttps://api.anthropic.com/v1/messagesにPOSTします。プロンプトは、ファーモグラフィックスが欠損している場合にスコアを1下げること、フォームの回答が実際のロールを証明しない限りフリーメールアドレスは4でキャップすることをClaudeに明示的に指示しています。どちらのルールもコードではなくプロンプトに記述することで、一元的に監査可能になります。
vs cronで動くDIY Pythonスクリプト。 5分ごとにHubSpotをポーリングするcron駆動スクリプトは、最悪ケースで5分、平均で2.5分のレイテンシを追加します。これは高インテントリードへのページングという目的を無効にします。webhookで駆動するn8nフローはハッピーパスでサブ秒です。さらにn8nの実行ログが無料の可観測性レイヤーとなり、スクリプトのstdoutをデバッグする必要がありません。
vs 既製のリードルーター(Chili Piper、Distribute、RevenueHero)。 これらはルーティングとミーティング予約のステップに優れており、最初の接触で予約するモーションであれば購入する価値があります。ただし「ルーブリックでリードをスコアリングしてから何をするか決める」という処理には対応していません。これがこのフローの担う役割であり、両者はきれいに連携できます。ここでルーティングし、スコア上位のリードをChili Piperに渡して予約体験を提供してください。
# Inbound lead triage and routing — n8n flow
This bundle contains a complete n8n workflow that triages every inbound demo request the moment it lands. A HubSpot form submission fires a webhook, the flow normalizes the payload, enriches the company via Apollo, asks Claude to score the lead against your ICP rubric (with a rule-based fallback if Claude is slow or returns malformed JSON), writes the score back to HubSpot, and routes the contact to one of four destinations: self-serve nurture, SDR queue by territory, AE Slack page, or an ops alert if anything looks wrong.
A second independent trigger — a nightly cron — sweeps HubSpot for any demo-submission contact created in the last 26 hours that has no `icp_score__c` property and replays it through the webhook. This is the backstop for silent webhook failures.
## What this flow does
The flow has two entry points:
- **Real-time path** — `Webhook — HubSpot Form Submit` accepts `POST /webhook/inbound-lead-triage`, immediately returns 202 to HubSpot so the form submission is never blocked, and processes the lead asynchronously.
- **Nightly backstop** — `Nightly Backstop Cron` (02:15 daily) finds any HubSpot contact in `subscriber` lifecycle stage with a `recent_conversion_date` in the last 26 hours and no `icp_score__c`, then replays each through the webhook with `batchSize: 5, batchInterval: 2000ms` so the catch-up doesn't burn through the Apollo or Anthropic rate limits.
The scoring prompt enforces a JSON-only response shape and tells Claude to bias the score down by 1 when firmographics are missing and to cap free-mail addresses at 4 unless the form responses prove a real role. If Claude times out (6s) or returns anything other than parseable JSON, the `Parse Score (with fallback)` Code node computes a deterministic score from headcount + job title + free-mail status so the flow never strands a lead.
## Import
1. In n8n, open **Workflows → Import from File** and select `inbound-lead-triage-n8n.json`.
2. Open the workflow's **Settings** and confirm `Execution Order` is `v1` and `Timezone` matches your business hours (defaults to `America/New_York`). The cron and the `recent_conversion_date` window both interpret times in this zone.
3. Set the workflow variable `ICP_RUBRIC` (Settings → Variables) to your ICP rubric Markdown. Keep it under ~2k tokens — it ships in every Claude call.
4. Set the environment variable `N8N_SELF_URL` to the public base URL of your n8n instance so the backstop can call its own webhook.
5. Activate the workflow only after the credentials below are wired and you've completed the first-run verification.
## Credentials
### `PLACEHOLDER_HUBSPOT_CRED_ID` — HubSpot OAuth
Used by `HubSpot — Upsert Score`, `HubSpot — Create SDR Task (mid)`, and `HubSpot — Find Missed Submissions`. Create a private app in HubSpot with scopes `crm.objects.contacts.read`, `crm.objects.contacts.write`, and `crm.objects.tasks.write`. In n8n, add a **HubSpot OAuth2 API** credential and complete the OAuth dance. Before activating, create the custom contact properties `icp_score__c` (number 1-10), `icp_score_reasoning__c` (single-line text), `icp_pain_hypothesis__c` (single-line text), and `icp_scoring_method__c` (single-line text, values `claude` or `rule-based`).
### `PLACEHOLDER_APOLLO_CRED_ID` — Apollo
Used by `Apollo — Enrich Domain`. In Apollo, generate an API key under **Settings → Integrations → API**. In n8n, add an **HTTP Header Auth** credential with header name `X-Api-Key` and value set to the key. Apollo's organization-enrich endpoint is rate-limited per plan; the node is configured with `neverError: true` so a 429 or timeout flows through to the rule-based fallback rather than killing the run.
### `PLACEHOLDER_ANTHROPIC_CRED_ID` — Anthropic
Used by `Claude — Score Lead`. Generate an API key at `https://console.anthropic.com`. In n8n, add an **HTTP Header Auth** credential with header name `x-api-key` and value set to the key. The node uses `claude-haiku-4-5` to keep the call under a second on the typical payload; swap to `claude-sonnet-4-6` only if you see scoring quality drop in your weekly audit.
### `PLACEHOLDER_GSHEETS_CRED_ID` — Google Sheets
Used by `Sheets — Territory Lookup`. Add a **Google Sheets OAuth2 API** credential. The sheet referenced by `PLACEHOLDER_TERRITORY_SHEET_ID` must have a tab named `Territories` with columns `country`, `sdr_email`, `sdr_owner_id`, `sdr_slack_handle`, `ae_email`, `ae_owner_id`, `ae_slack_handle`. Include a `default` row keyed on country code `*` so leads without a country still route somewhere.
### `PLACEHOLDER_SLACK_CRED_ID` — Slack bot token
Used by `Slack — Page AE (high)` and `Slack — Ops Alert (unrouted)`. Create a Slack app, add the `chat:write` bot scope, install it to the workspace, and invite the bot user to `#inbound-hot` and `#inbound-ops-alerts`. In n8n, add an **HTTP Header Auth** credential with header name `Authorization` and value `Bearer xoxb-...`.
### `PLACEHOLDER_SMTP_CRED_ID` — SMTP
Used by `Email — Self-serve Nurture (low)`. Any transactional SMTP provider works (Postmark, SendGrid, SES). Replace the `fromEmail: no-reply@example.com` and the four `https://example.com/...` links in the HTML body before activating.
## First-run verification
Before enabling the HubSpot trigger that fires this webhook in production, prove every branch with manual inputs:
1. **Low-score path.** Use n8n's **Execute Workflow** on the webhook node with a test payload from a free-mail address (`{ "body": { "contactId": "test-1", "contact": { "email": "test@gmail.com", "firstName": "Pat", "company": "Acme" } } }`). Expected: score capped at 4, the `Email — Self-serve Nurture (low)` branch fires, HubSpot upsert writes `icp_score__c: 4`.
2. **Mid-score path.** Send a payload with a corporate domain you know Apollo enriches (your own company is fine). Expected: score between 4 and 7, `HubSpot — Create SDR Task (mid)` creates a task on the contact.
3. **High-score path.** Manually edit the `Parse Score` node output to force `score: 9` and run from there. Expected: Slack message lands in `#inbound-hot` with the company name and pain hypothesis.
4. **Claude failure path.** Temporarily revoke the Anthropic credential and run any payload. Expected: `scoringMethod: "rule-based"` in the HubSpot record and the lead routes by the deterministic rule.
5. **Backstop path.** In HubSpot, set one test contact to lifecycle `subscriber` with a recent conversion event and no `icp_score__c`, then trigger `Nightly Backstop Cron` manually. Expected: the contact appears in the search results and gets replayed through the webhook within one batch interval.
Only after all five paths complete cleanly should you enable the HubSpot workflow that pushes form submissions to this webhook.