Transform
Reshape a value with JavaScript: build the output field by field, or write the expression yourself. Use it for split, join, sort and building one string out of a list: a CEL expression elsewhere in the document has none of them. It runs in the server, so it needs no agent. Here input is the run’s input. Read the node above by name, as anywhere else in the document: nodes.<id>.output.
- Kind:
transform - Palette: Activities
- Run it on its own: yes
When to use
Section titled “When to use”- The step above returns a wide record and the next one wants three fields under other names.
- Building one string out of a list, sorting, filtering or grouping: CEL expressions elsewhere
in a graph have no
joinorsort, and this node’s JavaScript does. - Any reshaping that shouldn’t cost an agent: a transform runs in the engine.
Configuration
Section titled “Configuration”| Field | Type | Required | Description |
|---|---|---|---|
expr |
string | no | A JavaScript expression, instead of fields. In each mode i is the item and index its position; in all mode input is the whole run input. nodes.<id>.output, vars and run are in reach in both, so reshaping the node above is nodes.<id>.output rather than input. |
fields |
array | no | The output, a field at a time: [{name, expr}] for a computed value or [{name, value}] for a fixed one. Either this or expr. |
include_input |
boolean | no | Keep the incoming fields too, with yours layered over them. Off by default, so what the table lists is what comes out. |
mode |
string | no | each (default) runs it per item; all runs it once over the whole input. One of "each", "all". |
Output
Section titled “Output”Not declared: what comes out depends on the configuration.
Recipes
Section titled “Recipes”Starting points the inspector offers, each shown with the input it was written against.
Keep a few fields, renamed
Section titled “Keep a few fields, renamed”Take a wide record and keep only what the next step needs, under the names it expects.
{ "fields": [ { "expr": "i.order_id", "name": "id" }, { "expr": "i.buyer.email", "name": "customer" }, { "expr": "i.amount_cents / 100", "name": "total" } ], "mode": "each"}Sample input:
[ { "amount_cents": 2500, "buyer": { "email": "kim@example.com" }, "internal_note": "ignore me", "order_id": "A-1" }]Add a field, keep the rest
Section titled “Add a field, keep the rest”Leave the record alone and add something computed from it - a flag, a label, a total.
{ "fields": [ { "expr": "i.amount > 100 ? 'large' : 'small'", "name": "size" } ], "include_input": true, "mode": "each"}Sample input:
[ { "amount": 250, "id": "A-1" }, { "amount": 40, "id": "A-2" }]Group rows and total them
Section titled “Group rows and total them”One row per group, with a count and a sum. Needs the code mode: a per-field table describes one output row, and this makes fewer rows than it was given.
{ "expr": "Object.entries(Object.groupBy(input, r => r.team)).map(([team, rows]) => ({ team, count: rows.length, total: rows.reduce((s, r) => s + r.n, 0) }))", "mode": "all"}Sample input:
[ { "n": 2, "team": "x" }, { "n": 3, "team": "y" }, { "n": 1, "team": "x" }]Look a value up in another list
Section titled “Look a value up in another list”Match each row against a second list and bring a field across - the shape you need when one step fetched the rows and another the thing they refer to.
{ "expr": "(() => { const by = Object.fromEntries(input.teams.map(t => [t.team, t.lead])); return input.items.map(i => ({ ...i, lead: by[i.team] })); })()", "mode": "all"}Sample input:
{ "items": [ { "name": "a", "team": "x" }, { "name": "b", "team": "y" } ], "teams": [ { "lead": "kim", "team": "x" }, { "lead": "sam", "team": "y" } ]}Split a text field into unique values
Section titled “Split a text field into unique values”Turn comma-separated text into a clean, sorted list with no repeats.
{ "expr": "[...new Set(input.flatMap(r => r.tags.split(',').map(t => t.trim())))].sort()", "mode": "all"}Sample input:
[ { "tags": "urgent, billing" }, { "tags": "billing" }]Keep only the rows that match
Section titled “Keep only the rows that match”Drop the rest. Needs the code mode: a table describes one row, and this changes how many there are.
{ "expr": "input.filter(r => r.status === 'open')", "mode": "all"}Sample input:
[ { "id": "A-1", "status": "open" }, { "id": "A-2", "status": "closed" }]Sort the rows
Section titled “Sort the rows”Order by a field. b - a instead of a - b reverses it; for text use localeCompare.
{ "expr": "[...input].sort((a, b) => b.amount - a.amount)", "mode": "all"}Sample input:
[ { "amount": 40, "id": "A-1" }, { "amount": 250, "id": "A-2" }]Keep the first few
Section titled “Keep the first few”The top of the list, once it is in the order you want. Sort first if the order matters.
{ "expr": "input.slice(0, 10)", "mode": "all"}Sample input:
[ { "n": 1 }, { "n": 2 }, { "n": 3 }]Remove duplicate rows
Section titled “Remove duplicate rows”One row per value of a field - the first one wins, so sort first if it matters which.
{ "expr": "input.filter((r, n) => input.findIndex(o => o.email === r.email) === n)", "mode": "all"}Sample input:
[ { "email": "kim@example.com", "n": 1 }, { "email": "sam@example.com", "n": 2 }, { "email": "kim@example.com", "n": 3 }]Bring a field across from another list
Section titled “Bring a field across from another list”Match each row against a second list on a shared key and copy a field over - the shape you need when one step fetched the rows and another fetched what they refer to.
{ "expr": "(() => { const by = new Map(input.customers.map(c => [c.id, c])); return input.orders.map(o => ({ ...o, customer_name: by.get(o.customer_id)?.name ?? null })); })()", "mode": "all"}Sample input:
{ "customers": [ { "id": 7, "name": "Kim" } ], "orders": [ { "customer_id": 7, "id": "A-1" }, { "customer_id": 9, "id": "A-2" } ]}Turn a list inside one row into rows
Section titled “Turn a list inside one row into rows”A record holding a list becomes one record per element, carrying the parent’s fields along.
{ "expr": "input.flatMap(r => r.items.map(item => ({ order: r.id, ...item })))", "mode": "all"}Sample input:
[ { "id": "A-1", "items": [ { "qty": 2, "sku": "x" }, { "qty": 1, "sku": "y" } ] }]Example
Section titled “Example”version: hyperspool-graph/v1nodes: - id: fetch type: http config: url: https://api.example.com/v1/orders fail_on_error_status: true - id: summary type: transform config: mode: all expr: | nodes.fetch.output.body.orders .filter(o => o.status === "open") .map(o => `${o.id}: ${o.total}`) .join("\n") - id: post type: shell config: run: { $template: "echo '{{ nodes.summary.output }}'" }edges: - { from: fetch, to: summary } - { from: summary, to: post }In all mode the expression runs once and its value is the output. To reshape the node above,
name it (nodes.fetch.output); input is the run’s input. The canvas shows what the transform
returns as you type, against a pinned sample. See Reshaping data.
See the node reference for every other node.