The JavaScript action executes custom JavaScript code within your workflow. Use it when built-in actions can’t handle your specific transformation, calculation, or logic requirements.
How it works
Write JavaScript code in the built-in editor
Access workflow data directly via the nodes object
Return a value that becomes the node’s output
Use the output in downstream actions via expressions
// Example: Calculate lead score
const { company_size , funding , industry } = nodes . enrich ;
let score = 0 ;
if ( company_size > 100 ) score += 30 ;
if ( funding > 10000000 ) score += 40 ;
if ( industry === "Technology" ) score += 20 ;
return { score , tier: score >= 70 ? "hot" : score >= 40 ? "warm" : "cold" };
Accessing workflow data
Inside JavaScript actions, you have direct access to all workflow data through global objects—no need for {{}} expression syntax.
The nodes object
Access output from any previous node in your workflow:
// Access the start node (tool/play inputs)
const companyDomain = nodes . start . company_domain ;
const contactEmail = nodes . start . contact_email ;
// Access output from an enrichment node
const companyName = nodes . enrich . company_name ;
const employeeCount = nodes . enrich . employee_count ;
// Access results from a search node
const contacts = nodes . search . results ;
// Access the full output object from any node
const allEnrichData = nodes . enrich ;
Expression Description nodes.startThe input data passed to the tool or play nodes.{node_name}Output from a specific node by its name nodes.{node_name}.fooAccess a specific property from a node’s output
The parentNodes object
When your JavaScript action is inside a Group node, use parentNodes to access data from nodes outside the group:
// Inside a Group: access the current item being iterated
const currentContact = nodes . group . item ;
const currentIndex = nodes . group . index ;
// Access data from nodes OUTSIDE the group
const originalCompany = parentNodes . enrich . company_name ;
const allSettings = parentNodes . config . settings ;
Expression Description nodes.group.itemThe current item in the loop nodes.group.indexThe zero-based index of the current item parentNodes.{node_name}Access nodes outside the current group parentNodes.startAccess the original tool/play input
Use parentNodes when you need to reference data that was computed before the
Group started—like company-level data when iterating over contacts.
Returning data
Your script must return a value. This becomes the node’s output:
// Return a simple value
return 42 ;
// Return an object
return {
processed: true ,
result: "Success" ,
timestamp: new Date (). toISOString (),
};
// Return an array
return nodes . search . results . map (( item ) => ({
... item ,
processed: true ,
}));
Access the returned data in downstream nodes:
Expression Returns {{nodes.javascript.output}}The full returned value {{nodes.javascript.processed}}true (if object returned){{nodes.javascript.result}}"Success"
Replace javascript with the actual name of your JavaScript node.
Common use cases
Reshape data between nodes:
const contacts = nodes . search . results ;
// Transform array of contacts into a formatted list
return contacts . map (( c ) => ({
full_name: ` ${ c . first_name } ${ c . last_name } ` ,
email: c . email . toLowerCase (),
domain: c . email . split ( "@" )[ 1 ],
}));
Complex calculations
Implement business logic that doesn’t fit built-in actions:
const { deal_size , contract_length , is_enterprise } = nodes . start ;
// Calculate annual contract value with enterprise multiplier
let acv = deal_size * 12 ;
if ( contract_length >= 24 ) acv *= 0.9 ; // Annual discount
if ( is_enterprise ) acv *= 1.2 ; // Enterprise premium
return {
acv: Math . round ( acv ),
tier: acv >= 100000 ? "enterprise" : acv >= 25000 ? "mid-market" : "smb" ,
};
String manipulation
Format and parse text data:
const raw_address = nodes . enrich . address ;
// Parse a raw address string into components
const parts = raw_address . split ( "," ). map (( p ) => p . trim ());
return {
street: parts [ 0 ] || "" ,
city: parts [ 1 ] || "" ,
state: parts [ 2 ]?. split ( " " )[ 0 ] || "" ,
zip: parts [ 2 ]?. split ( " " )[ 1 ] || "" ,
country: parts [ 3 ] || "USA" ,
};
Array operations
Filter, sort, and aggregate arrays:
const leads = nodes . search . results ;
// Filter high-value leads and sort by score
const qualified = leads
. filter (( lead ) => lead . score >= 50 && lead . email )
. sort (( a , b ) => b . score - a . score );
return {
qualified_leads: qualified ,
total: qualified . length ,
top_lead: qualified [ 0 ] || null ,
};
JSON parsing
Parse JSON strings from API responses:
const api_response = nodes . http_request . body ;
try {
const data = JSON . parse ( api_response );
return {
success: true ,
data: data . results || [],
count: data . total || 0 ,
};
} catch ( error ) {
return {
success: false ,
error: "Invalid JSON response" ,
raw: api_response ,
};
}
Date operations
Work with dates and timestamps:
const { created_at , trial_days } = nodes . start ;
const createdDate = new Date ( created_at );
const trialEnd = new Date ( createdDate );
trialEnd . setDate ( trialEnd . getDate () + trial_days );
const now = new Date ();
const daysRemaining = Math . ceil (( trialEnd - now ) / ( 1000 * 60 * 60 * 24 ));
return {
trial_end_date: trialEnd . toISOString (). split ( "T" )[ 0 ],
days_remaining: Math . max ( 0 , daysRemaining ),
is_expired: daysRemaining <= 0 ,
urgency: daysRemaining <= 3 ? "high" : daysRemaining <= 7 ? "medium" : "low" ,
};
Using parentNodes in a Group
When processing items inside a Group, combine item data with parent context:
// Inside a Group iterating over contacts
const contact = nodes . group . item ;
const index = nodes . group . index ;
// Access company data from outside the group
const companyName = parentNodes . enrich . company_name ;
const companyIndustry = parentNodes . enrich . industry ;
return {
contact_name: ` ${ contact . first_name } ${ contact . last_name } ` ,
company: companyName ,
industry: companyIndustry ,
position: index + 1 ,
email_subject: ` ${ contact . first_name } , a quick note about ${ companyName } ` ,
};
Available JavaScript features
The JavaScript action runs in a sandboxed JavaScript environment with:
Supported features
Feature Support Notes ES6+ syntax ✅ Arrow functions, destructuring, spread Array methods✅ map, filter, reduce, find, etc. Object methods✅ keys, values, entries, assign, etc. String methods✅ split, includes, replace, etc. JSON✅ parse, stringify Math✅ All standard methods Date✅ Date manipulation RegExp✅ Pattern matching Template literals ✅ String interpolation
Available libraries
The following npm packages are pre-installed and available in your scripts:
Library Description Example use axiosHTTP client for making requests API calls, webhooks cheerioHTML parsing and manipulation Web scraping, HTML extraction crypto-jsCryptographic functions Hashing, encryption, signatures date-fnsModern date utility library Date formatting, calculations jsonschemaJSON Schema validation Validate data structures knexSQL query builder Build dynamic queries lodashUtility library for data manipulation Deep cloning, merging, grouping uuidGenerate unique identifiers Create UUIDs for records zodTypeScript-first schema validation Runtime type checking urlURL parsing and formatting Parse and build URLs
// Example: Using lodash and date-fns
const _ = require ( "lodash" );
const { format , addDays } = require ( "date-fns" );
const contacts = nodes . search . results ;
// Group contacts by company
const grouped = _ . groupBy ( contacts , "company_name" );
// Calculate follow-up date
const followUpDate = format ( addDays ( new Date (), 7 ), "yyyy-MM-dd" );
return {
companies: Object . keys ( grouped ),
contacts_by_company: grouped ,
follow_up_date: followUpDate ,
};
// Example: Using zod for validation
const { z } = require ( "zod" );
const ContactSchema = z . object ({
email: z . string (). email (),
name: z . string (). min ( 1 ),
company: z . string (). optional (),
});
const contact = nodes . start ;
const result = ContactSchema . safeParse ( contact );
if ( ! result . success ) {
return { valid: false , errors: result . error . issues };
}
return { valid: true , data: result . data };
// Example: Using uuid and crypto-js
const { v4 : uuidv4 } = require ( "uuid" );
const CryptoJS = require ( "crypto-js" );
const record = nodes . start ;
return {
id: uuidv4 (),
hash: CryptoJS . SHA256 ( record . email ). toString (),
created_at: new Date (). toISOString (),
};
Not supported
Feature Reason fetchUse axios or HTTP actions instead setTimeoutUse Delay action for timing console.logNo console output (use return values) File system access Sandboxed environment
While axios is available for HTTP requests within scripts, consider using
dedicated HTTP Request or integration actions for better error handling and
monitoring.
Error handling
Try-catch blocks
Handle errors gracefully within your script:
const data = nodes . api . response ;
try {
const parsed = JSON . parse ( data );
const result = complexOperation ( parsed );
return { success: true , result };
} catch ( error ) {
return {
success: false ,
error: error . message ,
fallback: "Default value" ,
};
}
Validation
Validate data before processing:
const { email , company_size } = nodes . start ;
// Validate required fields
if ( ! email || typeof email !== "string" ) {
return { error: "Invalid email" , valid: false };
}
if ( ! email . includes ( "@" )) {
return { error: "Email must contain @" , valid: false };
}
if ( company_size && typeof company_size !== "number" ) {
return { error: "Company size must be a number" , valid: false };
}
return { valid: true };
Always validate data when it comes from external sources or user input. Return
structured error objects so downstream nodes can handle failures.
Debugging tips
Use descriptive returns
Instead of returning just a result, include debug information:
const items = nodes . search . results ;
const processed = items . filter (( i ) => i . active );
return {
result: processed ,
debug: {
input_count: items . length ,
output_count: processed . length ,
filtered_out: items . length - processed . length ,
},
};
Test with sample data
Before using in production:
Run the tool with test inputs
Check the JavaScript node’s output in the execution log
Verify the data structure matches what downstream nodes expect
Check for undefined values
JavaScript’s undefined can cause silent failures:
const user = nodes . enrich . user ;
// Dangerous: might access undefined
const email = user . contact . email ;
// Safer: use optional chaining
const email = user ?. contact ?. email || "[email protected] " ;
Practice Impact Keep scripts simple Faster execution, easier debugging Avoid deep nesting Better readability and performance Limit array operations Large arrays can slow execution Use early returns Skip unnecessary processing
Scripts have a default timeout of 30 seconds. Complex operations on large
datasets may need optimization or should be split across multiple nodes.
Best practices
Each script should do one thing well. If your script is growing complex,
consider splitting it into multiple JavaScript nodes or creating helper
functions within the script.
Always return objects with clear property names. This makes it easier to
access data in downstream nodes and debug issues.
Handle null and undefined
External data often contains null or undefined values. Use optional chaining
(?.) and nullish coalescing (??) to handle these gracefully.
Validate before processing
Check that required fields exist and have the expected types before
performing operations. Return early with error information when validation
fails.
Scripts should be pure functions—given the same inputs, they should always
return the same outputs. Don’t rely on external state.
Example: Lead qualification script
A complete example that qualifies leads based on multiple criteria:
const { company_size , industry , funding_amount , has_website , technologies } =
nodes . enrich ;
// Define scoring rules
const INDUSTRY_SCORES = {
Technology: 25 ,
Finance: 20 ,
Healthcare: 20 ,
"E-commerce" : 15 ,
};
const TECH_KEYWORDS = [ "react" , "node" , "python" , "aws" , "kubernetes" ];
// Calculate component scores
let score = 0 ;
const factors = [];
// Company size scoring
if ( company_size >= 500 ) {
score += 30 ;
factors . push ( "Enterprise size (+30)" );
} else if ( company_size >= 100 ) {
score += 20 ;
factors . push ( "Mid-market size (+20)" );
} else if ( company_size >= 20 ) {
score += 10 ;
factors . push ( "SMB size (+10)" );
}
// Industry scoring
const industryScore = INDUSTRY_SCORES [ industry ] || 5 ;
score += industryScore ;
factors . push ( `Industry: ${ industry } (+ ${ industryScore } )` );
// Funding scoring
if ( funding_amount >= 50000000 ) {
score += 25 ;
factors . push ( "Series C+ funding (+25)" );
} else if ( funding_amount >= 10000000 ) {
score += 15 ;
factors . push ( "Series A/B funding (+15)" );
}
// Technology match scoring
const techList = ( technologies || "" ). toLowerCase ();
const matchedTech = TECH_KEYWORDS . filter (( t ) => techList . includes ( t ));
if ( matchedTech . length > 0 ) {
const techScore = matchedTech . length * 5 ;
score += techScore ;
factors . push ( `Tech stack match: ${ matchedTech . join ( ", " ) } (+ ${ techScore } )` );
}
// Website presence
if ( has_website ) {
score += 5 ;
factors . push ( "Has website (+5)" );
}
// Determine qualification tier
let tier , priority ;
if ( score >= 70 ) {
tier = "A" ;
priority = "high" ;
} else if ( score >= 50 ) {
tier = "B" ;
priority = "medium" ;
} else if ( score >= 30 ) {
tier = "C" ;
priority = "low" ;
} else {
tier = "D" ;
priority = "nurture" ;
}
return {
score ,
tier ,
priority ,
qualified: score >= 50 ,
factors ,
summary: ` ${ tier } -tier lead (score: ${ score } ) - ${ priority } priority` ,
};