Skip to content

How do you use npm packages in HubSpot custom code actions?

HubSpot's native custom code runtime ships a fixed set of roughly a dozen packages and gives you no way to add more. There's no package.json and no requirements.txt. moment.js has been the most-requested addition since 2021 and still isn't there. Running the step through a connected app removes the restriction, because the code executes on the app's infrastructure.

Last updated

There is no supported way to add a package to HubSpot’s native custom code action. No package.json, no requirements.txt, no upload. You get what the runtime ships with.

What’s actually in the runtime

HubSpot pre-installs roughly a dozen packages in the Node.js custom code runtime. The HubSpot API client, an HTTP client, and a handful of general utilities among them. The Python runtime works the same way with its own fixed list.

For simple work this is genuinely fine. Reading a property, doing arithmetic, calling the HubSpot API and returning a value need nothing extra.

It stops being fine the moment you need a specific library: a PDF generator, an SDK for a vendor whose API you’re calling, a CSV parser that handles quoting correctly, a validation library, a crypto library for signing a request. All of that is off the table.

The moment.js problem

The most-requested addition, for years running, is moment.js, and it illustrates the cost well. Date handling in a CRM is unavoidable: business-day arithmetic, timezone conversion for a global sales team, “30 days after close date,” fiscal quarters.

Without a date library, people write this by hand against Date. That code is subtle in ways that don’t show up in testing. Daylight-saving transitions, month-end rollover, timezone assumptions that hold in your portal and break for a colleague. A meaningful share of “the workflow fired on the wrong day” bugs trace back to hand-rolled date arithmetic.

Worth noting that moment.js itself is now in maintenance mode; date-fns, luxon and dayjs are the modern choices. None of them are available either.

Workarounds, honestly assessed

Reimplement by hand. Works for small things. The cost is that you now own and must test logic that a maintained library already handles.

Vendor the source. Paste a dependency-free utility directly into the action. Viable for something tiny; fails for anything with its own dependency tree, adds to your 128 MB memory budget, and leaves you manually tracking upstream security fixes.

Call an external service. Move the work to your own API and call it from the action. This works, but it now has to complete inside the 20-second limit, and note that the native “Send a webhook” action is itself gated behind Data Hub, so you’d be doing it from custom code that you may not have access to either.

Use an app-provided action. The code runs on the app’s infrastructure, so the app’s runtime decides what’s installable rather than HubSpot’s.

How Turbyn handles packages

Turbyn runs your action in a sandboxed micro-VM, so the package set is genuinely open. Two design decisions are worth calling out:

Imports are the manifest. There’s no separate dependency file to maintain. Import what you need and it’s installed:

import { differenceInBusinessDays } from "date-fns";
import { z } from "zod";
const Input = z.object({ closeDate: z.string() });
exports.main = async (event, ctx) => {
const { closeDate } = Input.parse(event.inputFields);
const days = differenceInBusinessDays(new Date(closeDate), new Date());
return { outputFields: { output1: String(days) } };
};

Python is the same idea:

import httpx
from dateutil.relativedelta import relativedelta
async def main(event, ctx):
async with httpx.AsyncClient() as client:
res = await client.get("https://api.example.com/lookup")
return {"outputFields": {"output1": res.json()["value"]}}

Versions are pinned per published version. Each time you publish, the resolved dependency tree is locked to that version of your action. A published action keeps running exactly the dependency tree it was tested against. A package publishing a broken or compromised release upstream doesn’t silently change what’s already in production. You can pin explicitly too (axios@1.6.0) when you want a specific version.

Common packages are pre-warmed so the usual case doesn’t pay an install cost, and anything outside that set installs on first use and is then cached.

Given how many npm packages have been compromised in supply-chain attacks recently, “the code in production is the dependency tree you tested” is a property worth having rather than a footnote.

FAQ

Frequently asked

Can I add an npm package to a HubSpot custom code action?

Not in the native action. The runtime exposes a fixed set of pre-installed packages and there is no manifest file, no install step and no upload mechanism. If a package isn't already in the runtime, the only native options are to reimplement what you need by hand or to call an external service that has the dependency.

Is moment.js available in HubSpot custom code?

No. moment.js has been the single most requested addition to the HubSpot custom code runtime since 2021 and remains unavailable. Most people work around it by writing date arithmetic by hand against the built-in Date object, which is where a large share of date-handling bugs in HubSpot workflows come from.

Can I vendor a package by pasting its source into the action?

For a small, dependency-free utility, sometimes. It fails quickly for anything real: most packages have their own dependencies, the code counts against the 128 MB memory limit, and you take on manually tracking upstream security patches for vendored code. It's a workaround, not a solution.

What about Python packages?

The same restriction applies. HubSpot's Python custom code runtime provides a fixed set of libraries with no requirements.txt and no pip install step, so anything outside that set is unavailable.

Start with one workflow step

Connect a portal, write one action, and put it in a workflow. About ten minutes, and the free tier doesn't ask for a card.