Recipes
A recipe is a small function, written as JSON. It takes parameters, runs its steps in order on the server, and returns a result.
The editor
Every server has a node editor: open the server's page and choose Recipes. Add steps as nodes, connect them with wires, try them out in the side panel, and save. Press ? Controls in the editor for the mouse and keyboard shortcuts.
The coloured wires carry data. Without flow nodes, the dotted line with the arrows shows the order the steps run in, which follows from how you've wired them. With flow nodes, you decide the order yourself. Press Run and the nodes light up in the order they ran, each showing its result.
Recipes belong to one server. They're stored in its private .internal/recipes/ folder, which visitors can never reach.
They count towards your storage, and a zip deploy with Replace leaves them alone.
The format
Behind the nodes, a recipe is a JSON file. You can also write one by hand: use Import JSON in the editor, or upload it as
.internal/recipes/receipt_line.json with the API or in a zip, where the file name is the recipe's name.
{
"name": "One line of a receipt",
"parameters": {
"item": "string",
"price": "numeric[0..inf]",
"quantity": { "type": "integer[1..99]", "default": 1 }
},
"recipe": [
{ "action": "multiply", "values": ["$price", "$quantity"], "result": "total" },
{ "action": "format", "format": "%(item): %(total:.2f) €" }
],
"result": "$0"
}| Key | What it does |
|---|---|
name | A description for you. Optional |
parameters | What the recipe accepts, in order. Optional |
recipe | The list of steps. Each has an action, usually values, and optionally a result name for its output |
result | What the recipe returns. Optional: by default the output of the last step |
Other keys are ignored, so you can leave notes on a step, like "note": "work out the discount".
Recipes can't read files, reach the network or read the clock. To remember something between runs, use storage.
Flow and conditions
To make something happen only in some cases, or to use storage, add flow nodes. They have arrow-shaped pins, and thick flow wires between them decide what runs next. Wire the Start pin on the Inputs node to the first one.
| Node | Action | What it does |
|---|---|---|
| Branch | branch | Continues along True or False, depending on "condition" |
| Return early | return | Stops, and returns "value" |
| Stop with error | fail | Stops with "message" as the error. Nothing this run stored is saved |
All other nodes are data nodes: they don't have a place in the flow, and are worked out when a flow node needs their result, once per run. A data node nobody asks for doesn't run at all. When a path runs out of flow wires, the recipe ends with the Return node's result.
In JSON, a flow recipe has "nodes" and "start" instead of "recipe". Older recipes keep working as they are.
{
"parameters": { "score": "numeric[0..100]" },
"start": "check",
"nodes": {
"enough": { "action": "greater_equal", "values": ["$score", 50], "result": "is_enough" },
"check": { "action": "branch", "condition": "$is_enough", "then": "pass", "else": "failed" },
"pass": { "action": "return", "value": "passed" },
"failed": { "action": "return", "value": "failed" }
}
}Flow wires can't form a loop, so a recipe always finishes. A value from a flow node only exists once that node has run: using one from a branch that wasn't taken is an error.
Storage
Each site has its own storage: named values that are still there on the next run, for every visitor. These are all flow nodes.
| Action | Takes | What it does |
|---|---|---|
storage_get | key. Optional "default" | Returns the stored value, or the default |
storage_set | key, value | Stores a value: text, number, true/false, null, list or object |
storage_increment | key, optional amount | Adds to a stored number, from 0 if there's none, and returns the new number |
storage_has | key | Whether there's a value |
storage_delete | key | Removes the value, and returns whether there was one |
storage_keys | Optional "prefix" | A sorted list of the keys |
A visitor counter. Use it from a page with %{ visits() }%, which then runs again on every view:
{
"start": "count",
"nodes": {
"count": { "action": "storage_increment", "values": ["visits"], "result": "total" },
"show": { "action": "format", "format": "Visitor number %(total)", "result": "text" }
},
"result": "$text"
}- All or nothing. Changes are only saved if the whole run succeeds. If a recipe fails or is stopped, nothing it stored is kept, and that goes for all the recipes of a page together.
- No lost updates. Runs that use storage take turns per site, so two visitors can't overwrite each other. If a site is very busy, a run may get a
429instead of waiting. - Test runs don't save. The editor's test panel reads the real values, and shows what a real run would have saved. Stored values in that panel shows and clears what's there.
- Visitors can fill it up. If keys or values come from visitors through
%name%(, check them first: the limit is 1024 KB per site, and when it's full, storing fails. - It's a file. Values live in
.internal/storage.json, which visitors can't reach. It counts towards your storage and is in your downloads. Zip deploys with Replace keep it.
Parameters
Each parameter is checked before the recipe runs, so declare them as narrowly as you can, especially if visitors can call the recipe from JavaScript.
| Type | Accepts |
|---|---|
number, numeric[min..max] | Any number, optionally in a range. Bounds can be -inf and inf |
int, integer[min..max] | Whole numbers, optionally in a range |
string, string[a|b|c] | Text, optionally only from a set of options |
boolean | true or false |
list | A list |
any | Anything, unchecked |
To make a parameter optional, give it a default:
{
"parameters": {
"x": "number",
"y": { "type": "number", "default": 1 }
},
"recipe": [
{ "action": "add", "values": ["$x", "$y"] }
]
}Using values
Anywhere a step takes a value, you can use a reference instead of a fixed value, also inside lists and objects:
| Write | Means |
|---|---|
$price | A parameter, or the output of a step with "result": "price" |
$0, $1, ... | The output of the last step, the one before it, and so on |
$user.address.city, $items.0 | A value inside an object or list |
$$text | The text $text, with one $ |
The result
result can be a reference or a whole structure, which is what you want for returning several things to JavaScript:
{
"parameters": { "numbers": "list" },
"recipe": [
{ "action": "min", "values": ["$numbers"], "result": "low" },
{ "action": "average", "values": ["$numbers"], "result": "mean" }
],
"result": { "smallest": "$low", "average": "$mean" }
}Formatting text
The format action fills in a template. A placeholder takes the next value from values, or a variable by name:
| Placeholder | Gives |
|---|---|
%(s) | The next value, as text |
%(.2f) | The next value, formatted. Uses Python's format specs: .2f, ,.2f, 05d, >10 |
%(name) | A parameter or named result |
%(name:.2f) | A parameter or named result, formatted |
%% | A percent sign |
Math actions
The ones marked list also accept a single list: "values": ["$prices"].
| Action | Values | Returns |
|---|---|---|
add / sum | 1 or more, list | The sum |
subtract | 2 or more, list | The first minus all the others |
multiply / product | 1 or more, list | The product |
divide | 2 or more, list | The first divided by the others |
floor_divide | a, b | Whole-number division |
modulo / mod | a, b | The remainder |
power / pow | base, exponent | The power |
min, max | 1 or more, list | The smallest or largest |
average / mean | 1 or more, list | The arithmetic mean |
abs, negate, sqrt | 1 | Absolute value, opposite sign, square root |
floor, ceil | 1 | Rounded down or up |
round | 1 | Rounded to "digits" decimals (default 0) |
clamp | x, low, high | x, kept within the range |
Text actions
| Action | Values | Returns |
|---|---|---|
concat | Any number | The values joined as text, with an optional "separator" |
upper, lower, trim | 1 | Upper case, lower case, or without surrounding spaces |
replace | text, old, new | The text with every old replaced |
split | 1 | A list, split at "separator" (default: whitespace) |
join | 1 list | Text, joined with an optional "separator" |
substring / slice | text or list, start, end (optional) | That part of it |
contains | haystack, needle | Whether text, a list or an object contains it |
length | 1 | The length of text, a list or an object |
format | Any number | Fills in "format": see Formatting text |
Converting actions
| Action | Values | Returns |
|---|---|---|
to_number | 1 | A number, from text |
to_integer | 1 | A whole number, dropping any decimals |
to_string | 1 | Text |
to_boolean | 1 | true or false. "", "0", "false", "no", "off" and "null" are false |
Logic actions
| Action | Takes | Returns |
|---|---|---|
equals, not_equals, greater, less, greater_equal, less_equal | Values: a, b | true or false |
and, or | Values: 1 or more | Whether all or any are true |
not | Values: 1 | The opposite |
if | "condition", "then", "else" | The chosen branch. Only that branch's references are looked up |
assert | "condition", "message" | Stops the recipe with message unless the condition is true. Good for your own checks |
A recipe runs every step once, top to bottom. There are no loops, but if picks between values:
{
"parameters": { "total": "numeric[0..inf]" },
"recipe": [
{ "action": "greater_equal", "values": ["$total", 100], "result": "is_free" },
{ "action": "if", "condition": "$is_free", "then": "free", "else": "4.99 €" }
]
}Data actions
| Action | Takes | Returns |
|---|---|---|
set | "value" | The value. Objects and lists can contain references |
list | Values: any number | A list of them |
get | Values: container, key. Optional "default" | The item, or the default. An error if it's missing and there's no default |
first, last | Values: 1 list | The first or last item |
reverse | Values: 1 list | The list, backwards |
sort | Values: 1 list. Optional "descending" | The list, sorted |
unique | Values: 1 list | The list without duplicates, in the same order |
coalesce | Values: 1 or more | The first one that isn't null |
Random actions
| Action | Values | Returns |
|---|---|---|
random | None | A number from 0 up to, but not including, 1 |
random_int | low, high | A whole number, both ends included |
random_choice | 1 list | One item from the list |
shuffle | 1 list | The list in a random order |
{
"name": "Roll a die",
"recipe": [
{ "action": "random_int", "values": [1, 6] }
]
}
A .chtml page normally keeps its result, but a page where any recipe uses one of these actions runs again on every
view, so each visitor gets a new result. Every view then counts towards your limits,
and browsers are told not to keep the page.
Hashing actions
Each takes text, and returns its hash as hexadecimal, or as base64 with "output": "base64".
| Action | Values | Use it for |
|---|---|---|
sha256, sha512, sha3_256, blake2b | 1 | Fingerprints of data |
md5, sha1 | 1 | Only comparing with other systems: both are broken for security |
hmac_sha256 | text, key | Signing something with a secret key, for example to check a webhook |
{
"parameters": { "text": "string" },
"recipe": [
{ "action": "sha256", "values": ["$text"] }
]
}Password actions
| Action | Values | Returns |
|---|---|---|
password_hash | password | A hash to store instead of the password: salted, and slow on purpose. Different every time, so a page using it is never kept |
password_verify | password, hash | true if the password matches the hash |
Use these for passwords, never sha256 or md5: those are fast, which is exactly what someone guessing passwords wants. Keep the hash in storage, or in the recipe, like this password gate:
{
"parameters": { "password": "string" },
"recipe": [
{ "action": "password_verify", "values": ["$password", "pbkdf2_sha256$600000$AAECAwQFBgcICQoLDA0ODw==$lqWQTC4IyNpCMF28xdfPGOrSY21J9ZUmtgbyZpYoFHM="] }
]
}
Each of these takes a noticeable part of a run's time limit, so use one or two per run, and call them with %#name%( so the page doesn't freeze.
Never write a real password in a page's %{ }% tag or in its JavaScript: anyone can read those.
Encoding actions
| Action | Values | Returns |
|---|---|---|
base64_encode | 1 | Text as base64. With "url_safe": true, using - and _ |
base64_decode | 1 | Base64 back to text. Reads both forms |
hex_encode, hex_decode | 1 | Text to hexadecimal, and back |
base64_to_hex, hex_to_base64 | 1 | From one to the other, for data that isn't text |
Encoding isn't encryption: anyone can decode it. If decoded data isn't valid text, the decode actions report an error, and the conversions in the last row still work.
{
"parameters": { "text": "string" },
"recipe": [
{ "action": "base64_encode", "values": ["$text"] }
]
}Bitwise actions
These work on whole numbers. The ones marked list also accept a single list.
| Action | Values | Returns |
|---|---|---|
bit_and, bit_or, bit_xor | 2 or more, list | The bits set in all, in any, or in an odd number of them |
bit_not | 1 | Every bit flipped, which comes to -x - 1 |
shift_left, shift_right | value, bits | The bits moved, by 0 to 4096 places |
{
"parameters": { "value": "int" },
"recipe": [
{ "action": "bit_and", "values": ["$value", 255] }
]
}