Runtime verified Differs from official docs Test scripts included

Syntax

FormatDate(dateString[, dateFormat, timeFormat, localeCode])  →  string
1–4 arguments

Parameters

Name Type Required Description
dateString string | date Yes The date to format, either a real date value or a parseable date string
dateFormat string No The date pattern, or a single-letter standard format such as D, G or s
timeFormat string No The time pattern — the only argument in which mm means minutes
localeCode string No The locale for month and day names, written with a hyphen or an underscore

Example

%%[
  VAR @d, @out
  SET @d = "2026-03-04 13:52:07"
  SET @out = FormatDate(@d, "yyyy-MM-dd", "HH:mm:ss")
]%%
%%=v(@out)=%%

Renders 2026-03-04 13:52:07.

The split across two arguments is not optional styling — it is what makes the time correct. Putting the whole pattern in the date argument, the way a single .NET format string would be written, silently returns the wrong minutes:

%%[
  VAR @d, @wrong, @right
  SET @d = "2026-03-04 13:52:07"
  SET @wrong = FormatDate(@d, "yyyy-MM-dd HH:mm:ss")
  SET @right = FormatDate(@d, "yyyy-MM-dd", "HH:mm:ss")
]%%
<p>One argument: %%=v(@wrong)=%%</p>
<p>Two arguments: %%=v(@right)=%%</p>

The first line renders 2026-03-04 13:03:07 and the second 2026-03-04 13:52:07. Why the minutes became 03 is the subject of the next chapter.

Return value

string — the formatted date.

There is no closed set of sentinel values: the output is whatever the pattern produced. The one value worth testing for is the empty string, which comes back whenever the first argument cannot be read as a date — an unparseable string, an empty string and a number all returned it.

Behaviour

The two pattern arguments are separate token sets. Whatever the second argument contains is resolved as a date pattern and whatever the third contains as a time pattern, and the same letters can mean different things in each. This is why a pattern that looks correct as a single .NET format string comes out wrong: split it across the two arguments instead.

Tokens ignore case entirely. YYYY rendered 2026 exactly as yyyy did, mmmm rendered March exactly as MMMM did, and DDDD matched dddd. Capitalisation is therefore never a way to disambiguate two meanings of the same letters.

Single-letter tokens select a standard format, not an unpadded number. For 2026-03-04 13:52:07, d rendered 3/4/2026 and M rendered March 4. In the time argument, h or H on its own does not render an hour at all — it aborts the page. Use dd, MM, hh and HH.

A standard-format letter can stand in for the whole pattern. G rendered 3/4/2026 1:52:07 PM, F rendered Wednesday, March 4, 2026 1:52:07 PM, r rendered Wed, 04 Mar 2026 13:52:07 GMT and o rendered 2026-03-04T13:52:07.0000000. Two are worth knowing about: D gave the short date 3/4/2026 rather than a long one, and U rendered Wednesday, March 4, 2026 7:52:07 PM — six hours ahead, because it treats the input as local and renders it as UTC.

The locale accepts either separator and falls back silently. fr-FR and fr_FR produced identical output. uk_UA rendered 04.03.2026 where en-US rendered 3/4/2026, and French month names come back lower-cased (mars). A locale code that does not exist is not rejected — a made-up one still rendered a date.

Bad input is not reported, it is swallowed. An unparseable date, an empty date and a number each returned an empty string at HTTP 200. An unrecognised format token is echoed back literally — qqqq rendered qqqq. A caller cannot tell a failed parse from a legitimately empty result, so validate the input before formatting it.

Input can be a real date value, not only a string. The output of DateAdd(Now(), 1, "D") formatted directly, with no conversion step, as did Now() itself.

The date pattern’s mm is the month, and the day names are shifted

Call Renders
FormatDate(@d, "mm") 03 — the month
FormatDate(@d, "", "mm") 52 — the minutes
FormatDate(@d, "yyyy-MM-dd HH:mm:ss") 2026-03-04 13:03:07
FormatDate(@d, "yyyy-MM-dd", "HH:mm:ss") 2026-03-04 13:52:07
FormatDate(@d, "ddd") We4ne74a26
FormatDate(@d, "dddd") Wed
FormatDate(@d, "ddddd") Wednesday

All against @d = "2026-03-04 13:52:07", a Wednesday.

Minutes are simply not addressable from the date pattern: mm and MM are the same token there and both give the month. Move to the time pattern and both give the minutes. The day-name tokens are shifted by one repetition against what the reference promises, and ddd does not produce a day name at all — the digits in We4ne74a26 come from the date itself, and a December date produced ri25a26 the same way. Never use ddd. See the differs-from-docs card for the full comparison against the official reference.

Show test script
%%[
  VAR @b, @d
  SET @b = RequestParameter("b")
  SET @d = "2026-03-04 13:52:07"

  /* the documented custom tokens in the DATE pattern */
  IF @b == "safe" THEN
    OutputLine(Concat("--- safe start ---"))
    OutputLine(Concat("DEF=[", FormatDate(@d), "]"))
    OutputLine(Concat("yy=[", FormatDate(@d, "yy"), "]"))
    OutputLine(Concat("yyyy=[", FormatDate(@d, "yyyy"), "]"))
    OutputLine(Concat("M=[", FormatDate(@d, "M"), "]"))
    OutputLine(Concat("MM=[", FormatDate(@d, "MM"), "]"))
    OutputLine(Concat("MMM=[", FormatDate(@d, "MMM"), "]"))
    OutputLine(Concat("MMMM=[", FormatDate(@d, "MMMM"), "]"))
    OutputLine(Concat("d=[", FormatDate(@d, "d"), "]"))
    OutputLine(Concat("dd=[", FormatDate(@d, "dd"), "]"))

    /* the day-name tokens: ddd is corrupted, dddd is the abbreviation and
       the full name needs a fifth d */
    OutputLine(Concat("ddd=[", FormatDate(@d, "ddd"), "]"))
    OutputLine(Concat("dddd=[", FormatDate(@d, "dddd"), "]"))
    OutputLine(Concat("ddddd=[", FormatDate(@d, "ddddd"), "]"))

    OutputLine(Concat("gg=[", FormatDate(@d, "gg"), "]"))
    OutputLine(Concat("--- safe done ---"))
  ENDIF

  /* the tokens ignore case: each upper form matches its lower twin */
  IF @b == "case" THEN
    OutputLine(Concat("--- case start ---"))
    OutputLine(Concat("Y=[", FormatDate(@d, "Y"), "]"))
    OutputLine(Concat("YYYY=[", FormatDate(@d, "YYYY"), "]"))
    OutputLine(Concat("mmmm=[", FormatDate(@d, "mmmm"), "]"))
    OutputLine(Concat("mm=[", FormatDate(@d, "mm"), "]"))
    OutputLine(Concat("m=[", FormatDate(@d, "m"), "]"))
    OutputLine(Concat("DD=[", FormatDate(@d, "DD"), "]"))
    OutputLine(Concat("DDDD=[", FormatDate(@d, "DDDD"), "]"))
    OutputLine(Concat("--- case done ---"))
  ENDIF

  /* the headline: the minutes position of a one-argument pattern renders
     the month, and no capitalisation changes it */
  IF @b == "cross" THEN
    OutputLine(Concat("--- cross start ---"))
    OutputLine(Concat("REPRO=[", FormatDate(@d, "yyyy-MM-dd HH:mm:ss"), "]"))
    OutputLine(Concat("REPROLOW=[", FormatDate(@d, "yyyy-mm-dd hh:mm:ss"), "]"))
    OutputLine(Concat("dHH=[", FormatDate(@d, "HH"), "]"))
    OutputLine(Concat("dhh=[", FormatDate(@d, "hh"), "]"))
    OutputLine(Concat("dss=[", FormatDate(@d, "ss"), "]"))
    OutputLine(Concat("dtt=[", FormatDate(@d, "tt"), "]"))
    OutputLine(Concat("dzzz=[", FormatDate(@d, "zzz"), "]"))
    OutputLine(Concat("--- cross done ---"))
  ENDIF

  /* the same tokens in the TIME pattern, where mm finally means minutes */
  IF @b == "time" THEN
    OutputLine(Concat("--- time start ---"))
    OutputLine(Concat("thh=[", FormatDate(@d, "", "hh"), "]"))
    OutputLine(Concat("tHH=[", FormatDate(@d, "", "HH"), "]"))
    OutputLine(Concat("tmm=[", FormatDate(@d, "", "mm"), "]"))
    OutputLine(Concat("tMM=[", FormatDate(@d, "", "MM"), "]"))
    OutputLine(Concat("tss=[", FormatDate(@d, "", "ss"), "]"))
    OutputLine(Concat("ttt=[", FormatDate(@d, "", "tt"), "]"))
    OutputLine(Concat("tfff=[", FormatDate(@d, "", "fff"), "]"))
    OutputLine(Concat("tcombo=[", FormatDate(@d, "", "HH:mm:ss"), "]"))
    OutputLine(Concat("tboth=[", FormatDate(@d, "yyyy-MM-dd", "HH:mm:ss"), "]"))
    OutputLine(Concat("--- time done ---"))
  ENDIF

  /* the single-letter standard formats */
  IF @b == "std" THEN
    OutputLine(Concat("--- std start ---"))
    OutputLine(Concat("D=[", FormatDate(@d, "D"), "]"))
    OutputLine(Concat("F=[", FormatDate(@d, "F"), "]"))
    OutputLine(Concat("G=[", FormatDate(@d, "G"), "]"))
    OutputLine(Concat("o=[", FormatDate(@d, "o"), "]"))
    OutputLine(Concat("r=[", FormatDate(@d, "r"), "]"))
    OutputLine(Concat("T=[", FormatDate(@d, "T"), "]"))
    OutputLine(Concat("u=[", FormatDate(@d, "u"), "]"))
    OutputLine(Concat("U=[", FormatDate(@d, "U"), "]"))
    OutputLine(Concat("l=[", FormatDate(@d, "l"), "]"))
    OutputLine(Concat("iso=[", FormatDate(@d, "iso"), "]"))
    OutputLine(Concat("rfc=[", FormatDate(@d, "rfc"), "]"))
    OutputLine(Concat("--- std done ---"))
  ENDIF

  /* the locale argument: hyphen and underscore are interchangeable */
  IF @b == "locale" THEN
    OutputLine(Concat("--- locale start ---"))
    OutputLine(Concat("frD=[", FormatDate(@d, "D", "", "fr-FR"), "]"))
    OutputLine(Concat("frU=[", FormatDate(@d, "D", "", "fr_FR"), "]"))
    OutputLine(Concat("frMMMM=[", FormatDate(@d, "MMMM", "", "fr-FR"), "]"))
    OutputLine(Concat("jaD=[", FormatDate(@d, "D", "", "ja-JP"), "]"))
    OutputLine(Concat("ukS=[", FormatDate(@d, "S", "", "uk_UA"), "]"))
    OutputLine(Concat("enS=[", FormatDate(@d, "S", "", "en-US"), "]"))
    OutputLine(Concat("--- locale done ---"))
  ENDIF

  /* the accepted input forms, including a real date value */
  IF @b == "parse" THEN
    OutputLine(Concat("--- parse start ---"))
    OutputLine(Concat("ISO=[", FormatDate("2026-03-04T13:52:07.123-0700", "yyyy-MM-dd", "HH:mm:ss"), "]"))
    OutputLine(Concat("SDATE=[", FormatDate("2026-03-04", "yyyy-MM-dd", "HH:mm:ss"), "]"))
    OutputLine(Concat("USNP=[", FormatDate("3/4/2026", "yyyy-MM-dd"), "]"))
    OutputLine(Concat("WORDS=[", FormatDate("4 March 2026 1:52:07 PM", "yyyy-MM-dd", "HH:mm:ss"), "]"))
    OutputLine(Concat("DATEVAL=[", FormatDate(DateAdd(Now(), 1, "D"), "yyyy-MM-dd"), "]"))
    OutputLine(Concat("--- parse done ---"))
  ENDIF

  /* everything that returns empty or passes through instead of failing */
  IF @b == "empties" THEN
    OutputLine(Concat("--- empties start ---"))
    OutputLine(Concat("BAD=[", FormatDate("not a date at all", "yyyy"), "]"))
    OutputLine(Concat("EMPTY=[", FormatDate("", "yyyy"), "]"))
    OutputLine(Concat("NUM=[", FormatDate(20260304, "yyyy"), "]"))
    OutputLine(Concat("BADFMT=[", FormatDate(@d, "qqqq"), "]"))
    OutputLine(Concat("BADLOC=[", FormatDate(@d, "D", "", "zz-ZZ"), "]"))
    OutputLine(Concat("--- empties done ---"))
  ENDIF

  /* these two branches abort the page: the start marker never renders */
  IF @b == "hsingle" THEN
    OutputLine(Concat("--- hsingle start ---"))
    OutputLine(Concat("th=[", FormatDate(@d, "", "h"), "]"))
  ENDIF

  IF @b == "Hsingle" THEN
    OutputLine(Concat("--- Hsingle start ---"))
    OutputLine(Concat("tH=[", FormatDate(@d, "", "H"), "]"))
  ENDIF

  IF @b == "a0" THEN
    OutputLine(Concat("--- a0 start ---"))
    OutputLine(Concat("A0=[", FormatDate(), "]"))
  ENDIF

  IF @b == "a5" THEN
    OutputLine(Concat("--- a5 start ---"))
    OutputLine(Concat("A5=[", FormatDate(@d, "yyyy", "HH", "en-US", "extra"), "]"))
  ENDIF
]%%

Availability

Platform Available
Marketing Cloud Engagement Yes
Marketing Cloud Next Yes, from API 67.0

Marketing Cloud Next uses a different pattern dialect — Java-style format strings rather than the .NET-style ones proven here — so a pattern written for Engagement is not portable as-is.

See also