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.
Shared transformations
Section titled “Shared transformations”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
transformor a sharedtransformationId, never both. Sending both returnsVALIDATION_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_ENABLEDflag 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:
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}}'Choosing between transforms and templates
Section titled “Choosing between transforms and templates”- 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.
Returning a value
Section titled “Returning a value”| 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.
Limits
Section titled “Limits”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.
Ready-made examples
Section titled “Ready-made examples”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.
Stripe payment → Slack message
Section titled “Stripe payment → Slack message”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 + ")" : ""), };}GitHub issue → Linear issue
Section titled “GitHub issue → Linear issue”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", }, };}Flatten nested JSON
Section titled “Flatten nested JSON”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;}Remove PII fields
Section titled “Remove PII fields”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);}Pick a field subset
Section titled “Pick a field subset”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.
Debugging
Section titled “Debugging”- 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_ENABLEDflag. When it is off, saving a transform returnsVALIDATION_ERROR.
Related
Section titled “Related”- Payload templates — the declarative renderer the transform feeds.
- Destinations — where the transform field lives.
- Limits — CPU, memory, output and script-size ceilings.
- Retries and idempotency — replaying a failed delivery after fixing a script.