Quickstart
In about five minutes you'll name a component in Flex, add a script to your experience, and watch it change the page.
You'll need a Flex experience you can edit and publish, and a browser from 2023 or later (the SDK ships as a standard ES module and relies on import maps). There is nothing to install.
1. Give a component a locator
Open your experience in Flex, select the element you want your script to
control, and open the SDK Locators section of the inspector. Add a locator —
for this walkthrough, headline.
A locator is a name you choose. It's how your code finds the element, so pick something stable and descriptive. A few things worth knowing:
- Reuse a name to group elements. Give three cards the same
cardlocator and one call reaches all three. - One element can carry several names, space-separated, exactly like CSS
classes. An element with
cta herois found byctaorhero. - Locators survive restyling, resizing, and moving the element. They break only if someone renames them.
2. Add your script
Put a module script in your experience's custom body HTML (Flex →
experience settings). Anything valid in a <script type="module"> works.
<script type="module">
import { connect } from '@ceros/flex-experience-sdk'
const experience = await connect()
experience.findByLocator('headline').text.setText('Hello from the SDK')
</script>
connect() returns a promise that resolves once the experience has loaded and
is ready to script. Because it's a module script, top-level await is available
— no DOMContentLoaded wrapper needed.
3. Publish and check
Publish the experience and open the published URL. The headline should read "Hello from the SDK".
If it doesn't, open your browser's console. Rather than throwing, the SDK writes a message saying what it couldn't find — search for "flex" to spot it, then see Troubleshooting.
The SDK attaches to a published experience. In the editor there is nothing
for it to connect to, so connect() never resolves. Test on a published URL or
a preview link.
4. Go further
Everything else is built from the same three moves. A couple of examples to build on:
const experience = await connect()
// React to a click, anywhere in the experience
experience.findByLocator('cta').on('component.click', (event) => {
console.log('clicked', event.target.cmlLocator)
})
// Pause every video on the experience
experience.findByTag('cml-video').media.pause()
// Reveal something the design hides by default
experience.findByLocator('promo').visibility.show()
// Move to another page
experience.pages.goTo('pricing')
Next: Locators for finding components, Capabilities for what you can drive, or a complete worked example.
Loading the SDK
How you import the SDK depends on how your experience reaches the page. Both forms give you the same API.
On a Ceros-hosted page
A published experience served from its own Ceros URL — including one you've put
in an <iframe> — resolves the bare specifier for you:
<script type="module">
import { connect } from '@ceros/flex-experience-sdk'
const experience = await connect()
</script>
Every published page carries an import map that points that name at the SDK bundle. There's no loader script and no global to wait for, and nothing is downloaded until a script actually imports it.
On your own page (Flex Inline or Flex SSR)
When the experience is embedded in a page you own, that page has no Ceros import map, so import the bundle from its URL instead:
<script type="module">
import { connect } from 'https://assets.ceros.site/js/flex-experience-sdk.js'
const experience = await connect(/* a root — see below */)
</script>
- The URL is stable and always serves the current SDK:
https://assets.ceros.site/js/flex-experience-sdk.js. - A URL import is self-contained, so load order relative to Ceros' own embed scripts doesn't matter. The assets host sends the CORS headers a cross-origin module import needs.
- To fetch it early:
<link rel="modulepreload" href="https://assets.ceros.site/js/flex-experience-sdk.js">.
On Flex Inline and Flex SSR, Ceros does not inject your experience's custom head/body HTML into your page, and page navigation happens in place rather than as a full page load. Both change how you write SDK code — see Delivery modes.
connect() reference
function connect(
experienceRoot?: Document | ShadowRoot | Element | null,
options?: { timeout?: number },
): Promise<Component>
experienceRoot — where to look for the experience. Defaults to the
experience on the page, which is what you want for a page holding one
experience. Pass a Document, ShadowRoot, or Element to search inside a
specific container — see Multiple experiences.
options.timeout — milliseconds to wait before rejecting. Omit it (or pass
0) to wait indefinitely. Set it when the experience might be lazy-loaded and
you'd rather get a clear error than a promise that never settles:
try {
const experience = await connect(container, { timeout: 10_000 })
} catch (error) {
console.error('Experience never became ready', error)
}
The promise rejects immediately if you pass null — usually a
querySelector that found nothing. If the experience hasn't mounted yet, pass a
container that does exist and let connect wait for the experience to appear
inside it.
When it's safe to call
connect() is deliberately hard to call too early:
- It works from inline scripts,
asyncmodules, and tag-manager-injected scripts. Script order doesn't matter. - Given a container (
document, or a wrapper element), the experience need not exist yet —connectwatches that container and resolves when the experience appears and becomes ready. Lazy-mounted and late-hydrated embeds are fine. - Given the experience element itself, that element must already be in the DOM. Pass a container when it might not be.
Multiple experiences on one page
There's no experience ID to pass. Instead, hand connect a container that holds
the one you want. Every embed carries a data-flex-manifest-url attribute, so
that URL makes a reliable selector — and the same call works for both Flex
Inline and Flex SSR:
const experience = await connect(
document.querySelector(
'[data-flex-manifest-url="https://…/pricing/manifest.v1.json"]',
),
)
Call connect once per experience, and keep each handle with the code that uses
it. Lookups on one handle never reach into another experience.
Skipping the wait
If you already know the experience is ready — in a test, typically — you can wrap its element directly and skip the readiness check:
import { wrap } from '@ceros/flex-experience-sdk'
const experience = wrap(document.querySelector('ceros-experience-viewer'))
On a real page, prefer connect(). Lookups made before the experience is ready
return empty sets.
Things to know
- Re-connect after a page change on an embedded experience. Flex Inline and
Flex SSR swap pages in place and replace the experience element, which
silently invalidates a handle you captured earlier. Re-connect on the
flex.page.changeevent — see Delivery modes. On a Ceros-hosted page, navigation is a full page load, so your script simply runs again. - Look components up when you need them. A lookup queries the page at the moment you call it. Components that appear later aren't added to a set you captured earlier.
- There is no global. No
window.flexSdk— each script imports the module and gets its own handle.
Checking the version
import { SDK_VERSION } from '@ceros/flex-experience-sdk'
console.log(SDK_VERSION) // e.g. '0.1.0'
Useful when reporting an issue: it tells us exactly which bundle your page loaded.