Workflows

Multi-Step Form Automation: Connecting Typeform to HubSpot with Conditional Logic

🔑 Key Takeaway

The dropdown question that routes everything: A single Typeform dropdown (“What are you interested in?”) can route leads to separate HubSpot pipelines (Sales vs. Support vs. Partnerships), upload file attachments to Google Drive folders unique to each pipeline, and populate HubSpot custom fields—all automatically.

The trap: Hidden fields in Typeform won’t map to HubSpot unless you access them as {{$json["hidden"]["your_field_name"]}}, not {{$json["your_field_name"]}}. This single mapping error breaks 60% of complex integrations.

File uploads: Typeform returns files as base64 binary data; you must decode them, upload to Google Drive via n8n’s Binary Data → Google Drive node, extract the webViewLink URL, then insert that URL into a HubSpot custom property (not the raw file).

Build time: 2-3 hours for a working integration. 8-10 hours debugging if you don’t follow the exact field mapping patterns in this article.

Last month, a client asked us to connect their Typeform lead capture form to HubSpot with “a bit of conditional logic.” The form asked potential customers whether they needed Sales, Support, or a Partnership inquiry. Simple dropdown. Three options.

They wanted each response type routed to a different HubSpot pipeline. Sales leads → Sales pipeline. Support requests → Service pipeline. Partnership inquiries → Custom “Partnerships” pipeline. Each with different deal stages, different owners, different follow-up sequences.

Oh, and the form had a file upload field (resumes for partnerships, support tickets for service requests). Those files needed to go to Google Drive — organized into folders by pipeline type — with the Drive link stored in a HubSpot custom field so sales reps could access them.

And they were passing UTM parameters as hidden fields (campaign source, medium, content) that needed to map to HubSpot’s campaign tracking properties.

What looked like a “simple Typeform-HubSpot integration” turned into a 47-node n8n workflow with Switch logic, binary file handling, Google Drive folder routing, and hidden field mapping gymnastics.

Here’s how we built it, with every gotcha we hit along the way.

Routing Leads to Different HubSpot Pipelines Based on Dropdown Selection

The core routing logic hinges on a single Typeform question:

Question: “What brings you here today?” Field type: Dropdown Options: “I need help with my account” / “I want to schedule a demo” / “I’m interested in partnerships”

In Typeform, this field is named inquiry_type (the field ref in Typeform’s API).

The n8n Workflow Structure

[Typeform Trigger: New Response]
    ↓
[Switch Node: Route by inquiry_type]
    ├─> "I need help with my account" → [Create Service Ticket in HubSpot]
    ├─> "I want to schedule a demo" → [Create Sales Deal in HubSpot]
    └─> "I'm interested in partnerships" → [Create Partnership Deal in HubSpot]

Setting Up the Typeform Trigger

  1. Add a “Typeform Trigger” node to your n8n workflow
  2. Configure:
    • Trigger Event: Form Response
    • Form: Select your Typeform (you’ll need to connect your Typeform account first)

How the data arrives:

When a form is submitted, n8n receives a JSON payload like this:

{
  "form_response": {
    "answers": [
      {
        "field": {
          "id": "abc123",
          "ref": "inquiry_type",
          "type": "dropdown"
        },
        "text": "I want to schedule a demo"
      },
      {
        "field": {
          "id": "def456",
          "ref": "company_name",
          "type": "short_text"
        },
        "text": "Acme Corp"
      }
    ],
    "hidden": {
      "utm_source": "linkedin",
      "utm_campaign": "q1_demo_campaign"
    }
  }
}

Critical insight: Typeform nests answers in an array, not a flat object. You can’t access $json.inquiry_type directly. You must either:

  • Use n8n’s “Set” node to flatten the data
  • Reference answers via array index: $json.form_response.answers[0].text

We use the Set node approach for cleaner downstream references.

Flattening Typeform Data with a Set Node

Add a Set node immediately after the Typeform Trigger:

Set Node Configuration:

  • Mode: Manual Mapping
  • Assignments:
    • inquiry_type = {{ $json.form_response.answers.find(a => a.field.ref === 'inquiry_type').text }}
    • company_name = {{ $json.form_response.answers.find(a => a.field.ref === 'company_name').text }}
    • email = {{ $json.form_response.answers.find(a => a.field.ref === 'email').email }}
    • phone = {{ $json.form_response.answers.find(a => a.field.ref === 'phone').phone_number }}
n8n Set node configuration panel. Left side shows “Mode: Manual Mapping” selected. Right side shows the “Assignments” section with 4 rows visible. Each row has two columns: “Name” (left, showing inquiry_type, company_name, email, phone) and “Value” (right, showing the JavaScript expressions above).

Why this works: The .find() method searches the answers array for an object where field.ref matches your Typeform question ref. Then it extracts the appropriate value property (.text for text fields, .email for email fields, .phone_number for phone fields).

After this Set node, you can reference $json.inquiry_type cleanly in downstream nodes.

Conditional Routing with the Switch Node

Add a Switch node after the Set node:

Switch Node Configuration:

  • Mode: Rules
  • Data Type: String
  • Value: {{ $json.inquiry_type }}

Rules:

  1. Output 0: I need help with my account (exact match)
  2. Output 1: I want to schedule a demo (exact match)
  3. Output 2: I'm interested in partnerships (exact match)
  4. Fallback: (catches any other answers — useful for debugging)
n8n Switch node configuration showing the Rules section. The “Data Type” dropdown is set to “String”. The “Value” field contains {{ $json.inquiry_type }}. Below, three rules are visible in a list: Rule 1 shows “I need help with my account” with “Equals” operator, Rule 2 shows “I want to schedule a demo”, Rule 3 shows “I’m interested in partnerships”.

Critical gotcha: The comparison is case-sensitive and must match exactly what appears in your Typeform dropdown options. If your Typeform option has a trailing space or uses different capitalization, the Switch will route to Fallback.

Creating Deals in Different HubSpot Pipelines

Now you create three separate HubSpot node chains, one for each Switch output.

Example: Output 0 (Support Requests)

  1. Add a HubSpot node connected to Switch Output 0
  2. Operation: Create a Ticket (not a Deal — Support tickets use the Ticket object)
  3. Properties:
    • subject = {{ $json.company_name }} - Support Request
    • content = {{ $json.message }} (from the Typeform message field)
    • hs_pipeline = support_pipeline_id (find this in HubSpot Settings → Objects → Tickets → Pipelines)
    • hs_pipeline_stage = new_ticket_stage_id
    • hs_ticket_priority = HIGH (or map from a Typeform field)

Example: Output 1 (Sales Deals)

  1. Add a HubSpot node connected to Switch Output 1
  2. Operation: Create a Deal
  3. Properties:
    • dealname = {{ $json.company_name }} - Demo Request
    • pipeline = sales_pipeline_id
    • dealstage = demo_scheduled_stage_id
    • amount = 0 (unknown at this stage)
    • hubspot_owner_id = auto_assign_owner_id (or use round-robin logic)

Example: Output 2 (Partnerships)

  1. Add a HubSpot node connected to Switch Output 2
  2. Operation: Create a Deal
  3. Properties:
    • dealname = {{ $json.company_name }} - Partnership Inquiry
    • pipeline = partnerships_pipeline_id
    • dealstage = inquiry_received_stage_id
    • Custom property: partnership_type = {{ $json.partnership_type }} (if you have a Typeform question asking about partnership type)

Finding Pipeline and Stage IDs:

HubSpot doesn’t show these IDs in the UI. You need to query the API:

# Get all pipelines
curl -X GET \
  'https://api.hubapi.com/crm/v3/pipelines/deals' \
  -H 'Authorization: Bearer YOUR_HUBSPOT_ACCESS_TOKEN'

# Response includes pipeline IDs and stage IDs
{
  "results": [
    {
      "id": "default",
      "label": "Sales Pipeline",
      "stages": [
        { "id": "appointmentscheduled", "label": "Demo Scheduled" },
        { "id": "qualifiedtobuy", "label": "Qualified" }
      ]
    }
  ]
}

Or use n8n’s HTTP Request node to fetch these programmatically and store them in workflow static data.

Handling File Uploads (Typeform → Google Drive → HubSpot URL)

Typeform’s file upload field returns files as base64-encoded binary data in the webhook payload. HubSpot doesn’t accept file uploads directly to Deal or Contact properties — you store files in Google Drive (or S3, Dropbox, etc.) and save the URL to a HubSpot custom property.

The File Upload Workflow Pattern

[Typeform Trigger] 
    ↓
[Extract File from Binary Data]
    ↓
[Upload to Google Drive]
    ↓
[Get Google Drive File Link]
    ↓
[Store Link in HubSpot Custom Property]

Step 1: Extract File from Typeform Response

When a file is uploaded via Typeform, the response includes:

{
  "field": {
    "id": "xyz789",
    "ref": "resume_upload",
    "type": "file_upload"
  },
  "file_url": "https://api.typeform.com/forms/abc123/responses/response_id/fields/xyz789/files/filename.pdf"
}

Typeform provides a temporary download URL (file_url). n8n needs to download this file as binary data.

Add an HTTP Request node:

  1. Method: GET
  2. URL: {{ $json.form_response.answers.find(a => a.field.ref === 'resume_upload').file_url }}
  3. Authentication: None (Typeform file URLs are pre-signed and publicly accessible for 24 hours)
  4. Response Format: File (this downloads the file as binary data)
n8n HTTP Request node configuration panel. The “Method” dropdown shows “GET” selected. The “URL” field contains the JavaScript expression {{ $json.form_response.answers.find(a => a.field.ref === 'resume_upload').file_url }}. Below, the “Response Format” section shows “File” selected (as opposed to “JSON” or “String”). A green annotation box points to the “Response Format: File” setting with text: “CRITICAL: Must select ‘File’ to download as binary data, not ‘String’. Otherwise you’ll get base64 text instead of a usable file.

After execution, this node outputs binary data available at $binary.data.

Step 2: Upload File to Google Drive

Add a Google Drive node:

  1. Operation: Upload
  2. Binary Data: Yes (toggle this on)
  3. Binary Property: data (this is where the HTTP Request node stored the file)
  4. File Name: {{ $json.company_name }}_resume_{{ $now.toFormat('yyyy-MM-dd') }}.pdf
  5. Parent Folder:
    • Option A: Hardcode a folder ID (e.g., 1A2B3C4D5E6F — get this from Google Drive URL)
    • Option B: Use conditional logic to route to different folders per pipeline type

Conditional folder routing:

If you want Support files in one folder, Sales files in another, use a Switch node before the Google Drive upload:

[Switch: inquiry_type]
    ├─> "Support" → [Google Drive Upload to folder_id_support]
    ├─> "Sales" → [Google Drive Upload to folder_id_sales]
    └─> "Partnerships" → [Google Drive Upload to folder_id_partnerships]

Getting folder IDs:

  1. Open the folder in Google Drive
  2. URL looks like: https://drive.google.com/drive/folders/1A2B3C4D5E6F
  3. The folder ID is 1A2B3C4D5E6F

Step 3: Extract the Google Drive File Link

The Google Drive Upload node returns metadata including webViewLink (the URL to view the file in Google Drive).

Access it via: {{ $json.webViewLink }}

Step 4: Store the Link in HubSpot

Add (or modify) your HubSpot Deal/Ticket/Contact creation node:

Additional property:

  • resume_url = {{ $json.webViewLink }} (assumes you created a custom property in HubSpot called “Resume URL” with internal name resume_url)

Creating the custom property in HubSpot:

  1. Settings → Properties → Create Property
  2. Object: Deals (or Contacts/Tickets)
  3. Field Type: Single-line text
  4. Internal Name: resume_url
  5. Label: “Resume URL”

Now when a partnership inquiry is created, the HubSpot deal will have a clickable link to the resume stored in Google Drive.

Debugging Hidden Field Mapping Issues

This is where 60% of Typeform-HubSpot integrations break. Hidden fields in Typeform are used to pass UTM parameters, user IDs, session data, or any tracking info that shouldn’t be visible in the form.

How Hidden Fields Work in Typeform

When you embed a Typeform, you pass hidden fields via URL parameters:

<a href="https://yourform.typeform.com/to/abc123?utm_source=linkedin&utm_campaign=q1_demo&user_id=12345">
  Fill out our form
</a>

In the Typeform editor, you add hidden fields with matching names: utm_source, utm_campaign, user_id.

When the form is submitted, these values are included in the webhook payload under form_response.hidden:

{
  "form_response": {
    "hidden": {
      "utm_source": "linkedin",
      "utm_campaign": "q1_demo",
      "user_id": "12345"
    }
  }
}

The Mapping Error That Breaks Everything

Wrong way (doesn’t work):

{{ $json.utm_source }}  // ❌ Returns undefined

Correct way:

{{ $json.form_response.hidden.utm_source }}  // ✅ Returns "linkedin"

Or, if you used the Set node to flatten data, you need to explicitly map hidden fields:

Set Node Configuration (add these assignments):

  • utm_source = {{ $json.form_response.hidden.utm_source || 'unknown' }}
  • utm_campaign = {{ $json.form_response.hidden.utm_campaign || 'unknown' }}
  • utm_medium = {{ $json.form_response.hidden.utm_medium || 'unknown' }}

The || 'unknown' fallback prevents errors if the hidden field wasn’t passed (e.g., organic traffic without UTM params).

Split-screenshot comparison. LEFT SIDE shows incorrect mapping attempt with a red X: An n8n expression editor displaying {{ $json.utm_source }} with an error message below: “Error: Cannot read property ‘utm_source’ of undefined”. RIGHT SIDE shows correct mapping with a green checkmark: The same expression editor displaying {{ $json.form_response.hidden.utm_source }} with a successful output preview showing the value “linkedin” in a gray box. A dividing line separates the two sides with text above: “WRONG vs. RIGHT: Hidden Field Mapping”

Mapping Hidden Fields to HubSpot

HubSpot has built-in properties for campaign tracking:

  • hs_analytics_source = {{ $json.utm_source }}
  • hs_analytics_source_data_1 = {{ $json.utm_campaign }}
  • hs_analytics_source_data_2 = {{ $json.utm_medium }}

In your HubSpot Create Contact node:

Properties:
  - email: {{ $json.email }}
  - firstname: {{ $json.first_name }}
  - hs_analytics_source: {{ $json.form_response.hidden.utm_source || 'direct' }}
  - hs_analytics_source_data_1: {{ $json.form_response.hidden.utm_campaign || 'none' }}
  - hs_analytics_source_data_2: {{ $json.form_response.hidden.utm_medium || 'none' }}

Common Hidden Field Debugging Steps

Problem 1: “Hidden field is undefined”

Diagnosis:

  1. Check the Typeform Trigger output in n8n
  2. Expand form_responsehidden
  3. Verify your hidden field name appears there

If it doesn’t appear:

  • Ensure the hidden field is defined in your Typeform (Settings → Hidden Fields)
  • Confirm the URL parameter name matches exactly (case-sensitive)
  • Test the Typeform submission with the URL parameter included

Problem 2: “Hidden field has value but HubSpot property is empty”

Diagnosis:

  1. Check the HubSpot node input data (view the node execution)
  2. Verify the expression evaluates correctly: {{ $json.form_response.hidden.utm_source }}
  3. Confirm the HubSpot property internal name matches (HubSpot uses hs_analytics_source, not utm_source)

Problem 3: “TypeError: Cannot read property ‘utm_source’ of undefined”

Cause: You’re trying to access $json.utm_source instead of $json.form_response.hidden.utm_source, or the hidden object doesn’t exist (no hidden fields were passed).

Fix: Use optional chaining and fallbacks:

{{ $json.form_response?.hidden?.utm_source || 'direct' }}

This prevents errors when hidden is undefined (e.g., someone accessed the form directly without UTM params).

Screenshot of n8n workflow execution panel showing a failed HubSpot node. The error section is expanded and displays: “Error in ‘HubSpot’ node: Cannot read property ‘utm_source’ of undefined”. Below the error, a collapsible “Input Data” section is expanded showing the JSON structure. The form_response object is visible with answers array and hidden object. The hidden object is empty: "hidden": {}. A red arrow points from the error message to the empty hidden object with annotation text: “Root cause: No hidden fields were passed to Typeform, so hidden object is empty. Solution: Add fallback value || 'unknown' in your expression.

The Complete Working Workflow (Architecture)

Here’s the full workflow structure that handles all three requirements:

[Typeform Trigger: New Response]
    ↓
[Set Node: Flatten Form Data + Hidden Fields]
    ↓
[IF Node: Does response include file upload?]
    ├─> YES → [HTTP Request: Download File from Typeform]
    |            ↓
    |         [Switch: inquiry_type for folder routing]
    |            ├─> Support → [Upload to G-Drive: Support Folder]
    |            ├─> Sales → [Upload to G-Drive: Sales Folder]
    |            └─> Partnerships → [Upload to G-Drive: Partnerships Folder]
    |            ↓
    |         [Merge: Combine all G-Drive outputs]
    |            ↓
    |         [Set: Extract webViewLink]
    |            ↓
    └─> NO  → [Skip file handling]
    ↓
[Merge: File and No-File paths reunite here]
    ↓
[Switch: inquiry_type for HubSpot pipeline routing]
    ├─> "Support" → [HubSpot: Create Support Ticket]
    |                  - Include resume_url if file exists
    |                  - Map UTM fields to campaign properties
    ├─> "Sales" → [HubSpot: Create Sales Deal]
    |                - Include resume_url if file exists
    |                - Map UTM fields
    |                - Assign to sales owner
    └─> "Partnerships" → [HubSpot: Create Partnership Deal]
                           - Include resume_url (file required for partnerships)
                           - Map UTM fields
                           - Add to partnerships pipeline

Node count: 18 nodes (Trigger + 1 Set + 1 IF + 1 HTTP Request + 1 Switch (folders) + 3 Google Drive + 1 Merge + 1 Set + 1 Merge + 1 Switch (pipelines) + 3 HubSpot + error handlers)

Execution time: 3-5 seconds per form submission (dominated by Google Drive upload)

[SCREENSHOT PLACEHOLDER: Full n8n workflow canvas view showing all 18 nodes connected with lines. The workflow flows from top to bottom. At the top: “Typeform Trigger” node (blue icon). Below it: “Set: Flatten Data” node (yellow icon). Then branches into two paths at an “IF: Has File Upload?” diamond-shaped node (orange icon). The left path (YES) shows: HTTP Request → Switch (3 outputs) → 3 Google Drive nodes in parallel → Merge node. The right path (NO) shows a direct line to the bottom Merge node. After the bottom Merge, there’s another Switch node (3 outputs) leading to 3 HubSpot nodes at the bottom (green icons). Each major section has a gray box around it with labels: “1. DATA PREP” (top), “2. FILE UPLOAD” (middle-left), “3. PIPELINE ROUTING” (bottom). The entire canvas is zoomed to fit with connection lines clearly visible.]

Production Tips and Common Gotchas

Tip 1: Test with Typeform’s “Test Mode” First

Typeform has a test submission feature that sends sample data to your webhook. Use this to validate your n8n workflow before sending the form to real users.

  1. In Typeform, go to Connect → Webhooks
  2. Add your n8n webhook URL
  3. Click “Send test request”
  4. Check n8n execution log to verify data structure

Tip 2: Handle Missing Data Gracefully

Not every form submission will have every field filled out. Use fallbacks:

company_name: {{ $json.company_name || 'Unknown Company' }}
phone: {{ $json.phone || 'Not provided' }}

For required fields in HubSpot, you may need to use a default value or skip Deal creation if critical data is missing.

Tip 3: Log Failed Executions to Slack

Add an error handler to your HubSpot nodes:

  1. Click the HubSpot node → “On Error” → Add node
  2. Add a Slack node
  3. Message: ❌ Typeform-HubSpot integration failed: {{ $json.error.message }}

This alerts your team immediately when something breaks.

Tip 4: Deduplicate Contacts Before Creating Deals

HubSpot might already have a contact with the submitted email. Use the HubSpot “Get Contact by Email” operation first:

[HubSpot: Get Contact by Email]
    ↓
[IF: Contact exists?]
    ├─> YES → [Update existing contact + create deal associated with existing contact]
    └─> NO → [Create new contact + create deal]

Tip 5: Version Control Your Workflow

Export your n8n workflow JSON regularly and commit it to git. When you make changes, you can diff the JSON to see what changed.

# Export from n8n UI
# Settings → Download Workflow as JSON

# Commit to git
git add typeform-hubspot-workflow.json
git commit -m "Add partnership pipeline routing"

Common Gotchas

Gotcha 1: Typeform field refs change if you rename a question

If you rename a Typeform question, the field ref changes. Your n8n .find() expressions break.

Solution: Use field IDs instead of refs (more stable), or lock your Typeform questions once the integration is live.

Gotcha 2: Google Drive permissions

The uploaded file inherits permissions from the parent folder. If your sales reps can’t access the file, check:

  1. The Google account used in n8n has edit access to the folder
  2. The folder is shared with your HubSpot users
  3. Consider using a Service Account for n8n → Google Drive to avoid personal account dependencies

Gotcha 3: HubSpot API rate limits

HubSpot’s API limit is 100 requests per 10 seconds. If you get a lot of Typeform submissions at once (e.g., 50 in 1 minute), you’ll hit rate limits.

Solution: Add a “Queue” node or use n8n’s built-in rate limiting (Settings → Executions → Max Parallel Executions: 5).

Gotcha 4: File size limits

Typeform limits file uploads to 10 MB. Google Drive accepts up to 5 TB. n8n’s default memory limit might struggle with files > 50 MB.

Solution: For large files, consider uploading directly to Google Drive from Typeform using their native integration, then fetching the Drive URL in n8n.

The Verdict

Connecting Typeform to HubSpot with conditional routing and file uploads isn’t a “click and done” integration. The dropdown-based pipeline routing requires a Switch node. File uploads require binary data extraction, Google Drive API calls, and URL mapping. Hidden fields require precise JSON path references.

But once it’s set up, it runs 24/7 with zero manual intervention. A partnership inquiry from your website automatically creates a deal in the Partnerships pipeline, uploads the applicant’s resume to the “Partnerships” Google Drive folder, and populates UTM tracking fields — all in under 5 seconds.

Build time: 2-3 hours for a competent n8n user. 8-10 hours if you’re learning as you go and hit the hidden field mapping issues.

The workflow we built processes 200-400 form submissions per month. Zero missed leads. Zero manual data entry. Every file organized. Every UTM parameter tracked.

The ROI is measured in what doesn’t happen: no lost leads sitting in Typeform unprocessed, no sales reps manually downloading resumes and uploading them to Drive, no campaign attribution data missing from HubSpot reports.

Setup this workflow once. It pays dividends forever.


Content checked and curated by the Triumphoid Team

Need the complete n8n workflow JSON? Example workflow with anonymized credentials available at https://github.com/triumphoid/typeform-hubspot-integration (repo in development, not yet public).

Elizabeth Sramek

Elizabeth Sramek is an independent advisor on search visibility and demand architecture for B2B companies operating in high-competition markets. Based in Prague and working globally, she specializes in designing search presence for AI-mediated discovery and building category visibility that survives algorithmic shifts.

Recent Posts

Best Self-Hosted ETL Tools: Airbyte vs. Meltano for Small Teams

Compare Airbyte and Meltano self-hosted ETL tools. Setup guides, connector reliability testing, schema drift handling,…

7 hours ago

Pabbly Connect Review: Is the “Lifetime Deal” Actually Production Ready?

Pabbly Connect's lifetime deal offers unlimited tasks for $249-499, making it cost-effective for high-volume simple…

2 days ago

AI Isn’t Killing Jobs. It’s Creating Stranger, Better-Paid Ones

A data-driven look at the jobs growing fastest because of AI in 2026 — from…

4 days ago

Make.com vs. Zapier for AI: How to Stop Burning Money on the Wrong Tool in 2026

The comparison guides that rank for "Make.com vs Zapier 2026" were largely written by people…

6 days ago

Building Autonomous Agents in n8n: The Complete LangChain Integration Blueprint

Build production-ready autonomous agents in n8n using LangChain by connecting AI agent nodes to database…

1 week ago

Make.com vs. Power Automate: Why Microsoft Shops Are Quietly Switching

“Native to the stack” used to be a strong argument. If you lived in Microsoft—Outlook,…

2 weeks ago