Data Transformation
General-purpose nodes for reshaping, computing, encoding, and extracting data between steps. Most accept {{ ... }} expressions in their config fields, resolved against steps, vars, trigger, secret, and input (see Workflows & the DSL).
transform
Remaps fields from upstream step outputs into a new output shape. config.mapping is a JSON object where each key becomes an output field and each value is an expression resolved against the step's inputs (the merged map of all upstream step outputs, trigger data, vars, etc.).
{
"id": "shape_lead",
"type": "transform",
"config": {
"mapping": {
"email": "{{steps.fetch_lead.output.body.email}}",
"full_name": "{{steps.fetch_lead.output.body.first_name}} {{steps.fetch_lead.output.body.last_name}}",
"source": "hubspot"
}
}
}
| Field | Type | Default | Description |
|---|---|---|---|
mapping | object | — | Required. Map of output_key -> expression. Each expression is resolved with dsl.ResolveExpressions against the step's inputs. |
Output: steps.<id>.output is exactly the resolved mapping object — one field per mapping key, with {{...}} expressions replaced by their resolved values.
set
Creates or overrides fields on top of a copy of all current inputs (every upstream step's output, merged). Use it to add computed fields or constants without losing access to everything already produced upstream.
{
"id": "annotate",
"type": "set",
"config": {
"fields": {
"status": "processed",
"processed_at": "{{steps.now.output.iso}}"
}
}
}
| Field | Type | Default | Description |
|---|---|---|---|
fields | object | — | Required. Map of key -> value. Values support {{...}} expressions. |
Output: steps.<id>.output is a deep copy of the merged inputs map (all upstream step outputs, by step ID) with fields overlaid on top — fields keys win on conflicts.
code
Runs user-supplied JavaScript or WASM in-process (via goja and wazero — no Docker container, no external runtime). For shell commands or multi-step file processing pipelines that need a real filesystem, use the agent's terminal/file tools instead, which run in the Sandbox.
{
"id": "transform_payload",
"type": "code",
"config": {
"language": "javascript",
"code": "const reply = inputs.steps.llm.text.toUpperCase();\nreturn { reply, length: reply.length };",
"timeout_ms": 10000
}
}
| Field | Type | Default | Description |
|---|---|---|---|
language | string | "javascript" | "javascript" ("js" also accepted) or "wasm". |
code | string | — | Required. JS source for javascript; base64-encoded .wasm binary for wasm. |
timeout_ms | number | 10000 | Max execution time in milliseconds. Values <= 0 or > 120000 fall back to 10000. |
JavaScript (language: "javascript")
- Powered by goja, a pure-Go ES5.1+ engine with a Node-like API.
- Globals available:
inputs(object containing all trigger and step outputs),console.log/console.warn/console.error(captured into__logs). - Code is wrapped in
(function(inputs) { <code> })(inputs), so a top-levelreturn { ... }works. - The return value becomes the step output:
- If it's an object, its keys become
steps.<id>.output.*(with__logsappended if anyconsole.*calls were made). - If it's a primitive (string, number, etc.), the output is
{ "result": <value>, "__logs": [...] }. - If nothing is returned, the output is
{ "logs": [...] }.
- If it's an object, its keys become
const reply = inputs.steps.llm.text.toUpperCase();
return { reply, length: reply.length };
WASM (language: "wasm")
- Powered by wazero, a pure-Go WASM runtime with WASI preview1 support — no host filesystem or network access.
codemust be the base64-encoded.wasmmodule bytes.- The module must export:
memoryalloc(size i32) -> ptr i32execute(input_ptr i32, input_len i32) -> output_ptr i32
- The runtime writes a JSON-encoded
inputsmap into the module's memory (viaalloc), callsexecute, and reads a null-terminated JSON string (up to 64 KB) from the returned pointer as the output. The output JSON must be an object — it becomessteps.<id>.outputdirectly.
Output: Shape depends on the script's return value (see above) — steps.<id>.output.<field> for object returns, or steps.<id>.output.result for primitives.
csv
Converts between CSV text and JSON arrays of row objects.
{
"id": "parse_csv",
"type": "csv",
"config": {
"operation": "csv_to_json",
"csv": "{{steps.download.output.body}}",
"has_header": "true",
"delimiter": ","
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "csv_to_json" | "csv_to_json" or "json_to_csv". |
delimiter | string | "," | Single character used as the field delimiter. Only the first character is used; falls back to "," if empty. |
has_header | string | "true" | Set to "false" to treat the first row as data, not column names. |
csv_to_json
| Field | Type | Default | Description |
|---|---|---|---|
csv | string | — | Required. CSV text (resolved expression). Parsed with lazy quotes and leading-space trimming. |
Output:
| Field | Type | Description |
|---|---|---|
data | array of objects | One object per row. With has_header: "true", keys are the header row's column names; otherwise keys are col1, col2, ... |
rows | number | Row count (excluding header). |
columns | number | Column count. |
headers | array of strings | Column names (header row, or empty if has_header: "false"). |
json_to_csv
| Field | Type | Default | Description |
|---|---|---|---|
data | array | — | Required. Array of row objects (if has_header: "true") or arrays/scalars (if "false"). May be a JSON string, which is parsed first. |
With has_header: "true", headers are the union of keys from the first array element, sorted alphabetically; every row is rendered using those keys (missing keys render as <nil>).
Output:
| Field | Type | Description |
|---|---|---|
csv | string | The generated CSV text. |
rows | number | Number of data rows written (excluding the header row if present). |
xml
Parses XML into a nested JSON structure, or builds an XML document from a JSON object.
{
"id": "parse_feed",
"type": "xml",
"config": {
"operation": "parse",
"value": "{{steps.fetch.output.body}}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "parse" | "parse" or "build". |
parse
| Field | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. XML text to parse. |
Parsing rules:
- Each XML element becomes a
map[string]any. Attributes become keys prefixed with@(e.g.@id). - An element containing only text content collapses to that text string.
- An empty element collapses to
"". - Repeated sibling elements with the same tag name become a JSON array under that key.
Output: { "data": <parsed nested object> }.
build
| Field | Type | Default | Description |
|---|---|---|---|
data | object | — | Required. JSON object to render as XML (or a JSON string of one). |
root_tag | string | "root" | Root element tag name. |
Rendering rules: object keys become child elements; arrays render each item as <item>...</item>; scalars render as escaped element text.
Output: { "xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>...</root>" }.
graphql
Sends a GraphQL query or mutation as a POST request to any GraphQL endpoint.
{
"id": "fetch_repo",
"type": "graphql",
"config": {
"url": "https://api.github.com/graphql",
"query": "query($owner:String!,$name:String!){ repository(owner:$owner,name:$name){ stargazerCount } }",
"variables": { "owner": "goagents", "name": "platform" },
"bearer_token": "{{secret.GITHUB_TOKEN}}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
url | string | — | Required. GraphQL endpoint URL. |
query | string | — | Required. GraphQL query or mutation document. |
variables | object | — | Optional GraphQL variables, sent if non-empty. |
operation_name | string | — | Optional operationName, sent if non-empty. |
action | string | — | Semantic only ("query" or "mutation") — both send the same POST request. |
bearer_token | string | — | If set, sent as Authorization: Bearer <token>. |
headers | object | — | Additional request headers (key: value). |
timeout_seconds | number | 60 | HTTP client timeout. |
Output:
| Field | Type | Description |
|---|---|---|
data | any | The GraphQL response's data field. |
status | number | HTTP status code. |
ok | boolean | true if status is 2xx and the response had no errors field. |
errors | array | Present if the response included GraphQL errors. When present, the step also returns a Go error ("graphql: server returned errors: ..."), which fails the step unless continue_on: "error" is set. |
datetime
Date/time utilities: get the current time, format/parse, add/subtract durations, diff two timestamps, and convert timezones.
{
"id": "now",
"type": "datetime",
"config": {
"operation": "current",
"timezone": "America/New_York"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "current" | current | format | parse | add | subtract | diff | convert_timezone. |
Date format tokens (input_format / output_format) accept either a Go layout string (e.g. 2006-01-02) or friendly tokens YYYY, YY, MM, DD, HH, mm, ss, SSS — translated automatically (e.g. YYYY-MM-DD HH:mm:ss).
Operations
| Operation | Extra fields | Description |
|---|---|---|
current | timezone (default "UTC") | Returns the current time in the given timezone. |
format | value, input_format (optional), output_format (default Go RFC3339) | Parses value and re-formats it as output_format. |
parse | value, input_format (optional) | Parses value and returns the full date/time breakdown. |
add | value, input_format (optional), plus any of years, months, days, hours, minutes, seconds (numbers) | Parses value, adds the given duration, returns the result. |
subtract | Same as add | Parses value, subtracts the given duration, returns the result. |
diff | from, to (both date/time strings, auto-detected format) | Returns the difference to - from. |
convert_timezone | value, input_format (optional), timezone (default "UTC") | Parses value and re-expresses it in the target timezone. |
years/months/days are approximated as 365/30/24-hour and 24-hour multiples respectively (no calendar-aware arithmetic).
If input_format is omitted, parse/format/add/subtract/convert_timezone try, in order: RFC3339, RFC3339Nano, 2006-01-02T15:04:05, 2006-01-02 15:04:05, 2006-01-02, 01/02/2006, 02/01/2006, Jan 2, 2006, RFC1123Z, RFC1123, RFC822Z, RFC822.
timezone accepts "UTC", "Local", or any IANA timezone name (e.g. "Asia/Kolkata").
Output for current, parse, add, subtract, convert_timezone:
| Field | Type | Description |
|---|---|---|
iso | string | RFC3339 timestamp. |
unix | number | Unix seconds. |
unix_ms | number | Unix milliseconds. |
year / month / day / hour / minute / second | number | Date/time components. |
weekday | string | e.g. "Monday". |
timezone | string | Resolved timezone name. |
formatted | string | 2006-01-02 15:04:05. |
Output for format: { "result": "<formatted string>", "unix": <int64> }.
Output for diff: { "seconds": int64, "minutes": int64, "hours": int64, "days": int64 } (all derived from to - from; days is hours / 24, truncated).
math-helper
Arithmetic and numeric utilities. Numeric fields accept JSON numbers or numeric strings.
{
"id": "calc_total",
"type": "math-helper",
"config": {
"operation": "add",
"a": "{{steps.cart.output.subtotal}}",
"b": "{{steps.cart.output.tax}}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "add" | See table below. |
Operations
| Operation | Fields | Output |
|---|---|---|
add | a, b | { "result": a + b } |
subtract | a, b | { "result": a - b } |
multiply | a, b | { "result": a * b } |
divide | a, b (b != 0) | { "result": a/b, "quotient": trunc(a/b), "remainder": a mod b } |
modulo | a, b (b != 0) | { "result": a mod b } |
power | base, exponent | { "result": base ^ exponent } |
sqrt | a (a >= 0) | { "result": sqrt(a) } |
abs | a | { "result": abs(a) } |
ceil | a | { "result": ceil(a) } |
floor | a | { "result": floor(a) } |
round | a, decimals (default 0) | { "result": a rounded to "decimals" places } |
log | a (a > 0), base (default 10; 2 and 10 use optimized functions, otherwise natural-log change-of-base) | { "result": log_base(a) } |
percentage | value, total (total != 0) | { "result": (value/total) * 100 } |
random | min (default 0), max (default 100, min <= max), integer (string, default "true") | { "result": random number in [min,max] } — integer if integer != "false" |
min | a, b | { "result": min(a, b) } |
max | a, b | { "result": max(a, b) } |
All operations return a single numeric result field except divide (also quotient/remainder).
text-helper
String manipulation: concatenation, splitting, regex find/replace, case conversion, HTML/Markdown conversion, padding, truncation, and more.
{
"id": "make_slug",
"type": "text-helper",
"config": {
"operation": "slugify",
"text": "{{steps.fetch_post.output.title}}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "concat" | See table below. |
Operations
| Operation | Fields | Output |
|---|---|---|
concat | parts (array, preferred) or a, b; separator | If parts is set, joins all resolved items with separator; else { "result": a + separator + b } |
split | text, separator | { "result": [parts...], "count": n } |
replace | text, find, replace, use_regex ("true"/"false", default "false"), replace_all (default "true") | { "result": "<text with replacements>" }. With use_regex: "true", find is a regex. replace_all: "false" replaces only the first match. |
find | text, find, use_regex | Regex: { "found": bool, "first": string, "all": [...], "count": n }. Plain: { "found": bool, "index": int, "count": n } |
slugify | text | { "result": "lowercase-hyphenated-text" } — non-alphanumeric runs collapse to single -, trimmed at edges |
strip_html | text | { "result": "<text content only>" } |
html_to_markdown | text | { "result": "<markdown>" } — converts h1-h3, p, strong/b, em/i, code, pre, a, li, br, hr |
markdown_to_html | text | { "result": "<html>" } — converts headings, lists, blockquotes, ---/*** rules, code fences, and inline **bold**, _italic_, `code`, [text](url) |
default_value | text, default | { "result": text }, or default if text is empty/<nil> |
trim | text, chars (optional) | { "result": "<trimmed>" } — trims whitespace, or the given character set if chars is set |
uppercase | text | { "result": "<TEXT>" } |
lowercase | text | { "result": "<text>" } |
title_case | text | { "result": "<Title Case>" } |
length | text | { "result": <rune count>, "bytes": <byte count> } |
reverse | text | { "result": "<reversed text>" } |
count_occurrences | text, find | { "result": <count> } |
truncate | text, max_length (default 100), suffix (default "...") | { "result": "...", "truncated": bool, "original_length": n (only if truncated) } |
pad | text, length, pad_char (default " "), direction ("left" | "right", default "right") | { "result": "<padded text>" } |
extract_between | text, start, end | { "result": "<substring>", "found": bool } — text between the first occurrence of start and the following end |
text_splitter
Chunks text for RAG/embedding pipelines, mirroring the LangChain/Flowise text-splitter family. This is the same chunking logic used by Knowledge Base document processing (chunk_size/chunk_overlap), exposed as a standalone step so you can chunk arbitrary text mid-workflow (e.g. before a custom embedding call).
{
"id": "chunk_doc",
"type": "text_splitter",
"config": {
"text": "{{steps.fetch_doc.output.body}}",
"strategy": "recursive",
"chunk_size": 1000,
"chunk_overlap": 100
}
}
| Field | Type | Default | Description |
|---|---|---|---|
text | string | — | Required. Text to split. |
strategy | string | "recursive" | recursive | character | markdown | code | token. |
chunk_size | number | 1000 | Target max characters per chunk (or tokens for "token"). Values <= 0 reset to 1000. |
chunk_overlap | number | 100 | Characters/tokens shared between adjacent chunks. Negative resets to 0; if >= chunk_size, it's reduced to chunk_size / 5. |
separator | string | "\n\n" | Only used by the "character" strategy. |
Strategies
| Strategy | Behavior |
|---|---|
recursive (default) | RecursiveCharacterTextSplitter — tries separators "\n\n", "\n", " ", "" in order, recursing into oversized pieces with the remaining separators, then merges pieces back up to chunk_size with chunk_overlap. |
character | CharacterTextSplitter — splits on a single separator, then merges pieces up to chunk_size with chunk_overlap. |
markdown | Recursive split using Markdown-aware separators first (\n## , \n### , ... \n# ), then falling back to \n\n, \n, " ", "". |
code | Recursive split using code-aware separators first (\nclass , \ndef , \nfunc , \nfunction ), then \n\n, \n, " ", "". |
token | Approximate token-based split at ~4 characters per token (chunk_size * 4 / chunk_overlap * 4 characters). |
Resulting chunks are trimmed of surrounding whitespace and empty chunks are dropped.
Output:
| Field | Type | Description |
|---|---|---|
chunks | array of strings | The text chunks, in order. |
count | number | Number of chunks. |
strategy | string | The strategy used. |
data-summarizer
Aggregates numeric values from an array of items, optionally extracting a field from each object first.
{
"id": "avg_score",
"type": "data-summarizer",
"config": {
"operation": "average",
"data": "{{steps.fetch_results.output.items}}",
"field": "score"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "sum" | sum | average | count | min | max | count_unique | median | std_dev. |
data | array | — | Required. Array of numbers or objects. |
field | string | — | If set, each array element must be an object and the named field's value is extracted before aggregation; non-numeric values are skipped. |
Operations
| Operation | Output |
|---|---|
sum | { "result": <sum>, "count": <n numeric values> } |
average | { "result": <mean>, "sum": <sum>, "count": n } ({ "result": 0, "count": 0 } if no numeric values) |
count | { "result": <total array length>, "numeric_count": <n numeric values> } |
min | { "result": <min> } (errors if no numeric values) |
max | { "result": <max> } (errors if no numeric values) |
count_unique | { "result": <n unique stringified items>, "total": <array length> } |
median | { "result": <median>, "count": n } (errors if no numeric values) |
std_dev | { "result": <population std dev>, "variance": <variance>, "mean": <mean>, "count": n } (errors if no numeric values) |
crypto
Hashing, HMAC signing, AES-256-GCM encryption/decryption, and Base64 encoding/decoding.
{
"id": "sign_payload",
"type": "crypto",
"config": {
"action": "hmac",
"algorithm": "sha256",
"value": "{{steps.build_body.output.json}}",
"secret": "{{secret.WEBHOOK_SIGNING_KEY}}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
action | string | "hash" | hash | hmac | encrypt | decrypt | base64_encode | base64_decode. |
value | string | — | The input string for the selected action. |
Actions
| Action | Extra fields | Output |
|---|---|---|
hash | algorithm (default "sha256"; md5|sha1|sha256|sha512) | { "result": "<hex digest>", "algorithm": "<algo>", "input_length": <bytes> } |
hmac | algorithm (default "sha256"; sha1|sha256|sha512|md5), secret | { "result": "<hex HMAC>", "algorithm": "hmac-<algo>" } |
encrypt | key | { "result": "<base64 ciphertext>", "algorithm": "aes-256-gcm" } — key is SHA-256-derived into a 256-bit AES key; output is base64(nonce || ciphertext) |
decrypt | key | { "result": "<plaintext>" } — value must be the base64 output of encrypt with the same key |
base64_encode | — | { "result": "<base64>" } |
base64_decode | — | { "result": "<decoded string>" } — tries standard then raw (no-padding) base64 |
compression
Gzip and Zip compression/decompression. Binary data is passed and returned as base64.
{
"id": "zip_report",
"type": "compression",
"config": {
"operation": "zip_create",
"files": [
{ "name": "report.csv", "content": "{{steps.to_csv.output.csv}}" },
{ "name": "summary.json", "content": "{{steps.summarize.output}}" }
]
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "gzip_compress" | gzip_compress | gzip_decompress | zip_create | zip_extract. |
Operations
| Operation | Fields | Output |
|---|---|---|
gzip_compress | value (string) | { "data_base64": "<base64 gzip>", "original_size": <bytes>, "compressed_size": <bytes> } |
gzip_decompress | value (base64 or raw bytes) | { "data": "<decompressed string>", "size": <bytes> } |
zip_create | files: array of { "name": string, "content": string } | { "data_base64": "<base64 zip archive>", "size": <bytes> } |
zip_extract | value (base64 or raw zip bytes) | { "files": [{ "name", "content", "size" }, ...], "count": n } |
For gzip_decompress and zip_extract, value is first base64-decoded if possible; if decoding fails it's treated as raw bytes.
qrcode
Generates a QR code as a base64-encoded PNG.
{
"id": "make_qr",
"type": "qrcode",
"config": {
"operation": "generate",
"content": "{{steps.create_invite.output.url}}",
"size": 256,
"error_correction": "medium"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
operation | string | "generate" | Only "generate" is supported. |
content | string | — | Required. Text/URL to encode. |
size | number | 256 | PNG width/height in pixels. Clamped to [64, 2048]. |
error_correction | string | "medium" | low | medium | high | highest. |
Output:
| Field | Type | Description |
|---|---|---|
image_base64 | string | Base64-encoded PNG bytes. |
image_data_url | string | data:image/png;base64,<image_base64> — usable directly in an <img src> or sent as a Slack/email attachment. |
content | string | The encoded content. |
size | number | The (clamped) pixel size used. |
format | string | "png". |
html_extract
Extracts structured data from HTML using simple CSS-like selectors (tag, .class, #id, tag.class, tag#id — no descendant/combinator support).
{
"id": "scrape_page",
"type": "html_extract",
"config": {
"source": "url",
"value": "https://example.com/blog/post",
"extractions": [
{ "name": "title", "selector": "h1", "attribute": "text" },
{ "name": "links", "selector": "a", "attribute": "href", "multiple": true }
]
}
}
| Field | Type | Default | Description |
|---|---|---|---|
source | string | "html" | "html" (use value as inline HTML) or "url" (fetch value first, 30s timeout). |
value | string | — | Inline HTML string, or the URL to fetch when source: "url". |
extractions | array | — | List of extraction rules (see below). |
Extraction rule fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | "result" | Output key for this extraction. |
selector | string | — | Required (rules with empty selector are skipped). One of: tag, .class, #id, tag.class, tag#id. |
attribute | string | "text" | "text" (visible text content), "html" (rendered inner/outer HTML of the matched node), or any HTML attribute name (e.g. "href", "src"). |
multiple | boolean | false | If true, returns an array of values for every matching element; if false, only the first match. |
Output: One field per extraction rule, keyed by name:
multiple: false(default): the extracted string, ornullif no element matched.multiple: true: an array of extracted strings ([]if no elements matched).
Plus an always-present full_text field containing the trimmed text content of the entire parsed document.