Systems

Headless, singleton manager classes in the Aircada SDK decorated with @System to coordinate global state orchestration and background tasks.
Published: 7/10/2026

While Operators represent modular visual or physical instances scattered across 3D meshes, Systems represent headless, singleton controllers running continuously in the background of the Aircada experience.

They have the same capabilities as Operators, but they have no Visual Studio presence (so their @Property fields are not visually exposed) and are instantiated exactly once to coordinate global application logic, analytical triggers, or complex multi-component rules.

2. Configuration Orchestrator Example#

An example of a System managing global state and routing events.

Implementation of a hub orchestrator managing global state, applying rules, and communicating with spoke Operators.

ConfigOrchestrator.ts (typescript)
// src/systems/ConfigOrchestrator.ts

import {
	System, Context, Input, Output, Setup, Dispose,
	AircadaContext, AirEvent
} from "@aircada/spec";
import { Events, EventPayloads, PedestalColor, SpinState } from "../registry/registry.generated";

@System({
	name: "Configuration Orchestrator",
	type: "config_orchestrator"
})
export class ConfigOrchestrator {

	// 1. Context Fencing
	@Context()
	air!: AircadaContext;

	// 2. The Master State (The single source of truth)
	private currentColor: PedestalColor = "BLUE";
	private currentSpin: SpinState = "IDLE";

	// 3. Execution Commands (Outputs sent to the Spokes)
	@Output({ name: "Apply Color", emits: Events.APPLY_COLOR })
	onApplyColor = new AirEvent<EventPayloads[typeof Events.APPLY_COLOR]>();

	@Output({ name: "Apply Spin State", emits: Events.APPLY_SPIN_STATE })
	onApplySpinState = new AirEvent<EventPayloads[typeof Events.APPLY_SPIN_STATE]>();

	// 4. Facts (Outcomes sent back to the UI)
	@Output({ name: "Validation Failed", emits: Events.VALIDATION_FAILED })
	onValidationFailed = new AirEvent<EventPayloads[typeof Events.VALIDATION_FAILED]>();

	// Memory management closures
	private unsubValidation?: () => void;

	@Setup()
	setup() {
		console.log("ConfigOrchestrator initialized. Master state set to defaults.");

		// Example of capturing local events for logging and memory safety
		this.unsubValidation = this.onValidationFailed.addListener((payload) => {
			console.warn(`[Hub Validation Error]: ${payload.reason}`);
		});

		// Initialize the scene to the default state
		// We defer slightly to ensure all Spoke Operators have mounted
		setTimeout(() => {
			this.onApplyColor.invoke({ color: this.currentColor });
			this.onApplySpinState.invoke({ state: this.currentSpin });
		}, 100);
	}

	// 5. Hub Logic: Intercept UI Requests

	@Input({ name: "Handle Color Request", onEvent: Events.REQUEST_COLOR_CHANGE })
	handleColorRequest(payload: EventPayloads[typeof Events.REQUEST_COLOR_CHANGE]) {
		this.currentColor = payload.color;

		// Enforce implicit rule: If we turn red, we must stop spinning
		if (this.currentColor === "RED" && this.currentSpin === "SPINNING") {
			this.currentSpin = "IDLE";
			this.onApplySpinState.invoke({ state: "IDLE" });
		}

		// Fire the validated execution command
		this.onApplyColor.invoke({ color: this.currentColor });
	}

	@Input({ name: "Handle Spin Request", onEvent: Events.REQUEST_SPIN_TOGGLE })
	handleSpinRequest(payload: EventPayloads[typeof Events.REQUEST_SPIN_TOGGLE]) {
		// Enforce explicit business rule: Red cannot spin.
		if (this.currentColor === "RED" && payload.state === "SPINNING") {
			// Reject the command and inform the UI
			this.onValidationFailed.invoke({ reason: "The pedestal cannot spin while it is RED." });
			return;
		}

		// State is valid. Update master state and execute.
		this.currentSpin = payload.state;
		this.onApplySpinState.invoke({ state: this.currentSpin });
	}

	@Dispose()
	dispose() {
		if (this.unsubValidation) {
			this.unsubValidation();
		}
	}
}
System class orchestrating pedestal color and spin states based on requests.

1. Systems vs Operators#

Systems have the same capabilities as Operators but are singleton-managed by the backend and have no Studio UI presence.

A system class can implement all the same logic and use the same decorators that an operator can, but its instantiation and lifecycle are completely managed by the backend, in contrast to operators that can be instantiated numerous times.

And unlike operators, @Property declarations cannot be viewed or controlled in the docs.studio environment.

  • Singleton Lifecycle: Automatically managed by the backend.
  • No Studio UI: @Property declarations are not visible in the Visual Studio.

System Setup Lifecycle and Async Coordination#

How Aircada schedules, sequences, and resolves asynchronous setup actions in System components.

Headless @System controllers support the exact same lifecycle stages, async scheduling, and topological dependency sorting mechanisms as @Operator components.

Stage Scheduling and Coordination

Just like Operators, Systems can declare @Setup({ stage: 'bootstrap' | 'initialize' | 'finalize' | 'runtime' }) methods. Systems are typically used to coordinate and instantiate multiple Operator child instances during the early bootstrap stage, ensuring that child operators are fully mounted and ready before later stages run.

Topological Constraints Across Systems

Systems can declare before and after execution dependencies relative to other Systems or Operators. This makes it straightforward to establish a clear bootstrapping order for complex configurator backends.

OrchestratorSystem.ts (typescript)
import { System, Context, Setup, AircadaContext } from '@aircada/spec';

@System({
  name: 'OrchestratorSystem',
  type: 'orchestrator_system'
})
export class OrchestratorSystem {
  @Context()
  air!: AircadaContext;

  @Setup({ stage: 'bootstrap' })
  async setup() {
    console.log('[OrchestratorSystem] Instantiating child operators for dependency scheduler verification...');
    // Programmatically create the dependent operators so they participate in later stages
    await this.air.operators.create('normal_stage_operator');
    await this.air.operators.create('topo_a');
    await this.air.operators.create('topo_b');
    await this.air.operators.create('topo_c');
  }
}
A System instantiating dependent operators during the bootstrap stage.

3. Programmatic Operator Instantiation#

Details how custom Systems can dynamically create, configure, and manage Operators programmatically at runtime.

In advanced orchestration scenarios, a headless @System can dynamically spawn, configure, and manage Operator instances at runtime rather than relying purely on static, designer-placed operators in the Studio scene.

This is accomplished using the this.air.operators.create("OPERATOR_TYPE", { properties }) API, exposing dynamic event listening and lifecycle delegation.

Lifecycle Stage Scheduling

Operators instantiated during a System's @Setup({ stage: 'bootstrap' }) hook are registered with the engine's loading manager in time to participate in all subsequent lifecycle phases. Once created, their @Setup hooks for initialize, finalize, and runtime stages will be automatically scheduled and executed in order by the engine alongside static operators.

Programmatically created operators function as headless, runtime-managed processors that can process inputs and emit signals, completely governed by the orchestrating System.

  • Dynamic Creation: Instantiate helper operators on-demand during system initialization or in response to events.
  • Lifecycle Delegation: The system must explicitly manage and clean up any programmatically spawned operators.
  • Property Configuration: Properties are passed dynamically as configuration options matching the operator's @Property keys.
UnderlineSystem.ts (typescript)
import {
    System, Context, Setup, Dispose,
    AircadaContext
} from "@aircada/spec";

@System({
    name: "Underline System",
    type: "underline_system"
})
export class UnderlineSystem {
    @Context()
    air!: AircadaContext;

    private boundsOperator!: any;

    @Setup()
    async setup() {
        console.log("Initializing UnderlineSystem...");

        // Programmatically spawn a helper operator at runtime
        this.boundsOperator = await this.air.operators.create("BOUNDS", {
            targetUnitId: "some-unit-id"
        });

        // Event subscription mapping for programmatically created operator signals
        if (this.boundsOperator && this.boundsOperator.onBoundsChanged) {
            this.boundsOperator.onBoundsChanged.addListener((bounds: any) => {
                console.log("Dynamic bounds update received in System:", bounds);
            });
        }
    }

    @Dispose()
    async dispose() {
        console.log("Disposing UnderlineSystem and programmatically created operators...");
        
        // CRITICAL: Clean up dynamically created operators to prevent runtime leaks
        if (this.boundsOperator) {
            await this.boundsOperator.dispose();
        }
    }
}
System dynamically spawning a BOUNDS operator at setup and disposing it on cleanup.