Skip to main content
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

  1. Write JavaScript code in the built-in editor
  2. Access workflow data directly via the nodes object
  3. Return a value that becomes the node’s output
  4. 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;
ExpressionDescription
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;
ExpressionDescription
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:
ExpressionReturns
{{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

Data transformation

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

FeatureSupportNotes
ES6+ syntaxArrow functions, destructuring, spread
Array methodsmap, filter, reduce, find, etc.
Object methodskeys, values, entries, assign, etc.
String methodssplit, includes, replace, etc.
JSONparse, stringify
MathAll standard methods
DateDate manipulation
RegExpPattern matching
Template literalsString interpolation

Available libraries

The following npm packages are pre-installed and available in your scripts:
LibraryDescriptionExample use
axiosHTTP client for making requestsAPI calls, webhooks
cheerioHTML parsing and manipulationWeb scraping, HTML extraction
crypto-jsCryptographic functionsHashing, encryption, signatures
date-fnsModern date utility libraryDate formatting, calculations
jsonschemaJSON Schema validationValidate data structures
knexSQL query builderBuild dynamic queries
lodashUtility library for data manipulationDeep cloning, merging, grouping
uuidGenerate unique identifiersCreate UUIDs for records
zodTypeScript-first schema validationRuntime type checking
urlURL parsing and formattingParse 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

FeatureReason
fetchUse axios or HTTP actions instead
setTimeoutUse Delay action for timing
console.logNo console output (use return values)
File system accessSandboxed 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:
  1. Run the tool with test inputs
  2. Check the JavaScript node’s output in the execution log
  3. 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]";

Performance considerations

PracticeImpact
Keep scripts simpleFaster execution, easier debugging
Avoid deep nestingBetter readability and performance
Limit array operationsLarge arrays can slow execution
Use early returnsSkip 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.
External data often contains null or undefined values. Use optional chaining (?.) and nullish coalescing (??) to handle these gracefully.
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`,
};