Skip to content
Browse documentation

Build

Transformations

Run sandboxed JavaScript before a destination template: reshape events, drop PII, filter deliveries, and start from ready-made examples.

A destination can run a small JavaScript function before its declarative template. The transform receives the normalized event and returns either a replacement object for {{event.*}}, a replacement string for {{event.payload}}, or a skip instruction that filters the delivery.

function transform(event) {
return {
task: event.subject.title,
repo: event.repo,
labels: (event.subject.labels ?? []).join(", "),
};
}

The transform is stored on the destination (transform) and runs per delivery, before filters are applied. It cannot reach the network, environment variables, secrets or the filesystem.

Instead of pasting the same script into every destination, store it once on the Transformations page and reference it by id. A destination then sends transformationId instead of transform:

{
"url": "https://example.com/hooks",
"transformationId": "trf_01j9xk2m"
}
  • One transformation can back any number of destinations across endpoints.
  • The code is resolved at delivery time, so an edit reaches every referencing destination on its next delivery — no redeploy or resave.
  • A destination uses either an inline transform or a shared transformationId, never both. Sending both returns VALIDATION_ERROR; sending one clears the other.
  • Deleting a transformation that a destination still references returns 409 TRANSFORMATION_IN_USE. Detach the destinations first.
  • Shared transformations are gated by the same JS_TRANSFORM_ENABLED flag as inline scripts.

Manage them in the console under Transformations, or over the API:

Method Path Purpose
GET /v1/transformations List the project’s transformations
POST /v1/transformations Create one (name, code)
GET /v1/transformations/{id} Read one
PATCH /v1/transformations/{id} Update name or code (applies to every reference)
DELETE /v1/transformations/{id} Delete an unreferenced transformation
POST /v1/transformations/run Trial-run code or a stored transformationId

Trial runs accept an optional event object and return the transform output, so you can check a mapping before attaching it:

Terminal window
curl -X POST https://api.hooks.events/v1/transformations/run \
-H "authorization: Bearer $HOOKS_API_KEY" \
-H "content-type: application/json" \
-d '{"transformationId":"trf_01j9xk2m","event":{"type":"order.created","total":42}}'
  • Declarative templates (payload templates) render the outbound body, URL, headers and auth. They are the right tool when the change is a field mapping or a format string.
  • Transforms run arbitrary JavaScript over the event object. Use them for recursion (flattening, PII stripping), computed values, or conditional routing that the template syntax cannot express.
  • Both can be combined: the transform runs first, then the template renders against the transformed event.
Return value Effect
An object Replaces the normalized event that {{event.<path>}} reads
A string Replaces the inbound payload text that {{event.payload}} reads
null / undefined Keeps the event unchanged and the payload as received
{ skip: true, reason } Stops the delivery and records it as filtered with that reason

Returning an invalid value (for example a number or a function) fails the delivery with TRANSFORM_INVALID_OUTPUT.

The platform enforces these budgets; exceeding one fails the delivery with the listed code. See limits for the full platform table.

Limit Value Error code
Script length 20,000 characters Rejected when saving (VALIDATION_ERROR)
CPU time 50 ms TRANSFORM_TIMEOUT
Heap 16 MiB TRANSFORM_MEMORY_LIMIT
Output size 256 KiB TRANSFORM_OUTPUT_TOO_LARGE
Compile errors — TRANSFORM_INVALID_SCRIPT
Runtime exception — TRANSFORM_SCRIPT_ERROR with the first line of the message

A transform failure is treated like a permanent destination failure: the delivery is failed, not retried, because the same script would fail the same way. Fix the script and replay the event. A skip is not a failure; the delivery is filtered.

The console transform editor has Start from a template. Choosing one inserts the script and runs it against the sample event; the result appears in the dry-run output. The same scripts are listed below.

function transform(event) {
const subject = event.subject || {};
const amount = typeof subject.amount === "number" ? subject.amount / 100 : null;
const currency = (subject.currency || "usd").toUpperCase();
const state = subject.state || "updated";
return {
text:
state +
": " +
(subject.title || "payment") +
(amount === null ? "" : " — " + amount.toFixed(2) + " " + currency) +
(subject.customer ? " (customer " + subject.customer + ")" : ""),
};
}
function transform(event) {
const subject = event.subject || {};
const actor = event.actor || {};
return {
action: event.action || "create",
data: {
title: subject.title || "Untitled issue",
description: (subject.body || "") + "\n\n" + (subject.url || ""),
teamKey: event.repo || "ENG",
labels: subject.labels || [],
creator: actor.login || "unknown",
},
};
}
function transform(event) {
const out = {};
function walk(value, prefix) {
if (value && typeof value === "object") {
for (const key of Object.keys(value)) {
walk(value[key], prefix ? prefix + "." + key : key);
}
return;
}
out[prefix] = value;
}
walk(event, "");
return out;
}
function transform(event) {
const blocked = /^(email|e-mail|phone|mobile|name|fullName|firstName|lastName|address)$/i;
function clean(value) {
if (Array.isArray(value)) return value.map(clean);
if (value && typeof value === "object") {
const out = {};
for (const key of Object.keys(value)) {
if (!blocked.test(key)) out[key] = clean(value[key]);
}
return out;
}
return value;
}
return clean(event);
}
function transform(event) {
const PATHS = ["id", "type", "source", "subject.title", "subject.url", "actor.login"];
const out = {};
for (const path of PATHS) {
let value = event;
for (const part of path.split(".")) {
value = value == null ? undefined : value[part];
}
if (value !== undefined) out[path] = value;
}
return out;
}

Rename fields, route by type, truncate strings

Section titled “Rename fields, route by type, truncate strings”

The library also ships rename, route-by-type, truncate, labels-as-text, minor-units-to-decimal and skip-test-events examples. They follow the same shape: read event, return a new object, never mutate shared state.

  • The destination builder’s Transform tab runs the script against the built-in sample event or any stored event, and shows the transform input, output, skip reason or error code.
  • After saving, the delivery log records the transform outcome per attempt. A failed transform shows TRANSFORM_SCRIPT_ERROR (or the limit code) and the first line of the exception.
  • Save-time validation only compiles the script; runtime errors depend on the event shape. Guard optional fields with ?? or || defaults.
  • Transforms are gated by the deployment’s JS_TRANSFORM_ENABLED flag. When it is off, saving a transform returns VALIDATION_ERROR.