Actions

Register app handlers for Widget action buttons.

Last reviewed

Actions

Use a custom action when an Action button under End of flow should call code in your app. Custom actions are available for Widget flows, not hosted pages or embeds. They currently work only for surveys with one ending; conditional endings don't support them.

Custom action contract

The actions configuration has two fields:

FieldMeaning
definitionsThe actions available in this app, each with a stable key and positive-integer version.
handlerOne function that receives the exact definition selected in the flow.

Register a custom action

Register the same definition in every app version that can display a Flow using the action.

JavaScript SDK

TypeScriptapp.ts
import { createClient } from "@getuserfeedback/sdk";const client = createClient({apiKey: "YOUR_API_KEY",actions: {definitions: [{ key: "open-customer-workspace", version: 1 }],handler: ({ key, version }) => {if (key === "open-customer-workspace" && version === 1) {return openCustomerWorkspace();}},},});

React SDK

TypeScriptapp.tsx
import { GetUserFeedbackProvider } from "@getuserfeedback/react";export function App() {return (<GetUserFeedbackProviderclientOptions={{apiKey: "YOUR_API_KEY",actions: {definitions: [{ key: "open-customer-workspace", version: 1 }],handler: ({ key, version }) => {if (key === "open-customer-workspace" && version === 1) {return openCustomerWorkspace();}},},}}><AppRoutes /></GetUserFeedbackProvider>);}

React Native SDK

TypeScriptapp.tsx
import { GetUserFeedbackProvider } from "@getuserfeedback/react-native";export function App() {return (<GetUserFeedbackProviderclientOptions={{apiKey: "YOUR_API_KEY",actions: {definitions: [{ key: "open-customer-workspace", version: 1 }],handler: ({ key, version }) => {if (key === "open-customer-workspace" && version === 1) {return openCustomerWorkspace();}},},}}><AppRoutes /></GetUserFeedbackProvider>);}

React Native currently supports custom actions only. It does not support the browser open-url override.

JavaScript snippet

Define the registry before loading the Widget script:

HTMLindex.html
<script>window.__getuserfeedback_actions = [{kind: "custom",key: "open-customer-workspace",version: 1,handler: () => openCustomerWorkspace(),}];</script><scriptasyncsrc="https://cdn.getuserfeedback.com/widget/loader/v4/YOUR_API_KEY/loader.js"data-api-key="YOUR_API_KEY"></script>

The snippet takes one snapshot of this runtime-wide registry before bootstrap. It cannot be configured after bootstrap or per instance.

Find an action in the editor

Discovery is best-effort. After the runtime has observed the definition, open or refresh the survey editor to find it in the Action selector. Select the matching definition, set the button label, and save the flow.

The definition observed by the editor is setup history. Every live app still needs to register the exact definition.

Match the key and version

The Widget requires an exact key-and-version match between the authored action and the registered handler. It never guesses compatibility. Increase version when the meaning or expected behavior of an action changes incompatibly.

In the JavaScript SDK, definitions become fixed when loading starts. In React and React Native, they remain fixed for the provider's mounted lifetime. The snippet takes its fixed snapshot before bootstrap. Matching handler functions can be refreshed where the platform supports it.

Handler outcomes

The grouped SDK handler receives the selected action's normalized key and version. Legacy per-action handlers, including handlers in the JavaScript snippet, receive no arguments. Core waits up to 10 seconds for the result. Resolving during that window reports success, closes the Widget, and can emit Flow Action Succeeded.

Throwing or rejecting reports failure. If the handler is still pending after 10 seconds, the Widget stops waiting: the response stays saved, the acknowledgment stays open, the button is disabled, and the Widget does not retry automatically.

The deadline does not cancel your handler. Its work can still finish, and a later resolution can still emit Flow Action Succeeded, but it does not retroactively close the timed-out Widget. The event records that the handler settled successfully; it does not confirm that a later external effect completed.

Missing actions

Before display, an automatic Widget flow that requires a missing or mismatched action is suppressed. An explicit flow(flowId).open() request rejects instead of rendering an incomplete flow.

Handle URL actions

Use the dedicated links integration when your app owns navigation:

links: {router: ({ url, target }) => {appRouter.navigate(url, target);},}

The router receives authored HTTP(S) links synchronously and claims each one. It returns undefined; a thrown error or promise-like return is terminal failure. In browser SDKs, that failure also suppresses the Loader's browser fallback. target is "self" for the current tab or "blank" for a new tab, and may be omitted for older served links. Keep links.router present for the client's loaded lifetime. In React and React Native, its function may change while the provider remains mounted. In JavaScript, calling createClient() again for the same client may refresh the router function. Adding or removing the router after initialization throws.

In browser SDKs, omitting links keeps the browser's default navigation. In React Native, authored links are unavailable until you provide a router.

The legacy open-url registration remains supported for compatibility. Do not configure it together with links.router; the SDK rejects that ambiguous ownership at client creation.

The legacy JavaScript and React SDK registration-array form, and the JavaScript snippet registry, can give your app router the first chance to handle authored webpage actions. Register one open-url action alongside custom actions:

actions: [{kind: "custom",key: "open-customer-workspace",version: 1,handler: () => openCustomerWorkspace(),},{kind: "open-url",handler: ({ url, target }) => {if (target !== "self" || !canRouteInApp(url)) return "unhandled";void navigateInApp(url);return "handled";},},]

target is "self" for the current tab and "blank" for a new tab. It can be undefined when the disposition is unavailable. Return "handled" only after your router has synchronously accepted or committed the navigation command. Return "unhandled" to use the browser fallback.

A thrown handler or invalid result reports failure and suppresses the fallback to avoid an unexpected redirect. A later asynchronous router or destination load failure does not retract Flow Action Succeeded after navigation was accepted.