IndexOf
Returns the 1-based position of a substring, matching case-insensitively. Runtime-proven on a live Marketing Cloud Engagement CloudPage — including an undocumented third argument that selects which occurrence to locate.
Syntax
IndexOf(sourceString, substring[, occurrence]) → number
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
sourceString |
string | number | Yes | String to search in |
substring |
string | number | Yes | Substring to find |
occurrence |
string | number | No | Which occurrence to locate, as a whole number |
Example
%%[
VAR @pos
SET @pos = IndexOf("Hello World", "World")
]%%
Position: %%=v(@pos)=%%
Renders Position: 7.
The common pattern is splitting a value at a known separator — take the position, then subtract one to get the length of the part before it:
%%[
VAR @fullName, @space, @first
SET @fullName = "Dale Cameron"
SET @space = IndexOf(@fullName, " ")
IF @space > 0 THEN
SET @first = Substring(@fullName, 1, Subtract(@space, 1))
ENDIF
]%%
The search ignores letter case, and a third argument can reach past the first match — see below.
Return value
number — the 1-based position at which the substring starts.
0 is the only sentinel: it is returned when the substring is absent, when either argument is empty, and when a requested occurrence does not exist. Positions themselves are an open domain, so there is no closed set of values to test for beyond checking against 0.
Behaviour
Positions are 1-based. The leading character of Hello World is position 1, World starts at 7, and the trailing d is 11. A needle equal to the whole source returns 1, and a needle longer than the source returns 0.
Without a third argument only the first match is reported. IndexOf("banana", "na") gives 3 even though na also occurs at 5.
Any empty argument returns 0. An empty substring, an empty source, and both empty all return 0 rather than the position 1 some string APIs give for an empty needle.
Numbers are accepted for both text parameters and searched by their string form. Searching the numeric literal 9876543 for "65" gives 4. Booleans are not usable text: passing true as the source and searching for a fragment of the word it would spell returns 0, as does searching a string that literally contains that word for a boolean needle.
Positions count UTF-16 code units, the same unit Length counts. In a source built as caf + Char(233) + " time", which measures 9, the accented letter is at 4 and the following word at 6.
Argument counts outside two or three abort the page. Zero, one and four arguments each returned HTTP 422 with nothing rendered — including the markers printed before the call — so there is no error value to test for.
The search is case-insensitive
This is the finding most likely to bite, because no source mentions it and every published example happens to search with matching case.
| Call | Returns |
|---|---|
IndexOf("Hello World", "World") |
7 |
IndexOf("Hello World", "WORLD") |
7 |
IndexOf("Hello World", "world") |
7 |
IndexOf("Hello World", "h") |
1 |
There is no flag to make the match case-sensitive. When case matters, extract the located text with Substring and compare it yourself rather than expecting IndexOf to return 0 on a case mismatch.
The undocumented third argument selects an occurrence
A third argument does not abort the way a fourth does — it succeeds and chooses which match to report.
| Call | Returns |
|---|---|
IndexOf("Hello World", "o", 1) |
5 |
IndexOf("Hello World", "o", 2) |
8 |
IndexOf("Hello World", "o", 3) |
0 |
IndexOf("aaaa", "aa", 2) |
3 |
IndexOf("abcabcabc", "abc", -1) |
7 |
Overlapping matches are not counted separately, which is why the second aa inside aaaa is at 3 rather than 2. A count past the last match returns 0. 0 selects the first match, while a negative value resolves to the last match whatever its magnitude — -1, -2 and -9 all returned the third and final match above.
A numeric string works in that position; a decimal, a boolean, and a non-numeric string each abort the page. The argument is undocumented, so it carries no compatibility guarantee.
Both findings are catalogued on Differs from official docs. The docs are silent rather than wrong in both cases, so the entry is not flagged as contradicting them.
Show test script
%%[
VAR @b
SET @b = RequestParameter("b")
/* documented forms plus the case-insensitivity and the accepted types */
IF @b == "safe" THEN
OutputLine(Concat("FIRSTCHAR=[", IndexOf("Hello World", "H"), "]"))
OutputLine(Concat("WORD=[", IndexOf("Hello World", "World"), "]"))
OutputLine(Concat("LASTCHAR=[", IndexOf("Hello World", "d"), "]"))
OutputLine(Concat("WHOLE=[", IndexOf("Hello World", "Hello World"), "]"))
OutputLine(Concat("REPEATED=[", IndexOf("banana", "na"), "]"))
OutputLine(Concat("MISSING=[", IndexOf("Hello World", "zzz"), "]"))
OutputLine(Concat("LONGERNEEDLE=[", IndexOf("ab", "abcdef"), "]"))
OutputLine(Concat("UPPERNEEDLE=[", IndexOf("Hello World", "WORLD"), "]"))
OutputLine(Concat("LOWERNEEDLE=[", IndexOf("Hello World", "world"), "]"))
OutputLine(Concat("LOWERH=[", IndexOf("Hello World", "h"), "]"))
OutputLine(Concat("SPACECHAR=[", IndexOf("Dale Cameron", " "), "]"))
OutputLine(Concat("EMPTYNEEDLE=[", IndexOf("Hello", ""), "]"))
OutputLine(Concat("EMPTYHAY=[", IndexOf("", "a"), "]"))
OutputLine(Concat("BOTHEMPTY=[", IndexOf("", ""), "]"))
OutputLine(Concat("NUMHAY=[", IndexOf(9876543, "65"), "]"))
OutputLine(Concat("NUMNEEDLE=[", IndexOf("a1b2c3", 2), "]"))
OutputLine(Concat("BOOLHAY=[", IndexOf(true, "ru"), "]"))
OutputLine(Concat("BOOLNEEDLE=[", IndexOf("is true here", true), "]"))
ENDIF
/* the undocumented third argument picks which occurrence to locate */
IF @b == "occurrence" THEN
OutputLine(Concat("OCC1=[", IndexOf("Hello World", "o", 1), "]"))
OutputLine(Concat("OCC2=[", IndexOf("Hello World", "o", 2), "]"))
OutputLine(Concat("OCC3=[", IndexOf("Hello World", "o", 3), "]"))
OutputLine(Concat("OCCL3=[", IndexOf("Hello World", "l", 3), "]"))
OutputLine(Concat("OCCNA2=[", IndexOf("banana", "na", 2), "]"))
OutputLine(Concat("OCCOVERLAP=[", IndexOf("aaaa", "aa", 2), "]"))
OutputLine(Concat("OCCZERO=[", IndexOf("Hello World", "o", 0), "]"))
OutputLine(Concat("OCCSTR=[", IndexOf("banana", "na", "2"), "]"))
ENDIF
/* a negative occurrence resolves to the LAST match, whatever its size */
IF @b == "occneg" THEN
OutputLine(Concat("NEG1_BANANA=[", IndexOf("banana", "na", -1), "]"))
OutputLine(Concat("NEG2_BANANA=[", IndexOf("banana", "na", -2), "]"))
OutputLine(Concat("NEG1_ABC=[", IndexOf("abcabcabc", "abc", -1), "]"))
OutputLine(Concat("NEG2_ABC=[", IndexOf("abcabcabc", "abc", -2), "]"))
OutputLine(Concat("NEG9_ABC=[", IndexOf("abcabcabc", "abc", -9), "]"))
ENDIF
/* positions count UTF-16 code units, matching Length */
IF @b == "unicode" THEN
VAR @u
SET @u = Concat("caf", Char(233), " time")
OutputLine(Concat("ULEN=[", Length(@u), "]"))
OutputLine(Concat("UAFTER=[", IndexOf(@u, "time"), "]"))
OutputLine(Concat("UACCENT=[", IndexOf(@u, Char(233)), "]"))
ENDIF
/* every branch below aborts the page: the start marker never renders */
IF @b == "a0" THEN
OutputLine(Concat("--- a0 start ---"))
OutputLine(Concat("A0=[", IndexOf(), "]"))
ENDIF
IF @b == "a1" THEN
OutputLine(Concat("--- a1 start ---"))
OutputLine(Concat("A1=[", IndexOf("Hello World"), "]"))
ENDIF
IF @b == "a4" THEN
OutputLine(Concat("--- a4 start ---"))
OutputLine(Concat("A4=[", IndexOf("Hello World", "o", 1, 2), "]"))
ENDIF
IF @b == "occstrbad" THEN
OutputLine(Concat("--- occstrbad start ---"))
OutputLine(Concat("OCCSTRBAD=[", IndexOf("Hello World", "o", "abc"), "]"))
ENDIF
IF @b == "occdec" THEN
OutputLine(Concat("--- occdec start ---"))
OutputLine(Concat("OCCDEC=[", IndexOf("banana", "na", 1.5), "]"))
ENDIF
IF @b == "occbool" THEN
OutputLine(Concat("--- occbool start ---"))
OutputLine(Concat("OCCBOOL=[", IndexOf("banana", "na", true), "]"))
ENDIF
]%%
OutputLine given a bare string literal renders an empty line. Wrap the argument in Concat() or your start and done markers vanish silently — which looks exactly like the function failing.
Availability
| Platform | Available |
|---|---|
| Marketing Cloud Engagement | Yes |
| Marketing Cloud Next | Yes, from API 67.0 |
See also
- Differs from official docs — the case-insensitivity and the occurrence argument in full
Length— the counting unit the returned positions useConcat— builds the strings you search- Official reference · ampscript.guide