[object Object]

A workflow that started as five clicks now has 23 branches, four webhook calls, and a comment from someone who left the company three quarters ago. Programmable automation is what HubSpot offers when no-code workflows hit their ceiling, and the right answer is rarely “more clicks.” It is usually “extract the gnarly bit into custom code and document why.”

When no-code stops working

The signs that a workflow has outgrown the visual builder:

- More than 10 if/then branches
- Repeated similar branches that beg for a loop
- External API calls (CRM lookup, enrichment, notification)
- Property transformations the formula tool can't express
- Multi-object coordination (deal change updates contact and ticket)
- State that needs to persist between runs

Each of these is a candidate for code. Building them in branches works once; maintaining them at year two is where the cost shows up.

Custom code actions

Ops Hub Pro and above. JavaScript (Node) or Python inside a workflow step. Full access to event input, environment secrets, HTTP, and the HubSpot API. 20-second runtime cap per execution.

// Custom code action
exports.main = async (event, callback) => {
  const contactId = event.object.objectId;
  const apiResp = await fetch(`https://api.example.com/score?id=${contactId}`, {
    headers: { Authorization: `Bearer ${process.env.SCORE_API_KEY}` }
  });
  const score = (await apiResp.json()).score;
  callback({ outputFields: { external_score: score } });
};

Outputs flow into downstream actions as variables. Type your outputs so downstream steps reference them reliably.

Webhook actions

Send the workflow’s payload to an external URL. Useful for Zapier, Make, your own service, or a partner system that does not have a HubSpot connector:

URL:    https://hooks.example.com/hubspot/deal-stage-change
Method: POST
Body:   { "dealId": {{deal.id}}, "stage": {{deal.dealstage}} }
Auth:   Bearer ${SHARED_SECRET}

Combine webhook actions outbound with HubSpot’s webhook subscriptions inbound for full bidirectional flow.

Chained workflows

Enroll a contact (or any object) in a second workflow from the first. Each workflow owns one job, you compose them like functions:

Workflow A: Lead lifecycle
  Trigger: form submission
  Action: enrich, score, set lifecycle stage
  Action: enroll in Workflow B if SQL

Workflow B: SQL handoff
  Trigger: enrolled by another workflow
  Action: assign owner, create task, notify Slack

This pattern keeps each workflow understandable and lets you reuse “SQL handoff” from multiple lead-source flows.

Secret management

Never hardcode API keys in code actions. Use the Secrets manager (Ops Hub) and reference via process.env:

const apiKey = process.env.SCORE_API_KEY;
if (!apiKey) throw new Error("Missing SCORE_API_KEY secret");

Rotate secrets annually and immediately when an integration changes hands.

Idempotency

A workflow may re-execute on retry, on re-enrollment, or on manual replay. Custom code that creates external records must be idempotent — pass a deterministic key so duplicate runs do not create duplicate records:

const idempotencyKey = `${contactId}-${event.eventId}`;
await externalApi.upsert({ key: idempotencyKey, data });

Governance and documentation

Every custom action needs:

- Owner: named human with current email
- Purpose: one paragraph in the action description
- Dependencies: external services, secrets, scopes
- Last reviewed: date
- Test coverage: link to test mode payload

Without this, a six-month-old code action becomes the thing nobody dares touch and the thing that breaks first when HubSpot updates the runtime.

What to do this week

Audit workflows with more than 10 branches, identify candidates for custom code extraction, and add owner + purpose metadata to every existing custom code action before next quarter’s clean-up review.

[object Object]
Share