~ click(for: Documentation) ~
SwiftXState adapts the popular XState.js actor-as-state-machine-owner model to Swift's concurrency-based actor model, where actors are threadsafe, isolated background contexts and UI can update seamlessly from their @MainActor store.
- Cross-Platform Guides
- FAQ
- Code Examples
- Included Sample Apps (iPadOS/macOS)
- XState.js Adoption & Parity
- Roadmap
- Related Links
- Security Policy
- License
- Own your logic and events with (state)-flow-(state) graphs.
- Track it live with built-in JSON streams & 2D/3D visualizer.
- Rewind/replay your whole program with snapshots.
- Load statecharts from JSON at runtime to tweak behavior.
- Offload business logic to threadsafe background executors with asynchronous Swift
actors, which gain deterministic behavior thanks to XState wizardry.
- Server or client.
- Node.js: use Stately.a's XState.js.
- WebAssembly: experimental — the browser inspector runs SwiftXState in wasm with a WebGPU state-graph
- Linux (Linux build README, or get prebuilt NuGet)
- Windows (Windows build README, or get prebuilt NuGet)
- macOS/iPadOS (sample Chess app, sample visualizer app)
- iOS/visionOS/watchOS/tvOS: also supported, no sample apps yet.
Every effort has been made to ensure you can trust this library. For details, see: SECURITY.md.
- SwiftXState - all platforms - static library, core features
- SwiftXStateInspect - all platforms - localhost JSON streaming in XState.js-format
- SwiftXStateGraph - all Apple platforms - SwiftUI state graph renderer in 2D and 3D (note: does not render in 3D on visionOS yet)
- SwiftXStateInspectorUI - all Apple platforms - SwiftUI info displays for displaying Inspect streams
- SwiftXStateSwiftUI - all Apple platforms - wire your SwiftUI view states up to SwiftXState state stores
- SwiftXStateSwiftData - all Apple platforms - persistent data storage adapter
- ... plus some fun bonus items ;D
- Feature-complete beta phase (see roadmap items below).
- Now with documentation (thanks to the awesome swift-docc)
- Swift's
actoris about asynchronous isolation and data-race safety. - XState.js's
Actoris about state machines running synchronous, deterministic transitions with replayability. - The initial beta of SwiftXState used Swift's
classto implementActorfor compatibility with XState.js, whereActors deterministically orchestrate run-to-completion events on the main thread. - Now, since 1.0, SwiftXState uses
Swift actorfor ourActor.
- SwiftXState would not exist without the work of Stately and the XState team.
- Thank you to David Khourshid, founder of Stately.ai and XState, in particular for blessing this project.
- Thank you to everyone who contributed to XState.js and the Stately ecosystem.
- Thank you to Stately for their inspiring commitment to open source.
- SwiftXState/Inspect/URLSession: Foundation
- SwiftUI and/or SwiftData modules require Apple platforms
- WebAssembly requires JavaScriptKit
- Since 1.0, we dropped
swift-syntaxas a dependency and transitioned away from macros.
We offer two main API paths:
- Text mode, for compatibility with XState.js and prototyping, ease of juming in, etc.
- Typesafe mode, which models events as
StateEventtypes and states asStateNameenums for additional compile-time safety. - Documentation linked above has guides for both
-
Create a new state machine and "actor" to manage it:
import SwiftXState let toggle = createMachine(MachineConfig( id: "toggle", initial: "inactive", context: EmptyContext(), states: [ "inactive": StateNodeConfig(on: ["toggle": .to("active")]), "active": StateNodeConfig(on: ["toggle": .to("inactive")]), ] )) let actor = await createActor(toggle).start() await actor.send(Event("toggle")) print(await actor.snapshot.matches("active")) // true
-
Declare a
StateNameenum (oneString-backed case per state) for autocomplete and protection against typos and name drift. Nested states get a compound case name with a dotted raw value:enum AppState: String, StateName { case idle case active case activeFast = "active.fast" // AppState.activeFast → "#active.fast" (absolute target) } let config = MachineConfig(id: "app", initial: "idle", context: Ctx(), states: [ "idle": StateNodeConfig(on: transitions(on(Focus.self, to: AppState.active))), "active": StateNodeConfig(states: ["fast": StateNodeConfig(), "slow": StateNodeConfig()]), ])
-
Set the legal transition rules for a each node in your state graph:
StateNodeConfig(on: [ "input.focus": .to("active"), "input.change": .single(TransitionConfig(target: "debouncing")), ])
-
SwiftXState features two tiers of APIs. The "second tier" leans more heavily into Swift generics and type-safety.
-
In this example we declare a
StateEventtype,InputChange. Then we can send a typesafeInputChangeevent to ouractorrather than something likeactor.send(Event("toggle"))(as shown in the text API example above).struct InputChange: StateEvent { static let eventType = "input.change"; let searchInput: String } StateNodeConfig(on: transitions( on(Focus.self, target: "active"), on(InputChange.self, target: "debouncing", actions: [assign { (ctx: inout Ctx, e: InputChange) in ctx.searchInput = e.searchInput }]) )) await actor.send(InputChange(searchInput: "be")) // typed at the call site
-
You can also brand an actor with its state family at creation. If your state enum conforms to
StateID,createActor(_:as:)hands back aTypedActorwhose snapshots are typed — so state checks likeinState(_:)are compile-checked and autocompleted instead of stringly-typed:enum SearchState: String, StateID { case active, debouncing } let search = createActor(searchMachine, as: SearchState.self) // TypedActor<Ctx, SearchState> let snapshot = await search.start() print(snapshot.inState(.debouncing)) // ← checked at the call site, no "debouncing" string
-
As the library matures, we plan to increase the type-safe surface area of SwiftXState.
-
Note: for more examples of type-safe uses of SwiftXState, see our example app: Examples/SwiftXChess.
-
Like the original
@xstate/graph, our Swift version provides APIs to analyze your state graphs during testing to ensure that your assumptions are correct:let model = TestModel(toggle) for path in model.shortestPaths() { print(path.description) // e.g. "-toggle-> active" try model.test( path, onState: { snapshot in /* assert your UI matches snapshot.value */ }, onEvent: { event in /* drive your component with event */ } ) } // Static checks over the reachable graph: for issue in model.validate() { print(issue.kind, issue.stateKey) // .deadEnd / .unreachableState }
-
(Similar features:
getAdjacencyMap,getShortestPaths,getSimplePaths,validate.) -
You can also tune traversal with
TraversalOptions(custom event resolver, state serialization,maxStates).
-
Paste in JSON machine descriptions in XState JSON format to see a realtime visualization.
-
Note: does not yet support pasting in JavaScript functions.
-
Demonstrates the live inspection features of SwiftXGraph and SwiftXInspect
-
Shows the power of GPU-accelerated Metal rendering in SwiftUI
-
Each board square has different inspectable state depending upon which kind of piece might be present:
XState.js users considering SwiftXState as a native-code solution might benefit from the following information about API terminology and feature parity with Stately.ai's wonderful library, XState.js v5.
| XState (TS) | SwiftXState |
|---|---|
createMachine({ … }) |
createMachine(MachineConfig(…)) |
setup({ actions, guards, delays, actors }) |
setup(actions:guards:delays:actors:) |
setup({ types: { events, context } }) |
typed Context generic + Tier-2 StateEvent types |
on: { EVENT: 'target' } |
on: ["EVENT": .to("target")] (Tier 1) / on(EventType.self, target: "target") (Tier 2) |
target: 'someState' (string) |
to: AppState.someState — compile-checked via a StateName enum |
assign({ x: ({ event }) => … }) |
assign { (ctx: inout C, e: EventType) in ctx.x = … } (Tier 2) |
assertEvent(event, "…") |
not needed — the Tier-2 handler is already typed to the event |
guard: 'name' / ({ context, event }) => … |
guard: .named("name") / guarded { (c, e: EventType) in … } |
always, after, invoke, spawn, raise, sendTo, tags, meta |
same names, same model |
The table below summarizes where SwiftXState stands today relative to XState v5 and the broader Stately ecosystem. Status meanings:
- ✅ Parity — implemented and tested
- 🔶 Partial — works for common cases; known gaps listed
- ➕ SwiftXState only — not in stock XState (or not in the same form)
- 📋 Planned — intended; not implemented yet
- ➖ N/A — platform or ecosystem difference, not a goal for native Swift
| Capability | Status | Notes |
|---|---|---|
createMachine / setup().createMachine() |
✅ Parity | MachineConfig, StateNodeConfig mirror XState config |
| State types (atomic, compound, parallel, final, history) | ✅ Parity | Shallow and deep history |
Events (Eventable, Event("TAP"), string shorthand) |
✅ Parity | Custom Eventable types supported (see SwiftXChess) |
Wildcard transitions (*, prefix.*) |
✅ Parity | |
Guards (named, inline, and / or / not, stateIn) |
✅ Parity | |
Parameterized guards { type, params } |
✅ Parity | guardRef(_:params:), dynamicGuard, setup().registerGuard |
| Actions (assign, raise, sendTo, spawn, stop, log, emit, …) | ✅ Parity | |
enqueueActions |
✅ Parity | |
always transitions |
✅ Parity | |
after delayed transitions |
✅ Parity | Named delays via setup(delays:) |
| Internal transitions (actions only, no target) | ✅ Parity | |
reenter |
✅ Parity | |
| Parallel regions + multi-target transitions | ✅ Parity | |
Tags + snapshot.hasTag(_:) |
✅ Parity | |
State meta on config |
✅ Parity | StateNodeConfig.meta + snapshot.getMeta() |
Final state output + status: done |
✅ Parity | |
xstate.done.state.{id} (nested final completion) |
✅ Parity | StateNodeConfig.onDone + DoneStateEvent |
Pure transition() / initialTransition() |
✅ Parity | Side effects not run in pure path |
waitFor |
✅ Parity | |
SimulatedClock |
✅ Parity | Deterministic delays in tests |
| Capability | Status | Notes |
|---|---|---|
createActor + mailbox + send |
✅ Parity | See "Actor" vs. Swift actor |
invoke / spawnChild |
✅ Parity | |
fromMachine (child state machines) |
✅ Parity | |
fromTask (fromPromise) |
✅ Parity | async throws with structured scope |
fromCallback |
✅ Parity | Long-running listeners + cleanup |
fromTransition |
✅ Parity | |
fromObservable / Subscribable |
✅ Parity | |
fromStore |
✅ Parity | XState store actor logic |
fromTaskGroup |
➕ SwiftXState only | Structured concurrent child work via TaskGroup |
sendBack in callback actors |
✅ Parity | CallbackActorScope.sendBack — alias for sendToParent |
ActorSystem (register, get, inspect) |
✅ Parity | |
forwardTo, sendTo (with delay), sendParent |
✅ Parity | |
emit + actor.on("eventType") |
✅ Parity |
| Capability | Status | Notes |
|---|---|---|
getPersistedSnapshot / restoreSnapshot |
✅ Parity | Requires Codable context |
actor.start(from:) hydration |
✅ Parity | Two-step: createActor then start(from:) |
createActor(..., snapshot:) one-shot hydration |
✅ Parity | Already started; ActorPersistenceStore.createActor(_:key:) for SwiftData |
| Child actor state in persisted snapshots | ✅ Parity | Machine children round-trip recursively; opaque children persist status only — use onCancel + opaqueRestorePolicy for SwiftData cleanup / deferred re-spawn |
| Replay sessions (record, pure replay, scrub) | ➕ SwiftXState only | ReplaySession, RecordedStep, ReplayDriver |
| Replay with full custom event payloads | ✅ Parity | ReplayPayloadRepresentable, PayloadEvent, ReplayEventDecoder |
| SwiftData persistence | ➕ SwiftXState only | ActorPersistenceStore, ReplayPersistenceStore |
| Capability | Status | Notes |
|---|---|---|
Inspection protocol (@xstate.* events) |
✅ Parity | |
Stately wire format + @statelyai/inspect |
✅ Parity | StatelyWireConverter, WebSocket transport |
definitionJSON() export |
✅ Parity | Stately-compatible machine graphs |
| Machine JSON import | 🔶 Partial | Load any XState machine-definition JSON into the inspector (MachineDefinitionImporter / InspectorStore.loadDefinition): renders the graph and reconstructs the initial state value + context. A structural simulator (MachineSimulator) then lets you click through on / always / after / invoke.onDone transitions, with synthetic event + snapshot rows feeding the Events/Sequence tabs. Control-flow only — guards aren't evaluated and actions/assign/actors don't run (those are code, not data). See Examples/InspectorPasteApp. Full round-trip back to definitionJSON() is still planned. |
meta in exported definitions |
✅ Parity | |
@xstate/graph (paths, TestModel, validation) |
✅ Parity | Core, cross-platform (Linux too): getAdjacencyMap, getShortestPaths, getSimplePaths, TestModel (model-based path testing via test(_:onState:onEvent:)), and validate (dead-end + unreachable-state checks). Built on the faithful pure transition (guards evaluated, assign applied), with TraversalOptions for custom event resolvers / state serialization. Note: this is the algorithm layer — distinct from the like-named SwiftXStateGraph visualizer module (same collision exists in XState). |
| Native SwiftUI visualizer | ✅ | SwiftXStateGraph library: GPU-backed Canvas 2D renderer + SceneKit 3D mode, walks the real machine tree (nested compound/parallel regions, transitions, initial/final markers), live active-state highlighting, anchored zoom / pan / node-drag (+ mouse-wheel on macOS), themeable via GraphStyle. |
Browser __xstate__ devtools hook |
➖ N/A | Stately inspect covers cross-platform debugging |
| Capability | Status | Notes |
|---|---|---|
setup(actions:guards:delays:actors:) |
✅ Parity | |
setup({ types: { events, context } }) inference |
✅ Parity | Context is statically typed (MachineConfig<Context>). The Tier-2 typed API models each event as its own StateEvent type and keys transitions on it, so guard/action closures receive the concrete, narrowed event — no cast, no assertEvent. A hand-declared StateName enum gives compile-checked, autocompleted, rename-safe targets (to: AppState.running). Achieves XState's typing outcomes through Swift's type identity rather than TS literal inference. |
mapState |
✅ Parity | Nested StateMap → [MapStateEntry]; mapStateFirst for view models |
getNextSnapshot alias |
📋 Planned | transition() already provides this |
SwiftUI bindings (useMachine, useSelector, useMapState) |
➕ SwiftXState only | Apple platforms; parallel to @xstate/react |
Pluggable inspect transports (InspectTransport) |
➕ SwiftXState only | ClosureInspectTransport, file/mock transports; URLSession optional |
@xstate/react / Vue / Svelte bindings |
➖ N/A | SwiftUI is the Apple-native binding layer |
| Capability | Status | Notes |
|---|---|---|
| XState machine-definition JSON (export) | ✅ Parity | For Stately graph rendering |
| XState machine-definition JSON (import) | 🔶 Partial | Structural import into the inspector (graph + initial state + click-through stepping); see Inspector & tooling. Behavior (guards/actions/actors) is not reconstructed — it lives in code, not the definition |
| SCXML import / export | 📋 Planned | XState itself is SCXML-inspired rather than a full SCXML engine; we aim to support practical SCXML interchange for enterprise and telecom workflows |
| W3C SCXML execution semantics (full) | 📋 Planned | Large spec; will be incremental |
| Capability | Status | Notes |
|---|---|---|
| Compiled iOS / macOS / watchOS / tvOS / Linux / Windows apps | ➕ SwiftXState only | No JS runtime required |
Strict concurrency / Sendable machine model |
➕ SwiftXState only | Enabled on core targets |
| C / C++ / Objective-C interop from actions & actors | 📋 Planned | Invoke fromCallback / fromTask as integration points |
| Offline-first native persistence | ➕ SwiftXState only | SwiftData module; Core Data / file stores possible |
- Opaque child checkpoint payloads: optional persisted job ledger metadata beyond status-only snapshots
- SCXML interchange: import/export for standards-based workflows (as soon as we finish our XML parser)
- Machine JSON import: structural import + click-through simulation shipped (see
InspectorPasteApp); full round-trip back todefinitionJSON()still planned - On-device live run of imported machines: execute an imported XState machine's real behavior (guards/actions/actors) on iOS/macOS via in-process
JavaScriptCore, bridging XState'sinspectcallback intoInspectionEvent, so any JS machine runs live in the native inspector without a Node relay - Load machine configs / full machines from external sources: awaiting security review.
- First-class wasm WebGPU adapter.
- XState — the JavaScript reference implementation
- Stately — visual editor, inspector, and state-machine tooling
- @statelyai/inspect — inspector protocol SwiftXState speaks on the wire
- SCXML (W3C) — historical spec that influenced XState's design
SwiftXState is released under the MIT License.
Copyright (c) 2026 Jonathan Gilbert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SwiftXState is an independent open-source project inspired by and interoperable with Stately's XState. It is not affiliated with or endorsed by Stately. XState itself is licensed separately by its authors; see the XState repository for its terms.



