Submit Article Requests

Do you have a suggestion for an article you would like to see created?
Feel free to submit this form and add your suggestions to our document board.

Please fill out the contact form below and we will reply as soon as possible.

  • Integration Hub
  • Contact Us
  • Docs home
  • Web Experiences
  • Customization & Styling

Add JavaScript snippets

Run your own JavaScript inside a Flow 2.0 or Embed to render content or fire an action on click.

Updated at August 4th, 2026

Submit Article Requests

Do you have a suggestion for an article you would like to see created?
Feel free to submit this form and add your suggestions to our document board.

Please fill out the contact form with the details about the help content you'd like to see.

  • Home

  • Getting Started

    • Appcues App

      • Installation & Developers

        • Web Experiences

          • Mobile Experiences

            • Workflows

              • Analytics & Data

                • Account Management

                  • Integrations

                    Table of Contents

                    Snippet types Prerequisites Create a snippet from a template Content block templates Click action templates Write a snippet from scratch Define custom variables Insert dynamic variables Add a JavaScript block to a step Run a snippet when someone clicks Test a snippet Templates Display data from my API Embed a Calendly scheduler Mount a third-party widget Call a function on your page Open your support chat Prepare the page for the next tooltip step Confirm it worked Limits and notes If a snippet doesn't run Still stuck?

                    A JavaScript snippet is a piece of custom code you write once and reuse across experiences. Use one to render something a standard block can't, such as a scheduler, live data from your API, or a third-party widget. You can also run your own code when someone clicks a button, an image, or a text link.

                    JavaScript snippets are in beta and available on request. Email support@appcues.com to have them turned on.

                    Snippet types

                    • Content block — renders into a cell in your step. Add it like any other block.
                    • Click action — runs when a user clicks a button, an image, or an inline text link. In an Embed, the step itself can be clickable too.

                    Prerequisites

                    • A Flows 2.0 experience or an Embed. Snippets aren't available in classic Flows, NPS surveys, Banners, Launchpads, or mobile experiences.
                    • Admin permissions to create, edit, or delete a snippet. Anyone can select an existing snippet and apply it to an experience.

                    Create a snippet from a template

                    Templates are working snippets with their code, variables, and dependencies already filled in. Most of the time you only need to supply one or two values, like a scheduling link or an API URL.

                    1. Click Add a new Snippet from the Content block or Click action menus. The template picker opens.
                    2. Choose a template. The picker shows only templates that match what you're adding: content-block templates when you start from a Javascript block, click-action templates when you start from a click action.
                    1. Edit the prefilled name, code, and variables as needed.
                    2. Click Save.

                    Content block templates

                    Template What it does
                    Display data from my API Fetches JSON from an endpoint and renders it into the block
                    Embed a Calendly scheduler Puts a Calendly booking calendar in the block
                    Mount a third-party widget A scaffold for any vendor that gives you a script tag to paste

                    Click action templates

                    Template What it does
                    Open Calendly in a popup Opens your Calendly scheduler over the page
                    Open your support chat Opens whichever chat widget your site runs (Intercom, Zendesk, Drift, or HubSpot)
                    Open the Intercom messenger Opens Intercom, optionally with a prefilled message
                    Call a function on your page Runs one of your app's own JavaScript functions
                    Send an event to my analytics tool Sends the click to Google Analytics, Segment, or Amplitude
                    Prepare the page for the next step Clicks something on the page and waits for an element to appear, so the next step has an anchor

                    Write a snippet from scratch

                    1. Click Add a new Snippet, then Start from scratch.
                    2. Enter a Name. 
                    3. Write your code in the Code editor. It highlights syntax, shows line numbers, autocompletes config. and context., and underlines errors as you type.
                    4. Add any external scripts your code needs to the External dependencies field, separated by commas. Appcues loads them before your snippet runs.
                    5. Click Save & Publish. The button stays disabled until the name is filled in and the code has no errors.

                    Define custom variables

                    Custom variables let one snippet serve several experiences. You write the code once, and whoever adds the snippet to an experience supplies the values for that instance.

                    1. In the snippet editor, click + Add variable.
                    2. Enter a Key. This is what your code references as config.<key>.
                    3. Enter a Label. This is what users see in the settings panel when applying this snippet to an expereince.
                    4. Choose a Type: Text, Number, or True / False.
                    5. Set the remaining fields as needed:
                    • Required — the experience won't render the snippet until this has a value. True/False variables are always required, since they always hold a value.
                    • Default value — used when nobody enters anything.
                    • Placeholder — greyed-out example text in the input. Text variables only.
                    • Description — help text shown under the input. Use it to explain what a good value looks like.
                    1. Click Save.

                    Renaming a key doesn't update the references in your code. The editor warns you and tells you how many references it found, but you have to update them yourself.

                    Insert dynamic variables

                    Snippets can dynamically insert user profile information (eg: first_name) or information about the experience (eg context.element if the third party script needs to know the DOM element that the content will appear). 

                    Reference What it gives you
                    context.element The DOM element the snippet is attached to. Content blocks only.
                    context.user The current user's properties, for example context.user.firstName.
                    context.group The current user's group or account properties, for example context.group.name.
                    context.experience The running experience. context.experience.id is its ID.
                    context.step The current step. context.step.id is its ID.
                    context.querySelector() Finds elements inside the experience.
                    context.sendEvent() Tracks a custom Appcues event, for example context.sendEvent("cta_clicked").
                    context.loadScript() Loads an external script by URL and returns a Promise. Skips scripts already on the page.
                    context.reportError() Reports an error from your snippet back to Appcues.

                    When editing the snippet, type {{ to open a shortcut menu for all available:

                    Hover any context reference in the editor to see the same documentation inline.

                    Add a JavaScript block to a step

                    1. Click + in an empty cell. The Content Blocks picker opens.
                    1. Click Javascript. The block is added to the cell and its settings open.
                    1. On the Content tab, choose a snippet from the Snippet dropdown. If your account has no snippets yet, the dropdown is hidden. Create one first.
                    2. Fill in the snippet's variables. Required variables are marked with a red asterisk.

                    If a required variable is empty, the block won't render when the experience is published. The builder shows a Required variables missing warning until you fill it in.

                    The block shows a placeholder on the canvas with the snippet's name. Your code will not be executed until it's published to your site. To test, publish to internal users.

                    Run a snippet when someone clicks

                    1. Select your button, image, or text block. In an Embed, you can also use the whole step through Step Settings.
                    2. On a Button block, turn on the Trigger Javascript snippet toggle on the Content tab.  On an Image block, first turn on Action on click and then Trigger Javascript snippet. On a Text block, first highlight some text, turn on Action on click and then Trigger Javascript snippet. On an embed step, turn on Action on click and then Trigger Javascript snippet on the Step Settings > Settings tab.
                    3. Choose a snippet from the Snippet dropdown and fill in its variables.

                    Test a snippet

                    Snippets don't run in preview. In the builder and in preview, a content block shows a placeholder and a click action shows a message naming the snippet. This is why the canvas badge reads Publish to Test.

                    To test a snippet for real, publish the experience and open your app. Publishing a snippet and publishing an experience are separate: Save & Publish in the editor makes the snippet available to your account, and the experience itself still has to be published for anyone to see it.

                    Templates

                    Display data from my API

                    (async function () {
                     try {
                       if (!context.element) {
                         return;
                       }
                       context.element.textContent = config.loadingText || 'Loading…';
                       const response = await fetch(config.endpointUrl, {
                         headers: { Accept: 'application/json' },
                       });
                       if (!response.ok) {
                         throw new Error(`Request failed with status ${response.status}`);
                       }
                       const data = await response.json();
                       // Built as DOM nodes rather than an innerHTML string: the response is data,
                       // not trusted markup, and textContent can't smuggle in a <script>.
                       const card = document.createElement('div');
                       card.style.display = 'flex';
                       card.style.gap = '12px';
                       card.style.alignItems = 'flex-start';
                       if (data.thumbnail) {
                         const image = document.createElement('img');
                         image.src = data.thumbnail;
                         image.alt = '';
                         image.width = 72;
                         image.height = 72;
                         image.style.flexShrink = '0';
                         image.style.borderRadius = '6px';
                         image.style.objectFit = 'cover';
                         card.append(image);
                       }
                       const copy = document.createElement('div');
                       // Each line is added only if the field came back, so pointing this at your
                       // own API degrades to whatever it does return instead of printing
                       // "undefined". Swap these for your own fields.
                       // Longhand, not the `font` shorthand: the shorthand needs a font-family,
                       // and naming one here would override the experience's own type.
                       const addLine = function (text, options) {
                         if (text === undefined || text === null || text === '') {
                           return;
                         }
                         const line = document.createElement('p');
                         line.style.margin = '0 0 4px';
                         line.style.fontSize = options.size;
                         line.style.lineHeight = '1.35';
                         if (options.bold) {
                           line.style.fontWeight = 'bold';
                         }
                         line.textContent = String(text);
                         copy.append(line);
                       };
                       addLine(data.title, { size: '15px', bold: true });
                       addLine(
                         [data.brand, data.rating ? `★ ${data.rating}` : null]
                           .filter(Boolean)
                           .join(' · '),
                         { size: '13px' }
                       );
                       addLine(data.price ? `$${data.price}` : null, {
                         size: '15px',
                         bold: true,
                       });
                       addLine(data.description, { size: '13px' });
                       card.append(copy);
                       context.element.replaceChildren(card);
                     } catch (error) {
                       // Leave the block empty rather than stuck on the loading message.
                       if (context.element) {
                         context.element.textContent = '';
                       }
                       context.reportError(`Could not load data: ${String(error)}`);
                     }
                    })();
                     
                     

                    Embed a Calendly scheduler

                    Acco(function () {
                     try {
                       if (!context.element) {
                         return;
                       }
                       // Calendly's iframe is width="100%" height="100%", so the container needs a
                       // real `height` — a percentage resolves against `auto` and the iframe falls
                       // back to its intrinsic ~150px and scrolls its own content. `min-height` is
                       // NOT enough. This mirrors Calendly's own markup: min-width:320px;height:Npx.
                       context.element.style.height = `${config.height}px`;
                       context.element.style.minWidth = '320px';
                       // widget.js is declared under External dependencies, so it has already
                       // loaded by the time this runs — there is nothing to await.
                       if (!window.Calendly) {
                         context.reportError('Calendly did not load.');
                         return;
                       }
                       // Pasted scheduling links carry whatever the author copied — a `?month=`
                       // prefill, a tracking parameter, sometimes a `#` fragment. Appending the
                       // flag as text gets both wrong: a second `?` invalidates the URL, and
                       // anything after a `#` is fragment, which never reaches Calendly. `URL`
                       // puts it in the query regardless of what else is there.
                       const url = new URL(config.calendlyUrl);
                       if (config.hideEventTypeDetails) {
                         url.searchParams.set('hide_event_type_details', '1');
                       }
                       window.Calendly.initInlineWidget({
                         url: url.toString(),
                         // `parentElement` takes the node directly, so the widget lands inside the
                         // block without a document-scoped selector lookup.
                         parentElement: context.element,
                         // Calendly only self-styles when asked; its default is `inlineStyles:
                         // false`, which assumes you loaded widget.css. We can't declare a
                         // stylesheet (External dependencies is JavaScript-only), so let the
                         // widget position itself and handle mobile.
                         inlineStyles: true,
                       });
                       context.sendEvent('snippet_calendly_shown', { stepId: context.step.id });
                     } catch (error) {
                       context.reportError(`Calendly snippet failed: ${String(error)}`);
                     }
                    })();rdion Body
                     
                     

                    Mount a third-party widget

                    // 1. Paste the vendor's script URL into "External dependencies" below the code.
                    //    It loads before this runs. Don't take the URL from a custom variable —
                    //    anyone who uses this snippet in a flow can edit those.
                    // 2. Replace `VendorSDK` and the mount call with the vendor's own.
                    (function () {
                     try {
                       if (!context.element) {
                         return;
                       }
                       if (!window.VendorSDK) {
                         context.reportError('The vendor script did not load.');
                         return;
                       }
                       // Prefer an option that takes a DOM node — pass `context.element` — over one
                       // that takes a CSS selector string.
                       window.VendorSDK.render({
                         container: context.element,
                         id: config.widgetId,
                       });
                       context.sendEvent('snippet_vendor_mounted', { stepId: context.step.id });
                     } catch (error) {
                       context.reportError(`Could not mount the widget: ${String(error)}`);
                     }
                    })();
                     
                     

                    Call a function on your page

                    (function () {
                     try {
                       // ─── Edit these two lines ────────────────────────────────────────
                       // Point them at the function you want to call. `isAvailable` should
                       // check the same thing `call` is about to use, so a page that hasn't
                       // finished loading reports a clear error instead of throwing.
                       const isAvailable = () =>
                         typeof window.myApp?.support?.openPanel === 'function';
                       const call = argument => window.myApp.support.openPanel(argument);
                       // ─────────────────────────────────────────────────────────────────
                       if (!isAvailable()) {
                         context.reportError('That function is not available on this page.');
                         return;
                       }
                       // Empty text fields arrive as '', which is a real argument — send
                       // undefined instead so the function sees no argument at all.
                       call(config.argument || undefined);
                       context.sendEvent('snippet_function_called', { stepId: context.step.id });
                     } catch (error) {
                       context.reportError(`Could not make the call: ${String(error)}`);
                     }
                    })();
                     
                     

                    Open your support chat

                    (function () {
                     try {
                       // Add a provider by adding a row. `isPresent` should be cheap and must not
                       // throw; `open` runs only after `isPresent` returns true.
                       const providers = [
                         {
                           name: 'intercom',
                           isPresent: () => typeof window.Intercom === 'function',
                           open: () => window.Intercom('show'),
                         },
                         {
                           name: 'zendesk',
                           isPresent: () => typeof window.zE === 'function',
                           open: () => {
                             // Zendesk ships two widgets under the same `zE` global. The modern
                             // messenger takes 'messenger'; the older classic widget takes
                             // 'webWidget' and throws on the first. Trying rather than sniffing a
                             // version flag keeps this working across both.
                             try {
                               window.zE('messenger', 'open');
                             } catch {
                               window.zE('webWidget', 'open');
                             }
                           },
                         },
                         {
                           // Unverified: Drift's developer docs went offline after the Salesloft
                           // acquisition. `openChat` is the long-standing documented call, and
                           // `isPresent` checks for it specifically, so a rename fails over to the
                           // next provider rather than throwing. Test against a live Drift page.
                           name: 'drift',
                           isPresent: () => typeof window.drift?.api?.openChat === 'function',
                           open: () => window.drift.api.openChat(),
                         },
                         {
                           name: 'hubspot',
                           isPresent: () =>
                             typeof window.HubSpotConversations?.widget?.open === 'function',
                           open: () => window.HubSpotConversations.widget.open(),
                         },
                       ];
                       // First match wins, so a page running two widgets opens the one listed
                       // highest. Reorder the rows above to change that.
                       const provider = providers.find(candidate => candidate.isPresent());
                       if (!provider) {
                         context.reportError('No support chat widget was found on this page.');
                         return;
                       }
                       provider.open();
                       // `provider` rides along so you can see which tool people actually used.
                       context.sendEvent('snippet_support_chat_opened', {
                         provider: provider.name,
                         stepId: context.step.id,
                       });
                     } catch (error) {
                       context.reportError(`Could not open support chat: ${String(error)}`);
                     }
                    })();
                     
                     

                    Prepare the page for the next tooltip step

                    (async function () {
                     try {
                       // `context.querySelector` returns an array, not a node.
                       const trigger = context.querySelector(config.clickSelector)[0];
                       if (!trigger) {
                         context.reportError(`Could not find ${config.clickSelector} to click.`);
                         return;
                       }
                       trigger.click();
                       if (!config.waitForSelector) {
                         return;
                       }
                       // The manifest's 5000 seeds the form field, but an optional number the
                       // author clears is saved as nothing — and `setTimeout(fn, undefined)` fires
                       // on the next tick, so the wait would collapse to zero and report a timeout
                       // that never happened.
                       const timeoutMs = config.timeoutMs ?? 5000;
                       await new Promise(function (resolve) {
                         const found = function () {
                           return context.querySelector(config.waitForSelector)[0];
                         };
                         if (found()) {
                           resolve();
                           return;
                         }
                         const observer = new MutationObserver(function () {
                           if (found()) {
                             finish();
                           }
                         });
                         const timer = setTimeout(function () {
                           context.reportError(
                             `${config.waitForSelector} did not appear within ${timeoutMs}ms.`
                           );
                           finish();
                         }, timeoutMs);
                         // Declared last so both the observer and the timer can reach it (function
                         // declarations hoist). Every path routes through here, so nothing —
                         // observer or timer — survives the step.
                         function finish() {
                           clearTimeout(timer);
                           observer.disconnect();
                           resolve();
                         }
                         observer.observe(document.body, { childList: true, subtree: true });
                       });
                     } catch (error) {
                       context.reportError(`Could not prepare the page: ${String(error)}`);
                     }
                    })();
                     
                     

                    Confirm it worked

                    • On your app, the block renders your content instead of the placeholder, or the click runs your code.
                    • Any event you send with context.sendEvent() appears in your Appcues events.

                    Limits and notes

                    • A snippet's code is capped at 5KB.
                    • Your published snippet code is served as a public JavaScript file. Don't put API keys, tokens, or other secrets in it.
                    • Only properties you reference by name are delivered. context.user.plan works; context.user[myKey] returns nothing.
                    • Snippets work in Flows 2.0 and Embeds. Other experiences will be supported soon.
                    • A snippet error is caught and reported to Appcues rather than breaking the step, so a failing snippet shows nothing rather than an error.

                    If a snippet doesn't run

                    Check that the snippet is published. Open it from the Snippets list and click Save & Publish again.

                    Check for empty required variables. Open the block or action settings. A Required variables missing warning means the snippet won't render.

                    Check your external dependencies. Open your browser console and look for a failed script request. A script that's blocked by CORS or a Content Security Policy won't load, and code depending on it won't run. See Content Security Policies.

                    Check the console for your own errors. Appcues reports snippet errors rather than surfacing them in the UI, so the console is where a broken selector or a failed request shows up.

                    Still stuck?

                    Collect the following and contact support:

                    • The snippet's name and the experience URL from Studio.
                    • The page URL where you expect the snippet to run.
                    • The values you entered for the snippet's variables.
                    • Any console errors from your browser's developer tools.
                    • A screenshot or screen recording of what renders instead.
                    javascript snippet js snippet

                    Was this article helpful?

                    Yes
                    No
                    Give feedback about this article

                    Related Articles

                    • Add Text, Images and GIFs to Experiences
                    • Personalize experiences with user properties
                    • Embed a Video in a Flow
                    • Working with Custom HTML
                    • Customize and configure buttons
                    Appcues logo

                    Product

                    Why Appcues How it works Integrations Security Pricing What's new

                    Use cases

                    Appcues Integration Hub User Onboarding Software Feature Adoption Software NPS & Surveys Announcements Insights Mobile Adoption

                    Company

                    About
                    Careers

                    Support

                    Developer Docs Contact

                    Resources

                    The Appcues Blog Product Adoption Academy GoodUX Case studies Webinar Series Made with Appcues

                    Follow us

                    Facebook icon Twitter icon grey Linkedin icon Instagram icon
                    © 2022 Appcues. All rights reserved.
                    Security Terms of Service Privacy Policy

                    Knowledge Base Software powered by Helpjuice

                    Expand