Skip to main content
RapidDev - Software Development Agency
weweb-tutorial

How to Use Custom JavaScript in WeWeb: Workflows, Scripts, and NPM

WeWeb supports custom JavaScript in three places: workflow Custom JavaScript actions (for logic and DOM manipulation), App Settings → Custom Code → Head or Body (for global scripts and analytics), and the NPM plugin (for importing npm packages). Use the thisInstance API inside workflow JS actions for scoped DOM access. Return values from JS actions to pass data to subsequent workflow steps. Formula expressions are for bindings — JS actions are for imperative logic.

What you'll learn

  • How to add a Custom JavaScript action inside a WeWeb workflow and return values to subsequent steps
  • The difference between thisInstance (scoped DOM) and document.querySelector (global DOM) in WeWeb workflows
  • How to inject global scripts (analytics, tracking pixels, third-party libraries) via App Settings → Custom Code
  • How to import npm packages into your WeWeb project using the NPM plugin
  • When to use formula expressions vs Custom JavaScript actions for data manipulation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced13 min read45-60 minWeWeb Essential plan and above (NPM plugin requires paid plan); workflow JS available on all plansMarch 2026RapidDev Engineering Team
TL;DR

WeWeb supports custom JavaScript in three places: workflow Custom JavaScript actions (for logic and DOM manipulation), App Settings → Custom Code → Head or Body (for global scripts and analytics), and the NPM plugin (for importing npm packages). Use the thisInstance API inside workflow JS actions for scoped DOM access. Return values from JS actions to pass data to subsequent workflow steps. Formula expressions are for bindings — JS actions are for imperative logic.

Custom JavaScript in WeWeb: Workflows, Global Scripts, and NPM Packages

WeWeb is built on Vue.js 3, and while its visual workflow system covers most use cases, there are scenarios requiring raw JavaScript: complex data transformations, DOM manipulation, third-party library integration, or business logic too complex for formula expressions. WeWeb provides three injection points for custom JavaScript, each with a distinct purpose. Workflow-level JS actions run in response to user interactions and can read/write WeWeb variables and return values to the next workflow step. Project-level head/body injection runs at page load and is ideal for analytics tags or CDN-hosted libraries. The NPM plugin lets you import any npm package directly into your project. This tutorial covers all three patterns with working code examples.

Prerequisites

  • A WeWeb project with at least one workflow configured (so you can add JS actions to it)
  • Basic JavaScript knowledge (variables, functions, return statements, promises)
  • Understanding of WeWeb workflows — triggers and action sequences
  • WeWeb account on any plan (workflow JS is free; NPM plugin may require paid plan)

Step-by-step guide

1

Add a Custom JavaScript action to a workflow

Custom JavaScript actions live inside WeWeb workflows. To add one: select an element in the editor (e.g., a button) → click the Workflows tab in the right panel → click the trigger you want (e.g., On click) or add a new trigger → click the + icon to add an action → scroll through the action list and select Custom JavaScript. A code editor opens inline. You write standard JavaScript here — no async/await needed for simple operations, but async is fully supported. The action runs when the workflow trigger fires. Each Custom JavaScript action is a discrete step in the workflow sequence — it runs after all previous actions complete and before subsequent ones begin.

Expected result: A Custom JavaScript action appears in your workflow sequence with an inline code editor.

2

Access and manipulate WeWeb variables from JavaScript

Inside a Custom JavaScript action, you can read WeWeb variables using the formula syntax. WeWeb injects context variables automatically. To access a page variable named 'searchQuery' inside a JS action, reference it directly as a workflow variable by binding it: click the Variables icon in the code editor toolbar to insert a variable reference. Alternatively, you can access the window object for truly global state. To update a variable from within JavaScript, use the 'Change variable value' action that follows your JS action — return the new value from your JS action (see next step), then use that returned value in the Change variable step. The preferred pattern is JS action computes → returns result → Change variable action stores it.

typescript
1// Workflow Custom JavaScript action
2// Access a variable passed in from the workflow context
3const inputValue = variables['searchQuery']; // bound via workflow variable picker
4
5// Perform computation
6const cleaned = inputValue.trim().toLowerCase();
7const words = cleaned.split(' ').filter(w => w.length > 0);
8const formatted = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
9
10// Return the result for the next workflow step to use
11return { formattedValue: formatted, wordCount: words.length };

Expected result: Your JavaScript action runs, performs the computation, and the returned object is available to subsequent workflow actions.

3

Return values to subsequent workflow steps

The return value from a Custom JavaScript action is one of WeWeb's most powerful features — it allows you to pass computed data to later actions in the same workflow. Return any JavaScript value: a string, number, object, or array. After your JS action, add a 'Change variable value' action. In the value field of the Change variable action, click the plug icon (🔌) to open the formula editor. Select 'Workflow' context → your JS action's return value → the specific key you returned. For example, if your JS action returned { formattedValue: 'Hello' }, you would access it as workflowResults['YourJSActionName'].formattedValue. This pattern keeps your WeWeb variables as the single source of truth while using JS for computation.

typescript
1// Async example — fetch external data and return it
2async function fetchUserData() {
3 const response = await fetch('https://api.example.com/user/123', {
4 headers: { 'Authorization': 'Bearer ' + variables['authToken'] }
5 });
6
7 if (!response.ok) {
8 throw new Error(`HTTP error: ${response.status}`);
9 }
10
11 const data = await response.json();
12
13 return {
14 userId: data.id,
15 displayName: data.first_name + ' ' + data.last_name,
16 email: data.email,
17 fetchedAt: new Date().toISOString()
18 };
19}
20
21return await fetchUserData();

Expected result: The returned object from your JS action is accessible in subsequent workflow steps via the workflow context variable picker.

4

Use thisInstance for scoped DOM manipulation

When you need to manipulate the DOM from a workflow JS action (e.g., focus an input, scroll to an element, measure dimensions), WeWeb provides the thisInstance API. thisInstance refers to the root DOM element of the component that contains the workflow — scoped to that specific component instance. This is preferred over document.querySelector() for two reasons: it is scoped (will not accidentally match elements in other component instances when the component is repeated), and it is more performant. Use thisInstance.querySelector() to find child elements within the component. For actions like scrollIntoView, getBoundingClientRect, or direct style manipulation, thisInstance is the correct entry point.

typescript
1// thisInstance — scoped DOM access (preferred over document.querySelector)
2// Inject point: Workflow Custom JavaScript action
3
4// Scroll to this component instance
5thisInstance.scrollIntoView({ behavior: 'smooth', block: 'center' });
6
7// Focus an input inside this component
8const input = thisInstance.querySelector('input[type="text"]');
9if (input) {
10 input.focus();
11 input.select(); // Select all text in the input
12}
13
14// Get the dimensions of this component
15const rect = thisInstance.getBoundingClientRect();
16return {
17 width: rect.width,
18 height: rect.height,
19 top: rect.top,
20 left: rect.left
21};
22
23// Apply a temporary highlight (use CSS classes instead for persistent styles)
24thisInstance.style.outline = '2px solid #3b82f6';
25setTimeout(() => { thisInstance.style.outline = ''; }, 1500);

Expected result: DOM operations target only the specific component instance running the workflow, not all instances of that component on the page.

5

Inject global scripts via App Settings → Custom Code

For scripts that need to run once on every page load — analytics tags, tracking pixels, third-party chat widgets, or utility libraries — use the project-level code injection. In the editor, click the gear icon in the left navigation bar to open App Settings. Click Custom Code. You will see two text areas: Head (injected before the closing </head> tag) and Body (injected before the closing </body> tag). Head is for critical scripts, stylesheets, and preload hints. Body is for non-critical scripts that should load after the page renders. Paste your script tags here. Important: do NOT include opening or closing <html>, <head>, or <body> tags — only paste the script/style tags themselves. Note that custom code does NOT render in the editor preview — you must publish to see injected scripts active.

typescript
1<!-- Inject point: App Settings Custom Code Head section -->
2<!-- Google Tag Manager -->
3<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
4new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
5j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
6'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
7})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
8
9<!-- Intercom chat widget -->
10<script>
11 window.intercomSettings = {
12 api_base: "https://api-iam.intercom.io",
13 app_id: "YOUR_APP_ID"
14 };
15</script>
16<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/YOUR_APP_ID';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(document.readyState==='complete'){l();}else if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>

Expected result: After publishing, the scripts you added in Custom Code Head are present in the page HTML and third-party services initialize on page load.

6

Import npm packages using the NPM plugin

WeWeb's NPM plugin lets you import any npm package into your project, making it available as a global variable you can use in workflow JS actions. To install: navigate to Plugins in the left navigation bar → click Extensions → find NPM packages → click Install. In the NPM plugin settings, add the package you need: enter the package name (e.g., 'lodash', 'dayjs', 'uuid') and the variable name it should be exposed as globally (e.g., '_', 'dayjs', 'uuid'). WeWeb loads the package from a CDN and makes it available globally. In your workflow JS actions, use the global variable name you specified. For example, with lodash installed as '_', you can call _.groupBy(), _.sortBy(), _.cloneDeep() etc. directly in any JS action.

typescript
1// Inject point: Workflow Custom JavaScript action
2// Prerequisites: NPM plugin installed with dayjs as 'dayjs' and lodash as '_'
3
4// Using dayjs for date manipulation
5const orders = variables['ordersCollection'];
6
7// Group orders by month using lodash
8const byMonth = _.groupBy(orders, order => {
9 return dayjs(order.created_at).format('YYYY-MM');
10});
11
12// Calculate monthly totals
13const monthlyTotals = Object.entries(byMonth).map(([month, monthOrders]) => ({
14 month,
15 total: _.sumBy(monthOrders, 'amount'),
16 count: monthOrders.length,
17 avgOrder: _.meanBy(monthOrders, 'amount')
18}));
19
20// Sort by month descending
21const sorted = _.orderBy(monthlyTotals, ['month'], ['desc']);
22
23return { monthlyTotals: sorted };

Expected result: The npm package is globally available in all workflow JS actions and you can call its functions directly.

7

Know when to use formula expressions vs Custom JavaScript

WeWeb provides two ways to compute dynamic values: formula expressions (the binding formula editor, accessible via the plug icon on any property) and Custom JavaScript workflow actions. Choose formula expressions for: binding element properties to dynamic data, filtering collections, conditional visibility logic, text formatting, simple math, and any reactive binding that should update automatically when source data changes. Choose Custom JavaScript actions for: imperative logic that runs in response to a specific event, complex multi-step computations that are too long for a single formula, DOM manipulation, async operations (API calls, file processing), operations that should run only once when triggered (not reactively), and anything requiring error handling with try/catch. Mixing both is normal — formulas for display bindings, JS actions for business logic.

Expected result: You can identify which computation belongs in a formula expression vs a workflow JS action based on whether it is reactive or event-driven.

Complete working example

workflow-javascript-patterns.js
1// ============================================================
2// WeWeb Custom JavaScript — Common Workflow Patterns
3// Inject point: Workflow → Add action → Custom JavaScript
4// ============================================================
5
6// --- Pattern 1: Data transformation with return value ---
7// Transform raw API data into a format suitable for a WeWeb collection
8const rawData = variables['apiResponse'];
9
10const transformed = rawData.map(item => ({
11 id: item.uuid,
12 label: [item.first_name, item.last_name].filter(Boolean).join(' '),
13 email: item.email_address?.toLowerCase().trim(),
14 createdDate: new Date(item.created_at).toLocaleDateString('en-US', {
15 year: 'numeric', month: 'short', day: 'numeric'
16 }),
17 isActive: item.status === 'active' || item.status === 'verified',
18 tags: Array.isArray(item.tags) ? item.tags : []
19}));
20
21return { items: transformed, count: transformed.length };
22
23// --- Pattern 2: thisInstance DOM interaction ---
24// Scroll to and highlight an element within the current component
25// Use when: user submits a form with errors and you want to scroll to first error
26const firstError = thisInstance.querySelector('[data-error="true"]');
27if (firstError) {
28 firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
29 firstError.classList.add('highlight-error');
30 setTimeout(() => firstError.classList.remove('highlight-error'), 2000);
31}
32return { scrolledToError: !!firstError };
33
34// --- Pattern 3: Async fetch with error handling ---
35// Fetch from an endpoint and return structured result
36async function safeFetch(url, options = {}) {
37 try {
38 const response = await fetch(url, {
39 headers: {
40 'Content-Type': 'application/json',
41 ...options.headers
42 },
43 ...options
44 });
45
46 if (!response.ok) {
47 return { success: false, error: `HTTP ${response.status}: ${response.statusText}`, data: null };
48 }
49
50 const data = await response.json();
51 return { success: true, error: null, data };
52 } catch (err) {
53 return { success: false, error: err.message, data: null };
54 }
55}
56
57const result = await safeFetch('https://api.example.com/data', {
58 headers: { 'Authorization': 'Bearer ' + variables['token'] }
59});
60
61return result;
62
63// --- Pattern 4: Local storage read/write ---
64// Persist user preferences between sessions
65const PREF_KEY = 'weweb_user_prefs';
66
67// Read preferences
68function getPrefs() {
69 try {
70 return JSON.parse(localStorage.getItem(PREF_KEY) || '{}');
71 } catch { return {}; }
72}
73
74// Write preferences
75function savePrefs(newPrefs) {
76 const current = getPrefs();
77 localStorage.setItem(PREF_KEY, JSON.stringify({ ...current, ...newPrefs }));
78}
79
80const action = variables['prefAction']; // 'read' or 'write'
81if (action === 'write') {
82 savePrefs({ theme: variables['selectedTheme'], language: variables['selectedLang'] });
83 return { saved: true };
84} else {
85 return { prefs: getPrefs() };
86}

Common mistakes

Why it's a problem: Using document.querySelector() instead of thisInstance.querySelector() in component workflows

How to avoid: document.querySelector() is global and will match the first instance of a selector on the entire page — wrong behavior when your component appears multiple times (e.g., in a repeated list). Use thisInstance.querySelector() to scope DOM queries to the current component instance.

Why it's a problem: Trying to directly set a WeWeb variable from inside a JS action using assignment

How to avoid: You cannot directly write to WeWeb variables from within a JS action using JavaScript assignment. Instead, return the computed value from your JS action, then add a 'Change variable value' workflow action immediately after that reads the return value via the workflow context variable picker.

Why it's a problem: Adding analytics scripts to App Settings Custom Code and expecting them to appear in the editor preview

How to avoid: Custom Code injection does NOT render in the WeWeb editor preview. You must publish your project and view the live published version to see injected scripts active. Use browser DevTools → Sources or Network tab on the published URL to verify scripts are loading.

Why it's a problem: Using synchronous code for operations that return Promises without awaiting them

How to avoid: If you call async functions (fetch, setTimeout wrapped in promises, npm async packages) without await, WeWeb's workflow engine moves to the next action before the promise resolves. Always use async/await syntax: declare the outer function as async, use await on all async calls, and return await the final result.

Best practices

  • Return structured objects from JS actions (not raw primitives) — objects are self-documenting and you can return multiple values at once
  • Use thisInstance over document.querySelector in all component-level workflow JS to avoid cross-instance selector collisions
  • Keep JS actions focused on a single concern — split complex logic into multiple sequential JS actions rather than one long script
  • Add error handling (try/catch) to async JS actions and return a success/error flag so subsequent workflow steps can branch on the result
  • Inject third-party analytics and tracking scripts in App Settings → Custom Code → Head, not in workflow actions — head injection loads once, workflow actions run on every trigger
  • Prefer formula expressions for reactive bindings (display values, computed properties) and reserve JS actions for event-driven logic that runs once
  • Comment your JS actions thoroughly — future-you and team members will thank you when debugging a complex workflow six months later

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am using WeWeb and want to write a custom JavaScript workflow action that fetches data from an external API, transforms it, and saves it to a WeWeb variable. How do I return values from a Custom JavaScript action in WeWeb so the next workflow step can use them? Can you show me the correct async/await pattern?

WeWeb Prompt

In my WeWeb project I have a repeating list of cards, each with a 'Copy to clipboard' button. I added a Custom JavaScript workflow action to the button's On click trigger using document.querySelector to find the card's text, but it always copies the same card's text. How do I use thisInstance to scope the DOM query to only the card that was clicked?

Frequently asked questions

Can I use ES6+ syntax (arrow functions, destructuring, template literals) in WeWeb workflow JS actions?

Yes. WeWeb runs in modern browsers and supports all ES6+ JavaScript syntax including arrow functions, destructuring, template literals, spread operators, optional chaining (?.), nullish coalescing (??), and async/await. You do not need to transpile or use Babel.

Can I import a JavaScript module (using import statements) inside a workflow JS action?

No. Import statements are not supported inside workflow JS actions because they run in a browser script context, not a module context. Instead, use the NPM plugin to load packages as global variables, or use dynamic import() syntax (which returns a promise and must be awaited) for CDN-hosted modules.

How do I debug errors in my Custom JavaScript workflow actions?

Use console.log() inside your JS actions — output appears in the browser DevTools console (F12). WeWeb also has a workflow error log: open the workflow editor and look for red indicators on failed actions. Add try/catch blocks to catch and return error details. For async issues, log intermediate values before and after await calls.

Is there a size or performance limit for Custom JavaScript actions in WeWeb?

WeWeb does not enforce a hard code size limit on JS actions, but very large scripts slow down the editor and should be avoided. For large libraries, use the NPM plugin or CDN head injection rather than pasting library code directly into a JS action. For computationally heavy operations that run frequently, consider moving the logic to a Supabase Edge Function or Xano endpoint and calling it via an API request instead.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.