Marketing Tools

How to Format Dates in JavaScript for Zapier Code Steps (Moment.js Guide)

Zapier pre-loads moment and moment-timezone as global variables in all Code steps, removing the need for manual installation or imports. However, date formatting challenges frequently arise because different connected apps output distinct date schemas, such as Stripe using Unix seconds and HubSpot using Unix milliseconds. For reliable execution, review raw trigger data to confirm the input schema, map the appropriate parsing logic, and use IANA timezone identifiers to prevent server-side UTC offsets or daylight saving time errors.

To refine your automated data workflows, establish whether you need to convert app-specific timestamps, adjust regional timezones, or perform calendar date arithmetic.

moment is available in every Zapier Code step as a global variable. You do not need to require it, import it, or install anything. You can use it directly:

const formatted = moment(inputData.date).format('YYYY-MM-DD');
output = { formatted_date: formatted };

That one pattern covers about half the date formatting problems you’ll hit in Zapier. The other half involve timezones, Unix timestamps measured in seconds instead of milliseconds, and dates that come in as something other than what you’d expect from the trigger app. Those are what this post is actually about.

I have 22 active Zaps that include at least one date manipulation step. The first one took me 47 minutes to debug. Now the same class of problem takes under 5 minutes. The difference is knowing which three or four things are likely to be wrong before I start looking.


Why Dates in Zapier Are Annoying

Every app in your Zap has its own opinion about what a date looks like. Before you can format anything, you need to know what you’re starting with. The formats I encounter most often:

Stripe: Unix timestamp in seconds. 1710511380 means March 15, 2024 at 14:23 UTC.

HubSpot: Unix timestamp in milliseconds for date properties. 1710511380000. This is the same moment in time as the Stripe value above, multiplied by 1000. Missing this distinction took me 90 minutes the first time.

Google Sheets: Either an ISO string (2024-03-15) if the cell is formatted as a date, a serial number like 45366 (days since December 30, 1899) if the cell is formatted as a number, or occasionally a localized string like 15/03/2024 depending on the spreadsheet’s locale. All three are plausible from the same Sheets trigger depending on how the column was set up.

Salesforce: ISO 8601 with timezone offset. 2024-03-15T14:23:00.000+0000.

Typeform and Airtable: ISO 8601 in UTC. 2024-03-15T14:23:00.000Z.

Calendly: ISO 8601 with timezone. 2024-03-15T14:23:00+05:30 for an IST booking.

Gmail: RFC 2822. Mon, 15 Mar 2024 14:23:00 +0000. Moment parses this correctly, but it’s worth knowing it exists because it looks nothing like the others.

The first thing I do when writing a date step is run the Zap with a real trigger, look at the raw value in the Code step’s test data, and confirm which format I’m actually dealing with before writing a single line of formatting code.


The Patterns That Cover 90% of Cases

Parse and reformat

Takes a date string in one format and outputs it in another. The most common operation.

// Input: "2024-03-15T14:23:00.000Z" (ISO from Airtable, Typeform, etc.)
// Output: "03/15/2024" (MM/DD/YYYY, common for US-facing display)

const raw = inputData.date; // e.g. "2024-03-15T14:23:00.000Z"
const formatted = moment(raw).format('MM/DD/YYYY');

output = { formatted_date: formatted };

Common format tokens:

YYYY    — 4-digit year (2024)
YY      — 2-digit year (24)
MM      — 2-digit month (03)
MMMM    — full month name (March)
MMM     — abbreviated month name (Mar)
DD      — 2-digit day (15)
Do      — day with ordinal (15th)
HH      — 24-hour hour (14)
hh      — 12-hour hour (02)
mm      — minutes (23)
ss      — seconds (00)
A       — AM/PM

So moment(raw).format('MMMM Do, YYYY') gives you March 15th, 2024.

Convert a Unix timestamp (seconds) to a formatted date

Stripe, Intercom, and several other APIs send timestamps as Unix seconds. Moment expects milliseconds. Multiply by 1000 before passing it in.

// Input: 1710511380 (Stripe's current_period_end, seconds)
// Output: "2024-03-15"

const unixSeconds = parseInt(inputData.timestamp, 10);
const formatted = moment(unixSeconds * 1000).format('YYYY-MM-DD');

output = { formatted_date: formatted };

The parseInt call is there because Zapier sometimes passes numbers as strings. moment("1710511380" * 1000) evaluates to moment(NaN) and gives you Invalid date with no helpful error message. Parse it to an integer first.

Convert a Unix timestamp (milliseconds) to formatted date

HubSpot returns timestamps in milliseconds. Skip the multiplication.

const unixMs = parseInt(inputData.timestamp, 10);
const formatted = moment(unixMs).format('YYYY-MM-DD');

output = { formatted_date: formatted };

Get today’s date

const today = moment().format('YYYY-MM-DD');
output = { today: today };

Get today’s date in a specific format for a form or CRM field

// "March 15, 2024" — useful for email subject lines, doc titles
const label = moment().format('MMMM D, YYYY');
output = { date_label: label };

Timezone Handling

This is where most date bugs actually live. The short version: moment() uses the local timezone of the server running your Code step. Zapier’s servers run in UTC. If you don’t specify a timezone, all your dates are UTC, which may not be what you or your users expect.

moment-timezone is also pre-loaded in Zapier. Use it any time you need to work with a specific timezone.

// Convert a UTC ISO string to Eastern Time, formatted for display
const raw = inputData.date; // "2024-03-15T19:23:00.000Z"
const eastern = moment(raw).tz('America/New_York').format('MM/DD/YYYY h:mm A z');
// Output: "03/15/2024 3:23 PM EDT"

output = { formatted_date: eastern };

The timezone identifier has to be a valid IANA timezone string. Common ones:

America/New_York      — US Eastern
America/Chicago       — US Central
America/Denver        — US Mountain
America/Los_Angeles   — US Pacific
Europe/London         — UK (BST/GMT depending on DST)
Europe/Berlin         — Central European
Asia/Tokyo            — Japan Standard Time
Australia/Sydney      — AEST/AEDT

If you’re not sure which string to use, the full list is at momentjs.com/timezone.

The DST trap

Most timezone conversion bugs I’ve seen in Zaps involve daylight saving time. moment().tz('America/New_York') handles DST correctly — it knows whether New York is currently on EST (UTC-5) or EDT (UTC-4) and applies the right offset. Do not hardcode an offset like moment().utcOffset(-5) unless you specifically want a fixed offset that ignores DST.


Date Math

Adding or subtracting time is straightforward. These patterns show up in subscription expiry calculations, follow-up reminders, and trial period logic.

// Add 30 days to a date (subscription expiry)
const start = inputData.start_date; // "2024-03-15"
const expiry = moment(start).add(30, 'days').format('YYYY-MM-DD');
// Output: "2024-04-14"

output = { expiry_date: expiry };
// Subtract 7 days (reminder trigger: send email 7 days before expiry)
const expiry = inputData.expiry_date;
const reminder = moment(expiry).subtract(7, 'days').format('YYYY-MM-DD');

output = { reminder_date: reminder };
// How many days until expiry?
const expiry = inputData.expiry_date;
const daysLeft = moment(expiry).diff(moment(), 'days');
// Returns a negative number if the date is in the past

output = { days_until_expiry: daysLeft };

Units for .add(), .subtract(), and .diff(): 'days', 'weeks', 'months', 'years', 'hours', 'minutes', 'seconds'.

Month arithmetic gotcha

moment('2024-01-31').add(1, 'month') gives you 2024-02-29 (or 2024-02-28 in non-leap years) because February doesn’t have 31 days. Moment clamps to the last valid day of the resulting month. This is usually what you want, but if you need strict 30-day intervals rather than calendar months, use add(30, 'days') instead.


The Google Sheets Serial Number Problem

I ran into this one building a Zap that read contract end dates from a Google Sheet and sent renewal reminders. The dates in the sheet looked normal when I opened it, but the Zapier trigger was returning values like 45366 instead of 2024-03-15.

This happens when a Google Sheets date column is formatted as a number rather than a date in the spreadsheet itself. Sheets stores all dates internally as serial numbers — days since December 30, 1899. Moment has no idea what 45366 means as a date.

The fix is to convert the serial number before passing it to moment:

// Convert Google Sheets serial number to a real date
const serial = parseInt(inputData.date_serial, 10);

// Google's epoch is December 30, 1899
const msPerDay = 86400000;
const googleEpoch = new Date(1899, 11, 30).getTime();
const dateMs = googleEpoch + (serial * msPerDay);

const formatted = moment(dateMs).format('YYYY-MM-DD');
output = { formatted_date: formatted };

The cleaner fix is to go back to the Google Sheet and explicitly format the date columns as “Date” rather than “Number.” Then Zapier receives an ISO string and moment parses it normally. Sometimes you can’t control the sheet, though, so it’s useful to have the conversion ready.



Three Things That Produce “Invalid date”

1. Ambiguous format strings without explicit parsing.

moment('03/15/2024') usually works in a US context because moment assumes MM/DD/YYYY for that pattern. moment('15/03/2024') — a perfectly normal date in most of Europe — will parse incorrectly or return Invalid date because moment tries MM/DD/YYYY first and 15 isn’t a valid month.

If the format of your input date is known and fixed, specify it explicitly:

// Tell moment exactly what format to expect
const date = moment(inputData.date, 'DD/MM/YYYY').format('YYYY-MM-DD');

2. Passing a number as a string to Unix timestamp conversion without parseInt.

Covered above, but worth repeating: moment("1710511380" * 1000) produces NaN. Always parseInt first.

3. Empty or null inputData values.

If the field feeding the date is optional and the Zap runs when it’s empty, moment(undefined) returns the current date and time rather than an error. That’s usually not what you want — you’ll silently get today’s date wherever the expiry date or due date was supposed to go.

A guard at the top of any date step that handles optional fields:

if (!inputData.date) {
  output = { formatted_date: null };
  return;
}

const formatted = moment(inputData.date).format('YYYY-MM-DD');
output = { formatted_date: formatted };

A Note on Moment.js Being in Maintenance Mode

Moment.js has been in maintenance mode since September 2020. The team isn’t adding features, just fixing critical bugs. For new projects outside Zapier, the recommendation is to use Day.js (almost identical API, much smaller bundle) or Luxon (the spiritual successor from some of the same authors).

In Zapier Code steps, this matters less than it sounds. You’re not shipping a bundle to users, and you’re not installing dependencies — moment is just there. The API is stable and the functionality it covers is complete. I wouldn’t migrate existing working Code steps away from moment for any reason other than a specific feature need it can’t fulfill.

If you want to try Day.js in a Zapier step anyway, you can load it from a CDN at the top of your code:

const { JSDOM } = require('jsdom');
// Day.js isn't natively available, but you can use native JS Intl instead
// For Zapier, moment is still the path of least resistance

The native Intl.DateTimeFormat is available in Zapier’s Node.js environment and handles timezone formatting without any library:

const date = new Date(inputData.date);
const formatted = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  year: 'numeric',
  month: '2-digit',
  day: '2-digit'
}).format(date);

output = { formatted_date: formatted };

The output is 03/15/2024 for an Eastern Time formatted US date. Less flexible than moment for custom format strings, but no dependency at all.


Quick Reference: Formats by App

For copying when you’re in a hurry:

// From Stripe (Unix seconds) → YYYY-MM-DD
moment(parseInt(inputData.ts, 10) * 1000).format('YYYY-MM-DD')

// From HubSpot (Unix ms) → YYYY-MM-DD
moment(parseInt(inputData.ts, 10)).format('YYYY-MM-DD')

// From Airtable / Typeform / Calendly (ISO UTC) → local display
moment(inputData.date).tz('America/New_York').format('MM/DD/YYYY h:mm A z')

// From Salesforce (ISO with offset) → YYYY-MM-DD
moment(inputData.date).utc().format('YYYY-MM-DD')

// From Google Sheets (ISO string, well-formatted column) → any format
moment(inputData.date, 'YYYY-MM-DD').format('MMMM D, YYYY')

// From Google Sheets (serial number) → YYYY-MM-DD
const ms = new Date(1899, 11, 30).getTime() + parseInt(inputData.date, 10) * 86400000;
moment(ms).format('YYYY-MM-DD')

// Today's date
moment().format('YYYY-MM-DD')

// 30 days from a given date
moment(inputData.date).add(30, 'days').format('YYYY-MM-DD')

// Days until a future date (negative if in the past)
moment(inputData.date).diff(moment(), 'days')

FAQ

Do I need to require('moment') in a Zapier Code step? No. Moment is pre-loaded as a global variable. You can use moment(...) directly on the first line of your code. The same is true for Lodash (_) and moment-timezone. If you do write const moment = require('moment'), it still works — you’re just re-assigning the same library to the same variable name. Either way functions.

Why does my formatted date keep showing “Invalid date”? Most likely reasons in order of frequency: the input value is empty or undefined; you’re passing a string that looks like a number (Unix timestamp) to moment without parseInt first; or the date string format is ambiguous and moment is guessing wrong. Add console.log(inputData.date) at the top of your step and check what’s actually coming in before applying any formatting.

How do I output a date that HubSpot will accept? HubSpot’s date picker properties expect a Unix timestamp in milliseconds. moment(inputData.date).valueOf() returns the millisecond timestamp for any date moment can parse. For date-only properties (not datetime), HubSpot also requires the timestamp to represent midnight UTC on the target day, not the exact time — use moment.utc(inputData.date).startOf('day').valueOf().

Can I format dates in a Zapier Python Code step instead? Yes. In Python steps, the datetime module is available from the standard library. from datetime import datetime works without any install. The equivalent of moment(date).format('YYYY-MM-DD') in Python is datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ').strftime('%Y-%m-%d'). Python’s strftime format codes differ from moment’s — %Y is a 4-digit year, %m is month, %d is day.

What’s the difference between moment().format() and moment().toISOString()? format() returns a string in whatever pattern you specify. toISOString() returns the UTC ISO 8601 representation regardless of any timezone you’ve set. If you need to store a date for another system to parse (rather than display it), toISOString() is safer because it’s unambiguous. If you need a human-readable string or a format a specific app expects, use format().

My Zap runs on a schedule and I need “yesterday’s date.” How do I get it?

const yesterday = moment().subtract(1, 'day').format('YYYY-MM-DD');
output = { yesterday: yesterday };

Since Zapier’s servers run in UTC, this gives you yesterday in UTC. If you need yesterday in a specific timezone, apply .tz() before .subtract():

const yesterday = moment().tz('America/New_York').subtract(1, 'day').format('YYYY-MM-DD');

How do I check if one date is before or after another?

const a = moment(inputData.date_a);
const b = moment(inputData.date_b);

const isBefore = a.isBefore(b); // true or false
const isAfter  = a.isAfter(b);
const isSame   = a.isSame(b, 'day'); // compare at day granularity, ignoring time

output = { is_before: isBefore, is_after: isAfter };

The pattern that trips people up most often isn’t the formatting syntax — moment’s documentation covers that clearly. It’s the step before formatting: knowing what format the input date actually arrives in. The 47 minutes I spent debugging my first date Zap was almost entirely spent figuring out that Stripe was sending seconds and HubSpot was expecting milliseconds. The moment code itself was three lines.

Triumphoid Team

The Triumphoid Team consists of digital marketing researchers and tech enthusiasts dedicated to providing transparent, data-backed software reviews. Our content is independently researched and fact-checked

Recent Posts

How to A/B Test Your Website with AI: What Actually Changes (and What Doesn’t)

A/B testing with AI means using a language model to generate, critique, and prioritize test…

1 hour ago

Best B2B Data Providers in 2026: Compare Sales Intelligence, Enrichment and Intent Data Tools

The best B2B data provider in 2026 depends on what you need: verified sales contacts,…

13 hours ago

Triggering Multi-Agent CrewAI Workflows from Make.com Webhooks (Full Guide)

TL;DR — CrewAI + Make.com Webhook Integration Never run CrewAI synchronously in a webhook response.…

3 days ago

Why Your Webhooks Are Failing: Debugging Timeouts & Asynchronous Processing

Summery: Webhooks fail primarily because slow, synchronous processing leads to provider timeouts, causing repeated delivery…

4 days ago

The True Cost of WordPress Autoblogging in 2026

⚡ TL;DR The cost of autoblogging in 2026 is not mainly the API bill. That…

5 days ago

How to Use the WP Theme Customizer API for Dynamic Sites

⚡ TL;DR The WordPress theme customizer API is still a very practical tool for classic…

6 days ago