Functions and expressions
AppNoy doesn't have a "function library" in the style of UPPER() or DATE(). Instead there's a small, precise expression language built entirely on the {{...}} syntax, including chainable filters, and dedicated filters for passwords and encryption.
A. Inserting variables into text
Any text field (an element's text, a toast message, a link, etc.) can contain {{name}}, which gets replaced with its value at runtime. It's resolved in order: first "system tokens" (see the table below), then defined variables, and if nothing is found - empty text is shown.
B. System tokens
| Token | Returns |
|---|---|
| {{now}} | Current date and time, formatted per language |
| {{today}} | Current date in YYYY-MM-DD format |
| {{epochMs}} | Milliseconds since epoch, as text - handy for chronological sorting |
| {{uuid}} | A UUID v4-style unique ID |
| {{random}} | Random integer between 0 and 999999 |
| {{genId}} | A long, time-based unique ID (recommended when you need reliable identification) |
| {{shortId}} | A short 3-digit code (100–999) - easy to type, but not guaranteed unique |
| {{joystickX}} / {{joystickY}} | Live -1..1 position of a Joystick element (game builder), refreshed continuously while it's held |
| {{tiltX}} / {{tiltY}} | Live -1..1 device tilt, once the "Enable tilt controls" action has run |
| {{lastKey}} | The key (e.key - "Enter", "ArrowUp", a letter, etc.) that most recently triggered a "Key pressed" event - see the "Events" chapter |
C. Filters (with |)
You can pipe a value through a filter written as {{name|upper}}. Here are all the existing filters:
| Category | Filter | What it does |
|---|---|---|
| Basic text | upper | Converts to uppercase |
| lower | Converts to lowercase | |
| trim | Removes whitespace from the start and end | |
| Text formatting | capitalize | Capitalizes only the first letter |
| title | Capitalizes the first letter of every word | |
| reverse | Reverses the character order | |
| length | Number of characters in the text | |
| Cleanup & detection | digitsOnly | Keeps only digits (handy for cleaning up phone numbers) |
| slug | Converts to a URL-friendly string: lowercase letters and hyphens instead of spaces/symbols | |
| mask | Hides everything except the last 4 characters (••••1234) - good for showing sensitive info without exposing it | |
| Numbers | round | Rounds to a whole number |
| currency | Adds ₪ and thousands separators, two digits after the decimal point | |
| percent | Adds % at the end | |
| Security & privacy | hash | One-way - for storing passwords, see section D right below |
| encrypt | Reversible - for encrypting data for storage, see section E right below | |
| decrypt | Turns encrypt back into the original text |
Chaining several filters in one field
You can chain several filters in a row on the same value, one after another with | - each filter gets the result of the one before it:
{{name|trim|lower|capitalize}}
This example strips extra whitespace, lowercases everything, then capitalizes just the first letter - so " danny " and "DANNY" are shown in the same consistent form, "Danny". There's no limit on how many filters you can chain in one go.
D. Storing passwords without storing the password itself (hash)
The hash filter turns any text into a long, fixed code (SHA-256) in a one-way fashion: the same text will always produce exactly the same code, but there's no way to take the code and recover the original text from it. That's exactly why it's used for password checks - you store only the code, not the password itself.
mode: click
key: {{username}}
fields: [ passwordHash = {{inpw|hash}} ]
mode: click
key: {{username}}
target: storedHash (property: passwordHash)
operator: ==
right: storedHash
This is fine as a simple entry gate for a small app (orders, a loyalty club, a staff area). It's not a substitute for a real, secure login system with a dedicated server - there's no rate limiting, two-factor authentication, or bank-grade SSL certificates here. Don't use this for truly sensitive data (finances, legal documents, etc.).
E. Encrypting data you need to read again (encrypt / decrypt)
Unlike hash, encrypt is reversible: you store an encrypted value in a table, and whenever you need to show it again - you run decrypt on it and get back exactly the original text. Useful when you want some data (a private note, an ID detail) to not appear as plain text to anyone who opens the "Data" page in the dashboard, but the app itself still needs to show it to the right user.
{{sdb.notes[{{userId}}].secret = {{privateNote|encrypt}}}}
And to display that same value again on screen:
{{element[3].text = {{sdb.notes[{{userId}}].secret|decrypt}}}}
The key for encrypt/decrypt is fixed and embedded in the app's public code (the same JS file every visitor downloads) - meaning it keeps casual eyes from seeing the value (e.g. in the dashboard's data table), but it won't hold up against someone determined who inspects the browser's code. For passwords and anything truly critical to protect, use hash (one-way), not encrypt.
F. "Run code" (advanced) - a mini path language
The Run code action is not real JavaScript (not eval) - it's a small path language for reading/writing that runs inside {{ }} blocks, and supports:
| Syntax | Meaning |
|---|---|
| element[N].prop | Read/write to element number N on the current screen only (e.g. element[2].text) |
| db.path.to.value | Local storage on this device (a dotted path into an object) |
| sdb.table[key].col | A row in a shared server table - when reached via runCode it's always global (unlike the dedicated dbSet/dbGet actions, whose default is private) |
| myVar | A bare identifier (no dots) - a defined cache variable |
| path = value | Assignment, e.g. {{myVar = 5}}. Blocks can be nested inside each other - the inner one resolves first |
| a + b, a - b, a * b, a / b | One operator at a time (with a space on both sides of the sign) - no precedence and no chaining ("2 + 3 * 4" does NOT evaluate as 14; nest {{ }} blocks instead), and no string functions. Dividing by zero returns 0, not an error. That's the entire arithmetic support in the language - enough for counters, and for basic game math like gravity/speed |
A common example - a counter that goes up by 1 on every click:
{{sdb.scores[id].points = {{sdb.scores[id].points}} + 1}}
G. Condition expressions
The simple condition action ("Condition") is just left-side / operator / right-side. Available operators: == equals, != not equal, > greater than, < less than, >=, <=, contains, empty, notEmpty. Full detail in the "Conditions" chapter.
When you need more complex logic, there's "Compound condition (free text)" - a real grammar with AND/OR/NOT and parentheses, for example:
(age >= 18 OR vip) AND NOT banned