[object Object]

A workflow has a custom code action that has worked for two years. Then HubSpot bumps the Node runtime, the action throws on a breaking change in the standard library, and 4,000 stranded contacts wait for routing. Custom code actions are powerful and easy to abandon. The discipline that prevents incidents looks a lot like writing real backend code, because that is what it is.

What custom code actions are

A workflow step that runs JavaScript (Node) or Python with access to the workflow event data, environment secrets, network, and the HubSpot API. Available on Operations Hub Professional and above. Execution capped at 20 seconds per run.

exports.main = async (event, callback) => {
  const contactId = event.object.objectId;
  const enrichmentApiKey = process.env.ENRICH_API_KEY;

  const resp = await fetch(`https://enrich.example.com/v1/${contactId}`, {
    headers: { Authorization: `Bearer ${enrichmentApiKey}` }
  });
  if (!resp.ok) {
    callback({ outputFields: { enriched: false, error: `${resp.status}` } });
    return;
  }
  const data = await resp.json();
  callback({
    outputFields: {
      enriched: true,
      industry: data.industry,
      employee_count: data.employees
    }
  });
};

What you can and cannot do

Can:
  - Fetch HTTP from any allowed domain
  - Call HubSpot API with platform-bound auth
  - Read inputs from prior steps
  - Write outputs consumed by downstream steps
  - Read portal secrets

Cannot:
  - Run longer than 20 seconds per execution
  - Persist state between runs (no filesystem)
  - Open long-lived connections (no websockets)
  - Use native modules outside the runtime allowlist

For multi-second external calls, batch and parallelize within the 20-second budget or move the work to your own queue and have the workflow poll for completion.

Secret management

Use Settings > Secrets to register portal-level secrets. Access via process.env.NAME. Never commit a secret in code or paste one in the editor:

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

Rotate secrets annually and immediately when an integration owner changes.

Outputs that downstream steps can rely on

Outputs declared in the action’s UI become workflow variables. Type them and document expected values:

Output: enriched
  Type: Boolean
  Expected: true on success, false on error or skip

Output: industry
  Type: Single-line text
  Expected: SIC industry classification or "unknown"

Downstream branches reference outputs by name. Renaming an output silently breaks every reference — search workflow JSON before any rename.

Test mode is mandatory

The test mode panel runs the action with a sample event payload. Build a test corpus covering:

- Happy path with full input
- Missing optional input fields
- Upstream API failure (5xx)
- Upstream API timeout
- Rate-limit response (429)
- Malformed external response

Save sample payloads in your repo so any change can re-run the same tests.

Logging that survives the rotation

console.log output appears in the action run history for a limited window. For longer-term observability, ship structured logs to your own platform via a non-blocking call:

function log(level, msg, fields) {
  const payload = { ts: new Date().toISOString(), level, msg, ...fields };
  fetch(process.env.LOG_INGEST_URL, {
    method: "POST",
    headers: { "X-Token": process.env.LOG_INGEST_TOKEN },
    body: JSON.stringify(payload)
  }).catch(() => {});
}

Idempotency and retries

A workflow may retry on platform error. Build the action so a retry is safe — pass deterministic idempotency keys to external systems and check before writing.

What to do this week

Inventory your custom code actions, add owner + purpose metadata to each, build a test payload set for the top three, and confirm secrets are not hardcoded before next quarter’s runtime upgrade.

[object Object]
Share