Skip to main content

Data Transformation

The Transform Data node is one of the most powerful nodes in Expedify. It lets you write JavaScript to process, format, combine, and transform data between workflow steps.

Time to complete: 20 minutes


When to Use Transform Data

Use the Transform Data node when you need to:

  • Format data (names, dates, currencies)
  • Combine data from multiple sources
  • Extract specific fields from complex objects
  • Calculate values (totals, averages, scores)
  • Validate data before processing
  • Convert between formats (arrays, objects, strings)

Basic Structure

The Transform Data node uses JavaScript with a specific pattern:

// Access input data from previous nodes
const data = input.webhooktrigger_1.payload;

// Process the data
const result = {
formatted_name: data.name.toUpperCase(),
email: data.email.toLowerCase()
};

// Return the output
return result;

The Input Object

All upstream node outputs are available in input:

input.webhooktrigger_1.payload      // Webhook data
input.transformdata_1.result // Previous transform
input.llmintegration_1.output // AI output
input.contact // From contact lookup

The Return Statement

Whatever you return becomes the node's output:

return {
field1: "value1",
field2: "value2"
};

Access it downstream as:

{{transformdata_1.field1}}
{{transformdata_1.field2}}

Common Transformations

String Formatting

const name = input.contact.name;

return {
uppercase: name.toUpperCase(), // "JOHN SMITH"
lowercase: name.toLowerCase(), // "john smith"
trimmed: name.trim(), // Remove whitespace
first_word: name.split(' ')[0], // "John"
initials: name.split(' ').map(n => n[0]).join(''), // "JS"
};

Name Handling

const fullName = input.payload.name || "";
const parts = fullName.trim().split(' ');

return {
first_name: parts[0] || "",
last_name: parts.slice(1).join(' ') || "",
full_name: fullName,
display_name: parts[0] || "Friend"
};

Email Formatting

const email = input.payload.email || "";

return {
email: email.toLowerCase().trim(),
domain: email.split('@')[1] || "",
is_valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
};

Date Formatting

const dateStr = input.payload.date;
const date = new Date(dateStr);

return {
iso: date.toISOString(), // "2024-03-15T10:30:00.000Z"
date_only: date.toISOString().split('T')[0], // "2024-03-15"
formatted: date.toLocaleDateString('en-US'), // "3/15/2024"
time: date.toLocaleTimeString('en-US'), // "10:30:00 AM"
timestamp: date.getTime() // 1710498600000
};

Number Formatting

const amount = input.order.total;

return {
rounded: Math.round(amount * 100) / 100, // 19.99
currency: `$${amount.toFixed(2)}`, // "$19.99"
percentage: `${(amount * 100).toFixed(1)}%`, // "19.9%"
with_commas: amount.toLocaleString() // "1,234.56"
};

Combining Data

Merge Objects

const contact = input.contact;
const company = input.company;
const deal = input.deal;

return {
contact_name: contact.name,
contact_email: contact.email,
company_name: company.name,
deal_value: deal.value,
summary: `${contact.name} from ${company.name} - $${deal.value}`
};

Build Email Content

const contact = input.contact;
const order = input.order;

const itemList = order.items.map(item =>
`- ${item.name}: $${item.price}`
).join('\n');

return {
subject: `Order Confirmation #${order.id}`,
body: `
Hello ${contact.first_name},

Thank you for your order!

Order Details:
${itemList}

Total: $${order.total}

We'll notify you when it ships.

Best regards,
The Team
`.trim()
};

Create Structured Response

const results = input.kbsearch_1.results;
const question = input.usermessagetrigger_1.message;

return {
question: question,
answer_count: results.length,
top_result: results[0]?.content || "No results found",
sources: results.map(r => ({
title: r.metadata?.title || "Untitled",
relevance: Math.round(r.similarity * 100) + "%"
}))
};

Array Operations

Filtering

const contacts = input.data.contacts;

return {
active: contacts.filter(c => c.is_active),
high_value: contacts.filter(c => c.deal_value > 10000),
with_email: contacts.filter(c => c.email && c.email.length > 0)
};

Mapping (Transform Each Item)

const items = input.order.items;

return {
formatted_items: items.map(item => ({
name: item.name.toUpperCase(),
price: `$${item.price.toFixed(2)}`,
quantity: item.qty
}))
};

Reducing (Aggregate)

const items = input.order.items;

return {
total: items.reduce((sum, item) => sum + item.price, 0),
count: items.length,
average: items.reduce((sum, item) => sum + item.price, 0) / items.length
};

Sorting

const contacts = input.data.contacts;

return {
by_name: [...contacts].sort((a, b) => a.name.localeCompare(b.name)),
by_score_desc: [...contacts].sort((a, b) => b.score - a.score),
by_date: [...contacts].sort((a, b) => new Date(b.date) - new Date(a.date))
};

Finding

const contacts = input.data.contacts;
const searchEmail = input.search.email;

return {
found: contacts.find(c => c.email === searchEmail) || null,
exists: contacts.some(c => c.email === searchEmail)
};

Grouping

const deals = input.data.deals;

const grouped = deals.reduce((acc, deal) => {
const stage = deal.stage;
if (!acc[stage]) acc[stage] = [];
acc[stage].push(deal);
return acc;
}, {});

return {
by_stage: grouped,
stages: Object.keys(grouped),
counts: Object.fromEntries(
Object.entries(grouped).map(([stage, items]) => [stage, items.length])
)
};

Conditional Logic

Simple Conditions

const score = input.contact.lead_score;

let priority;
if (score >= 80) {
priority = "high";
} else if (score >= 50) {
priority = "medium";
} else {
priority = "low";
}

return { priority };

Ternary Operator

const contact = input.contact;

return {
greeting: contact.first_name ? `Hello ${contact.first_name}` : "Hello there",
status: contact.is_active ? "Active" : "Inactive",
tier: contact.spend > 1000 ? "Premium" : "Standard"
};

Null Handling

const data = input.payload;

return {
// Using || for falsy fallback
name: data.name || "Unknown",

// Using ?? for null/undefined only
count: data.count ?? 0,

// Using optional chaining
city: data.address?.city || "Not provided",

// Checking existence
has_email: Boolean(data.email),
has_phone: data.phone !== null && data.phone !== undefined
};

Validation Patterns

Required Field Validation

const data = input.payload;
const errors = [];

if (!data.email) errors.push("Email is required");
if (!data.name) errors.push("Name is required");
if (data.phone && !/^\+?[\d\s-]+$/.test(data.phone)) {
errors.push("Invalid phone format");
}

return {
valid: errors.length === 0,
errors: errors,
data: errors.length === 0 ? data : null
};

Email Validation

const email = input.payload.email;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

return {
email: email,
is_valid: emailRegex.test(email),
domain: email ? email.split('@')[1] : null
};

Schema Validation

const data = input.payload;

function validate(data) {
const errors = [];

// Type checks
if (typeof data.email !== 'string') errors.push("email must be string");
if (typeof data.age !== 'number') errors.push("age must be number");

// Range checks
if (data.age < 0 || data.age > 120) errors.push("age out of range");

// Format checks
if (data.email && !data.email.includes('@')) errors.push("invalid email");

return errors;
}

const errors = validate(data);

return {
valid: errors.length === 0,
errors,
sanitized_data: errors.length === 0 ? {
email: data.email.toLowerCase().trim(),
age: parseInt(data.age)
} : null
};

Working with JSON

Parse JSON String

const jsonString = input.payload.data;

try {
const parsed = JSON.parse(jsonString);
return { success: true, data: parsed };
} catch (e) {
return { success: false, error: "Invalid JSON" };
}

Stringify Object

const data = input.contact;

return {
json_string: JSON.stringify(data),
pretty_json: JSON.stringify(data, null, 2)
};

Extract from Nested JSON

const response = input.apiresponse_1.data;

return {
// Safely access nested properties
user_id: response?.user?.id,
user_name: response?.user?.profile?.name,
items: response?.data?.items || []
};

API Request Preparation

Build Query Parameters

const filters = input.filters;

const params = new URLSearchParams();
if (filters.status) params.append('status', filters.status);
if (filters.limit) params.append('limit', filters.limit);
if (filters.offset) params.append('offset', filters.offset);

return {
query_string: params.toString(),
url: `https://api.example.com/users?${params.toString()}`
};

Build Request Body

const contact = input.contact;
const action = input.action;

return {
request_body: {
action: action,
timestamp: new Date().toISOString(),
data: {
contact_id: contact.id,
email: contact.email,
metadata: {
source: "workflow",
workflow_id: "{{workflow.id}}"
}
}
}
};

Debugging Tips

Log Values

const data = input.payload;

// Log for debugging (visible in execution logs)
console.log("Input data:", data);
console.log("Email:", data.email);

return { processed: true };

Return All Input (Debug)

// Useful for debugging - see everything available
return {
debug: true,
all_input: input
};

Check Data Types

const value = input.payload.amount;

return {
value: value,
type: typeof value,
is_number: typeof value === 'number',
is_string: typeof value === 'string',
parsed: parseFloat(value) || 0
};

Error Handling

Try-Catch

try {
const data = input.payload;
const result = JSON.parse(data.json_string);

return {
success: true,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}

Safe Property Access

const data = input.payload || {};

return {
// Won't error if nested properties don't exist
email: data?.user?.email || "no-email@example.com",
name: data?.user?.profile?.name || "Unknown",
items: data?.order?.items || []
};

Best Practices

1. Always Handle Missing Data

// Bad
const name = input.contact.name;

// Good
const name = input.contact?.name || "Unknown";

2. Use Descriptive Variable Names

// Bad
const x = input.a.b;

// Good
const customerEmail = input.contact.email;

3. Keep Transforms Focused

// Bad: Does too many things
// Good: One transform per concern, chain them

4. Return Clean Objects

return {
customer_name: name, // snake_case for consistency
order_total: total,
items_count: items.length
};

5. Document Complex Logic

// Calculate lead score based on:
// - Email opens (20 points each)
// - Website visits (5 points each)
// - Form submissions (50 points each)
const score = (opens * 20) + (visits * 5) + (forms * 50);

Quick Reference

String Methods

str.toUpperCase()      // "HELLO"
str.toLowerCase() // "hello"
str.trim() // Remove whitespace
str.split(',') // ["a", "b", "c"]
str.replace('a', 'b') // Replace first
str.includes('x') // true/false

Array Methods

arr.filter(fn)         // Filter items
arr.map(fn) // Transform items
arr.reduce(fn, init) // Aggregate
arr.find(fn) // Find first match
arr.some(fn) // Any match?
arr.every(fn) // All match?
arr.sort(fn) // Sort items
arr.slice(0, 5) // First 5 items

Object Methods

Object.keys(obj)       // ["key1", "key2"]
Object.values(obj) // [val1, val2]
Object.entries(obj) // [["key1", val1], ...]
{ ...obj1, ...obj2 } // Merge objects

Safe Access

obj?.prop              // Optional chaining
obj ?? default // Nullish coalescing
obj || default // Falsy fallback

Next Steps

Continue with:

  1. Using Conditions & Branches - Use transformed data in logic
  2. Testing Workflows - Test your transforms
  3. Error Handling - Handle transform errors