A template rewrites the JSON body of a request before Middleman forwards it. You write the JSON you want to send and fill in values from the incoming body (the input).
Content-Type is JSON (application/json or application/*+json). Other bodies, and empty ones, are forwarded unchanged.422 by default and nothing is forwarded (the Config tab's "On request template failure" setting can forward the original body instead). Either way the error shows up in the History tab.| Write | To |
|---|---|
$.email $ | read a field of the input, or the whole input |
$.a.b $.a["x-id"] $.a[0] $.a[-1] $.a[1:3] $["x-id"] | drill into objects and arrays |
+ - * / % == != < <= > >= && || ! | compute, compare, combine |
"user_{$.id}" | put a value in text |
a ?? b | a, or b when a is missing or null |
exists($.a.b) | tell a field the sender left out from one sent as null |
len(x) | count elements, characters or fields |
s[0:3] lower(s) split(s, ",") join(xs, ",") replace(s, a, b) | work with text |
toUnix(d) fromUnix(n) base64(s) sha256(s) | work with dates and encodings |
number(x) string(x) toJSON(x) fromJSON(s) | convert between text, numbers and JSON |
if (c) a : b | pick a value |
if (c) a | leave a field or element out when c is false |
for (x in xs) ... | map or filter an array, or loop over an object |
[...xs] {...obj} | flatten arrays or merge objects |
let x = ... | name a value to reuse |
@headers @query @method @path | read the request's headers, query, method and path |
[expr]: value | compute a field name |
// note | comment to end of line |
$.email reads that field of the input: $ is the whole input and .email picks a field of it. A name with no $. in front is a variable, never an input field. A field whose name isn't a plain word is read as $["first-name"]. Everything else is plain JSON (trailing commas allowed). Fields keep the order you write them in, and numbers pass through exactly as sent.
$.user.name reads a nested field, $.headers["x-id"] one whose name isn't a plain word.$.items[0] reads an element; $.items[-1] is the last one.$.items[1:3] takes a slice; $.items[:10] is at most the first 10.$.name[0], $.name[:3].{
"user": {"first-name": "Ada", "email": "ada@example.com"},
"orders": [{"id": 7}, {"id": 8}, {"id": 9}]
}{
"to": $.user.email,
"name": $.user["first-name"],
"last": $.orders[-1].id,
"rest": $.orders[1:]
}{
"to": "ada@example.com",
"name": "Ada",
"last": 9,
"rest": [{"id": 8}, {"id": 9}]
}Reading a field that isn't there is an error, so a typo never sends null. To handle an optional field, give it a default with ??, or compare it to null, which reads a missing field as null anywhere:
{"name": "Ada"}{
"user": $.id ?? "guest",
"has_id": $.id != null
}{"user": "guest", "has_id": false}From tightest to loosest: ! - (unary), * / %, + -, ??, < > <= >=, == !=, &&, ||. Use parentheses to group.
+ adds numbers. To build text, interpolate values into a string; + with a string on either side is an error.7 / 2 is 3.5; there is no integer division.== compares values by content, objects and arrays included.&& and || stop early, so $.x != null && $.x.ok is safe.a ?? b is a unless it is missing or null, then b, which is only evaluated in that case. $.user.address.city ?? "unknown" works when any step is missing, and for (x in $.items ?? []) loops over nothing without items. It binds looser than arithmetic ($.n ?? 0 + 1 is $.n ?? (0 + 1)) and groups to the right. Reading a field of the wrong type (a string's field) is still an error.len($.x) counts an array's elements, a string's characters or an object's fields.1 < "2") are errors.{"id": 42, "qty": 3, "price": 250, "status": "vip", "items": [1, 2]}{
"ref": "user_{$.id}",
"total": $.qty * $.price * 1.2,
"vip": $.status == "vip",
"count": len($.items)
}{
"ref": "user_42",
"total": 900,
"vip": true,
"count": 2
}Write {expr} inside a string to put a value in it: "user_{$.id}", "{$.first} {$.last}". Any expression works, including strings (which can interpolate in turn) and objects with braces of their own; the expression ends at the } that closes it. Only strings and numbers can be interpolated, and a number keeps the text it was sent with (1.50 stays 1.50). Interpolating anything else, such as null or a missing value, is an error, so you never send "user_null".
To write a literal { in a string, escape it as \{; a lone } needs no escape. A { that doesn't start a valid expression, as in "{}" or "{name}" with no variable name, is an error when you save, and the message suggests \{. Comments are not allowed inside the braces. A field name interpolates too: "user_{$.id}": true.
{"id": 7, "first": "Ada", "last": "Lovelace", "price": 1.50}{
"ref": "user_{$.id}",
"name": "{$.first} {$.last}",
"label": "{if ($.price > 1) "pricey" : "cheap"}: {$.price}",
"literal": "a \{brace} and a } too"
}{
"ref": "user_7",
"name": "Ada Lovelace",
"label": "pricey: 1.50",
"literal": "a {brace} and a } too"
}These work on strings and fail on anything else. Positions count characters, not bytes, so "héllo"[1] is "é".
lower(s), upper(s) change the case; trim(s) removes the whitespace at both ends.split(s, sep) returns an array of strings; with an empty sep it splits into characters. join(xs, sep) is the reverse, and accepts strings and numbers (a number keeps the text it was sent with).replace(s, old, new) replaces every occurrence.contains(s, sub), startsWith(s, prefix) and endsWith(s, suffix) give true or false.{
"name": " Ada Lovelace ",
"tags": "a,b,c",
"ids": [1, 2.50, "x"],
"email": "ADA@Example.com"
}{
"initial": trim($.name)[0],
"first": split(trim($.name), " ")[0],
"tags": split($.tags, ","),
"ids": join($.ids, "-"),
"email": lower($.email),
"dashed": replace($.tags, ",", "-"),
"is_example": endsWith(lower($.email), "example.com")
}{
"initial": "A",
"first": "Ada",
"tags": ["a", "b", "c"],
"ids": "1-2.50-x",
"email": "ada@example.com",
"dashed": "a-b-c",
"is_example": true
}Dates are strings in RFC 3339 form (2024-01-31T12:00:00Z, with an offset or fractions of a second if you like); Unix times are numbers.
toUnix(d) and toUnixMs(d) turn a date into Unix seconds or milliseconds (fractions are rounded down). fromUnix(n) and fromUnixMs(n) are the reverse, giving a date in UTC; n must be a whole number for a time between the years 0 and 9999. An invalid date or number is an error.base64(s) encodes a string with the standard alphabet and padding; unbase64(s) decodes it, and fails if the text is not valid base64 (line breaks are ignored) or does not decode to valid UTF-8 text.sha256(s) gives the digest as 64 lowercase hex characters.{"at": "2024-01-31T12:00:00Z", "ms": 1706702400123, "token": "ada:secret"}{
"seconds": toUnix($.at),
"millis": toUnixMs($.at),
"date": fromUnixMs($.ms),
"encoded": base64($.token),
"decoded": unbase64(base64($.token)),
"digest": sha256($.token)
}{
"seconds": 1706702400,
"millis": 1706702400000,
"date": "2024-01-31T12:00:00.123Z",
"encoded": "YWRhOnNlY3JldA==",
"decoded": "ada:secret",
"digest": "7a57c89b3477a13317bc0d38118202f9d36e9804547ccf18e753754e041e0aee"
}Senders often send numbers as text, and some targets want JSON inside a string. Four functions convert, and each takes exactly one argument:
number(x) reads a string as a JSON number ("10", "-1.50", "1e3") and keeps its text. A number passes through. Text that isn't a JSON number, such as " 1" or "abc", and any other type are errors.string(x) gives a number's text exactly as interpolation does (1.50 stays "1.50"), "true" or "false" for a boolean, and passes a string through. null, arrays and objects are errors: use toJSON for those.toJSON(x) encodes any value as a JSON string, compactly and keeping field order. toJSON($) is the whole input as text. A very large result stops the template for doing too much work.fromJSON(s) decodes a JSON string into a value. Invalid JSON, or arrays and objects nested more than 1000 deep, are errors.{
"amount": "10",
"price": 1.50,
"meta": {"id": 7},
"payload": "{\"tags\": [\"a\", \"b\"]}"
}{
"big": number($.amount) > 5,
"total": number($.amount) * 2,
"price": string($.price),
"meta_text": toJSON($.meta),
"tags": fromJSON($.payload).tags
}{
"big": true,
"total": 20,
"price": "1.50",
"meta_text": "{\"id\":7}",
"tags": ["a", "b"]
}if (c) a : b gives a when c is true, b otherwise. Conditions must be true, false or null (which counts as false); nothing else counts as “truthy”.
{"score": 72, "is_premium": false}{
"tier": if ($.is_premium) "paid" : "free",
"grade": if ($.score >= 90) "A"
: if ($.score >= 70) "B" : "C"
}{"tier": "free", "grade": "B"}Leave out : b and a false condition produces nothing: the field is omitted, the array element dropped, and a template left out entirely produces an empty body (see below). Written in place of a field, an if adds a whole group of fields.
{"discount": null, "gift": true, "plan": "pro"}{
"code": if ($.discount != null) $.discount,
"extras": ["base", if ($.gift) "wrap"],
if ($.plan != "free") {
"plan": $.plan,
"paid": true
}
}{
"extras": ["base", "wrap"],
"plan": "pro",
"paid": true
}A template that is absent as a whole does not filter the request. It is still forwarded, with an empty body and the original Content-Type (the request template you wrote is honored as is), and a target that expects JSON may reject an empty body.
To tell a field the sender left out from one sent as null, use exists(path). It is true when every step of the path is there (a null counts as there) and false otherwise. It only takes a path, like $.a.b, $.xs[0] or a variable, and a step on the wrong type (a field of a string) is still an error. A variable holding nothing, from an if without : b, doesn’t exist. A null in the middle of the path is still an error (exists($.a.b) fails when a is null), so use $.a != null to guard it.
{"note": null, "title": "Hi"}{
"title": if (exists($.title)) $.title,
"note": if (exists($.note)) $.note,
"tags": if (exists($.tags)) $.tags
}{"title": "Hi", "note": null}for (x in xs) body builds an array from each element of xs. for (i, x in xs) also gives the index, and for (k, v in obj) loops over an object's fields. A body that is an if without : b filters.
{"orders": [{"id": 1, "paid": true}, {"id": 2, "paid": false}]}{
"lines": for (i, o in $.orders) {
"n": i + 1, "ref": "order_{o.id}"
},
"paid": for (o in $.orders) if (o.paid) o.id
}{
"lines": [
{"n": 1, "ref": "order_1"},
{"n": 2, "ref": "order_2"}
],
"paid": [1]
}Inside an array, a loop adds its results as elements. In place of a field, it merges each result's fields into the object, e.g. to turn name/value pairs into an object:
{
"headers": [
{"name": "X-Env", "value": "prod"},
{"name": "X-Team", "value": "core"}
],
"extra": ["c"]
}{
for (h in $.headers) { [h.name]: h.value },
"tags": ["a", "b", for (t in $.extra) t]
}{
"X-Env": "prod",
"X-Team": "core",
"tags": ["a", "b", "c"]
}...xs in an array adds each element of the array xs, so [...$.a, ...$.b] joins two arrays and a loop whose body is a spread flattens. In an object, ...obj merges the fields of obj; a field that is already there is replaced, in its place. A spread of nothing (an if without : b that is false) adds nothing, and spreading the wrong kind (an array in an object, an object in an array, null or a scalar) is an error. A spread is only allowed where it adds something: directly in an array or object, as the body of a for, or as a branch of an if that is itself there. Anywhere else (an if in a field value, a let, a function argument, an operand) it is rejected when the template is saved.
{
"orders": [{"items": ["a", "b"]}, {"items": ["c"]}],
"user": {"name": "Ada", "role": "dev"}
}{
...$.user,
"role": "admin",
"items": [for (o in $.orders) ...o.items, "d"]
}{
"name": "Ada",
"role": "admin",
"items": ["a", "b", "c", "d"]
}let name = value names a value for reuse, at the top of the template, inside an object before the fields that use it, or at the start of a parenthesized value, (let y = x * 2; y + 1). Inside the braces or parentheses it is visible only there. A let ends at the end of its line or at ;, never at a comma. To continue one, start the next line with an operator like + or -, or break inside brackets. A line starting with [ starts the next value, and so does one starting with (, except inside an object, where ( continues the let (no field starts with one). A template whose value starts with a negation right after a let line needs parentheses, (-x), or a ; after the let. A variable and an input field can share a name: a bare name is always a variable and $.name always an input field. A bare name that isn't declared is an error when you save.
{"items": [{"qty": 2, "price": 5}, {"qty": 1, "price": 7}], "currency": "EUR"}let cur = " {$.currency}"
for (it in $.items) {
let total = it.qty * it.price
"total": "{total}{cur}",
"big": total >= 10
}[
{"total": "10 EUR", "big": true},
{"total": "7 EUR", "big": false}
]Parentheses let a for name a value even when its body isn't an object, and the line rules above apply inside them as they do at the top level:
{"xs": [1, 2, 3]}for (x in $.xs) ( let y = x * 2 y + 1 )
[3, 5, 7]
"name": value is a fixed name, used at most once per object. A string with { } in it computes its name. [expr]: value computes one, as does an input field like $.ref: value or $.a.b: value, or a variable like v: value. Names must be strings or numbers.
{"event": "order.paid", "payload": {"id": 5}, "id": 5}{
$.event: $.payload,
"user_{$.id}": true
}{
"order.paid": {"id": 5},
"user_5": true
}The body isn't all a request says: webhooks often put what the target needs in a header or the URL. Four variables starting with @ hold those. They are predeclared and read-only, so you can't declare a variable with such a name, and any other @name is an error when you save (with a "did you mean" when it is close to one of these).
@headers is an object keyed by the lowercased header name, so @headers["x-github-event"] works whatever case the sender used. A header sent more than once is one string, joined with ", ". It holds every header with its real value, redacted ones included (the app's own cookies aside). Redaction only masks the headers shown in History, not what a request template writes into the body.@query is an object of strings, with the first value of a key that is repeated.@method is the HTTP method in uppercase, like "POST".@path is the path after the endpoint, like "/orders/5" ("/" when there is none), without the query.POST /orders/5?page=2&page=3
Accept: text/plain
Accept: application/json
X-Github-Event: push
{"id": 5}{
"event": @headers["x-github-event"],
"accept": @headers.accept,
"page": @query.page ?? "1",
"route": "{@method} {@path}",
"id": $.id
}{
"event": "push",
"accept": "text/plain, application/json",
"page": "2",
"route": "POST /orders/5",
"id": 5
}Errors in a template point at the line:col where they happened (an input that is not valid JSON is reported without a position):
{"user": {"name": "Ada"}}{
"name": $.user.name,
"email": $.user.email
}3:18: field "email" not found
An error inside a for also names the iteration it happened in, outermost loop first:
{"orders": [{"price": 5}, {}]}for (o in $.orders) o.price
1:22: field "price" not found (in $.orders[1])
Some errors end with a likely fix: brackets for a name with a - in it ($.user.first-name reads as $.user.first - name), or != null or ?? for a condition that is set but isn't true or false.
A template also stops with an error if it does too much work (say, nested loops over large arrays), produces more than 10 MiB, or meets an input or template nested more than 1000 levels deep.
← Docs