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
Add a Custom JavaScript action to a workflow
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.
Access and manipulate WeWeb variables from JavaScript
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.
1// Workflow Custom JavaScript action2// Access a variable passed in from the workflow context3const inputValue = variables['searchQuery']; // bound via workflow variable picker45// Perform computation6const 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(' ');910// Return the result for the next workflow step to use11return { formattedValue: formatted, wordCount: words.length };Expected result: Your JavaScript action runs, performs the computation, and the returned object is available to subsequent workflow actions.
Return values to subsequent workflow steps
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.
1// Async example — fetch external data and return it2async 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}2021return await fetchUserData();Expected result: The returned object from your JS action is accessible in subsequent workflow steps via the workflow context variable picker.
Use thisInstance for scoped DOM manipulation
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.
1// thisInstance — scoped DOM access (preferred over document.querySelector)2// Inject point: Workflow Custom JavaScript action34// Scroll to this component instance5thisInstance.scrollIntoView({ behavior: 'smooth', block: 'center' });67// Focus an input inside this component8const input = thisInstance.querySelector('input[type="text"]');9if (input) {10 input.focus();11 input.select(); // Select all text in the input12}1314// Get the dimensions of this component15const rect = thisInstance.getBoundingClientRect();16return {17 width: rect.width,18 height: rect.height,19 top: rect.top,20 left: rect.left21};2223// 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.
Inject global scripts via App Settings → Custom Code
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.
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>89<!-- 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.
Import npm packages using the NPM plugin
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.
1// Inject point: Workflow Custom JavaScript action2// Prerequisites: NPM plugin installed with dayjs as 'dayjs' and lodash as '_'34// Using dayjs for date manipulation5const orders = variables['ordersCollection'];67// Group orders by month using lodash8const byMonth = _.groupBy(orders, order => {9 return dayjs(order.created_at).format('YYYY-MM');10});1112// Calculate monthly totals13const monthlyTotals = Object.entries(byMonth).map(([month, monthOrders]) => ({14 month,15 total: _.sumBy(monthOrders, 'amount'),16 count: monthOrders.length,17 avgOrder: _.meanBy(monthOrders, 'amount')18}));1920// Sort by month descending21const sorted = _.orderBy(monthlyTotals, ['month'], ['desc']);2223return { monthlyTotals: sorted };Expected result: The npm package is globally available in all workflow JS actions and you can call its functions directly.
Know when to use formula expressions vs Custom JavaScript
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
1// ============================================================2// WeWeb Custom JavaScript — Common Workflow Patterns3// Inject point: Workflow → Add action → Custom JavaScript4// ============================================================56// --- Pattern 1: Data transformation with return value ---7// Transform raw API data into a format suitable for a WeWeb collection8const rawData = variables['apiResponse'];910const 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}));2021return { items: transformed, count: transformed.length };2223// --- Pattern 2: thisInstance DOM interaction ---24// Scroll to and highlight an element within the current component25// Use when: user submits a form with errors and you want to scroll to first error26const 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 };3334// --- Pattern 3: Async fetch with error handling ---35// Fetch from an endpoint and return structured result36async function safeFetch(url, options = {}) {37 try {38 const response = await fetch(url, {39 headers: {40 'Content-Type': 'application/json',41 ...options.headers42 },43 ...options44 });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}5657const result = await safeFetch('https://api.example.com/data', {58 headers: { 'Authorization': 'Bearer ' + variables['token'] }59});6061return result;6263// --- Pattern 4: Local storage read/write ---64// Persist user preferences between sessions65const PREF_KEY = 'weweb_user_prefs';6667// Read preferences68function getPrefs() {69 try {70 return JSON.parse(localStorage.getItem(PREF_KEY) || '{}');71 } catch { return {}; }72}7374// Write preferences75function savePrefs(newPrefs) {76 const current = getPrefs();77 localStorage.setItem(PREF_KEY, JSON.stringify({ ...current, ...newPrefs }));78}7980const 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.
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?
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation