QANode Logo

Custom JavaScript Node

The Custom JavaScript node allows you to execute custom JavaScript code during flow execution. It is ideal for data transformations, complex calculations, and logic not covered by native nodes.

In addition to plain JavaScript, this node can also work with already-open Web and Mobile sessions, allowing you to run advanced automation with Playwright and Appium directly from code.


Overview

PropertyValue
Typecustom-js
CategoryUtilities
Color⚪ Gray (#6b7280)
Inputin
Outputout

Configuration

FieldTypeDescription
CodestringJavaScript code to be executed

The code editor uses the Monaco Editor (same as VS Code) with:

  • Syntax highlighting
  • Autocomplete for steps, variables, web, mobile, page, app, return, console.log
  • Syntax validation

Execution Context

The code has access to:

VariableTypeDescription
stepsobjectOutputs from all previous nodes
variablesobjectGlobal and runtime variables
expectfunctionPlaywright assertions for Web scenarios
assertobjectNode.js assertions
fetchfunctionDirect HTTP requests via JavaScript
webobjectNamespace for working with already-open Web sessions
mobileobjectNamespace for working with already-open Mobile sessions
pageobjectGlobal alias for the current Web page
contextobjectGlobal alias for the current Web context
browserobjectGlobal alias for the current browser
appobjectGlobal alias for the current Mobile session
clientobjectCompatibility alias for app

Accessing data from previous nodes

// Output from an HTTP Request
const userData = steps["http-request"].outputs.json;
const userName = userData.name;

// Output from a PostgreSQL query
const rows = steps["postgres-query"].outputs.rows;
const firstUser = rows[0];

// Output from web extraction
const title = steps["web-flow"].outputs.extracts.pageTitle;

Accessing variables

const apiUrl = variables.API_URL;
const environment = variables.ENVIRONMENT;

Web and Mobile Sessions

The Custom JavaScript node does not automatically create a Web or Mobile session. It uses a session that was already opened by a previous node.

Web session

To use web, page, context, or browser, you need an active Web session created by a node such as:

  • Web Flow
  • Smart Locators

Mobile session

To use mobile, app, or client, you need an active Mobile session created by a node such as:

  • Mobile Flow

Practical rule

  • Use web when you want to extend an already-open Web automation
  • Use mobile when you want to extend an already-open Mobile automation
  • If there is no active session, these APIs will fail

Web Automation with Playwright

The web namespace exposes the current Web session and advanced utilities powered by Playwright.

web namespace methods

MethodDescription
web.hasSession(sessionId?)Checks whether there is an active Web session
web.ids()Returns the IDs of available Web sessions
web.sessions()Returns all available Web sessions
web.current()Returns the current Web session
web.session(sessionId)Returns a specific Web session
web.page(sessionId?)Returns the Playwright page
web.context(sessionId?)Returns the Playwright context
web.browser(sessionId?)Returns the Playwright browser
web.screenshot(name?, options?, sessionId?)Captures a screenshot from the Web session
web.run(fn, sessionId?)Runs advanced code against the Web session

Object returned by web.current() and web.session()

Field / MethodDescription
idInternal session ID
sessionIdPublic session ID
pagePlaywright page instance
contextPlaywright context instance
browserPlaywright browser instance
sessionRaw session object
storageStrategySession storage strategy
persistedKeySession persistence key
url()Returns the current URL
title()Returns the current page title
locator(selector)Creates a Playwright locator
screenshot(name?, options?)Captures a screenshot from that session

Most common page methods

The page object is a Playwright instance. Common methods include:

MethodDescription
page.goto(url, options?)Navigates to a URL
page.url()Returns the current URL
page.title()Returns the current title
page.reload()Reloads the page
page.goBack()Goes back in history
page.goForward()Goes forward in history
page.locator(selector)Creates a locator
page.getByRole(...)Finds by accessible role
page.getByText(...)Finds by text
page.getByLabel(...)Finds by label
page.getByPlaceholder(...)Finds by placeholder
page.getByTestId(...)Finds by data-testid
page.waitForURL(...)Waits for a URL change
page.waitForResponse(...)Waits for a network response
page.waitForSelector(...)Waits for a selector to appear
page.evaluate(...)Executes JavaScript in the page
page.screenshot(...)Captures a screenshot via Playwright

Web screenshots

By default, web.screenshot() captures only the current viewport, following the same behavior as the Web nodes.

await web.screenshot("current-page");

To capture the full page:

await web.screenshot("full-page", { fullPage: true });

web.run(...)

web.run(...) is the recommended way to execute advanced code against the Web session. The callback receives:

FieldDescription
pageCurrent page
contextCurrent context
browserCurrent browser
sessionCurrent session
sessionIdSession ID
idInternal session ID
storageStrategyStorage strategy
persistedKeyPersistence key
urlHelper for current URL
titleHelper for current title
locatorHelper for locators
screenshotHelper for screenshots
expectPlaywright assertions
assertNode.js assertions

Web example: validate products and take a screenshot

return await web.run(async ({ page, expect }) => {
  await page.goto('https://automationexercise.com/products', {
    waitUntil: 'domcontentloaded'
  });

  const products = page.locator('.features_items .product-image-wrapper');
  await expect(products).toHaveCount(34);

  await web.screenshot('web-products');

  return {
    url: page.url(),
    title: await page.title(),
    productCount: await products.count()
  };
});

Web example: use the current session without navigating

return await web.run(async ({ page }) => {
  return {
    url: page.url(),
    title: await page.title()
  };
});

Web example: use the global page alias

await page.goto('https://example.com');
await web.screenshot('example-home');

return {
  title: await page.title(),
  url: page.url()
};

Mobile Automation with Appium

The mobile namespace exposes the current Mobile session and advanced utilities powered by Appium.

mobile namespace methods

MethodDescription
mobile.hasSession(sessionId?)Checks whether there is an active Mobile session
mobile.ids()Returns the IDs of available Mobile sessions
mobile.sessions()Returns all available Mobile sessions
mobile.current()Returns the current Mobile session
mobile.session(sessionId)Returns a specific Mobile session
mobile.app(sessionId?)Returns the main Mobile helper
mobile.client(sessionId?)Compatibility alias for mobile.app()
mobile.screenshot(name?, sessionId?)Captures a screenshot from the Mobile session
mobile.source(sessionId?)Returns the current screen XML/source
mobile.run(fn, sessionId?)Runs advanced code against the Mobile session

Object returned by mobile.current() and mobile.session()

Field / MethodDescription
idInternal session ID
sessionIdPublic session ID
appMain Mobile helper
clientCompatibility alias for app
capabilitiesSession capabilities
sessionRaw session object
screenshot(name?)Captures a screenshot from the session
source()Returns the screen source/XML
request(method, path, body?)Makes a direct call to the Appium session
executeScript(script, args?)Executes an Appium script

app and client

The main Mobile name is app.
client still exists for compatibility, but app is the recommended usage.

Methods exposed in app

MethodDescription
createSession(capabilities)Creates a new Appium session
deleteSession(sessionId)Ends a session
screenshot(sessionId)Captures a session screenshot
getPageSource(sessionId)Returns the screen XML/source
findElement(sessionId, using, value)Finds an element
findElements(sessionId, using, value)Finds multiple elements
elementClick(sessionId, elementId)Clicks an element
elementSendKeys(sessionId, elementId, text)Types into an element
sendKeys(sessionId, text)Sends text directly to the session
elementClear(sessionId, elementId)Clears a field
elementText(sessionId, elementId)Returns element text
elementAttribute(sessionId, elementId, name)Returns an element attribute
elementDisplayed(sessionId, elementId)Checks whether the element is visible
tapAt(sessionId, x, y)Taps at coordinates
doubleTapAt(sessionId, x, y)Performs a double tap at coordinates
swipe(sessionId, startX, startY, endX, endY, durationMs?)Performs a swipe
pressKeyCode(sessionId, keyCode)Presses an Android keycode
req(method, path, body?)Makes a raw Appium request
executeScript(sessionId, script, args?)Executes an Appium script
launchApp(sessionId)Launches the app again
resetApp(sessionId)Resets the app

Mobile screenshots

await mobile.screenshot('mobile-current-screen');

mobile.run(...)

mobile.run(...) is the recommended way to execute advanced code against the Mobile session. The callback receives:

FieldDescription
appMain Mobile helper
clientAlias for app
sessionIdSession ID
idInternal session ID
capabilitiesCurrent capabilities
sessionCurrent session
screenshotScreenshot helper
sourceSource helper
requestDirect request helper
executeScriptAppium script helper
assertNode.js assertions

Mobile example: tap by coordinates and take a screenshot

return await mobile.run(async ({ app, sessionId }) => {
  const source = await app.getPageSource(sessionId);

  await app.tapAt(sessionId, 200, 400);
  await mobile.screenshot('mobile-after-tap');

  return {
    sessionId,
    sourceLength: source.length,
    tapped: true
  };
});

Mobile example: click by accessibility id

return await mobile.run(async ({ app, sessionId }) => {
  const elementId = await app.findElement(sessionId, 'accessibility id', 'login-button');
  await app.elementClick(sessionId, elementId);

  await mobile.screenshot('login-button-clicked');

  return {
    clicked: true,
    elementId
  };
});

Mobile example: use the global app alias

const current = mobile.current();
const source = await app.getPageSource(current.sessionId);

return {
  sessionId: current.sessionId,
  sourceLength: source.length
};

Returning Values

The code must return a value using return. The returned value is available in the outputs:

// Return a simple value
return "Hello World";

// Return an object
return {
  fullName: steps["Query"].outputs.rows[0].first_name + " " + steps["Query"].outputs.rows[0].last_name,
  isAdmin: steps["Query"].outputs.rows[0].role === "admin"
};

// Return an array
return steps["Query"].outputs.rows.map(row => row.email);

Returning the result from web.run() and mobile.run()

If you want the value returned inside web.run(...) or mobile.run(...) to appear in outputs.result, the main script must also return that value:

const result = await web.run(async ({ page }) => {
  return { url: page.url() };
});

return result;

Or directly:

return await mobile.run(async ({ app, sessionId }) => {
  await app.tapAt(sessionId, 200, 400);
  return { tapped: true };
});

Outputs

OutputTypeDescription
resultanyValue returned by the code

Accessing the result

{{ steps["custom-js"].outputs.result }}
{{ steps["custom-js"].outputs.result.fullName }}
{{ steps["custom-js"].outputs.result[0] }}

Practical Examples

Transform API data

const items = steps["http-request"].outputs.json.items;

// Filter and transform
const activeItems = items
  .filter(item => item.active)
  .map(item => ({
    id: item.id,
    name: item.name.toUpperCase(),
    price: (item.price * 1.1).toFixed(2)
  }));

return {
  items: activeItems,
  count: activeItems.length,
  totalPrice: activeItems.reduce((sum, item) => sum + parseFloat(item.price), 0)
};

Generate test data

const timestamp = Date.now();
return {
  email: `test-${timestamp}@example.com`,
  username: `user_${timestamp}`,
  password: `Pass${timestamp}!`
};

Complex validation

const response = steps["API Check"].outputs.json;
const dbRecord = steps["DB Query"].outputs.rows[0];

const validations = [];

if (response.name !== dbRecord.name) {
  validations.push(`Name mismatch: API="${response.name}" DB="${dbRecord.name}"`);
}

if (response.email !== dbRecord.email) {
  validations.push(`Email mismatch: API="${response.email}" DB="${dbRecord.email}"`);
}

return {
  valid: validations.length === 0,
  errors: validations,
  errorCount: validations.length
};

Format a report

const runs = steps["Query Runs"].outputs.rows;

const summary = {
  total: runs.length,
  passed: runs.filter(r => r.status === 'success').length,
  failed: runs.filter(r => r.status === 'failed').length,
  passRate: 0
};

summary.passRate = ((summary.passed / summary.total) * 100).toFixed(1) + '%';

return summary;

Plain JavaScript

const now = new Date().toISOString();
const total = [10, 20, 30].reduce((sum, value) => sum + value, 0);

return {
  now,
  total,
  average: total / 3
};

Limitations

  • The code runs in a sandbox environment
  • There is no access to:
    • File system (fs)
    • Node.js modules via require / import
    • process.env directly
  • For environment variables, use variables
  • For Web and Mobile interactions, there must already be an active session
  • client is still supported, but app is the recommended Mobile name

fetch is available. Even so, for structured HTTP flows that are easier to maintain, prefer the HTTP Request node.


Tips

  • Use descriptive node names: "Transform Data", "Generate Credentials", "Validate Response", "Advanced Web Code", "Advanced Mobile Code"
  • Always return a value — without return, the output will be undefined
  • For debugging, use console.log() — messages appear in the node logs
  • Monaco Editor offers autocomplete — type steps., web., or mobile. to see available options
  • For advanced Web automation, prefer web.run(...)
  • For advanced Mobile automation, prefer mobile.run(...)
  • Use web.screenshot() and mobile.screenshot() when you need extra evidence
  • Use it to transform data between nodes and to solve advanced cases not covered by visual nodes