Stately
Packages

@statelyai/sdk

Embed the Stately editor, inspect running actor systems over WebSockets, talk to the Stately Studio API, and convert between Studio graph data and code. Fully typed.

Embed the Stately editor, inspect running actor systems over WebSockets, talk to the Stately Studio API, and convert between Studio graph data and code. Fully typed.

Install

npm install @statelyai/sdk

What It Includes

  • createStatelyEmbed() for browser embeds backed by postMessage
  • createStatelyInspector() for inspecting live actor systems over WebSockets
  • createStatelyClient() for Stately Studio API access
  • graph conversion and codegen helpers such as fromStudioMachine(), toStudioMachine(), graphToMachineConfig(), and graphToXStateTS()
  • sync helpers under @statelyai/sdk/sync and a stately CLI binary

Authentication

The embed supports three common deployment models:

  • Hosted Stately: pass an API key to createStatelyEmbed()
  • Same-origin deployments: rely on the host application's session/cookie auth
  • Self-hosted deployments: configure auth in the editor server and omit apiKey when no token is required

With Stately (default)

An API key is required. To get one:

  1. Go to your Stately settings
  2. Select the API Key tab
  3. Click Create API Key (Project or Account scope)
  4. Copy and store it securely

See the Studio API docs for more details.

Pass the key to the SDK:

const embed = createStatelyEmbed({
  baseUrl: 'https://stately.ai',
  apiKey: 'your-api-key',
});

When the embed host and the editor share a domain, you can omit apiKey and rely on the host application's auth/session layer:

const embed = createStatelyEmbed({
  baseUrl: process.env.NEXT_PUBLIC_BETA_EDITOR_URL ?? window.location.origin,
});

Self-hosting

When self-hosting the editor, authentication is enforced by the editor server, not by this npm package.

The common environment variables are:

VariablePurpose
AUTH_PROVIDERAuth strategy used by the editor host
STATELY_API_KEYServer-side API key for Stately data fetching
STATELY_API_URLStately API base URL override
NEXT_PUBLIC_BASE_URLPublic-facing editor URL

For a fully self-contained deployment with no auth, omit apiKey in the SDK and configure the host/editor to allow unauthenticated access:

const embed = createStatelyEmbed({
  baseUrl: 'https://your-editor.example.com',
});

Quick Start

Third-party embed (with API key)

import { createStatelyEmbed } from '@statelyai/sdk';

const embed = createStatelyEmbed({
  baseUrl: 'https://stately.ai',
  apiKey: 'your-api-key',
});

embed.mount(document.getElementById('editor')!);

embed.init({
  machine: myMachineConfig,
  format: 'xstate',
  mode: 'editing',
  theme: 'dark',
});

Comments

Comments are optional and integrator-configured. Pass a comments object to embed.init() when you want Liveblocks-backed commenting enabled.

embed.init({
  machine: machineConfig,
  mode: 'editing',
  comments: {
    roomId: 'machine:checkout',
    publicApiKey: 'pk_live_...',
    userId: currentUserId ?? null,
  },
});

You can also use a custom auth endpoint instead of a public key:

embed.init({
  machine: machineConfig,
  comments: {
    roomId: 'machine:checkout',
    authEndpoint: '/api/liveblocks-auth',
    userId: currentUserId ?? null,
  },
});

roomId is required when comments are enabled. userId is optional and only used for comment identity metadata.

Module Layout

The SDK ships root exports for the most common entry points and helpers:

import {
  createStatelyClient,
  createStatelyEmbed,
  createStatelyInspector,
  fromStudioMachine,
  graphToMachineConfig,
  graphToXStateTS,
  toStudioMachine,
} from '@statelyai/sdk';

It also supports narrower subpath imports:

import { createStatelyClient } from '@statelyai/sdk/studio';
import { createStatelyInspector } from '@statelyai/sdk/inspect';
import { createStatelyEmbed } from '@statelyai/sdk/embed';
import { fromStudioMachine, toStudioMachine } from '@statelyai/sdk/graph';
import { planSync, pullSync } from '@statelyai/sdk/sync';
import type { GraphPatch } from '@statelyai/sdk/patchTypes';

Studio API Client

import { createStatelyClient } from '@statelyai/sdk';

const studio = createStatelyClient({
  apiKey: process.env.STATELY_API_KEY,
});

const project = await studio.projects.get('project-id');
const machine = await studio.machines.get('machine-id', { version: '42' });
const extracted = await studio.code.extractMachines(sourceCode);

Available client methods:

MethodDescription
studio.auth.verify(apiKey?)Verify an API key against the registry API
studio.projects.get(projectId)Fetch a project and its machines
studio.machines.get(machineId, { version? })Fetch a machine, optionally pinned to a version
studio.code.extractMachines(code, { apiKey? })Extract machine configs from source text

Inspector

createStatelyInspector() streams actor-system state to the Stately inspector over WebSockets. It supports both automatic XState actor adoption and manual actor registration.

import { createActor } from 'xstate';
import { createStatelyInspector } from '@statelyai/sdk';

const actor = createActor(machine);
const inspector = createStatelyInspector({
  actor,
  url: 'ws://localhost:4242',
  autoOpen: true,
});

actor.start();

Key options:

OptionDescription
actorRoot actor to adopt and inspect automatically
urlDevtools relay URL. Defaults to ws://localhost:4242
autoOpenWhether to ask the relay to open the inspector UI
sessionIdOverride the relay session id
nameDisplay name shown to the inspector
serializeSnapshotCustomize snapshot serialization before sending it over the wire
extractMachineConfigCustomize how machine config is derived from an actor
selectedActorIdFocus a specific actor first
panels, theme, readOnly, depthInitial inspector UI options
transportInject an existing transport instead of opening a new WebSocket

Key methods:

  • inspector.export(format, options?)
  • inspector.actor(id, options?)
  • inspector.snapshot(actorId, snapshot, event?)
  • inspector.event(actorId, event, { source? })
  • inspector.stop(actorId)
  • inspector.destroy()

Embed API

createStatelyEmbed(options)

Creates an embed instance.

OptionTypeDescription
baseUrlstringRequired. Base URL of the Stately app
apiKeystringAPI key for hosted Stately deployments
originstringCustom target origin for postMessage
assetsAssetConfigAsset upload configuration
onReady() => voidCalled when the embed is ready
onLoaded(graph) => voidCalled when a machine is loaded
onChange(graph, machineConfig) => voidCalled on every change
onSave(graph, machineConfig) => voidCalled on save
onError({ code, message }) => voidCalled when the embed reports an error

Embed methods

embed.mount(container) / embed.attach(iframe)

mount() creates an iframe inside a container element. attach() connects to an existing iframe.

const iframe = embed.mount(document.getElementById('editor')!);

embed.attach(document.querySelector('iframe')!);

embed.init(options)

Initialize the embed with a machine and display options.

embed.init({
  machine: machineConfig,
  format: 'xstate',
  mode: 'editing',
  theme: 'dark',
  readOnly: false,
  depth: 3,
  panels: {
    leftPanels: ['code'],
    rightPanels: ['events'],
    activePanels: ['code'],
  },
  unsavedIndicator: {
    enabled: true,
    mode: 'structural',
  },
  comments: {
    roomId: 'machine:checkout',
    publicApiKey: 'pk_live_...',
  },
});

comments accepts:

FieldTypeDescription
roomIdstringRequired. Liveblocks room identifier
publicApiKeystringLiveblocks public key
authEndpointstringCustom Liveblocks auth endpoint
baseUrlstringCustom Liveblocks base URL for self-hosting
userIdstring | nullOptional user identity metadata

unsavedIndicator accepts:

FieldTypeDescription
enabledbooleanShow the persistent "Save to apply" pill
mode'structural' | 'all'Track only structural graph edits or all edits

embed.updateMachine(machine, format?)

Update the displayed machine.

embed.setMode(mode) / embed.setTheme(theme)

Change the embed mode or theme at runtime.

embed.setSettings(settings)

Update editor settings at runtime. Settings are merged with the existing editor settings.

embed.setSettings({
  appearance: { colorMode: 'light' },
  canvas: { showGrid: false },
});

Available core settings:

PathTypeDefault
appearance.colorMode'light' | 'dark' | 'system''dark'
canvas.showGridbooleantrue
canvas.viewMode'graph' | 'list''graph'
canvas.enableSnapLinesbooleantrue
canvas.dimUnselectedbooleantrue
validation.showValidationsbooleantrue
autolayout.autoEnabledbooleanfalse
developer.devModebooleanfalse

embed.export(format, options?)

Export the current machine. Returns a promise.

const xstateCode = await embed.export('xstate', { version: 5 });
const digraph = await embed.export('digraph');
const rtk = await embed.export('rtk');
const aslYaml = await embed.export('asl-yaml');

Supported formats: xstate, json, digraph, mermaid, rtk, zustand, asl-json, asl-yaml, scxml

embed.on(event, handler) / embed.off(event, handler)

Event names are ready, loaded, change, save, error, and snapshot.

createStatelyEmbed() emits ready, loaded, change, save, and error for browser embeds:

embed.on('change', ({ graph, machineConfig, patches }) => {
  console.log('Machine changed', graph, machineConfig, patches);
});

embed.on('save', ({ validations }) => {
  console.log('Save validations', validations);
});

embed.toast(message, type?)

Show a toast notification in the embed. Type: 'success' | 'error' | 'info' | 'warning'

embed.destroy()

Tear down the embed. Removes listeners, rejects pending promises, and removes the iframe if it was created via mount().

Asset uploads

By default, dropped files are stored as base64 data URLs. To upload assets to your own storage, pass an assets config:

const embed = createStatelyEmbed({
  baseUrl: 'https://stately.ai',
  assets: {
    onUploadRequest: async (file, { stateNodeId }) => {
      return { url: await uploadToStorage(file, stateNodeId) };
    },
    accept: ['image/*'],
    maxFileSize: 5 * 1024 * 1024,
  },
});
OptionTypeDescription
onUploadRequest(file: File, context: { stateNodeId: string }) => Promise<UploadResult>Required. Called when the editor needs to upload a file
acceptstring[]Accepted MIME types. Supports wildcards like image/*
maxFileSizenumberMax file size in bytes. Defaults to 10_485_760

UploadResult:

interface UploadResult {
  url: string;
  name?: string;
  metadata?: Record<string, unknown>;
}

If onUploadRequest throws or rejects, the editor shows an error toast. If no assets config is provided, files are stored inline and no upload request is sent.

Graph And Codegen Helpers

Use the conversion helpers to move between Studio digraph data, generic Stately graphs, machine config objects, and XState TypeScript source.

import {
  fromStudioMachine,
  graphToMachineConfig,
  graphToXStateTS,
  toStudioMachine,
} from '@statelyai/sdk';

const graph = fromStudioMachine(studioMachine);
const machineConfig = graphToMachineConfig(graph, {
  showDescriptions: true,
  showMeta: true,
});
const source = graphToXStateTS(graph, {
  exportStyle: 'named',
});
const digraph = toStudioMachine(graph);

Other exported helpers:

  • studioMachineConverter for reusable format conversion
  • serializeJS(), raw(), and RawCode for emitting JavaScript source
  • jsonSchemaToTSType(), contextSchemaToTSType(), and eventsSchemaToTSType() for generating inline TypeScript types from JSON Schema
  • GraphPatch and ActionLocation types from @statelyai/sdk/patchTypes

Sync Helpers And CLI

The sync helpers compare or materialize machines across local files, Stately machine IDs, and Stately URLs.

Programmatic usage:

import { planSync, pullSync } from '@statelyai/sdk/sync';

const plan = await planSync({
  source: './checkout.machine.ts',
  target: 'machine-id',
  apiKey: process.env.STATELY_API_KEY,
});

if (plan.summary.hasChanges) {
  console.log(plan.summary);
}

await pullSync({
  source: 'machine-id',
  target: './checkout.machine.ts',
  apiKey: process.env.STATELY_API_KEY,
});

Supported locators:

  • local files
  • Stately machine IDs
  • full Stately machine URLs

Installing the package also exposes a stately binary:

stately plan ./checkout.machine.ts machine-id
stately diff ./checkout.machine.ts machine-id --fail-on-changes
stately pull machine-id ./checkout.machine.ts

Available commands:

CommandDescription
stately plan <source> <target>Print a semantic sync summary
stately diff <source> <target>Diff two locators and optionally fail on changes
stately pull <source> <target>Materialize a source into a local target file

Common flags:

  • --api-key for remote machine resolution
  • --base-url for self-hosted or non-default Stately deployments
  • --fail-on-changes to return a nonzero exit code when a diff is detected

Transport Helpers

For advanced integrations, the root package also exports:

  • createPostMessageTransport() for iframe-based clients
  • createWebSocketTransport() for relay-based integrations

These power the embed and inspector internals, but they are also available when you need lower-level control over the @statelyai.* protocol.

On this page