Apollo、Output.ai、Zapier SDKでHubSpotのCompaniesをOAuthなしで強化する

Dev.to / 2026/4/15

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • この記事は、Apolloによるエンリッチメント、Zapier SDK、LLM(大規模言語モデル)によるフィールド・マッピングを組み合わせることで、HubSpotのOAuth/トークン処理コードを書かずにHubSpotの会社エンリッチメント・ワークフローを実現する内容を紹介する。
  • HubSpotの `industry` フィールドは固定のenumである一方、Apolloは自由形式のindustry文字列を返すため、このワークフローでは実行時に最新のHubSpotのenumリストを取得し、Claude Haikuを使って最も近いセマンティック・マッチを選択する。
  • パイプラインは、Webサイトから会社ドメインを抽出し、Apolloでエンリッチ(業種、従業員数、資金調達ステージ、LinkedIn、所在地、キーワード)を行った上で、Zapier SDKのアクションを使ってエンリッチ済みペイロードをHubSpotにアップサートする。
  • 4つのステップで構成されており、独立した呼び出し(ApolloのエンリッチメントとHubSpotの業種選択肢の取得)は並列実行する。また、小さなTypeScript/Zodのコードベースの構成と、プロンプト駆動のマッピング関数が用意されている。
  • この設計は可搬性を重視しており、同じセマンティック・マッピングのパターンを、CRMアプリキーとenumのソースを差し替えることで、他のZapier対応CRMにも再利用できることを位置付けている。

これまでにHubSpotにワークフローを配線したことがあるなら、そのつらさを知っているはずです。OAuthフロー、トークン更新、スコープ、そしてHubSpot APIのあらゆる変更に追随し続ける必要があるSDK。この記事では、別のアプローチを紹介します。豊富なエンリッチメントデータには直接のREST APIを使い、CRMへの書き込みのロングテールはZapier SDKで行い、両者の間をLLMがセマンティックに“つなぐ”というものです。

このワークフローは会社のWebサイトを受け取り、Apolloでエンリッチし、返ってきた業種文字列をClaude Haikuで有効なHubSpotのenumにマッピングして、HubSpotにレコードをアップサートします。全4ステップで、そのうち2つは並列実行、そしてHubSpotの認証コードは一切不要です。

Why this shape?

HubSpotのindustryフィールドは、フリーテキストではなく事前定義されたドロップダウンです。Apolloは"Internet Software & Services"のようなフリーフォームの文字列として業種を返します。マッピングテーブルをハードコードするのは脆いです。HubSpotは選択肢を追加・削除し、さらにApolloのタクソノミーは非常に巨大だからです。

そこでワークフローは、Zapier SDKを使って実行時に現在のHubSpotのenumリストを取得し、Claude Haikuに最も近いセマンティックな一致を選ばせます。HubSpotをSalesforce、Pipedrive、または他のZapier対応CRMに置き換える場合は、アプリキーを変更するだけです。マッピングのパターンは引き続き機能します。

The four steps

  1. Apolloでエンリッチ — 入力されたWebサイトからドメインを抽出し、Apolloの組織APIを呼び出して、業種、従業員数、資金調達フェーズ、LinkedIn、所在地、キーワードを取得します。
  2. HubSpotの業種を取得 — Zapier SDKのlistInputFieldChoicesヘルパーを使って、HubSpotで有効な業種enum値の“現在の集合”を取得します。
  3. Claudeで業種をマッピング — Apolloの生の業種文字列と、HubSpotのenumリスト全体をclaude-haiku-4-5に渡し、最良のセマンティック一致を1つ選ばせます。Apolloが業種を返さない場合はスキップします。
  4. HubSpotにアップサート — エンリッチされたペイロード(マッピングされたHubSpotの業種を含む)を、Zapier SDKのsearch_or_writeアクションに渡します。

ステップ1と2は並列で実行されます。どちらも独立した呼び出しなので、直列化する理由はありません。

File structure

zapier_hubspot_company_enrichment/
├── workflow.ts          # オーケストレーション — 並列取得、次にマップ、最後にアップサート
├── steps.ts             # 4ステップ: エンリッチ, fetchIndustries, mapIndustry, upsert
├── types.ts             # ZodスキーマとTypeScript型
├── prompts/
│   └── map_hubspot_industry@v1.prompt  # Haiku — セマンティックな業種マッピング
└── scenarios/
    └── stripe.json      # テスト入力: Stripe

2つの共通クライアントがI/Oを担当します:

  • apollo.tsenrichOrganizationApollo API 経由でドメインから会社をエンリッチします。
  • zapier.tscreateZapierClient は、@outputai/credentialsから読み込んだ認証情報でZapier SDKを初期化します。

workflow.ts

Apolloによるエンリッチと、HubSpotの業種取得は、Promise.allで並列に実行します。LLMのマッピングステップは条件付きです。Apolloが業種を返さなかった場合、マッピングするものがないため、そのままundefinedをアップサートへ渡します。すべての呼び出しはstep()でラップされているため、Outputはそれぞれを追跡し、再試行し、キャッシュできます。

import { workflow } from '@outputai/core';
import {
  enrichCompanyWithApollo,
  fetchHubspotIndustries,
  mapHubspotIndustry,
  upsertHubspotCompany,
} from './steps.js';
import { workflowInputSchema, workflowOutputSchema } from './types.js';

export default workflow({
  name: 'zapier_company_enrichment',
  description:
    'Enriches a company profile using Apollo via REST API and upserts the result into HubSpot via Zapier SDK',
  inputSchema: workflowInputSchema,
  outputSchema: workflowOutputSchema,
  fn: async (input) => {
    // ステップ1 + 2 — Apolloでエンリッチしつつ、HubSpotの業種選択肢を並列に取得
    const [apolloData, { industries }] = await Promise.all([
      enrichCompanyWithApollo({ website: input.website }),
      fetchHubspotIndustries(),
    ]);

    // ステップ3 — Apolloの生の業種文字列を、有効なHubSpotのenumにマップ(何かしら返ってきた場合)
    const hubspotIndustry = apolloData.industry
      ? (
          await mapHubspotIndustry({
            industry: apolloData.industry,
            hubspotIndustries: industries,
          })
        ).hubspotIndustry
      : undefined;

返却形式: {"translated": "翻訳されたHTML"}

// ステップ 4 -- Zapier 経由で HubSpot に、強化 + マッピング済みの会社情報を upsert する const { hubspotCompanyId, action } = await upsertHubspotCompany({ ...apolloData, hubspotIndustry, }); return { companyName: apolloData.name, website: input.website, hubspotCompanyId, apolloData, action, }; }, });

steps.ts

Apollo ステップは、クライアント呼び出しの前に入力 URL からドメインを抽出します。Apollo の参照は完全な URL ではなく、ドメインをキーにしています。fetchHubspotIndustries は、HubSpot の会社オブジェクト上の industry ドロップダウンに対する、実際の列挙(enum)値をページングして取得します。mapHubspotIndustry は、システム/ユーザーテキストをその場でインライン展開するのではなく、プロンプトファイルに委譲します。アップサートステップでは、検索キーとして search_or_writename を使用するため、Zapier は名前で既存の HubSpot 会社を更新するか、存在しなければ新しく作成します。connectionId は、書き込み先がどのユーザーの HubSpot アカウントであるかを識別します。

import { step } from '@outputai/core';
import { generateText, Output } from '@outputai/llm';
import { enrichOrganization } from '../../shared/clients/apollo.js';
import { createZapierClient } from '../../shared/clients/zapier.js';
import {
  enrichCompanyInputSchema,
  apolloCompanySchema,
  fetchHubspotIndustriesOutputSchema,
  mapHubspotIndustryInputSchema,
  mapHubspotIndustryOutputSchema,
  hubspotUpsertInputSchema,
  hubspotUpsertOutputSchema,
  zapierHubspotResponseSchema,
} from './types.js';

const HUBSPOT_CONNECTION_ID = 'your-hubspot-connection-id';

function extractDomain(website: string): string {
  const url = new URL(website);
  return url.hostname.replace(/^www\./, '');
}

// --- ステップ 1: Apollo REST API 経由で会社情報を強化する ---

export const enrichCompanyWithApollo = step({
  name: 'enrich_company_with_apollo',
  description: 'Enriches company data using Apollo REST API directly',
  inputSchema: enrichCompanyInputSchema,
  outputSchema: apolloCompanySchema,
  fn: async ({ website }) => {
    const domain = extractDomain(website);
    const org = await enrichOrganization(domain);

    if (!org?.name) {
      throw new Error(`Apollo returned no data for domain: ${domain}`);
    }

返却形式: {"translated": "翻訳されたHTML"}return {
      name: org.name,
      website: org.website_url ?? website,
      domain: org.primary_domain ?? domain,
      industry: org.industry ?? undefined,
      employeeCount: org.estimated_num_employees ?? undefined,
      estimatedRevenue: org.annual_revenue_printed ?? undefined,
      description: org.short_description ?? undefined,
      linkedinUrl: org.linkedin_url ?? undefined,
      city: org.city ?? undefined,
      country: org.country ?? undefined,
      keywords: Array.isArray(org.keywords) ? org.keywords : undefined,
      totalFunding: org.total_funding ?? undefined,
      latestFundingRound: org.latest_funding_round_date ?? undefined,
      fundingStage: org.latest_funding_stage ?? undefined,
    };
  },
});

// --- Step 2: Zapier SDK を介して HubSpot の業種エヌム選択肢を取得する ---

export const fetchHubspotIndustries = step({
  name: 'fetch_hubspot_industries',
  description: 'Zapier SDK を介して利用可能な HubSpot の業種フィールドの選択肢を取得します',
  outputSchema: fetchHubspotIndustriesOutputSchema,
  fn: async () => {
    const zapier = createZapierClient();

    const industries: string[] = [];
    for await (const item of zapier
      .listInputFieldChoices({
        appKey: 'hubspot',
        actionType: 'search_or_write',
        actionKey: 'company_crmSearch',
        inputFieldKey: 'industry',
        connectionId: HUBSPOT_CONNECTION_ID,
      })
      .items()) {
      const value = item.value ?? item.key ?? item.label;
      if (value) industries.push(value);
    }

    return { industries };
  },
});

// --- Step 3: LLM を介して Apollo の業種文字列を HubSpot のエヌムにマッピングする ---

返却形式: {"translated": "翻訳されたHTML"}export const mapHubspotIndustry = step({
  name: 'map_hubspot_industry',
  description: 'LLMを使って、生の業種文字列を有効なHubSpot業種enum値にマッピングします',
  inputSchema: mapHubspotIndustryInputSchema,
  outputSchema: mapHubspotIndustryOutputSchema,
  fn: async ({ industry, hubspotIndustries }) => {
    const { output } = await generateText({
      prompt: 'map_hubspot_industry@v1',
      variables: {
        industry,
        hubspotIndustries: hubspotIndustries.join(', '),
      },
      output: Output.object({ schema: mapHubspotIndustryOutputSchema }),
    });

    return output;
  },
});

// --- Step 4: Zapier SDK経由でHubSpotへアップサート ---

export const upsertHubspotCompany = step({
  name: 'upsert_hubspot_company',
  description:
    'Zapier SDKを介して、豊富化したApolloデータを使ってHubSpotの会社レコードを作成または更新します',
  inputSchema: hubspotUpsertInputSchema,
  outputSchema: hubspotUpsertOutputSchema,
  fn: async (input) => {
    const zapier = createZapierClient();

    const domain = input.domain ?? extractDomain(input.website ?? '');

    const inputs = {
      first_search_property_name: 'name',
      first_search_property_value: input.name,
      name: input.name,
      domain: domain ?? '',
      website: input.website ?? '',
      city: input.city ?? '',
      country: input.country ?? '',
      industry: input.hubspotIndustry ?? '',
      numberofemployees: input.employeeCount ? String(input.employeeCount) : '',
      description: input.description ?? '',
      linkedin_company_page: input.linkedinUrl ?? '',
      total_money_raised: input.totalFunding ? String(input.totalFunding) : '',
    };const { data: result } = await zapier.apps.hubspot.search_or_write.company_crmSearch({
      inputs,
      connectionId: HUBSPOT_CONNECTION_ID,
    });

    const [record] = zapierHubspotResponseSchema.parse(result);

    return {
      hubspotCompanyId: record.id,
      action: record.isNew ? 'created' : 'updated',
    };
  },
});

types.ts

Apollo のレスポンスには多くの任意フィールドがあるため、スキーマでは .optional() をふんだんに使用しています。 hubspotUpsertInputSchema は Apollo のスキーマを、単一の hubspotIndustry フィールド(LLM でマッピングされた値)を追加することで拡張します。ワークフローの出力には、 action の判別子(created または updated)が含まれるため、アップサートが新しいレコードを挿入したのか更新したのかを呼び出し側で把握でき、たとえば「新しい会社が登録されたら営業に通知する」といった下流トリガーに役立ちます。

import { z } from '@outputai/core';

export const workflowInputSchema = z.object({
  companyName: z.string().describe('The name of the company to enrich'),
  website: z.string().url().describe('The company website URL (e.g. https://acme.com)'),
});

export const apolloCompanySchema = z.object({
  name: z.string(),
  website: z.string().optional(),
  domain: z.string().optional(),
  industry: z.string().optional(),
  employeeCount: z.number().optional(),
  estimatedRevenue: z.string().optional(),
  description: z.string().optional(),
  linkedinUrl: z.string().optional(),
  city: z.string().optional(),
  country: z.string().optional(),
  keywords: z.array(z.string()).optional(),
  totalFunding: z.number().optional().describe('Total funding raised in USD'),
  latestFundingRound: z.string().optional(),
  fundingStage: z.string().optional(),
});

返却形式: {"translated": "翻訳されたHTML"}export const workflowOutputSchema = z.object({
  companyName: z.string(),
  website: z.string(),
  hubspotCompanyId: z.string(),
  apolloData: apolloCompanySchema,
  action: z.enum(['created', 'updated']),
});

export const hubspotUpsertInputSchema = apolloCompanySchema.extend({
  hubspotIndustry: z.string().optional(),
});

The prompt

claude-haiku-4-5 は、語彙が制限された分類タスクには十分です。temperature: 0 により、同じ入力に対して対応付けが決定的になります。HubSpot の列挙(enum)リスト全体は実行時にシステムメッセージへ補間されるため、モデルは選択すべき正確な語彙を持ちます。— HubSpot が拒否するような業種の値をモデルが幻覚的に生成してしまうリスクはありません。

---
provider: anthropic
model: claude-haiku-4-5
temperature: 0
maxTokens: 256
---

<system>
あなたは、企業の業種文字列を HubSpot の事前定義済みの業種 ENUM 値へ対応付けることに精通した専門家です。

業種カテゴリが与えられたら、最も適切に一致する 1 つの HubSpot 業種値を返してください。

有効な HubSpot 業種値:
{{ hubspotIndustries }}

ルール:
- 上のリストから、ちょうど 1 つの値だけを返す
- 完全に一致する文字列でなくても、最も近い意味的な一致を選ぶ
- 妥当な一致が存在しない場合は、最も近いカテゴリを返す
</system>

<user>
この業種を HubSpot の業種値に対応付けてください:

業種: {{ industry }}
</user>

Zapier SDK について

Zapier SDK は、Zapier の 9,000 件以上のアプリ連携へのプログラムによるアクセスを提供する TypeScript ライブラリです。OAuth フロー、トークンの更新、アプリごとの API 特有の癖を自分で管理する代わりに、SDK はユーザーが既に持っている Zapier の接続(connections)を通じてアクションを実行します。

概念 説明
アプリキー 連携されたアプリの識別子(hubspotslackgoogle_calendar、…)
接続 特定のアプリに紐づけられた、ユーザー認証済みのアカウント(接続 ID により識別されます)
アクション search(検索)、write(作成/更新)、read(一覧取得)、または search_or_write(アップサート)

認証では、クライアント ID とシークレットのペアを使用します:

import { createZapierSdk } from '@zapier/zapier-sdk';

const zapier = createZapierSdk({
  credentials: { clientId: '...', clientSecret: '...'},
});

アクションは、連鎖した zapier.apps.<appKey>.<actionType>.<actionKey>() のパターンで呼び出されます:

const { data: result } = await zapier.apps.hubspot.search_or_write.company_crmSearch({
  inputs: {
    first_search_property_name: 'name',
    first_search_property_value: 'Stripe',
    name: 'Stripe',
    domain: 'stripe.com',
    industry: 'COMPUTER_SOFTWARE',
  },
  connectionId: HUBSPOT_CONNECTION_ID,
});

実行アクションに加えて、SDKはメタデータのヘルパーも提供します。listInputFieldChoices は、ドロップダウン/enum 入力として受け付けられる現在の値の集合をページングして取得するため、ワークフローは古いハードコードのリストではなく、常に最新の語彙を参照できます。

借りる価値のあるパターン

ライフサイクルステージ、リード元、ディールステージ、チケットの優先度といったドロップダウンフィールドを用いて下流のシステムに書き込む処理を行うたびに、listInputFieldChoices と手頃なモデルを組み合わせることで、コードのリリースなしに、上流側の語彙変更にも統合を耐えられるものにできます。

より大きなパターン――豊富なエンリッチメントデータのためのダイレクトAPI、CRM書き込みのロングテール向けのZapier SDK、そしてそれらをつなぐ意味的な“接着剤”としてのLLM――は、一方のソースから深いデータを取得し、もう一方へ広く届ける必要があるあらゆる統合にスケールします。

単一のアプリキーを変更するだけで、HubSpotをSalesforce、Pipedrive、またはZapierの9,000以上の連携済みアプリに置き換えできます。あるいはHubSpotを維持し、action === 'created' のときに営業へ通知するSlackステップを追加すれば、新しいアカウントが着地した瞬間に担当者がそれを学べます。

完全なコードはここで確認できます:https://github.com/growthxai/output-examples/tree/main/src/workflows/zapier_hubspot_company_enrichment
このチュートリアルが気に入った場合はOutput https://github.com/growthxai/output