DateAdd
Adds a whole number of intervals to a date. Runtime-proven on a live Marketing Cloud Engagement CloudPage — including the fact that any unit outside the documented five destroys the page rather than returning a value.
Syntax
DateAdd(date, amountToAdd, unitToAdd) → date
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
date |
string | date | Yes | The date to adjust, either a real date value or a parseable date string |
amountToAdd |
string | number | Yes | A whole number of intervals — negative subtracts, and a decimal aborts the page |
unitToAdd |
string | Yes | One of Y, M, D, H or MI, in any capitalisation. Nothing else is accepted |
Example
%%[
VAR @d, @due
SET @d = "2026-03-04 13:52:07"
SET @due = DateAdd(@d, 14, "D")
]%%
%%=v(@due)=%%
Renders 3/18/2026 1:52:07 PM.
The amount may arrive as a string, which is what happens when it comes out of a data extension field — no conversion step is needed:
%%[
VAR @d, @a, @b
SET @d = "2026-03-04 13:52:07"
SET @a = DateAdd(@d, 1, "D")
SET @b = DateAdd(@d, "1", "D")
]%%
<p>Number: %%=v(@a)=%%</p>
<p>String: %%=v(@b)=%%</p>
Both lines render 3/5/2026 1:52:07 PM.
Return value
date — the adjusted date, which the other date functions accept directly without formatting or re-parsing. DateAdd(DateAdd(@d, 1, "D"), 1, "D") works, and so does passing the result straight into FormatDate.
There is no failure value to test for. Every rejected argument aborts the page with HTTP 422 instead of returning an empty string, so a result that exists is always a real date.
Behaviour
The five units are the whole list, and they ignore case. Y, M, D, H and MI each advance exactly one field and leave the rest alone. Capitalisation makes no difference anywhere: y, m, d, h, mi, Mi and mI all matched their upper-case spelling exactly.
Anything else costs you the entire page. There is no seconds unit, no weeks unit, no quarters, no milliseconds, and no spelled-out long forms — the word for a day is rejected just as an unknown two-letter token is. Each of those returned HTTP 422 with no output at all, while a control call in the same deployment rendered normally. Since nothing is written when the page aborts, a bad unit is not something the caller can detect and recover from; validate it before the call. For seconds, adjust in minutes or use a different approach entirely.
The amount must be whole. Negative subtracts, zero returns the date unchanged, and a large amount rolls the year over correctly. A numeric string is fine in either sign. A decimal is not — neither 1.5 nor "1.5" is rounded or truncated, and both abort, as does a non-numeric word.
A date string and a real date value are interchangeable. ISO, US slash and spelled-out month forms all parsed, and the output of Now() was accepted with no conversion. But unlike FormatDate, which quietly returns an empty string for input it cannot read, DateAdd aborts: an unparseable string, an empty string and a plain number each killed the page. Two neighbouring functions, opposite failure modes.
Month arithmetic clamps, and the clamp is not reversible
| Call | Renders |
|---|---|
DateAdd("2026-01-31 13:52:07", 1, "M") |
2/28/2026 1:52:07 PM |
DateAdd("2026-01-31 13:52:07", 2, "M") |
3/31/2026 1:52:07 PM |
DateAdd(DateAdd("2026-01-31 13:52:07", 1, "M"), -1, "M") |
1/28/2026 1:52:07 PM |
DateAdd("2028-02-29 09:00:00", 1, "Y") |
2/28/2029 9:00:00 AM |
Adding a month to the 31st of a month pulls the day back to the last valid one rather than spilling into the following month. The second row shows the clamp is applied to each result rather than carried forward — two months from the same base restores the 31st.
The third row is the one that bites. Stepping forward a month and back again lands on the 28th, three days from where you started, so any loop that walks a date across month boundaries and expects to return to its origin will drift. If you need month-end semantics, compute the day of month yourself.
Show test script
%%[
VAR @b, @d, @j
SET @b = RequestParameter("b")
SET @d = "2026-03-04 13:52:07"
SET @j = "2026-01-31 13:52:07"
/* the five accepted units, plus a numeric-string amount */
IF @b == "safe" THEN
OutputLine(Concat("--- safe start ---"))
OutputLine(Concat("BASE=[", @d, "]"))
OutputLine(Concat("Y=[", DateAdd(@d, 1, "Y"), "]"))
OutputLine(Concat("M=[", DateAdd(@d, 1, "M"), "]"))
OutputLine(Concat("D=[", DateAdd(@d, 1, "D"), "]"))
OutputLine(Concat("H=[", DateAdd(@d, 1, "H"), "]"))
OutputLine(Concat("MI=[", DateAdd(@d, 1, "MI"), "]"))
OutputLine(Concat("STRAMT=[", DateAdd(@d, "1", "D"), "]"))
OutputLine(Concat("STRAMTNEG=[", DateAdd(@d, "-1", "D"), "]"))
OutputLine(Concat("--- safe done ---"))
ENDIF
/* the unit token ignores case in every spelling */
IF @b == "case" THEN
OutputLine(Concat("--- case start ---"))
OutputLine(Concat("y=[", DateAdd(@d, 1, "y"), "]"))
OutputLine(Concat("m=[", DateAdd(@d, 1, "m"), "]"))
OutputLine(Concat("d=[", DateAdd(@d, 1, "d"), "]"))
OutputLine(Concat("h=[", DateAdd(@d, 1, "h"), "]"))
OutputLine(Concat("mi=[", DateAdd(@d, 1, "mi"), "]"))
OutputLine(Concat("Mi=[", DateAdd(@d, 1, "Mi"), "]"))
OutputLine(Concat("mI=[", DateAdd(@d, 1, "mI"), "]"))
OutputLine(Concat("--- case done ---"))
ENDIF
/* negative, zero and large amounts */
IF @b == "amt" THEN
OutputLine(Concat("--- amt start ---"))
OutputLine(Concat("NEG=[", DateAdd(@d, -1, "D"), "]"))
OutputLine(Concat("ZERO=[", DateAdd(@d, 0, "D"), "]"))
OutputLine(Concat("NEGH=[", DateAdd(@d, -3, "H"), "]"))
OutputLine(Concat("BIG=[", DateAdd(@d, 400, "D"), "]"))
OutputLine(Concat("--- amt done ---"))
ENDIF
/* month-end clamping, and the fact that it is not reversible */
IF @b == "roll" THEN
OutputLine(Concat("--- roll start ---"))
OutputLine(Concat("JAN31M=[", DateAdd(@j, 1, "M"), "]"))
OutputLine(Concat("JAN31M2=[", DateAdd(@j, 2, "M"), "]"))
OutputLine(Concat("JAN31BACK=[", DateAdd(DateAdd(@j, 1, "M"), -1, "M"), "]"))
OutputLine(Concat("LEAPY=[", DateAdd("2028-02-29 09:00:00", 1, "Y"), "]"))
OutputLine(Concat("MAR31MINUS=[", DateAdd("2026-03-31 09:00:00", -1, "M"), "]"))
OutputLine(Concat("--- roll done ---"))
ENDIF
/* date strings and real date values are interchangeable */
IF @b == "dtype" THEN
OutputLine(Concat("--- dtype start ---"))
OutputLine(Concat("ISOSTR=[", DateAdd("2026-01-01", 1, "D"), "]"))
OutputLine(Concat("USSTR=[", DateAdd("1/1/2026", 1, "D"), "]"))
OutputLine(Concat("WORDSTR=[", DateAdd("1 January 2026", 1, "D"), "]"))
OutputLine(Concat("NOWVAL=[", FormatDate(DateAdd(Now(), 0, "D"), "yyyy"), "]"))
OutputLine(Concat("CHAIN=[", DateAdd(DateAdd(@d, 1, "D"), 1, "D"), "]"))
OutputLine(Concat("--- dtype done ---"))
ENDIF
/* the control - fetch this whenever a branch below renders nothing */
IF @b == "ctrl" THEN
OutputLine(Concat("--- ctrl start ---"))
OutputLine(Concat("CTRL=[", DateAdd(@d, 1, "D"), "]"))
OutputLine(Concat("CTRLJ=[", DateAdd(@j, 1, "M"), "]"))
OutputLine(Concat("--- ctrl done ---"))
ENDIF
/* every branch below aborts the page - each renders NOTHING at all */
IF @b == "sec" THEN
OutputLine(Concat("--- sec start ---"))
OutputLine(Concat("S=[", DateAdd(@d, 30, "S"), "]"))
ENDIF
IF @b == "wk" THEN
OutputLine(Concat("--- wk start ---"))
OutputLine(Concat("W=[", DateAdd(@d, 1, "W"), "]"))
ENDIF
IF @b == "longd" THEN
OutputLine(Concat("--- longd start ---"))
OutputLine(Concat("LONGD=[", DateAdd(@d, 1, "day"), "]"))
ENDIF
IF @b == "bogus" THEN
OutputLine(Concat("--- bogus start ---"))
OutputLine(Concat("BOGUS=[", DateAdd(@d, 1, "ZZ"), "]"))
ENDIF
IF @b == "emptytok" THEN
OutputLine(Concat("--- emptytok start ---"))
OutputLine(Concat("EMPTYTOK=[", DateAdd(@d, 1, ""), "]"))
ENDIF
IF @b == "frac" THEN
OutputLine(Concat("--- frac start ---"))
OutputLine(Concat("FRAC=[", DateAdd(@d, 1.5, "D"), "]"))
ENDIF
IF @b == "fracstr" THEN
OutputLine(Concat("--- fracstr start ---"))
OutputLine(Concat("FRACSTR=[", DateAdd(@d, "1.5", "D"), "]"))
ENDIF
IF @b == "amtword" THEN
OutputLine(Concat("--- amtword start ---"))
OutputLine(Concat("AMTWORD=[", DateAdd(@d, "two", "D"), "]"))
ENDIF
IF @b == "baddate" THEN
OutputLine(Concat("--- baddate start ---"))
OutputLine(Concat("BADDATE=[", DateAdd("not a date at all", 1, "D"), "]"))
ENDIF
IF @b == "emptydate" THEN
OutputLine(Concat("--- emptydate start ---"))
OutputLine(Concat("EMPTYDATE=[", DateAdd("", 1, "D"), "]"))
ENDIF
IF @b == "numdate" THEN
OutputLine(Concat("--- numdate start ---"))
OutputLine(Concat("NUMDATE=[", DateAdd(20260304, 1, "D"), "]"))
ENDIF
IF @b == "a2" THEN
OutputLine(Concat("--- a2 start ---"))
OutputLine(Concat("A2=[", DateAdd(@d, 1), "]"))
ENDIF
IF @b == "a4" THEN
OutputLine(Concat("--- a4 start ---"))
OutputLine(Concat("A4=[", DateAdd(@d, 1, "D", "extra"), "]"))
ENDIF
]%%
AMPscript has no try/catch, so a bad unit, a decimal amount or an unreadable date aborts the render and throws away everything already written above it. When testing, put each risky call behind its own RequestParameter branch and fetch them one at a time — otherwise a single failure hides every result on the page.
Availability
| Platform | Available |
|---|---|
| Marketing Cloud Engagement | Yes |
| Marketing Cloud Next | Yes, from API 67.0 |
See also
- Now — the value most often passed in as the first argument
- FormatDate — formats the result, and swallows bad input where this function aborts
- The differs-from-docs cards — what the official reference leaves unsaid
- Official reference · ampscript.guide