FEEL

FEEL string functions: substring, contains, split, replace and more

FEEL guide · 14 built-in functions · DMN 1.6

FEEL has no methods on strings. Everything you do with text in a DMN decision is a built-in function call or the + operator. This guide covers all of them, with the edge cases that tend to surprise people: positions start at 1 and can be negative, a null argument makes the result null, and three of the functions take a regular expression where you might expect plain text.

Each result on this page was computed by the FEEL engine behind the evaluator at the top. Click Try it to run any example, then change it.

All FEEL string functions at a glance

FunctionSignatureReturns
substringsubstring(string, start position, length?)Returns the part of the string that starts at start position
string lengthstring length(string)Returns the number of characters in the string
upper caseupper case(string)Returns the string with every character converted to upper case
lower caselower case(string)Returns the string with every character converted to lower case
substring beforesubstring before(string, match)Returns everything before the first occurrence of match; the empty string if match does not occur
substring aftersubstring after(string, match)Returns everything after the first occurrence of match; the empty string if match does not occur
containscontains(string, match)Returns true if the string contains match anywhere, false otherwise
starts withstarts with(string, match)Returns true if the string begins with match
ends withends with(string, match)Returns true if the string ends with match
matchesmatches(input, pattern, flags?)Returns true if the input matches the regular expression
replacereplace(input, pattern, replacement, flags?)Returns the input with every match of the regular expression replaced by the replacement, which may reference capture groups as $1, $2, …
splitsplit(string, delimiter)Splits the string at every match of the delimiter pattern and returns the parts as a list of strings
string joinstring join(list, delimiter?)Concatenates a list of strings into one string, optionally separated by a delimiter
stringstring(from)Converts any FEEL value to its string representation

Rules every string function follows

Positions are 1-based. A negative position counts from the end, so -1 is the last character. DMN 1.6 restricts substring to positions that exist: the start position must not be 0 and must lie within the string, and the length must not run past its end. Outside that range the result is null (Table 75). Some engines are more lenient there, so a portable model does not rely on out-of-range behavior. A fractional position or length is truncated to an integer.

Arguments are not converted. If a string function receives a number, a date or null where it expects a string, the result is null; nothing is turned into a string implicitly. Use string() to convert first.

Comparison is case-sensitive. contains, starts with, ends with and = compare characters exactly. To compare without case, lower-case both sides, or use matches with the i flag.

How substring counts start positions from either end. Each cell is the result of the column expression with that start position.
start positionsubstring("foobar", start)substring("foobar", start, 2)
1foobarfo
3obarob
-2arar
-6foobarfo
substring(123, 1)

A number is not a string; there is no implicit conversion.

substring(string(123), 1)

Convert first with string().

substring("foobar", 3, 100)

The length runs past the end of the string.

contains("FooBar", "foo")

Matching is case-sensitive.

contains(lower case("FooBar"), "foo")

Lower-case the input to compare without case.

Concatenating strings

FEEL has no concat function for two strings. Use the + operator. To join a list of strings, use string join, which also takes a separator. Neither one converts numbers for you, so wrap numbers and dates in string().

"Hello, " + "world"
Hello, world Try it ↑
"Total: " + string(42)
Total: 42 Try it ↑

Numbers have to be converted explicitly.

"Total: " + 42

Without string() the result is null in standard FEEL. B-FEEL converts the number (see the table at the end).

"a" + null

One null operand makes the whole concatenation null. string join skips null elements instead.

string join(["a", null, "c"], "-")
string join(["Due on", string(date("2026-07-24"))], " ")
Due on 2026-07-24 Try it ↑

Regular expressions: matches, replace and split

matches, replace and split take a regular expression in XPath/XQuery syntax, not a plain string. This catches people out most often with ., which matches any character. To match a literal dot, escape it as \\. inside a FEEL string. The FEEL string literal turns \\ into one backslash, and the regex then reads \..

replace("a.b.c", ".", "-")

An unescaped dot matches every character.

replace("a.b.c", "\\.", "-")

Escaped, it matches only the dots.

split("a.b.c", "\\.")
[a, b, c] Try it ↑
replace("2026-07-24", "(\\d+)-(\\d+)-(\\d+)", "$3.$2.$1")
24.07.2026 Try it ↑

Capture groups are referenced as $1, $2, … in the replacement. This one reformats an ISO date.

matches("abc", "[")

An invalid pattern gives null, not false.

matches("Invoice", "invoice", "i")

The i flag matches without regard to case.

Function reference

Each function with its examples and edge cases. The heading links to the function's own page with its parameter table.

substring

substring(string, start position, length?)

Returns the part of the string that starts at start position. Without length, it runs to the end of the string; a negative start position counts backwards from the end.

A negative start position counts from the end: -1 is the last character, -3 the third from last. DMN 1.6 defines substring only for positions inside the string: the start position must be non-zero and within [-length..length], and the length must be at least 1 and must not run past the end. Anything else is outside the function's domain, and the result is null. A fractional start position or length is truncated, as for every integer parameter of a built-in.

substring("foobar", 3)
substring("foobar", 3, 3)
substring("foobar", -2, 1)
substring("foobar", -3)

Last three characters.

substring("foobar", -2, 2)

The last two characters: start at -2, take two.

substring("foobar", 1.5, 2)

The fractional start position is truncated to 1.

substring("foobar", 0)

There is no position 0.

substring("foobar", 10)

A start position beyond the string is outside the domain. B-FEEL returns the empty string.

substring("foobar", 3, 100)

So is a length that runs past the end.

substring(null, 1)

A null input gives null in FEEL; B-FEEL returns the empty string.

string length

string length(string)

Returns the number of characters in the string.

The length counts characters (Unicode code points), so accented letters and emoji count as one each. Spaces count too. A non-string argument such as a number gives null; convert it with string() first if you want the length of its text.

string length("foo")
string length("")
string length("äöü")

Each accented letter counts as one character.

string length("ab😀")

An emoji is one code point, so it counts as one character.

string length(" ")

Whitespace counts.

string length(123)

Numbers are not converted. B-FEEL returns 0.

string length(string(123))
string length(null)

upper case

upper case(string)

Returns the string with every character converted to upper case.

upper case("aBc4")
upper case("straße")
STRASSE Try it ↑

Upper-casing can change the length: ß becomes SS.

upper case(null)

lower case

lower case(string)

Returns the string with every character converted to lower case.

lower case("aBc4")

substring before

substring before(string, match)

Returns everything before the first occurrence of match; the empty string if match does not occur.

substring before("foobar", "bar")
substring before("foobar", "xyz")
(empty string) Try it ↑
substring before("a-b-c", "-")

Only the first occurrence counts.

substring before("foobar", "")
(empty string) Try it ↑

substring after

substring after(string, match)

Returns everything after the first occurrence of match; the empty string if match does not occur.

substring after("foobar", "ob")
substring after("", "a")
(empty string) Try it ↑
substring after("a-b-c", "-")

Everything after the first occurrence, including later delimiters.

substring after("foobar", "")
foobar Try it ↑

contains

contains(string, match)

Returns true if the string contains match anywhere, false otherwise.

contains takes a plain string, not a pattern, so a dot is just a dot. To test whether a list contains a value, use list contains instead.

contains("foobar", "of")
contains("foobar", "oba")
contains("FooBar", "foo")

Case-sensitive.

contains("a.b", ".")

No regex: the dot is literal.

contains("foobar", "")

Every string contains the empty string.

contains(null, "a")

Null in, null out. B-FEEL returns false.

starts with

starts with(string, match)

Returns true if the string begins with match.

starts with("foobar", "fo")
starts with("Foobar", "foo")

Case-sensitive.

starts with("foobar", "")
starts with(null, "a")

ends with

ends with(string, match)

Returns true if the string ends with match.

ends with("foobar", "r")
ends with("file.PDF", ".pdf")

Case-sensitive, so check file extensions with a lower-cased name.

ends with(lower case("file.PDF"), ".pdf")

matches

matches(input, pattern, flags?)

Returns true if the input matches the regular expression. Flags: i for case-insensitive, s for dot-all, m for multi-line, x to ignore whitespace in the pattern.

The pattern only has to match somewhere in the input. Anchor it with ^ and $ to test the whole string.

matches("foobar", "^fo*b")
matches("FOO", "foo", "i")
matches("abc", "b")

Unanchored: a match anywhere is enough.

matches("abc", "^b$")
matches("abc", "[")

Invalid pattern: null, not false.

matches(null, "a")

replace

replace(input, pattern, replacement, flags?)

Returns the input with every match of the regular expression replaced by the replacement, which may reference capture groups as $1, $2, ….

replace("banana", "a", "o")
bonono Try it ↑
replace("abcd", "(ab)|(a)", "[1=$1][2=$2]")
[1=ab][2=]cd Try it ↑
replace("a.b.c", ".", "-")

The pattern is a regex; an unescaped dot matches every character.

replace("a.b.c", "\\.", "-")
replace("abc", "b", "$")

A lone $ in the replacement is invalid and gives null.

replace("Hello", "l", "L", "i")

split

split(string, delimiter)

Splits the string at every match of the delimiter pattern and returns the parts as a list of strings.

The delimiter is a regular expression. A plain comma or semicolon works as expected, but a dot or a pipe has to be escaped. Empty parts between consecutive delimiters are kept. A pattern such as \\s*,\\s* splits and trims in one step.

split("John Doe", "\\s")
[John, Doe] Try it ↑
split("a;b;c;;", ";")
[a, b, c, , ] Try it ↑
split("a.b.c", ".")
[, , , , , ] Try it ↑

Unescaped, the dot matches every character, which leaves only empty strings.

split("a.b.c", "\\.")
[a, b, c] Try it ↑
split("a,b,,c", ",")
[a, b, , c] Try it ↑

Empty parts are kept.

split("a, b ,c", "\\s*,\\s*")
[a, b, c] Try it ↑

Split and trim whitespace in one pattern.

split("", ",")
split(null, ",")

B-FEEL returns an empty list.

string join

string join(list, delimiter?)

Concatenates a list of strings into one string, optionally separated by a delimiter. Null elements in the list are skipped.

This is the list counterpart of the + operator. Elements must be strings: a list of numbers gives null, so convert with for x in list return string(x) first.

string join(["a", "b", "c"], ", ")
a, b, c Try it ↑
string join(["a", null, "c"])
string join([], ", ")
(empty string) Try it ↑
string join([1, 2], ", ")

Numbers are not converted.

string join(for x in [1, 2] return string(x), ", ")

string

string(from)

Converts any FEEL value to its string representation.

string(1.1)
string(date("2026-07-24"))
2026-07-24 Try it ↑
string([1, 2])
[1, 2] Try it ↑
string(null)

The one value string does not convert. B-FEEL returns the empty string.

How B-FEEL differs

In standard FEEL an invalid argument makes the result null. The business-friendly B-FEEL dialect returns a type default instead. Both columns were evaluated by the same engine:

ExpressionFEELB-FEEL
"total: " + 5nulltotal: 5
upper case(null)null(empty string)
contains("abc", null)nullfalse
split(null, ",")null[]

More FEEL guides

FEEL numeric functions · FEEL list functions · FEEL date and time functions · Language constructs · Examples cheat sheet

Defined by the OMG DMN 1.6 specification, chapter 10.3.4 (built-in functions).