Guides & Tutorials11 min read

Build LinkedIn Leads to HubSpot with OpenClaw Workflow Automation Guide

By ButterGrow Team

TL;DR

This tutorial shows how to connect LinkedIn Lead Gen Forms to HubSpot using OpenClaw, with polling, field mapping, and safe upserts. You will set up auth, design a mapping layer, and schedule runs that pick up only new submissions. The result is a fast pipeline that reduces manual CSV uploads and shortens time to first touch. The primary focus is workflow automation, but we will also cover practical guardrails like rate limits, retries, and idempotency. You will also see a minimal playbook and example requests that you can run right away.

What You Will Build

You will build an automated workflow that pulls new submissions from LinkedIn Lead Gen Forms and creates or updates contacts in HubSpot. The flow uses OpenClaw for orchestration and reliability features such as retries, idempotency keys, and schedules. By the end, new LinkedIn leads will appear in HubSpot with normalized names, emails, phone numbers, and UTMs, and the run will safely skip anything it already processed.

If you are new to ButterGrow or OpenClaw, skim the AI marketing automation features at what ButterGrow does. You can always jump to answers to common questions if you need quick clarifications while implementing.

For adjacent techniques, see how we build lead scoring that feeds your CRM. It pairs well with this pipeline once contacts start flowing.

Prerequisites

  • A LinkedIn Ads account with Lead Gen Forms and a developer app with the correct permissions.
  • A HubSpot account with API access to the CRM objects you plan to update.
  • An OpenClaw workspace in ButterGrow where you can create playbooks and schedules.
  • Familiarity with JSON payloads, OAuth credentials, and basic REST.

Environment secrets to prepare

  • LI_ACCESS_TOKEN for the LinkedIn Marketing API.
  • HS_PRIVATE_TOKEN for the HubSpot CRM API.
  • ACCOUNTS and FORM_IDS as lists if you manage multiple ad accounts and forms.

Store these values in your workspace secret manager and reference them in steps. Never hardcode credentials in playbooks committed to source control.

Architecture Overview

At a high level, the pipeline performs five stages in serial order.

  1. Poll the LinkedIn Marketing API for new lead submissions since the last run checkpoint.
  2. Normalize and validate fields, including name splitting, email validation, phone formatting, and UTM coercion.
  3. Query HubSpot to find an existing contact by email or fallback composite key.
  4. Upsert the contact and attach original raw lead data to a debug property for traceability.
  5. Save the latest processed timestamp per form so the next run queries only incremental data.

The orchestrator is OpenClaw. We will implement the polling logic without webhooks to reduce moving parts and to keep permissions simple. The mapping layer sits in a dedicated transform step so you can unit test it in isolation and reuse it for other destinations.

Implementation Steps

Step 1Prepare API access

Create a LinkedIn developer app tied to your ad account and obtain an access token with scopes that include lead read for ads. Generate a HubSpot private app token with CRM read and write for contacts. Put both tokens in your workspace secret manager under the names LI_ACCESS_TOKEN and HS_PRIVATE_TOKEN.

Test tokens with curl before you wire them into a playbook.

curl -sS -H "Authorization: Bearer $LI_ACCESS_TOKEN" \
  "https://api.linkedin.com/v2/adAccounts?q=search&search.account~{id}" | jq '.elements | length'

curl -sS -H "Authorization: Bearer $HS_PRIVATE_TOKEN" \
  "https://api.hubapi.com/crm/v3/properties/contacts" | jq '.results | length'

If either call fails, recheck scopes and token freshness. For LinkedIn, confirm the app is approved for the advertiser you plan to poll.

Step 2Plan the data model

List the fields captured by your LinkedIn lead form, then map them to HubSpot contact properties. Decide which fields are required and which are optional. Set validation and normalization rules up front so you do not ship malformed data into your CRM.

Example mapping plan:

LinkedIn field label Normalized key HubSpot property Rule
First Name first_name firstname Trim whitespace and capitalize
Last Name last_name lastname Trim whitespace and capitalize
Email email email Lowercase and validate format
Phone Number phone phone E164 format with country if present
Company Name company company As provided
Job Title title jobtitle As provided
UTM Source utm_source hs_latest_source Keep raw and normalized copies
UTM Campaign utm_campaign hs_utm_campaign Keep raw and normalized copies

Record this plan in version control. It will become the contract between marketing and sales for this integration.

Step 3Build the OpenClaw playbook

Start with a minimal playbook that polls one form and writes one contact. Then add multiple forms and accounts once the happy path is working.

# playbooks/linkedin_to_hubspot.yaml
name: linkedin-to-hubspot
version: 1
triggers:
  - type: schedule
    cron: "*/5 * * * *"    # poll every 5 minutes
vars:
  form_id: "123456"        # replace with your form id
  account_id: "urn:li:sponsoredAccount:999999"
  since_minutes: 10
steps:
  - id: fetch_leads
    uses: http.request
    with:
      method: GET
      url: "https://api.linkedin.com/v2/leadFormResponses"
      headers:
        Authorization: "Bearer {{ secrets.LI_ACCESS_TOKEN }}"
      params:
        q: "leadGenFormId"
        leadGenFormId: "{{ vars.form_id }}"
        createdAfter: "{{ now() | minus_minutes(vars.since_minutes) | to_unix_ms }}"
    retry:
      attempts: 3
      backoff: exponential
      max_delay_ms: 20000

  - id: normalize
    uses: js.transform
    with:
      input: "{{ steps.fetch_leads.data.elements }}"
      script: |
        function toTitle(s) { return s ? s.trim().toLowerCase().replace(/\b\w/g, c => c.toUpperCase()) : null; }
        function cleanPhone(s) { return s ? s.replace(/[^\d+]/g, '') : null; }
        function isEmail(s) { return /.+@.+\..+/.test(s || ''); }
        const out = [];
        for (const e of input) {
          const li = e || {};
          const f = li.fieldData || {};
          const record = {
            firstname: toTitle(f.firstName || f.first_name || null),
            lastname: toTitle(f.lastName || f.last_name || null),
            email: (f.email || f.Email || '').toLowerCase(),
            phone: cleanPhone(f.phoneNumber || f.phone || null),
            company: f.companyName || f.company || null,
            jobtitle: f.jobTitle || f.title || null,
            hs_latest_source: f.utm_source || null,
            hs_utm_campaign: f.utm_campaign || null,
            _raw: li
          };
          if (isEmail(record.email)) {
            out.push(record);
          }
        }
        return out;

  - id: upsert
    uses: foreach
    with:
      items: "{{ steps.normalize.data }}"
      concurrency: 3
      body:
        - id: find_contact
          uses: http.request
          with:
            method: POST
            url: "https://api.hubapi.com/crm/v3/objects/contacts/search"
            headers:
              Authorization: "Bearer {{ secrets.HS_PRIVATE_TOKEN }}"
              Content-Type: "application/json"
            json:
              filterGroups:
                - filters:
                    - propertyName: email
                      operator: EQ
                      value: "{{ item.email }}"
              properties: ["email"]
          retry:
            attempts: 3
            backoff: exponential

        - id: upsert_contact
          uses: http.request
          with:
            method: "{{ steps.find_contact.data.total > 0 ? 'PATCH' : 'POST' }}"
            url: "{{ steps.find_contact.data.total > 0 ? 'https://api.hubapi.com/crm/v3/objects/contacts/' + steps.find_contact.data.results[0].id : 'https://api.hubapi.com/crm/v3/objects/contacts' }}"
            headers:
              Authorization: "Bearer {{ secrets.HS_PRIVATE_TOKEN }}"
              Content-Type: "application/json"
            json:
              properties:
                email: "{{ item.email }}"
                firstname: "{{ item.firstname }}"
                lastname: "{{ item.lastname }}"
                phone: "{{ item.phone }}"
                company: "{{ item.company }}"
                jobtitle: "{{ item.jobtitle }}"
                hs_latest_source: "{{ item.hs_latest_source }}"
                hs_utm_campaign: "{{ item.hs_utm_campaign }}"
                li_raw_payload: "{{ item._raw | to_json }}"
          retry:
            attempts: 3
            backoff: exponential

  - id: checkpoint
    uses: storage.write
    with:
      key: "li_form_{{ vars.form_id }}_latest_ts"
      value: "{{ now() | to_unix_ms }}"

Notes:

  • The fetch step filters by a createdAfter timestamp so each run only processes new submissions.
  • The transform step rejects records without a valid email to keep your CRM clean.
  • The upsert uses the HubSpot search endpoint to decide whether to update or create.
  • The checkpoint writes the run timestamp to simple storage for the next poll.

Tip for multiple forms and accounts: parameterize form_id, account_id, and since_minutes, then iterate over a list of objects. Use a unique checkpoint key per form so you can move accounts independently without losing state.

Step 4Validate the mapping logic

Before you touch production, feed the transform with captured payloads from a test form. Keep three fixtures that represent common cases: a complete submission, a missing optional field, and malformed phone. Run the transform step locally and assert that it emits the correct properties for HubSpot. The goal is a stable mapping that does not drift when someone edits the form.

Example unit test for the normalization function:

// tests/normalize.test.js
const { normalize } = require('../transforms/normalize');

test('keeps only valid emails', () => {
  const input = [
    { fieldData: { Email: 'ok@example.com', firstName: 'a', lastName: 'b' } },
    { fieldData: { Email: 'bad', firstName: 'x' } }
  ];
  const out = normalize(input);
  expect(out.length).toBe(1);
  expect(out[0].email).toBe('ok@example.com');
});

You can also add property assertions for country calling codes and capitalization rules.

Step 5Test end to end in a sandbox

Create a sandbox form that posts predictable sample data, such as a test email domain and a unique campaign tag. Run the playbook once with the schedule disabled and inspect the contact created in HubSpot. Confirm that normalization rules applied and that the raw payload property stored the submission for audit.

Then run the playbook twice and verify that the second run performs no new writes. This checks your incremental query parameters and your search before upsert logic.

Step 6Schedule and monitor

Enable the schedule at 5 minutes. If your sales team needs faster handoffs, try 2 minutes but watch your LinkedIn and HubSpot rate limits. Add an alert for nonzero error counts and for sudden drops in processed items, which often signal a credential expiration or a form change.

For reporting, add a metric that tracks latency from submission time to CRM create time. Keep the 95th percentile under 10 minutes for most campaigns. You can improve this by increasing concurrency on the foreach step, but do not exceed your API rate limits.

Step 7Add enrichment and routing

Once the base contact is created, many teams enrich with firmographics and then route to sales by territory or segment. Insert an enrichment call after normalization. Merge the returned fields into the HubSpot payload. After that, call your router that creates a task or assigns the contact to an owner.

If you want a deeper play, pair this pipeline with our guide on build lead scoring that feeds your CRM.

Troubleshooting

Common API errors

  • Unauthorized or permission denied. Recreate the token with appropriate scopes and confirm the ad account is in the app configuration.
  • Too many requests. Increase the polling interval, enable exponential backoff, and lower foreach concurrency.
  • Property not found in HubSpot. Create the custom property first or rename your key to match an existing property.

Dedupe collisions

When email is missing or malformed, rely on a composite key such as phone plus country code and last name. Store the composite in a hidden property and use it only as a last resort. Monitor the rate of composite matches and investigate any spikes.

Field drift after form edits

Marketing may change a field label or add a new question. Guard against drift by writing a unit test per field you depend on and checking the transform step into source control. Fail the run if a required field is missing rather than silently dropping data.

Data governance

Always collect only what you need. Avoid storing sensitive personal data unless you have a legitimate purpose. Keep your retention settings in sync with your privacy policy and your data processing agreements.

What this solves for your team

This automation workflow removes manual CSV downloads, improves lead response time, and reduces data quality issues. Sales gets contacts with consistently formatted names and phones. Marketing gains reliable UTMs and a clear audit trail. The operations team retains control through schedules, retries, and strict mapping contracts.

Building this pipeline is a concrete starting point for bigger systems. If you later add paid media feedback loops, revisit your upsert step to write scoring properties.

Long tail scenarios to keep in mind

  • How to connect LinkedIn Lead Gen to HubSpot when some fields are optional.
  • Field mapping for LinkedIn leads to HubSpot contacts with strict validation.
  • Schedule a LinkedIn lead sync to run every 5 minutes with safe retries.

This checklist helps the implementation pass a real audit by sales and marketing operations.

Finally, if you need help designing the mapping or testing path, see more from the ButterGrow blog for tutorials that build on the same patterns.

Your next step is simple. Open your workspace, add the tokens, paste the playbook, and press run.

You now have a fast path from LinkedIn lead forms to a sales ready contact in HubSpot with a clean audit trail.

ButterGrow bundles OpenClaw with a visual playbook editor, shared secrets, and built in schedules. If you want a guided path, follow get started in minutes to import the playbook, connect tokens, and schedule your first run. With that in place, your team can focus on routing, scoring, and reporting.

References

Frequently Asked Questions

How do I authenticate the LinkedIn Marketing API in OpenClaw without exposing tokens?+

Store the access token and client secret in your workspace secret manager, then reference them in the HTTP step as environment variables. Rotate tokens on a schedule and scope permissions to the specific ad accounts you need.

What is the best dedupe rule for creating HubSpot contacts from LinkedIn leads?+

Use email as the primary key when possible, then fall back to a composite key such as hashed phone plus country code. Query HubSpot for an existing contact before creating one, and upsert rather than insert to avoid duplicates.

How often should I poll for new LinkedIn lead form submissions to keep ad SLAs tight?+

Polling every 5 minutes strikes a balance between freshness and API limits for most teams. Start at 5 minutes, measure latency to first touch in your CRM, and increase to 2 minutes only if your rate limits allow it.

Where should I put UTM parameters captured by lead forms in HubSpot?+

Map UTMs into custom contact properties like hs_latest_source and hs_utm_campaign, or your own prefixed fields. Keep raw values for analytics and also store normalized values for reporting.

How do I handle LinkedIn API rate limits when running multiple ad accounts?+

Stagger schedules per account, enable backoff and retry with jitter, and cache the latest processed timestamp per form. If you hit hard limits, prioritize high spend forms and delay low volume forms to the next run.

Can I extend this pipeline to do lead enrichment before HubSpot?+

Yes. Insert an enrichment step after normalization and before the HubSpot upsert. Many teams call an enrichment API with an email, then merge the returned firmographic fields into the contact payload.

Ready to try ButterGrow?

See how ButterGrow can supercharge your growth with a quick demo.

Book a Demo