salesintermediate10 min setupby Mantle

New spreadsheet row to a HubSpot contact

A new row in a Google Sheet becomes a HubSpot contact, and the team hears about it in Slack.

New rowCreate the contaTell the team

Drag to pan, pinch to zoom. This is the real graph the template installs.

Purpose

A new row in a Google Sheet becomes a HubSpot contact, and the team hears about it in Slack.

How it works

What it does

  • Polls a sheet every 5 minutes for rows that are new since the last check.
  • Creates a HubSpot contact from the row, carrying company and lead status.
  • Posts the new contact's id to a Slack channel.

Validation

This template is contract-validated: the unit suite checks every node type, every config key and the whole graph against the same rules a saved workflow passes. It has not been run against the live provider — the ids, names and columns below are placeholders to replace with your own.

Nodes 3

  1. 01New rowtrigger.google_sheets_rowTrigger
  2. 02Create the contacthubspot.create_contactApp
  3. 03Tell the teamslack.post_messageApp
View template JSON
{
  "format": "mantle-workflow/v1",
  "id": "sheets-row-to-hubspot-contact",
  "name": "New spreadsheet row to a HubSpot contact",
  "graph": {
    "nodes": [
      {
        "id": "row",
        "type": "trigger.google_sheets_row",
        "label": "New row",
        "config": {
          "spreadsheet_id": "1AbCdEfGhIjKlMnOpQrStUvWxYz0123456789",
          "sheet_name": "Leads",
          "mode": "new_rows",
          "key_column": "Email",
          "poll_interval_sec": 300
        },
        "worker_type": "generic-worker",
        "position": {
          "x": 60,
          "y": 220
        }
      },
      {
        "id": "contact",
        "type": "hubspot.create_contact",
        "label": "Create the contact",
        "config": {
          "email": "{{ $json.row.Email }}",
          "firstname": "{{ $json.row[\"First name\"] }}",
          "lastname": "{{ $json.row[\"Last name\"] }}",
          "properties": {
            "company": "{{ $json.row.Company }}",
            "hs_lead_status": "NEW"
          }
        },
        "worker_type": "generic-worker",
        "position": {
          "x": 380,
          "y": 220
        }
      },
      {
        "id": "notify",
        "type": "slack.post_message",
        "label": "Tell the team",
        "config": {
          "channel": "#sales",
          "text": "New lead from the sheet: {{ $json.row.Email }} → HubSpot contact {{ steps.contact.id }}"
        },
        "worker_type": "generic-worker",
        "position": {
          "x": 700,
          "y": 220
        }
      }
    ],
    "edges": [
      {
        "from": "row",
        "to": "contact"
      },
      {
        "from": "contact",
        "to": "notify"
      }
    ]
  }
}

Requirements

The connections and permissions each step needs, from the node contracts in the catalog.

  • Google SheetsOAuth 2.0

    Used by New row.

    Credential: {"client_id", "client_secret", "refresh_token"}

    • The sheet is never replayed: the first poll after publish only records its current state.
    • Runs are idempotent per (row, content) — a re-poll cannot start a second run.
    • new_rows: re-ordering the sheet does not re-fire the rows that moved, and if the rows the last poll ended on are all gone (the tab was cleared or refilled) the cursor resets and emits nothing rather than firing every row.
    • new_rows remembers the last 5,000 rows to tell a row that moved from a row that is new; a blank spacer row never starts a run.
    • updated_rows watches the first 5,000 rows (and at most 512 KiB of cell text); rows past that never fire.
  • HubSpotBearer token

    Used by Create the contact.

    Credential: the HubSpot private-app token

  • SlackAPI key

    Used by Tell the team.

    Credential: the bot token (xoxb-…), or {"token": "xoxb-…"}

    • The bot must be a member of a private channel before it can post there (Slack error not_in_channel otherwise).

Setup

  • Put the spreadsheet id (the long part of its URL) in spreadsheet_id and name the tab in sheet_name.
  • key_column is what makes a row the same row across polls — use a column that is unique and filled in.
  • The expressions read the sheet's header names (Email, First name, …). Change them to match your header row; a header with a space needs bracket form, {{ $json.row["First name"] }}.
  • Pick Google Sheets, HubSpot and Slack connections.

Sample input and output

Synthetic examples: node-contract samples or placeholders, never customer data.

Input

What the New row trigger receives, as its node contract samples it.

{
  "change": "added",
  "key": "",
  "previous_row": {},
  "row": {},
  "row_number": 42,
  "row_values": [],
  "sheet_name": "",
  "spreadsheet_id": ""
}

Output

Tell the team slack.post_message

{
  "channel": "",
  "ok": true,
  "ts": ""
}

Failure handling

What the graph does when a step fails, read from its retry settings, error branches and error policies.

  • A step that fails, after any retries, fails the run. The failed step is kept with its input and error.

See it run, then make it yours.

Templates install as drafts. Edit any step, and Workflow Health re-checks the graph on every save.