Skip to content

Rule functions

Rule functions

Functions transform columns inside SELECT and WHERE. Scalar functions map one row to one value. Aggregate functions fold a window into one value.

Use this page to pick a function. You get the call shape and the arity of each name. The broker serves this same list at GET /api/v1/rules/functions.

flowchart LR
Col["column value"] -- "scalar call" --> Val["one value"]
Win["window rows"] -- "aggregate call" --> Agg["one value"]
Agg -- "GROUP BY window" --> Row["result row"]
Val -- "SELECT AS" --> Row

Before you start

  • Read Rule SQL for SELECT and WHERE.
  • Wrong argument counts yield null. They never stop the rule.
  • Names are lowercase. Calls use parentheses.

[!NOTE] The docs run checks each name against the live catalogue. It runs the sample calls below. It does not run each call shape. The failure record lists the rest.

Math

FunctionArityWhat it doesCall shape
abs1Absolute value.abs(-3)
ceil1Smallest integer above the value.ceil(1.2)
ceiling1Alias of ceil.ceiling(1.2)
floor1Largest integer below the value.floor(1.8)
round1-2Round to decimals (default 0).round(1.235, 2)
sqrt1Square root; null for negatives.sqrt(9)
power2Raise x to the power y.power(2, 10)
pow2Alias of power.pow(2, 10)
sin1Sine of radians.sin(1)
cos1Cosine of radians.cos(1)
tan1Tangent of radians.tan(1)
asin1Arc sine; null outside [-1, 1].asin(1)
acos1Arc cosine; null outside [-1, 1].acos(1)
atan1Arc tangent.atan(1)
atan22Arc tangent of y/x.atan2(1, 1)
exp1e raised to the value.exp(1)
ln1-2Natural or base-b logarithm.ln(10)
log1-2Natural or base-b logarithm.log(8, 2)
log21Base-2 logarithm.log2(8)
log101Base-10 logarithm.log10(100)
sign1Sign: -1, 0 or 1.sign(-5)
mod2Remainder of x divided by y.mod(7, 3)
cosh1Hyperbolic cosine.cosh(1)
sinh1Hyperbolic sine.sinh(1)
tanh1Hyperbolic tangent.tanh(1)
cot1Cotangent of radians.cot(1)
radians1Degrees to radians.radians(180)
degrees1Radians to degrees.degrees(3.14159)
bitand2Bitwise AND.bitand(6, 3)
bitor2Bitwise OR.bitor(6, 3)
bitxor2Bitwise XOR.bitxor(6, 3)
bitnot1Bitwise NOT.bitnot(6)
pi0The constant pi.pi()
rand0Random float in [0, 1).rand()
conv3Number string between radix bases.conv('ff', 16, 10)

Strings

FunctionArityWhat it doesCall shape
concat1+Concatenate all arguments.concat('a', 'b')
lower1Lowercase the string.lower('AB')
upper1Uppercase the string.upper('ab')
length1Character length.length('abc')
trim1Strip edge whitespace.trim(' a ')
ltrim1Strip leading whitespace.ltrim(' a ')
rtrim1Strip trailing whitespace.rtrim(' a ')
lpad2-3Left-pad to length.lpad('ab', 5, '0')
rpad2-3Right-pad to length.rpad('ab', 5, '0')
replace3Replace all occurrences.replace('aaa', 'a', 'b')
split2Split on a separator.split('a,b', ',')
reverse1Reverse characters.reverse('abc')
substr2-3Substring, 1-based start.substr('abcdef', 2, 3)
substring2-3Alias of substr.substring('abcdef', 2, 3)
startswith2Prefix check.startswith('abc', 'a')
endswith2Suffix check.endswith('abc', 'c')

Arrays and objects

FunctionArityWhat it doesCall shape
array_contains2Whether the array holds the value.array_contains([1, 2], 2)
array_join1-2Join elements with a separator.array_join([1, 2], ',')
keys1Object keys as an array.keys(obj)
values1Object values as an array.values(obj)
object_construct0+Build an object from pairs.object_construct('a', 1)
object_concat2+Merge objects left to right.object_concat(o1, o2)
erase2+Remove keys from an object.erase(obj, 'a')
object_erase2+Alias of erase.object_erase(obj, 'a')
object_pick2+Keep only listed keys.object_pick(obj, 'a')
obj_to_kvpair_array1Object to {key, value} array.obj_to_kvpair_array(obj)
object_to_kvpair_array1Alias of obj_to_kvpair_array.object_to_kvpair_array(obj)
to_json1Serialize a value to JSON text.to_json(obj)
tojson1Alias of to_json.tojson(obj)
parse_json1Parse JSON text to a value.parse_json('{"a":1}')
parsejson1Alias of parse_json.parsejson('{"a":1}')
json_parse1Alias of parse_json.json_parse('{"a":1}')
array_create0+Array from arguments.array_create(1, 2)
array_position21-based position, or null.array_position([1, 2], 2)
array_length1Element count.array_length([1, 2])
array_slice3Slice, 1-based inclusive bounds.array_slice([1, 2, 3], 1, 2)
array_concat2Concatenate two arrays.array_concat([1], [2])
deduplicate1Remove duplicate elements.deduplicate([1, 1, 2])
cardinality1Element count.cardinality([1, 2])
array_cardinality1Alias of cardinality.array_cardinality([1, 2])
element_at2Element at 1-based index.element_at([1, 2], -1)
array_contains_any2Whether any listed value is held.array_contains_any([1, 2], [2, 3])
array_remove2Remove all occurrences.array_remove([1, 1], 1)
array_distinct1Distinct elements.array_distinct([1, 1, 2])
array_intersect2Elements present in both arrays.array_intersect([1, 2], [2, 3])
array_union2Distinct union.array_union([1], [2])
array_except2Elements missing from the second array.array_except([1, 2], [2])
array_max1Maximum element.array_max([1, 2])
array_min1Minimum element.array_min([1, 2])
array_avg1Average of numeric elements.array_avg([1, 2])
array_flatten1Flatten one nesting level.array_flatten([[1], [2]])
array_sort1Sort ascending.array_sort([2, 1])
repeat2Repeat a value n times.repeat('a', 3)
sequence2-3Integer sequence with step.sequence(1, 5)
kvpair_array_to_obj1{key, value} array to object.kvpair_array_to_obj(pairs)
object2Build object from keys array and values array.object(['a', 'b'], [1, 2])
items1Object to array of key-value pair arrays.items(obj)
array_positions2All 0-based positions of a value.array_positions([1, 2, 2, 3, 2], 2)
array_last_position21-based index of last match; 0 when absent.array_last_position([1, 2, 1], 1)
array_shuffle1Randomly permute array elements.array_shuffle([1, 2, 3])
array_map2Apply a scalar function to each element.array_map('abs', [-1, 2])

Date and time

FunctionArityWhat it doesCall shape
now0Current epoch milliseconds.now()
current_timestamp0Current timestamp.current_timestamp()
local_timestamp0Current local timestamp.local_timestamp()
current_date0Current date.current_date()
cur_date0Alias of current_date.cur_date()
current_time0Current time.current_time()
cur_time0Alias of current_time.cur_time()
local_time0Current local time.local_time()
format_date2Format epoch millis with a pattern.format_date(now(), 'yyyy-MM-dd')
from_unix_time1-2Epoch seconds to timestamp.from_unix_time(1700000000)
date_parse2Parse text with a pattern.date_parse('2024-01-01', 'yyyy-MM-dd')
date_add3Add n units to a timestamp.date_add(now(), 1, 'day')
date_diff3Difference in units.date_diff(t1, t2, 'hour')
year1Year of a timestamp.year(ts)
month1Month, 1-12.month(ts)
day1Day of month.day(ts)
day_of_week1Weekday, 1=Sunday to 7=Saturday.day_of_week(ts)
day_of_month1Day of month.day_of_month(ts)
day_of_year1Day of year.day_of_year(ts)
day_name1Weekday name.day_name(ts)
month_name1Month name.month_name(ts)
microsecond1Microsecond part.microsecond(ts)
last_day1Last day of the month.last_day(ts)
to_seconds1Days since year 0 as seconds.to_seconds(ts)
from_days1Days since year 0 to a date.from_days(739000)
hour1Hour of a timestamp.hour(ts)
minute1Minute of a timestamp.minute(ts)
second1Second of a timestamp.second(ts)

JSON paths

FunctionArityWhat it doesCall shape
json_path_query2All values at a JSON path.json_path_query(payload, '$.a')
json_path_query_first2First value at a JSON path.json_path_query_first(payload, '$.a')
json_path_exists2Whether a JSON path matches.json_path_exists(payload, '$.a')
json_map0+Build an object from pairs.json_map('a', 1)

Hash and text codes

FunctionArityWhat it doesCall shape
md51MD5 hex digest.md5('ab')
sha2561SHA-256 hex digest.sha256('ab')
sha5121SHA-512 hex digest.sha512('ab')
sha11SHA-1 hex digest.sha1('ab')
sha3841SHA-384 hex digest.sha384('ab')
crc321IEEE CRC-32 checksum.crc32('ab')
regexp_matches2Whether text matches the regex.regexp_matches('abc', '^a')
regexp_replace3Replace regex matches.regexp_replace('aaa', 'a', 'b')
regexp_substring2First regex match.regexp_substring('abc123', '[0-9]+')
regexp_substr2First regex match, group 1 preferred.regexp_substr('abc123', '[0-9]+')
split_value30-based split segment.split_value('a,b', ',', 1)
numbytes1UTF-8 byte length.numbytes('ab')
chr1Character for a code point.chr(65)
trunc1-2Truncate toward zero.trunc(1.9)
hex2dec1Hex text to decimal.hex2dec('ff')
dec2hex1Decimal to 0x hex.dec2hex(255)
encode2Encode with the named method.encode('ab', 'base64')
base64_encode1Base64-encode a value.base64_encode('ab')
decode2Decode with the named method.decode('YWI=', 'base64')
base64_decode1Base64-decode text.base64_decode('YWI=')

Conversion

FunctionArityWhat it doesCall shape
cast1Convert to the target type.cast('42')
coalesce1+First non-null argument.coalesce(a, b, 0)
isnan1Whether the value is NaN.isnan(x)
isnumeric1Whether the value is numeric.isnumeric('42')
nvl2Value, or a default when null.nvl(a, 0)
isnull1Whether the value is null.isnull(a)
tstamp0Current epoch milliseconds.tstamp()
uuid0New random UUID text.uuid()
newuuid0Alias of uuid.newuuid()
delay2Wait n ms, then return the value.delay(10, 'ok')
unnest1Expand an array into rows.unnest(arr)
extract1Expand a map onto the current row.extract(obj)

Aggregates

Aggregates need GROUP BY with a window. See Rule SQL.

FunctionArityWhat it doesCall shape
avg1Average of column values.avg(temperature)
collect1Column values as an array.collect(temperature)
count1Row or non-null count.count(*)
last_value1-2Last non-null value.last_value(temperature)
latest1Most recent value.latest(temperature)
lead1-3Value n rows ahead.lead(temperature, 1)
max1Maximum value.max(temperature)
median1Median value.median(temperature)
merge_agg1Merge object values left to right.merge_agg(obj)
min1Minimum value.min(temperature)
percentile2Continuous percentile.percentile(temperature, 0.95)
percentile_cont2Continuous percentile by interpolation.percentile_cont(temperature, 0.9)
percentile_disc2Discrete percentile value.percentile_disc(temperature, 0.95)
row_number01-based row number.row_number()
stddev1Population standard deviation.stddev(temperature)
stddevs1Sample standard deviation.stddevs(temperature)
sum1Sum of values.sum(temperature)
var1Population variance.var(temperature)
vars1Sample variance.vars(temperature)
last_agg_hit_count0Trigger count of aggregate rule.last_agg_hit_count()
last_agg_hit_time0Time of last aggregate rule hit.last_agg_hit_time()

Analytics

FunctionArityWhat it doesCall shape
acc_avg1Running average.acc_avg(temperature)
acc_count1Running non-null count.acc_count(temperature)
acc_map_agg2Running map keyed by first argument.acc_map_agg(device, temperature)
acc_max1Running maximum.acc_max(temperature)
acc_max_by2Running value with the maximum key.acc_max_by(temperature, ts)
acc_min1Running minimum.acc_min(temperature)
acc_min_by2Running value with the minimum key.acc_min_by(temperature, ts)
acc_sum1Running sum.acc_sum(temperature)
changed_col1Value when it changed, else null.changed_col(temperature)
had_changed1Whether the value changed.had_changed(temperature)
lag1-3Value n rows behind.lag(temperature, 1)
changed_cols2+Columns that changed since last emit.changed_cols('', true, col1)
acc_collect1Collect non-null values into an array.acc_collect(val)
acc_distinct_collect1Collect unique non-null values in order.acc_distinct_collect(val)
distinct_acc1Alias of acc_distinct_collect.distinct_acc(val)

System

FunctionArityWhat it doesCall shape
event_time0Event time of the record.event_time()
meta1Message metadata by key.meta('id')
mqtt1MQTT message field by key.mqtt('topic')
rule_id0Id of the running rule.rule_id()
window_end0End bound of the window.window_end()
window_start0Start bound of the window.window_start()
rule_start0Start time of the rule.rule_start()
last_hit_count0Trigger count of continuous rule.last_hit_count()
last_hit_time0Time of last continuous rule hit.last_hit_time()
get_keyed_state3Keyed state value with type and default.get_keyed_state('k', 'string', '')

Message context

These broker functions read the current message and client. They take no payload argument.

FunctionWhat it doesCall shape
msgidMessage id of the current message.msgid()
qosQoS of the current message.qos()
flagsFlag map of the current message.flags()
flagOne flag by name.flag('retain')
topicTopic of the current message.topic()
clientidClient id of the publisher.clientid()
clientipIP of the publisher.clientip()
peerhostHost of the publisher.peerhost()
usernameUsername of the publisher.username()
payloadRaw payload of the current message.payload()
topic_matchWhether the topic matches a filter.topic_match('sensors/+')
contains_topicWhether a list holds the topic.contains_topic(topics)
contains_topic_matchWhether a list matches the topic.contains_topic_match(filters)

Checks and conversion extras

FunctionWhat it doesCall shape
nullThe null value.null()
coalesce_neFirst argument not equal to the second.coalesce_ne(a, b)
divInteger division.div(7, 2)
fmodFloat remainder.fmod(7.5, 2)
strValue to string.str(42)
str_utf8Value to UTF-8 text.str_utf8(7)
str_utf16_leValue to base64 UTF-16LE text.str_utf16_le('A')
boolValue to boolean.bool(1)
intValue to integer.int('42')
floatValue to float.float('2.5')
float2strNumber to fixed-precision text.float2str(2.567, 2)
mapJSON text or pair list to an object.map('{"a":1}')
is_null, is_null_varWhether the value is null.is_null(a)
is_not_null, is_not_null_varWhether the value is not null.is_not_null(a)
is_strWhether the value is text.is_str(a)
is_boolWhether the value is boolean.is_bool(a)
is_intWhether the value is an integer.is_int(a)
is_floatWhether the value is a float.is_float(a)
is_numWhether the value is numeric.is_num(a)
is_mapWhether the value is an object.is_map(a)
is_arrayWhether the value is an array.is_array(a)
is_emptyWhether the value is empty.is_empty(a)

String extras

FunctionWhat it doesCall shape
strlenCharacter length.strlen('abc')
substrSubstring, 0-based start.substr('abcdef', 1, 3)
splitSplit on a separator.split('a,b', ',')
concatConcatenate arguments.concat('a', 'b')
tokensToken list from text.tokens('a b')
sprintf_sFormat with a pattern and an array.sprintf_s('%s-%s', ['a', 'b'])
padPad a string to width.pad('ab', 5)
replaceReplace all occurrences.replace('aaa', 'a', 'b')
regex_matchWhether the regex matches.regex_match('abc', '^a')
regex_replaceReplace regex matches.regex_replace('aaa', 'a', 'b')
regex_extractFirst regex group.regex_extract('abc123', '[0-9]+')
asciiCode point of the first character.ascii('A')
findPosition of a substring.find('abc', 'b')
join_to_stringJoin array elements to text.join_to_string([1, 2], ',')
join_to_sql_values_stringArray to a SQL values list.join_to_sql_values_string(rows)
map_to_redis_hset_argsObject to Redis HSET arguments.map_to_redis_hset_args(obj)
jqQuery with a dot path.jq('.a', payload)
unescapeUnescape text escapes.unescape('a\\nb')
rm_prefixRemove a leading prefix.rm_prefix('sensors/01', 'sensors/')
rtrimStrip trailing characters.rtrim('ab ')
bin2hexstrString to upper-case hex text.bin2hexstr('AB')
hexstr2binHex text to string.hexstr2bin('4142')
sqlserver_bin2hexstrString to 0x hex text.sqlserver_bin2hexstr('AB')
bitsizeBit length of a string.bitsize('abc')
bytesizeByte length of a string.bytesize('abc')
subbitsBit slice of a string.subbits('A', 1, 8)
bitslShift bits left.bitsl(1, 3)
bitsrShift bits right.bitsr(8, 2)
acoshInverse hyperbolic cosine.acosh(1)
asinhInverse hyperbolic sine.asinh(1)
atanhInverse hyperbolic tangent.atanh(0.5)
format_timeFormat datetime or epoch millis with a pattern.format_time(now(), 'yyyy-MM-dd HH:mm:ss')
indexof0-based index of first match; -1 when absent.indexof('hello', 'll')
formatFormat number with decimals and grouping.format(1234.567, 2)

Map and array extras

FunctionWhat it doesCall shape
map_newNew empty object.map_new()
map_getValue at a path.map_get(obj, 'a.b')
map_putObject with a path set.map_put(obj, 'a', 1)
map_keysKeys of an object.map_keys(obj)
map_valuesValues of an object.map_values(obj)
map_to_entriesObject to entry array.map_to_entries(obj)
map_sizeKey count.map_size(obj)
mgetSeveral paths at once.mget(obj, ['a', 'b'])
mputSeveral paths set at once.mput(obj, ['a'], [1])
nthElement at an index.nth([1, 2], 0)
lengthLength of text or array.length('abc')
sublistArray slice.sublist([1, 2, 3], 0, 2)
firstFirst element.first([1, 2])
lastLast element.last([1, 2])
containsWhether the array holds the value.contains([1, 2], 2)

Hash, codes, and compression extras

FunctionWhat it doesCall shape
shaDigest with the named algorithm.sha('ab', 'sha256')
hashHash of a value.hash('ab')
hash_to_rangeHash mapped into [min, max].hash_to_range('ab', 0, 99)
map_to_rangeValue mapped into [min, max].map_to_range(5, 0, 99)
zipCompress bytes (zip).zip(bytes)
unzipDecompress zip bytes.unzip(bytes)
gzipCompress bytes (gzip).gzip(bytes)
gunzipDecompress gzip bytes.gunzip(bytes)
zip_compressCompress text (zip).zip_compress('ab')
zip_uncompressDecompress zip text.zip_uncompress(bytes)
lz4_compressCompress bytes (lz4).lz4_compress(bytes)
lz4_uncompressDecompress lz4 bytes.lz4_uncompress(bytes)
base64_encodeBase64-encode a value.base64_encode('ab')
base64_decodeBase64-decode text.base64_decode('YWI=')
json_encodeSerialize a value to JSON text.json_encode(obj)
json_decodeParse JSON text to a value.json_decode('{"a":1}')
term_encodeEncode a value to binary term form.term_encode(obj)
term_decodeDecode binary term form.term_decode(bytes)
compressCompress input with zlib, gzip, flate, or zstd.compress('abc', 'zlib')
decompressDecompress input with zlib, gzip, flate, or zstd.decompress('eJzT0yMAAGUAAM8=', 'zlib')

Date extras

FunctionWhat it doesCall shape
now_rfc3339Current time as RFC 3339 text.now_rfc3339()
unix_ts_to_rfc3339Epoch seconds to RFC 3339 text.unix_ts_to_rfc3339(1700000000)
rfc3339_to_unix_tsRFC 3339 text to epoch seconds.rfc3339_to_unix_ts('2024-01-01T00:00:00Z')
now_timestampCurrent epoch milliseconds.now_timestamp()
format_dateFormat a timestamp with a pattern.format_date(ts, 'yyyy-MM-dd')
date_to_unix_tsDate text to epoch seconds.date_to_unix_ts('2024-01-01')
timezone_to_offset_secondsZone name to offset seconds.timezone_to_offset_seconds('UTC')
mongo_dateDate in MongoDB form.mongo_date()
date_calcCalculate date from date and duration text.date_calc('2019-01-01', '1h30m')
convert_tzConvert timestamp to target time zone.convert_tz(ts, 'Asia/Shanghai')

[!NOTE] timezone_to_second is an alias of timezone_to_offset_seconds. Both names work.

Random, environment, and state

FunctionWhat it doesCall shape
getenvEnvironment value by name. Keys above 4 KiB yield null.getenv('REGION')
randomRandom integer.random()
uuid_v4New UUID text with hyphens.uuid_v4()
uuid_v4_no_hyphenNew UUID text without hyphens.uuid_v4_no_hyphen()
proc_dict_getRead a process dictionary key.proc_dict_get('k')
proc_dict_putWrite a process dictionary key.proc_dict_put('k', 1)
proc_dict_delDelete a process dictionary key.proc_dict_del('k')
kv_store_getRead a rule key-value key.kv_store_get('k')
kv_store_putWrite a rule key-value key.kv_store_put('k', 1)
kv_store_delDelete a rule key-value key.kv_store_del('k')

[!WARNING] State functions share mutable state across messages. Two rules that write one key race. Prefer stateless transforms for routing.

Vectors

FunctionWhat it doesCall shape
cosine_similarityCosine similarity of two vectors.cosine_similarity([1.0, 0.0], [0.0, 1.0])
vector_l2Euclidean distance of two vectors.vector_l2([1.0, 2.0], [4.0, 6.0])
vector_dotDot product of two vectors.vector_dot([1.0, 2.0], [3.0, 4.0])
vector_matchTop matches by cosine similarity.vector_match([1.0, 0.0], [[1.0, 0.0], [0.0, 1.0]], 1)

Plug-ins

FunctionWhat it doesCall shape
wasm_runRun a function from a loaded WASM module.wasm_run('math', 'add', 10, 20)

Internal aliases

The engine rewrites these colliding names to the documented semantics. Use the plain names in rules.

AliasBehaves as
std_substrsubstr
std_splitsplit
std_concatconcat
std_lengthlength
std_base64_encodebase64_encode
std_base64_decodebase64_decode
std_rtrimrtrim

Check that it works

Ask the broker for its function list. The list holds the names of this page.

[node]
id = "docs-rules-functions"
[listeners.tcp]
bind = "0.0.0.0:1883"
[listeners.api]
bind = "0.0.0.0:18083"
[logging]
level = "warn"
[auth]
allow_anonymous = true
Terminal window
python3 - <<'PY'
import json, os, urllib.request
port = os.environ.get("API_PORT", "18083")
key = os.environ["API_KEY"]
req = urllib.request.Request(
f"http://127.0.0.1:{port}/api/v1/rules/functions",
headers={"Authorization": f"Bearer {key}"})
with urllib.request.urlopen(req, timeout=5) as res:
body = json.load(res)
names = {entry["name"] for entry in body}
for want in ("upper", "lower", "abs", "concat", "avg", "count",
"json_path_query", "sha256", "coalesce", "lag",
"date_add", "uuid", "rule_id", "percentile"):
assert want in names, want
print("ok")
PY
ok

The broker runs these sample calls through the dry-run API. They cover math and string groups only. They prove the path from SQL text to result.

Terminal window
python3 - <<'PY'
import json, os, urllib.request
port = os.environ.get("API_PORT", "18083")
key = os.environ["API_KEY"]
def dry_run(sql):
body = json.dumps({"sql_query": sql, "topic_filter": "sensors/+",
"topic": "sensors/kitchen",
"payload": {"temperature": 72.5}}).encode()
req = urllib.request.Request(
f"http://127.0.0.1:{port}/api/v1/rules/test", data=body, method="POST",
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as res:
return json.load(res)
hit = dry_run('SELECT abs(-3) AS a, ceil(1.2) AS c FROM "sensors/+"')
assert hit["matched"] is True, hit
assert hit["projected"] == {"a": 3, "c": 2}, hit
hit = dry_run("SELECT lower('AB') AS l, upper('ab') AS u FROM \"sensors/+\"")
assert hit["matched"] is True, hit
assert hit["projected"] == {"l": "ab", "u": "AB"}, hit
hit = dry_run("SELECT concat('a', 'b') AS s, length('abc') AS n FROM \"sensors/+\"")
assert hit["matched"] is True, hit
assert hit["projected"] == {"s": "ab", "n": 3}, hit
print("ok")
PY
ok

Next steps