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"
}
KeyWhat it does
nameA description for you. Optional
parametersWhat the recipe accepts, in order. Optional
recipeThe list of steps. Each has an action, usually values, and optionally a result name for its output
resultWhat 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.

NodeActionWhat it does
BranchbranchContinues along True or False, depending on "condition"
Return earlyreturnStops, and returns "value"
Stop with errorfailStops 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.

ActionTakesWhat it does
storage_getkey. Optional "default"Returns the stored value, or the default
storage_setkey, valueStores a value: text, number, true/false, null, list or object
storage_incrementkey, optional amountAdds to a stored number, from 0 if there's none, and returns the new number
storage_haskeyWhether there's a value
storage_deletekeyRemoves the value, and returns whether there was one
storage_keysOptional "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 429 instead 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.

TypeAccepts
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
booleantrue or false
listA list
anyAnything, 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:

WriteMeans
$priceA 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.0A value inside an object or list
$$textThe 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:

PlaceholderGives
%(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"].

ActionValuesReturns
add / sum1 or more, listThe sum
subtract2 or more, listThe first minus all the others
multiply / product1 or more, listThe product
divide2 or more, listThe first divided by the others
floor_dividea, bWhole-number division
modulo / moda, bThe remainder
power / powbase, exponentThe power
min, max1 or more, listThe smallest or largest
average / mean1 or more, listThe arithmetic mean
abs, negate, sqrt1Absolute value, opposite sign, square root
floor, ceil1Rounded down or up
round1Rounded to "digits" decimals (default 0)
clampx, low, highx, kept within the range

Text actions

ActionValuesReturns
concatAny numberThe values joined as text, with an optional "separator"
upper, lower, trim1Upper case, lower case, or without surrounding spaces
replacetext, old, newThe text with every old replaced
split1A list, split at "separator" (default: whitespace)
join1 listText, joined with an optional "separator"
substring / slicetext or list, start, end (optional)That part of it
containshaystack, needleWhether text, a list or an object contains it
length1The length of text, a list or an object
formatAny numberFills in "format": see Formatting text

Converting actions

ActionValuesReturns
to_number1A number, from text
to_integer1A whole number, dropping any decimals
to_string1Text
to_boolean1true or false. "", "0", "false", "no", "off" and "null" are false

Logic actions

ActionTakesReturns
equals, not_equals, greater, less, greater_equal, less_equalValues: a, btrue or false
and, orValues: 1 or moreWhether all or any are true
notValues: 1The 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

ActionTakesReturns
set"value"The value. Objects and lists can contain references
listValues: any numberA list of them
getValues: container, key. Optional "default"The item, or the default. An error if it's missing and there's no default
first, lastValues: 1 listThe first or last item
reverseValues: 1 listThe list, backwards
sortValues: 1 list. Optional "descending"The list, sorted
uniqueValues: 1 listThe list without duplicates, in the same order
coalesceValues: 1 or moreThe first one that isn't null

Random actions

ActionValuesReturns
randomNoneA number from 0 up to, but not including, 1
random_intlow, highA whole number, both ends included
random_choice1 listOne item from the list
shuffle1 listThe 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".

ActionValuesUse it for
sha256, sha512, sha3_256, blake2b1Fingerprints of data
md5, sha11Only comparing with other systems: both are broken for security
hmac_sha256text, keySigning something with a secret key, for example to check a webhook
{
  "parameters": { "text": "string" },
  "recipe": [
    { "action": "sha256", "values": ["$text"] }
  ]
}

Password actions

ActionValuesReturns
password_hashpasswordA hash to store instead of the password: salted, and slow on purpose. Different every time, so a page using it is never kept
password_verifypassword, hashtrue 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

ActionValuesReturns
base64_encode1Text as base64. With "url_safe": true, using - and _
base64_decode1Base64 back to text. Reads both forms
hex_encode, hex_decode1Text to hexadecimal, and back
base64_to_hex, hex_to_base641From 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.

ActionValuesReturns
bit_and, bit_or, bit_xor2 or more, listThe bits set in all, in any, or in an odd number of them
bit_not1Every bit flipped, which comes to -x - 1
shift_left, shift_rightvalue, bitsThe bits moved, by 0 to 4096 places
{
  "parameters": { "value": "int" },
  "recipe": [
    { "action": "bit_and", "values": ["$value", 255] }
  ]
}