Skip to content

API Orchestrator

iKryptonic edited this page May 1, 2026 · 14 revisions

API: Orchestrator

Orchestrator is the top-level runtime facade for RBXStateMachine. It bootstraps core modules, exposes Factory and Scheduler services, provides high-level create/delete/cancel delegates, wraps networking APIs, and manages client/server replication startup.

Important

This page documents FSM/Orchestrator/init.luau and the delegate behavior it exposes over Factory, NetworkManager, and Scheduler.


Overview

In most games, Orchestrator is the entry point you actually require and call.

What it owns

Member Meaning
Orchestrator.Factory Compiled class creation / registry / persistence pipeline
Orchestrator.NetworkManager Remotes, server request handlers, entity command routing, event buses
Orchestrator.Scheduler Runtime scheduler instance created during initialization
Orchestrator.Settings Shared framework settings module
Orchestrator.History Legacy/service-manager-facing history table
Orchestrator._serviceManager Cached ServiceManager module, if started/registered

Bootstrap flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RegisterComponents β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Initialize Logger β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Set shared.fsm proxy β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Initialize NetworkManager / Factory / Scheduler β”‚
β”‚ modules                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Create Scheduler instance β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Disable BaseStateMachine internal loop  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Schedule GlobalStateMachineHeartbeat  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ NetworkManager.Initialize   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Register server snapshot callbacks or client listeners β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Optional ServiceManager wiring β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Bootstrap API

Orchestrator:RegisterComponents()

Orchestrator:RegisterComponents() -> boolean

Main framework bootstrap. This is the call most games make during startup.

What it does

  1. calls internal Initialize()
  2. on success, if running on the server and _serviceManager is still nil, requires script.ServiceManager
  3. calls serviceManager.RegisterServerHandlers(self)
  4. caches that module in _serviceManager
  5. returns the Initialize() result

Returns

  • true when initialization succeeds
  • false while an initialization is already in progress
  • an error object / falsy result if internal initialization fails unexpectedly

Side effects

  • @reads: runtime context (RunService:IsServer/IsClient), service manager module presence
  • @writes: Factory, NetworkManager, Scheduler, shared globals, callbacks, _serviceManager

Example

local FSM = require(game:GetService("ReplicatedStorage").RBXStateMachine)
FSM.Orchestrator:RegisterComponents()

Important startup behavior

  • Idempotent: once initialized, later calls return immediately.
  • Re-entrance guarded: while initialization is in progress, concurrent calls return false instead of racing.
  • Server side: registers entity snapshot request handlers and ServiceManager server callbacks.
  • Client side: creates the pending update buffer, hooks entity remotes, and requests the initial entity snapshot.

Orchestrator:StartServiceManager()

Orchestrator:StartServiceManager() -> any

Lazy-launches the ServiceManager UI/module.

Returns

  • existing _serviceManager if already available
  • otherwise the required / initialized ServiceManager module

Side effects

  • @reads: script.ServiceManager
  • @writes: _serviceManager, ServiceManager UI visibility state

Example

local FSM = require(game:GetService("ReplicatedStorage").RBXStateMachine)
FSM.Orchestrator:RegisterComponents()

if game:GetService("RunService"):IsStudio() then
    FSM.Orchestrator:StartServiceManager()
end

Orchestrator:GetServiceManager()

Orchestrator:GetServiceManager() -> any?

Returns the cached ServiceManager module reference, or nil if it has not been started / registered yet.


Entity Delegates

Orchestrator.CreateEntity(params)

Orchestrator.CreateEntity(params: {
    EntityClass: any,
    EntityId: string,
    Context: {[any]: any}?,
    Persistent: boolean?,
    PersistenceKey: string?,
}) -> any?

Shared-context wrapper over Factory.CreateEntity(...).

  • @reads: initialization state
  • @writes: delegated to Factory
local door = FSM.Orchestrator.CreateEntity({
    EntityClass = "DoorEntity",
    EntityId = "door:front",
    Context = { Model = workspace.FrontDoor },
    Persistent = true,
})

Orchestrator.GetEntity(entityId)

Orchestrator.GetEntity(entityId: string) -> any?

Returns the live entity from Factory.Registry.

Orchestrator.GetEntities()

Orchestrator.GetEntities() -> {[string]: any}

Returns Registry.GetAllEntities().

Orchestrator.DeleteEntity(entityId)

Orchestrator.DeleteEntity(entityId: string) -> boolean

Finds the entity and calls entity:Destroy(). Factory's destroy wiring then handles registry cleanup and network notifications.

Returns

  • true if an entity existed and destroy was requested
  • false if not found

Orchestrator.DeleteAllEntities()

Orchestrator.DeleteAllEntities() -> boolean

Destroys every live entity.

Important behavior

  • snapshots the registry before iterating so destroy-time unregisters do not corrupt the loop
  • wraps each destroy in pcall
  • always returns true once initialized

Orchestrator.PoolEntity(entityId)

Orchestrator.PoolEntity(entityId: string) -> boolean

Moves a live entity into the registry's class pool.

Behavior

  1. looks up the live entity
  2. marks entity._privateProperties.IsValid = false
  3. unregisters it from the live entity registry
  4. if running on the server, broadcasts OnEntityPooled
  5. pushes it into Registry.PoolEntity(className, entity)

Returns

  • true if an entity was pooled
  • false if not found

Gotcha

The broadcast is server-only by design. Clients receive OnEntityPooled and call PoolEntity(...) locally; rebroadcasting from the client would recurse into an invalid server-only path.


StateMachine Delegates

Orchestrator.CreateStateMachine(params)

Orchestrator.CreateStateMachine(params: {
    StateMachineClass: any,
    StateMachineId: string,
    Context: {[any]: any}?,
}) -> any?

Shared wrapper over Factory.CreateStateMachine(...).

Orchestrator.GetStateMachine(stateMachineId)

Orchestrator.GetStateMachine(stateMachineId: string) -> any?

Returns the live FSM from Factory.Registry.

Orchestrator.GetStateMachines()

Orchestrator.GetStateMachines() -> {[string]: any}

Returns Registry.GetAllStateMachines().

Orchestrator.CancelStateMachine(stateMachineId)

Orchestrator.CancelStateMachine(stateMachineId: string) -> boolean

Finds the FSM and calls sm:Cancel().

Returns

  • true if found
  • false if not found

Orchestrator.RetryStateMachine(stateMachineId)

Orchestrator.RetryStateMachine(stateMachineId: string) -> boolean?

Destroys a live FSM and recreates it from its compiled class and shallow-cloned context.

Exact behavior

  1. read old FSM from registry
  2. shallow clone oldSM.Context
  3. resolve the compiled class using Factory.Get("StateMachine", oldSM.Name)
  4. unregister the old FSM
  5. destroy the old FSM
  6. create a new FSM with the same ID and cloned context

Returns

  • true if retry was requested successfully
  • nil / falsy if the FSM or compiled class cannot be found

Gotcha

The context clone is shallow. Nested tables, instances, and other reference types remain shared.

Orchestrator.CancelAll()

Orchestrator.CancelAll() -> boolean

Cancels every live FSM after snapshotting the registry table.


Network API Delegates

Orchestrator re-exposes the core networking helpers from NetworkManager.

Context restrictions

Method Allowed caller Notes
RegisterEntityCommandCallback shared client or server
ServerCommandEntity client only sends EntityCommandEvent:FireServer(...)
RegisterServerRequestCallback server only installs sync or async server request handlers
ServerRequest client only synchronous RemoteFunction request
ServerRequestAsync client only async RemoteEvent request
BroadcastEntityCommand server only broadcasts to all clients

Orchestrator.RegisterEntityCommandCallback(entityId, command, handler)

Orchestrator.RegisterEntityCommandCallback(
    entityId: string,
    command: string,
    handler: (Player, ...any) -> ()
) -> boolean

Registers a command handler under NetworkManager.EntityCommandCallbacks[entityId][command].

Orchestrator.ServerCommandEntity(entityId, command, ...)

Orchestrator.ServerCommandEntity(entityId: string, command: string, ...: any) -> ()

Client-side fire-and-forget entity command.

Example

FSM.Orchestrator.ServerCommandEntity("door:front", "Toggle", true)

Orchestrator.RegisterServerRequestCallback(requestName, handler, aSync)

Orchestrator.RegisterServerRequestCallback(
    requestName: string,
    handler: (Player, ...any) -> any,
    aSync: boolean
) -> boolean

Registers a named server request handler.

  • aSync = false or omitted -> synchronous ServerRequest
  • aSync = true -> async ServerRequestAsync

Orchestrator.ServerRequest(requestName, ...)

Orchestrator.ServerRequest(requestName: string, ...: any) -> any

Client-side synchronous request/response path using RemoteFunction:InvokeServer(...).

Orchestrator.Request

Alias of Orchestrator.ServerRequest.

Orchestrator.ServerRequestAsync(requestName, ...)

Orchestrator.ServerRequestAsync(requestName: string, ...: any) -> ()

Client-side async request path using RemoteEvent:FireServer(...).

Orchestrator.BroadcastEntityCommand(requestType, entityId, ...)

Orchestrator.BroadcastEntityCommand(requestType: string, entityId: string, ...: any) -> boolean

Server-side broadcast helper.

Example

FSM.Orchestrator.BroadcastEntityCommand("SyncClient", "door:front", "ApplyState", "Open")

EventBus API

Event buses are local Signal objects, not network replicated buses.

Orchestrator.RegisterEventBus(name)

Orchestrator.RegisterEventBus(name: string) -> any

Creates the named bus if it does not exist and returns it.

Orchestrator.GetEventBus(name)

Orchestrator.GetEventBus(name: string) -> any?

Returns an existing bus, or nil.

Orchestrator.FireEventBus(name, ...)

Orchestrator.FireEventBus(name: string, ...: any) -> ()

Fires the named bus if it exists. Missing buses are ignored.

Orchestrator.AwaitEventBus(name, timeout?)

Orchestrator.AwaitEventBus(name: string, timeout: number?) -> any?

Waits in a polling loop until the bus exists or the timeout elapses.

Orchestrator.UnregisterEventBus(name)

Orchestrator.UnregisterEventBus(name: string) -> ()

Destroys the signal if it has a Destroy method, then removes it from the registry.

Example

local bus = FSM.Orchestrator.RegisterEventBus("QuestUpdated")

bus:Connect(function(questId, state)
    print("Quest changed", questId, state)
end)

FSM.Orchestrator.FireEventBus("QuestUpdated", "daily-1", "Completed")

Event bus lifecycle

[RegisterEventBus] ──→ [Signal stored in EventBuses] ──→ [GetEventBus / AwaitEventBus]
                              β”‚
                              └──→ [UnregisterEventBus]

[GetEventBus / AwaitEventBus] ──→ [FireEventBus] ──→ [Signal listeners run]

Client/Server Replication Startup

Server-side callback registration

During initialization, Orchestrator registers a server request called RequestEntitySnapshot.

That handler returns an array of sanitized entity snapshots containing:

  • Id
  • ClassName
  • Instance
  • Data (only schema fields with Replicate = true, plus _v)
  • Version

It also registers __DebugRemoteStats, which exposes NetworkManager._remoteStats for debugging dashboards.

Client-side startup work

When running on the client, initialization:

  1. creates _pendingUpdates buffer
  2. connects EntityUpdateEvent
  3. buffers updates that arrive before the entity exists locally
  4. validates _v version ordering
  5. applies replicated data directly into entity committed Data
  6. calls ApplyChanges(...) with _v stripped out
  7. connects EntityCommandEvent
  8. reacts to OnEntityCreated, OnEntityPooled, and OnEntityDestroyed
  9. calls SyncEntitiesFromServer() to fetch the initial snapshot

Replication data flow

[Server entity UpdateEntity]
└──→ [Factory StateUpdated listener]
     └──→ [BroadcastEntityUpdate]
          └──→ [Client EntityUpdateEvent]
               └──→ {Entity exists locally?}
                    β”œβ”€β”€ no  β†’ [Buffer in _pendingUpdates]
                    β”‚         β†’ [OnEntityCreated / snapshot create entity]
                    β”‚         β†’ [Drain buffered updates in version order]
                    └── yes β†’ [Validate version and schema]
                              β†’ [Write committed Data]
                              β†’ [ApplyChanges]

Scheduler Integration

During initialization, Orchestrator creates a scheduler instance and starts it. It then disables the internal BaseStateMachine heartbeat and schedules a recurring scheduler task:

TaskName = "GlobalStateMachineHeartbeat"
Priority = 1
Event = "Heartbeat"
IsRecurringTask = true
TaskExecutionDelay = 0

That task computes dt from wall clock, clamps it to 0.1, and calls:

Core.Factory.BaseModules.BaseStateMachine.Step(dt)

This is the reason FSM updates run through Scheduler instead of through BaseStateMachine's own RunService.Heartbeat connection once Orchestrator is active.


Real Examples

Minimal server bootstrap

local FSM = require(game:GetService("ReplicatedStorage").RBXStateMachine)
FSM.Orchestrator:RegisterComponents()

Studio client bootstrap with lazy dashboard

local RunService = game:GetService("RunService")
local FSM = require(game:GetService("ReplicatedStorage").RBXStateMachine)

FSM.Orchestrator:RegisterComponents()

if RunService:IsStudio() then
    FSM.Orchestrator:StartServiceManager()
end

Registering a server request and calling it from a client

-- Server
FSM.Orchestrator.RegisterServerRequestCallback("GetDoorState", function(player, entityId)
    local door = FSM.Orchestrator.GetEntity(entityId)
    return door and door.IsOpen or nil
end, false)

-- Client
local isOpen = FSM.Orchestrator.ServerRequest("GetDoorState", "door:front")

Edge Cases & Gotchas

  1. RegisterComponents() is the one true bootstrap. Calling lower-level pieces manually can leave shared state half initialized.
  2. StartServiceManager() is lazy UI startup, not core boot. Core systems already came up during RegisterComponents().
  3. Request is just an alias of ServerRequest. It is still client-only.
  4. RetryStateMachine() uses oldSM.Name to resolve the compiled class. Renamed or dynamically wrapped FSMs can fail lookup.
  5. Orchestrator.Entities / Orchestrator.StateMachines are snapshots captured during init. Use GetEntities() / GetStateMachines() for fresh clones.
  6. Event buses are local only. They do not cross the network.
  7. Client replication writes directly into committed Data. That path bypasses UpdateEntity() and only calls ApplyChanges(...) after validation.
  8. Server-only methods assert in the wrong context. BroadcastEntityCommand and RegisterServerRequestCallback must not be called from clients.
  9. Snapshot sync is version-aware but shallow. Existing entity context objects such as Instance references still come from creation-time context, not from replicated deltas.
  10. Initialization can return false while already booting. Guard startup code accordingly if multiple scripts may race to initialize.

Related Pages

🏠 Home


πŸš€ Getting Started


πŸ—οΈ Architecture


πŸ”§ API Reference


πŸ“š Guides


βš™οΈ Advanced


πŸ› οΈ Development


πŸ“– Reference

Clone this wiki locally