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:
- Using Conditions & Branches - Use transformed data in logic
- Testing Workflows - Test your transforms
- Error Handling - Handle transform errors