Skip to content

Defining a product tour in JSON

createWorkflowFromConfig turns a plain JSON configuration into the same WorkflowDefinition produced by the JavaScript builder. Tours can therefore be authored in a CMS, stored in a database, or served by an API without changing how GlowTour renders or runs them.

The config API ships as a separate entry point, so applications using only the builder do not pay for it.

Import the config entry point for your framework, load a JSON object, then run the generated workflow on a tour that is rendered by the adapter. This React example uses useGlowTour(); other adapters follow the same pattern with their own component setup:

import "@glowhop/styles-tour/default.css";
import { GlowTourDefault, useGlowTour } from "@glowhop/react-tour";
import { createWorkflowFromConfig } from "@glowhop/react-tour/config";
export function OnboardingTour() {
const { tour, start } = useGlowTour();
async function startTour() {
const config = await fetch("/tours/onboarding.json").then((response) => response.json());
await start(createWorkflowFromConfig(config));
}
return (
<>
<button onClick={() => void startTour()}>Start tour</button>
<GlowTourDefault tour={tour} />
</>
);
}

createWorkflowFromConfig accepts unknown, validates the complete configuration, and throws a ConfigValidationError before building when anything is invalid.

{
"name": "onboarding",
"version": "1.1",
"overlay": { "opacity": 0.55 },
"popover": { "gap": 16 },
"steps": [
{
"id": "invite-button",
"target": "#invite-button",
"title": "Invite your team",
"content": "Send an invite to get started.",
"data": { "trackingId": "invite-step" },
"actions": [
{ "type": "waitUntilElement", "selector": "#invite-button", "timeout": 5000 },
{ "type": "clickTarget" }
],
"targetEvents": [
{ "event": "click", "action": { "type": "focusTarget" } }
]
},
{
"id": "done-banner",
"target": "#done-banner",
"title": "All set!",
"content": "You're ready to go.",
"actions": [{ "type": "wait", "ms": 400 }]
}
]
}
  • version, name, and steps are required. version is the version of the config format, currently "1.1".
  • Every step requires id, target, and content; title is optional. Step ids must be unique within the workflow; they are what start(workflow, { startAt }) uses to resume a tour.
  • target is a CSS selector. Function and HTMLElement targets remain builder-only.
  • title and content are strings for JSON loaded from a CMS or API.
  • overlay, popover, indicator, behavior, and classNames use the same options as the builder, globally or per step. A step’s classNames entry overrides the global one for the same component, as described in Class name options.
  • data accepts string, number, boolean, and null values.

Unknown keys, invalid nested options, and unsupported values are rejected rather than silently ignored.

Use WorkflowConfig while authoring a JSON payload to get autocomplete and catch structural errors in the editor:

import type { WorkflowConfig } from "@glowhop/react-tour/config";
const config: WorkflowConfig = {
name: "onboarding",
version: "1.1",
steps: [
{
id: "invite-button",
target: "#invite-button",
title: "Invite your team",
content: "Send an invite to get started.",
actions: [
{ type: "waitUntilElement", selector: "#invite-button" },
{ type: "clickTarget" },
],
},
],
};
const json = JSON.stringify(config, null, 2);

Keep this object declarative when it must be transported as JSON. WorkflowConfig also accepts same-runtime JavaScript callbacks, but functions are not preserved by JSON.stringify.

actions[] and targetEvents[].action accept declarative action objects:

type Fields Builder equivalent
"wait" ms: number .wait(ms)
"waitUntilElement" selector: string, interval?: number, timeout?: number .waitUntilElement(...)
"clickTarget" - .clickTarget()
"focusTarget" - .focusTarget()

.waitUntil(predicate) has no JSON equivalent because an arbitrary predicate cannot be serialized. Use waitUntilElement or the JavaScript builder instead.

All problems are reported together with their path:

import { ConfigValidationError } from "@glowhop/react-tour/config";
try {
createWorkflowFromConfig(config);
} catch (error) {
if (error instanceof ConfigValidationError) {
for (const issue of error.issues) {
console.error(`${issue.path}: ${issue.message}`);
}
}
}

This makes CMS payloads and generated configuration easier to diagnose in one pass.

Plain JSON covers targets, presentation options, data, and built-in actions. Same-runtime configuration objects can additionally contain functions, but they can no longer be transported as JSON.

actions[] and targetEvents[].action accept an inline function:

import type { WorkflowConfig } from "@glowhop/react-tour/config";
const config: WorkflowConfig = {
name: "onboarding",
version: "1.1",
steps: [
{
id: "invite-button",
target: "#invite-button",
title: "Invite your team",
content: "Send an invite to get started.",
actions: [
{ type: "waitUntilElement", selector: "#invite-button" },
async (context) => {
await analytics.track("invite_step_shown", { target: context.target.id });
},
],
},
],
};
const workflow = createWorkflowFromConfig(config);

beforeEnter and beforeLeave (the config form of .beforeEnter() and .beforeLeave()), plus onStart, onCancel, and onFinish, accept only functions. Built-in actions cannot run in those contexts.

For CMS-driven behavior, keep an identifier in data and attach the implementation in application code:

import { type StepConfig, validateWorkflowConfig, type WorkflowConfig } from "@glowhop/react-tour/config";
const parsed = validateWorkflowConfig(await fetch("/tours/onboarding.json").then((response) => response.json()));
const config: WorkflowConfig = {
...parsed,
steps: parsed.steps.map((step): StepConfig => ({
...step,
beforeLeave: ({ direction, props }) => {
if (direction !== "advance") return;
analytics.track(String(props.get().data?.trackingId ?? "unknown-step"));
},
})),
};

Framework adapters expose pre-bound config entry points:

  • @glowhop/react-tour/config
  • @glowhop/vue-tour/config
  • @glowhop/solid-tour/config
  • @glowhop/angular-tour/config
  • @glowhop/vanilla-tour/config

@glowhop/core-tour/config is the unbound entry point for custom integrations that render the tour themselves. The core has no UI of its own: a workflow run on a bare createGlowTour() from @glowhop/core-tour shows nothing unless your integration renders it. With an adapter, use the adapter’s entry point above.

Each entry point also exports the config types, ConfigValidationError, and validateWorkflowConfig.

The default validation requires string values because that is the safe format for JSON from a CMS or API. For a configuration object assembled inside the application, framework content such as a React node, Vue VNode, Angular TemplateRef, or DOM Node can be accepted with the optional validateContent callback:

const workflow = createWorkflowFromConfig(
{
name: "onboarding",
version: "1.1",
steps: [
{
id: "invite-button",
target: "#invite-button",
title: <b>Invite your team</b>,
content: "Send an invite to get started.",
},
],
},
{
validateContent: (value) => (value === undefined ? "must not be undefined" : null),
},
);

validateContent(value, path) returns an error message or null. Keep the default string validation for untrusted JSON.

The config module never evaluates strings as code: it uses no eval, new Function, or id-to-function registry. A payload produced by JSON.parse() cannot introduce executable functions.

  • CSS selector targets only.
  • No declarative conditions such as showIf.
  • No config exporter yet; the generated definition retains its validated source in .source.
  • No built-in i18n integration. Translation keys and interpolation values can be stored in content and data.

For the JavaScript builder equivalent, see the Builder reference and Programmatic control guide.