DocumentationHelper libraries

Helper libraries

Import curated helper libraries into Webhook Relay functions with require() — JMESPath queries, JSON Schema validation, CSV, XML, YAML, JWT, HMAC, Mustache templates, time and HTML utilities.

JavaScript and Lua functions can import a curated set of built-in helper libraries with require(). They cover the things webhook transformations need most — safely reading nested data, querying JSON, converting between formats, verifying signatures and rendering templates — without npm packages or dependency management.

const data = require("data")

const event = JSON.parse(r.body)
const city = data.get(event, "customer.address.city", "unknown")
local json = require("json")
local data = require("data")

local event, err = json.decode(r.RequestBody)
if err then error(err) end

local city = data.get(event, "customer.address.city", "unknown")

The same module names and camelCase methods are available in both runtimes. Only the documented built-in modules can be imported — npm packages, filesystem modules and arbitrary host modules are not available.

Available modules

ModuleMethods
dataget, set, pick, omit, renameKeys, deepMerge, flatten, unflatten, groupBy, keyBy, uniqBy, sortBy, chunk
jmespathsearch(expression, value)
schemavalidate(schema, value), assert(schema, value)
urlparse, build, encodeQuery, decodeQuery, encodeForm
csvparse, stringify
xmlparse, stringify
yamlparse, stringify
iduuid, ulid
jwtsign, verify, decode
secureequal, verifyHMAC, base64urlEncode, base64urlDecode
templaterender
timeparseISO, formatISO, add, subtract, startOf, endOf
htmlsanitize, toText, toMarkdown

The existing runtime modules (http, json, crypto, time, jwt, bigquery and mailgun, where supported) remain available and can also be imported with require(). The full generated API reference — every method with parameters, returns and tested examples in both languages — is available in the function editor's autocomplete and via the MCP server.

Reading and reshaping data

The data module works with dotted paths and zero-based array indexes, such as customer.address.city or items[0].sku. Methods that write (set, omit, deepMerge and the array helpers) return new values and never mutate the input:

const data = require("data")

const event = JSON.parse(r.body)

// pick only the fields the destination needs
const summary = data.pick(event, ["id", "type", "customer.email"])

// group line items by SKU
const bySku = data.groupBy(data.get(event, "items", []), "sku")

r.setBody(JSON.stringify({ summary: summary, by_sku: bySku }))
local json = require("json")
local data = require("data")

local event, err = json.decode(r.RequestBody)
if err then error(err) end

-- pick only the fields the destination needs
local summary = data.pick(event, {"id", "type", "customer.email"})

-- group line items by SKU
local by_sku = data.groupBy(data.get(event, "items", {}), "sku")

local encoded, err = json.encode({summary = summary, by_sku = by_sku})
if err then error(err) end

r:SetRequestBody(encoded)

For more complex queries, jmespath.search evaluates a full JMESPath expression against the payload:

const jmespath = require("jmespath")

const event = JSON.parse(r.body)

// names of all completed orders over $100
const bigOrders = jmespath.search(
  "orders[?status == 'completed' && total > `100`].name",
  event
)
local json = require("json")
local jmespath = require("jmespath")

local event, err = json.decode(r.RequestBody)
if err then error(err) end

-- names of all completed orders over $100
local big_orders = jmespath.search(
  "orders[?status == 'completed' && total > `100`].name",
  event
)

Validating payloads with JSON Schema

Reject malformed webhooks before they reach your destination. schema.validate returns a result you can inspect; schema.assert stops the function with an error when validation fails:

const schema = require("schema")

const event = JSON.parse(r.body)

const result = schema.validate({
  type: "object",
  required: ["id", "email"],
  properties: {
    id: { type: "string" },
    email: { type: "string", format: "email" }
  }
}, event)

if (!result.valid) {
  r.setResponseStatus(400)
  r.setResponseBody("invalid payload")
  r.stopForwarding()
}
local json = require("json")
local schema = require("schema")

local event, err = json.decode(r.RequestBody)
if err then error(err) end

local result = schema.validate({
  type = "object",
  required = {"id", "email"},
  properties = {
    id = { type = "string" },
    email = { type = "string", format = "email" }
  }
}, event)

if not result.valid then
  r:SetResponseStatusCode(400)
  r:SetResponseBody("invalid payload")
  r:StopForwarding()
end

Converting between formats

The csv, xml and yaml modules convert to and from JSON-compatible values, so you can accept a webhook in one format and forward it in another:

const csv = require("csv")

// header-aware by default: returns an array of objects
const rows = csv.parse(r.body)

r.setBody(JSON.stringify({ rows: rows }))
r.setHeader("Content-Type", "application/json")
local json = require("json")
local csv = require("csv")

-- header-aware by default: returns an array of objects
local rows = csv.parse(r.RequestBody)

local encoded, err = json.encode({rows = rows})
if err then error(err) end

r:SetRequestBody(encoded)
r:SetRequestHeader("Content-Type", "application/json")

A few safety notes on the format helpers:

  • csv.stringify prefixes spreadsheet-formula cells by default to prevent formula injection — only disable it (preventFormulaInjection: false) for trusted data. delimiter supports CSV or TSV, and header: false switches to plain arrays.
  • xml.parse returns an explicit element tree with name, attributes, children and text. External entity resolution is disabled.
  • yaml.parse accepts a single document and returns JSON-compatible values.

Verifying JWTs and signatures

The jwt and secure modules handle token and webhook-signature verification with pinned algorithms and constant-time comparison:

const jwt = require("jwt")
const secure = require("secure")

// verify a JWT from a header (HS256, HS384, HS512 supported)
const claims = jwt.verify(r.headers["X-Auth-Token"], cfg.get("jwt_secret"), {
  algorithm: "HS256",
  issuer: "my-app"
})

// or verify a provider HMAC signature, e.g. "sha256=<hex>"
const ok = secure.verifyHMAC("sha256", cfg.get("signing_secret"), r.body,
  r.headers["X-Hub-Signature-256"], { prefix: "sha256=" })

if (!ok) {
  r.setResponseStatus(401)
  r.stopForwarding()
}
local jwt = require("jwt")
local secure = require("secure")

-- verify a JWT from a header (HS256, HS384, HS512 supported)
local claims = jwt.verify(r.RequestHeader["X-Auth-Token"], cfg:GetValue("jwt_secret"), {
  algorithm = "HS256",
  issuer = "my-app"
})

-- or verify a provider HMAC signature, e.g. "sha256=<hex>"
local ok = secure.verifyHMAC("sha256", cfg:GetValue("signing_secret"), r.RequestBody,
  r.RequestHeader["X-Hub-Signature-256"], { prefix = "sha256=" })

if not ok then
  r:SetResponseStatusCode(401)
  r:StopForwarding()
end

jwt.decode returns the header and claims with verified: false — it is for inspection only and must never be used for authorization. secure.verifyHMAC accepts hex, base64 and base64url signatures plus an optional provider prefix. See webhook signature verification for provider-specific walkthroughs.

Rendering templates

template.render renders Mustache templates — handy for building notification messages without string concatenation:

const template = require("template")

const event = JSON.parse(r.body)

const text = template.render(
  "Deploy {{version}} to {{environment}} finished with status {{status}}",
  event
)

r.setBody(JSON.stringify({ text: text }))
r.setHeader("Content-Type", "application/json")
local json = require("json")
local template = require("template")

local event, err = json.decode(r.RequestBody)
if err then error(err) end

local text = template.render(
  "Deploy {{version}} to {{environment}} finished with status {{status}}",
  event
)

local encoded, err = json.encode({text = text})
if err then error(err) end

r:SetRequestBody(encoded)
r:SetRequestHeader("Content-Type", "application/json")

{{name}} is HTML-escaped; triple braces ({{{name}}}) insert raw text. Partials are not available.

Time, IDs, URLs and HTML

  • time — portable date arithmetic: parseISO returns Unix seconds, the other methods accept RFC3339 text or Unix seconds and return RFC3339 text. Timezone options use IANA names, for example { timezone: "America/New_York" }. See working with time for the existing runtime time module.
  • id — generate uuid() or sortable ulid() identifiers for correlation IDs and idempotency keys.
  • urlparse/build URLs and encodeQuery/decodeQuery/encodeForm query strings and form bodies. See also URL-encoded data.
  • htmlsanitize untrusted HTML, or convert it with toText and toMarkdown, useful when relaying emails as webhooks into chat tools.

Limits

Helper inputs and outputs must be JSON-compatible and are bounded to 1 MiB, 10,000 nested items and 64 levels of nesting. A helper error stops the function with the module and method named in the error message. HTTP calls made from functions follow the platform egress policy.

Using secrets in functions

API tokens and signing secrets used by your functions don't belong in source code. Store them as write-only secret service connections and import them by alias:

const apiToken = require("secret:api-token")

See the secrets documentation for the full setup and binding flow.

Did this page help you?