Builtin Functions
Botgami ships a large library of built-in functions for working with numbers, strings, arrays, objects, JSON, time, types, crypto, regex, ids, money, geo, random, and CSV. They're called in expressions, namespaced by category:
set { flow.name = str.lower(str.trim(user.name)) }
set { flow.count = util.len(user.todos) }
when array.includes(shared.admins, user.id) { send.text("Hi admin") }
Type signatures below use short type names: NUM (Number), STR (String), BOOL (Boolean), ANY, OBJECT, and ARRAY<T>. A ...name? parameter is optional/variadic.
High-value examplesβ
Arrays β the workhorse for ledgers and listsβ
// Filter, find, reject (the upsert trio for cross-user data)
set { flow.admins = array.where(shared.users, "role", "admin") }
set { flow.me = array.find(shared.ledger, "uid", user.id) }
set { shared.ledger = array.reject(shared.ledger, "uid", user.id) }
// Sort + take a top-N leaderboard
set { flow.top10 = array.slice(array.sortBy(shared.scores, "points", true), 0, 10) }
Stringsβ
set { flow.parts = str.split(callback.data, ":") } // prefix-callback parsing
set { flow.slug = str.slugify("My Cool Title") } // "my-cool-title"
set { flow.safe = str.mask(user.card, 4) } // mask the middle
Crypto β signing, auth, and secure storageβ
// Sign your own outgoing request (secret from global.*)
set { flow.sig = crypto.hmacSha256(flow.payload, global.api_secret) }
set { flow.hash = crypto.sha256(flow.token) }
set { flow.b64 = crypto.base64Encode(flow.raw) }
// Authenticate a Telegram Mini App before trusting its data
set { flow.ok = crypto.verifyInitData(web_app.data, global.bot_token) }
// Encrypt a user's API key at rest, decrypt on demand
set { user.enc_key = crypto.aesEncrypt(flow.api_key, global.enc_key) }
set { flow.api_key = crypto.aesDecrypt(user.enc_key, global.enc_key) }
Money β decimal-safe carts and splitsβ
set { flow.total = money.add(money.mul(9.99, flow.qty), flow.shipping) }
set { flow.label = money.format(flow.total, "USD") } // "$31.97"
set { flow.shares = money.allocate(100, [1, 1, 1]) } // [33.34, 33.33, 33.33]
Regex β validation and extractionβ
when regex.test(flow.input, "^[^@]+@[^@]+\\.[^@]+$") {
send.text("Looks like a valid email.")
}
set { flow.numbers = regex.findAll(flow.text, "[0-9]+") }
Ids, time, and JSONβ
set { flow.order_id = id.uuid() }
set { flow.today = time.date() } // "2026-06-08"
set { flow.until = time.format(time.unix(user.expires_ts), "YYYY-MM-DD") }
set { flow.data = json.parse(http_response_body) }
set { flow.value = json.path(flow.data, "data.user.id") }
Complete referenceβ
Every namespace and function:
mathβ
| Function | Returns | Description |
|---|---|---|
math.abs(number:NUM) | NUM | Absolute value |
math.avg(arr:ARRAY<ANY>) | NUM | Mean of a number array; 0 for empty |
math.ceil(number:NUM) | NUM | Round up to nearest integer |
math.clamp(n:NUM,lo:NUM,hi:NUM) | NUM | Clamp n to [lo, hi] |
math.currency(n:NUM,...sym?:STR) | STR | Format number as currency, e.g. math.currency(9.5, "$") = "$9.50" |
math.factorial(n:NUM) | NUM | n! (0 for negative input) |
math.floor(number:NUM) | NUM | Round down to nearest integer |
math.format(n:NUM,decimals:NUM) | STR | Format number to fixed decimals, e.g. math.format(3.14159, 2) = "3.14" |
math.gcd(a:NUM,b:NUM) | NUM | Greatest common divisor |
math.greaterThan(a:NUM,b:NUM) | BOOL | True if a > b |
math.greaterThanOrEqual(a:NUM,b:NUM) | BOOL | True if a >= b |
math.hypot(a:NUM,b:NUM) | NUM | Hypotenuse sqrt(aΒ²+bΒ²) |
math.lcm(a:NUM,b:NUM) | NUM | Least common multiple |
math.lessThan(a:NUM,b:NUM) | BOOL | True if a < b |
math.lessThanOrEqual(a:NUM,b:NUM) | BOOL | True if a <= b |
math.log(number:NUM) | NUM | Natural logarithm |
math.log10(number:NUM) | NUM | Base-10 logarithm |
math.log2(number:NUM) | NUM | Base-2 logarithm |
math.max(a:NUM,b:NUM) | NUM | Maximum of two values |
math.min(a:NUM,b:NUM) | NUM | Minimum of two values |
math.mod(a:NUM,b:NUM) | NUM | Modulo (a % b) |
math.percentage(part:NUM,whole:NUM) | NUM | What percent part is of whole (0 when whole is 0) |
math.pow(base:NUM,exp:NUM) | NUM | Power (base^exp) β uses math.Pow; correct for floats and negatives |
math.random() | NUM | Random float in [0.0, 1.0) |
math.randomInt(min:NUM,max:NUM) | NUM | Random int in [min, max] inclusive |
math.round(number:NUM) | NUM | Round to nearest integer |
math.roundTo(n:NUM,decimals:NUM) | NUM | Round to N decimal places |
math.sign(number:NUM) | NUM | Sign of n: -1, 0, or 1 |
math.sqrt(number:NUM) | NUM | Square root |
math.sum(arr:ARRAY<ANY>) | NUM | Sum of a number array |
math.truncate(number:NUM) | NUM | Truncate toward zero |
strβ
| Function | Returns | Description |
|---|---|---|
str.capitalize(text:STR) | STR | Uppercase the first character only |
str.charAt(text:STR,index:NUM) | STR | Character at index i ('' if out of range) |
str.contains(text:STR,search:STR) | BOOL | True if text contains search substring |
str.endsWith(text:STR,suffix:STR) | BOOL | True if text ends with suffix |
str.escapeHtml(text:STR) | STR | Escape &, <, > for Telegram HTML parse_mode |
str.escapeMarkdown(text:STR,...v2?:BOOL) | STR | Escape Telegram MarkdownV2 specials (pass false for legacy Markdown). Prevents parse errors in dynamic text. |
str.extractHashtags(text:STR) | ARRAY | All #hashtags in a string |
str.extractMentions(text:STR) | ARRAY | All @usernames in a string |
str.extractUrls(text:STR) | ARRAY | All http(s) URLs in a string |
str.fields(text:STR) | ARRAY | Split a string by whitespace into an array of words |
str.includes(text:STR,search:STR) | BOOL | True if text contains the substring (alias of contains) |
str.indexOf(text:STR,search:STR) | NUM | Index of first occurrence of search in text, -1 if not found |
str.isEmpty(text:STR) | BOOL | True if the string is empty or whitespace-only |
str.isNumber(value:ANY) | BOOL | True if the value is numeric |
str.join(array:ARRAY<ANY>,separator:STR) | STR | Join an array of values into a string with separator |
str.lastIndexOf(text:STR,search:STR) | NUM | Index of the last occurrence of search, -1 if not found |
str.len(value:STR) | NUM | Unicode character count of a string |
str.levenshtein(a:STR,b:STR) | NUM | Edit distance between two strings (fuzzy match, near-dupe detection) |
str.lower(text:STR) | STR | Lowercase a string |
str.mask(text:STR,visible:NUM) | STR | Show first/last visible chars, mask the middle with β’ (e.g. card/secret display) |
str.normalizeConfusables(text:STR) | STR | Fold Cyrillic/Greek homoglyphs to Latin (anti-spam evasion), e.g. 'ΡΠ°ypal' β 'paypal' |
str.padLeft(text:STR,length:NUM,...pad?:STR) | STR | Left-pad a string to a minimum length |
str.padRight(text:STR,length:NUM,...pad?:STR) | STR | Right-pad a string to a minimum length |
str.printf(format:STR,...args:ANY) | STR | Format a string with %-verbs (Go fmt.Sprintf) |
str.repeat(text:STR,n:NUM) | STR | Repeat a string n times |
str.replace(text:STR,old:STR,new:STR) | STR | Replace all occurrences of old with new |
str.replaceFirst(text:STR,old:STR,new:STR) | STR | Replace the first occurrence of old with new |
str.reverse(text:STR) | STR | Reverse a string (Unicode-aware) |
str.similarity(a:STR,b:STR) | NUM | Similarity ratio 0β1 (1 = identical). Fuzzy command/answer matching. |
str.slugify(text:STR) | STR | Lowercase URL slug: keep [a-z0-9], spaces/underscores β single dash |
str.split(text:STR,separator:STR) | ARRAY | Split a string by separator |
str.startsWith(text:STR,prefix:STR) | BOOL | True if text starts with prefix |
str.stripEmoji(text:STR) | STR | Remove emoji and pictographs (normalize display names) |
str.substr(text:STR,start:NUM,length:NUM) | STR | Extract a substring by start index and length |
str.template(tpl:STR,data:OBJECT) | STR | Fill a template with {key} placeholders from an object β data-driven messages/broadcasts |
str.titleCase(text:STR) | STR | Capitalize the first letter of every word |
str.toNumber(text:STR) | NUM | Parse a string to a number (0 if not numeric) |
str.trim(text:STR) | STR | Trim leading and trailing whitespace |
str.trimEnd(text:STR) | STR | Trim trailing whitespace |
str.trimStart(text:STR) | STR | Trim leading whitespace |
str.truncate(text:STR,n:NUM) | STR | Truncate to n chars, appending 'β¦' when cut |
str.upper(text:STR) | STR | Uppercase a string |
str.urlDecode(text:STR) | STR | Decode a percent-encoded URL string ('' on malformed input) |
str.urlEncode(text:STR) | STR | Percent-encode a string for safe use in a URL query/path (RFC 3986) |
str.wordWrap(text:STR,width:NUM) | STR | Wrap text to a maximum line width on word boundaries |
arrayβ
| Function | Returns | Description |
|---|---|---|
array.avg(arr:ARRAY<ANY>) | NUM | Mean of a numeric array; 0 for empty |
array.chunk(array:ARRAY<ANY>,size:NUM) | ARRAY | Split into chunks of the given size |
array.compact(arr:ARRAY<ANY>) | ARRAY | Remove nil, empty string, empty object, and empty array elements |
array.concat(arr1:ARRAY<ANY>,arr2:ARRAY<ANY>) | ARRAY | Concatenate two arrays |
array.count(arr:ARRAY<ANY>,key:STR,val:ANY) | NUM | Count objects whose field key == val |
array.drop(array:ARRAY<ANY>,n:NUM) | ARRAY | All elements after the first n |
array.find(arr:ARRAY<ANY>,key:STR,val:ANY) | ANY | First object whose field key == val, else null |
array.first(array:ARRAY<ANY>) | ANY | First element of an array, or nil if empty |
array.flatDeep(array:ARRAY<ANY>) | ARRAY | Recursively flatten all nested arrays |
array.flatten(array:ARRAY<ANY>) | ARRAY | Flatten one level deep |
array.groupBy(array:ARRAY<ANY>,key:STR) | OBJECT | Group objects into a map keyed by a field value |
array.includes(arr:ARRAY<ANY>,item:ANY) | BOOL | True if array contains item |
array.indexOf(array:ARRAY<ANY>,item:ANY) | NUM | Index of the first element equal to item, -1 if absent |
array.keyBy(array:ARRAY<ANY>,key:STR) | OBJECT | Index objects into a map keyed by a field value (last wins) |
array.last(array:ARRAY<ANY>) | ANY | Last element of an array, or nil if empty |
array.len(array:ARRAY<ANY>) | NUM | Number of elements in an array |
array.max(array:ARRAY<ANY>) | NUM | Maximum of a numeric array (0 for empty) |
array.min(array:ARRAY<ANY>) | NUM | Minimum of a numeric array (0 for empty) |
array.pluck(arr:ARRAY<ANY>,key:STR) | ARRAY | Extract key field from each object in the array |
array.range(start:NUM,end:NUM,...step?:NUM) | ARRAY | Numbers from start (incl) to end (excl), optional step |
array.reject(arr:ARRAY<ANY>,key:STR,val:ANY) | ARRAY | All objects whose field key != val (filter drop) |
array.reverse(array:ARRAY<ANY>) | ARRAY | Reverse an array |
array.slice(array:ARRAY<ANY>,start:NUM,end:NUM) | ARRAY | Extract a slice from start to end (exclusive). Negative indices count from the end. |
array.sortBy(arr:ARRAY<ANY>,keys:ANY,...desc?:BOOL) | ARRAY | Sort array of objects by key(s). keys: string (single field/path) or [{key,desc?}] for multi-key. |
array.sortNums(arr:ARRAY<ANY>,...desc?:BOOL) | ARRAY | Sort a number array ascending; pass true for descending |
array.sum(arr:ARRAY<ANY>) | NUM | Sum of a numeric array |
array.take(array:ARRAY<ANY>,n:NUM) | ARRAY | First n elements |
array.unique(array:ARRAY<ANY>) | ARRAY | Remove duplicate values, preserving order |
array.where(arr:ARRAY<ANY>,key:STR,val:ANY) | ARRAY | All objects whose field key == val (filter keep) |
array.zip(arr1:ARRAY<ANY>,arr2:ARRAY<ANY>) | ARRAY | Pair elements of two arrays by index β [[a0,b0],...] |
objectβ
| Function | Returns | Description |
|---|---|---|
object.entries(object:OBJECT) | ARRAY | Array of {key, value} pairs |
object.fromEntries(entries:ARRAY<ANY>) | OBJECT | Build an object from an array of {key, value} pairs |
object.get(object:OBJECT,key:STR,...default?:ANY) | ANY | Read a key with an optional default |
object.hasKey(object:OBJECT,key:STR) | BOOL | True if key exists in object |
object.isEmpty(object:OBJECT) | BOOL | True if the object has no keys |
object.keys(object:OBJECT) | ARRAY | Return the keys of an object |
object.len(object:OBJECT) | NUM | Number of keys in an object |
object.merge(obj1:OBJECT,obj2:OBJECT) | OBJECT | Merge two objects; obj2 overrides obj1 |
object.omit(object:OBJECT,...keys:STR) | OBJECT | New object removing the listed keys |
object.pick(object:OBJECT,...keys:STR) | OBJECT | New object keeping only the listed keys |
object.values(object:OBJECT) | ARRAY | Return the values of an object |
jsonβ
| Function | Returns | Description |
|---|---|---|
json.isValid(json_string:STR) | BOOL | True if the string is valid JSON |
json.parse(json_string:STR) | ANY | Parse a JSON string into a value |
json.path(object:ANY,path:STR) | ANY | Read a dotted path into a parsed value (e.g. "data.user.id") |
json.stringify(value:ANY) | STR | Convert any value to a JSON string |
timeβ
| Function | Returns | Description |
|---|---|---|
time.add(t:ANY,dur:STR) | ANY | Add a duration to a Carbon. Supports "24h", "7d", "3w", "2month", "1year". |
time.after(t1:ANY,t2:ANY) | BOOL | True if t1 is chronologically after t2 |
time.before(t1:ANY,t2:ANY) | BOOL | True if t1 is chronologically before t2 |
time.carbon(...t?:ANY) | ANY | Alias for time.from(). Also callable as top-level carbon(). |
time.date(...format?:STR) | STR | Current date formatted as string (default: YYYY-MM-DD) |
time.diff(t1:ANY,t2:ANY,...unit?:STR) | NUM | Difference t1 β t2 in the given unit (s/m/h/d/w/month/year) |
time.endOf(t:ANY,unit:STR) | ANY | End of the given unit (second/minute/hour/day/week/month/quarter/year) |
time.format(t:ANY,format:STR) | STR | Format a Carbon/time as a string. Supports Carbon layouts, and aliases (RFC3339, YYYY-MM-DD). |
time.formatDuration(seconds:NUM) | STR | Format total seconds into a human-readable string, e.g. "2d 3h 14m 5s" |
time.from(t:ANY) | ANY | Convert any time-like value (timestamp, string, time.Time) to a Carbon. |
time.humanize(t:ANY) | STR | Human-friendly relative time, e.g. "2 hours ago" / "in 3 days" |
time.now() | ANY | Current date/time as a chainable Carbon. See: https://github.com/golang-module/carbon |
time.parse(str:STR,...fmt?:STR) | ANY | Parse a date/time string into a Carbon. Returns zero Carbon on failure β never silently falls back to now(). |
time.startOf(t:ANY,unit:STR) | ANY | Start of the given unit (second/minute/hour/day/week/month/quarter/year) |
time.sub(t:ANY,dur:STR) | ANY | Subtract a duration from a Carbon. Supports "24h", "7d", "3w", "2month", "1year". |
time.timestamp() | NUM | Current Unix timestamp (seconds) |
time.timezone(t:ANY,tz:STR) | ANY | Return a copy of the Carbon shifted to the given IANA timezone (e.g. "Asia/Kolkata") |
time.unix(epoch:NUM) | ANY | Create a Carbon from a Unix timestamp (seconds) |
typesβ
| Function | Returns | Description |
|---|---|---|
types.bool(value:ANY) | BOOL | Convert to boolean |
types.float(value:ANY) | NUM | Convert to float |
types.int(value:ANY) | NUM | Convert to integer |
types.isArray(value:ANY) | BOOL | True if value is an array |
types.isBool(value:ANY) | BOOL | True if value is a boolean |
types.isNil(value:ANY) | BOOL | True if value is nil |
types.isNumber(value:ANY) | BOOL | True if value is numeric |
types.isObject(value:ANY) | BOOL | True if value is an object/map |
types.isString(value:ANY) | BOOL | True if value is a string |
types.string(value:ANY) | STR | Convert to string |
types.typeOf(value:ANY) | STR | Runtime type name: nil/bool/number/string/array/object |
utilβ
| Function | Returns | Description |
|---|---|---|
util.coalesce(...values:ANY) | ANY | Return the first non-nil, non-empty argument |
util.default(value:ANY,fallback:ANY) | ANY | Return fallback if value is nil or empty string |
util.eval(expression:STR) | NUM | Alias for util.evaluate |
util.evaluate(expression:STR) | NUM | Safely evaluate an arithmetic expression string (sandboxed: numbers + math only, no variables) |
util.inEnum(value:ANY,...allowed:ANY) | BOOL | Alias for util.isEnum |
util.isEnum(value:ANY,...allowed:ANY) | BOOL | True if value equals one of the allowed values |
util.len(value:ANY) | NUM | Unified length: string (Unicode chars), array (elements), object (keys) |
cryptoβ
| Function | Returns | Description |
|---|---|---|
crypto.base64Decode(input:STR) | STR | Decode a standard base64 string ('' on invalid input) |
crypto.base64Encode(input:STR) | STR | Base64-encode a string (standard encoding) |
crypto.base64UrlDecode(input:STR) | STR | Decode an unpadded base64url string ('' on invalid input) |
crypto.base64UrlEncode(input:STR) | STR | Base64url-encode (unpadded) β safe for URLs/JWT segments |
crypto.hexDecode(input:STR) | STR | Decode a hex string ('' on invalid input) |
crypto.hexEncode(input:STR) | STR | Hex-encode a string |
crypto.hmacSha256(message:STR,key:STR) | STR | HMAC-SHA256 of message with key, hex-encoded. Use to sign/verify webhooks & API requests. |
crypto.hmacSha256Base64(message:STR,key:STR) | STR | HMAC-SHA256 of message with key, base64-encoded (some APIs require base64 signatures) |
crypto.hmacSha512(message:STR,key:STR) | STR | HMAC-SHA512 of message with key, hex-encoded |
crypto.md5(input:STR) | STR | MD5 hash of a string, hex-encoded (non-cryptographic; for checksums/etags only) |
crypto.randomToken(bytes:NUM) | STR | Cryptographically-random hex token of n bytes (default 16, max 256). Use for secrets/nonces. |
crypto.sha256(input:STR) | STR | SHA-256 hash of a string, hex-encoded |
crypto.sha512(input:STR) | STR | SHA-512 hash of a string, hex-encoded |
crypto.aesEncrypt(text:STR,key:STR) | STR | AES-256-GCM encrypt (key = sha256(passphrase)); base64 output. Store secrets in user.*/shared.* at rest. |
crypto.aesDecrypt(text:STR,key:STR) | STR | Decrypt an AES-256-GCM base64 blob ('' on failure/tamper) |
crypto.totp(secret:STR,...period?:NUM) | STR | Current TOTP 2FA code (RFC 6238, 6 digits) for a base32 secret. Optional period (default 30s). |
crypto.totpVerify(secret:STR,code:STR,...window?:NUM) | BOOL | Verify a user-entered TOTP code, allowing Β±window steps (default 1) |
crypto.jwtSign(claims:OBJECT,secret:STR,...alg?:STR) | STR | Sign a JWT (HS256 default, or HS512). Add an exp claim (unix seconds) for expiry. |
crypto.jwtVerify(token:STR,secret:STR) | OBJECT | Verify a JWT signature + expiry. Returns { valid: Boolean, claims: Object }. |
crypto.verifyInitData(initData:STR,botToken:STR) | BOOL | Verify a Telegram Mini App initData string against the bot token. Gate every Mini App login with this. |
crypto.constantTimeEqual(a:STR,b:STR) | BOOL | Constant-time string compare (timing-attack-safe) for secrets/tokens |
regexβ
| Function | Returns | Description |
|---|---|---|
regex.count(input:STR,pattern:STR) | NUM | Number of non-overlapping matches |
regex.findAll(input:STR,pattern:STR) | ARRAY | All substrings matching the pattern |
regex.groups(input:STR,pattern:STR) | ARRAY | Submatch groups of the first match (index 0 = whole match) |
regex.match(input:STR,pattern:STR) | STR | First substring matching the pattern, or '' if none/invalid |
regex.replace(input:STR,pattern:STR,replacement:STR) | STR | Replace all matches; replacement supports $1, ${name} backrefs |
regex.split(input:STR,pattern:STR) | ARRAY | Split a string by a regex pattern |
regex.test(input:STR,pattern:STR) | BOOL | True if the string matches the pattern (RE2 syntax). Invalid pattern β false. |
idβ
| Function | Returns | Description |
|---|---|---|
id.nanoid(...size?:NUM) | STR | URL-safe random id (default length 21) |
id.short() | STR | Short URL-safe random id (8 chars) |
id.uuid() | STR | Random UUID v4 string |
moneyβ
Decimal-safe money math β no float drift. Use for carts, balances, and bill-splitting.
| Function | Returns | Description |
|---|---|---|
money.add(a:NUM,b:NUM) | NUM | Add two money amounts without float drift (cent-precise) |
money.sub(a:NUM,b:NUM) | NUM | Subtract money amounts (cent-precise) |
money.mul(amount:NUM,factor:NUM) | NUM | Multiply a money amount by a factor (rounded to cents) |
money.div(amount:NUM,divisor:NUM) | NUM | Divide a money amount (rounded to cents; 0 on divide-by-zero) |
money.format(amount:NUM,currency:STR) | STR | Format with a currency symbol, e.g. money.format(9.5, "USD") β "$9.50" |
money.parse(str:STR,currency:STR) | NUM | Parse a money string to a number, stripping symbols/separators |
money.allocate(total:NUM,ratios:ARRAY<NUM>) | ARRAY | Split a total into parts by ratio so the parts sum EXACTLY to the total (no lost cents) |
geoβ
| Function | Returns | Description |
|---|---|---|
geo.distance(lat1:NUM,lon1:NUM,lat2:NUM,lon2:NUM) | NUM | Great-circle distance in km between two lat/lon points (haversine). Nearest-store / radius checks. |
geo.bearing(lat1:NUM,lon1:NUM,lat2:NUM,lon2:NUM) | NUM | Initial compass bearing in degrees (0β360) from point 1 to point 2 |
randomβ
For games, giveaways, and raffles.
| Function | Returns | Description |
|---|---|---|
random.pick(array:ARRAY<ANY>) | ANY | Random element from an array (nil if empty) |
random.shuffle(array:ARRAY<ANY>) | ARRAY | A shuffled copy of an array |
random.sample(array:ARRAY<ANY>,n:NUM) | ARRAY | n random elements without replacement (raffle winners) |
random.weighted(items:ARRAY<ANY>,weights:ARRAY<NUM>) | ANY | Random element chosen by weights (loot tables, odds) |
csvβ
| Function | Returns | Description |
|---|---|---|
csv.parse(text:STR,...delimiter?:STR) | ARRAY | Parse CSV text (first row = header) into an array of objects. Optional delimiter. |
csv.stringify(rows:ARRAY<OBJECT>) | STR | Serialize an array of objects to CSV text (sorted header from the first row) |
note
Many builtins also have a flat alias (e.g. len() for util.len(), round() for math.round()). The namespaced form shown above is the recommended, unambiguous style.