ServicesECS

Systems and the Coordinator

A system is a named processing unit: a stored entity query plus scheduling metadata. The server never runs your game logic - systems are workers you write, and the per-index coordinator drives them: it ticks on a timer, hands each connected system the entities matching its query, and applies the mutations the system sends back.

Registering systems

snug ecs system register -x game1 --name MovementSystem \
  --has-all Position,Velocity --priority 100 --timeout-ms 5000 \
  --include-resources GameConfig
snug ecs system list -x game1
snug ecs system get -x game1 --name MovementSystem
snug ecs system update -x game1 --name MovementSystem --priority 10
snug ecs system delete -x game1 --name MovementSystem --force

Registering a name that already exists fails with 409 system_exists. --include-resources names resources whose current values are bundled into every tick payload the system receives.

Execution order

Lower priority values run first (the CLI help says so, and it is verified: priorities 50 and 100 ordered the 50 first; updating the 100 to 10 moved it to the front):

snug --output json ecs system execution-order -x game1
["MovementSystem", "RegenSystem"]

The order is a topological sort over each system's runs_after / runs_before dependencies, with ascending priority (then name) breaking ties. The dependency fields exist only in the HTTP register request - the CLI exposes priority alone. Disabled systems are excluded from the result.

Enabled, paused, and manual runs

Two independent off-switches, both verified to skip the system during coordinator ticks:

  • enable / disable flips the enabled flag stored on the system definition - the durable configuration switch.
  • pause / resume flips a runtime flag stored next to the system's stats - for temporarily suspending a live system.
snug ecs system disable -x game1 --name RegenSystem
snug ecs system enable -x game1 --name RegenSystem
snug ecs system pause -x game1 --name MovementSystem
snug ecs system resume -x game1 --name MovementSystem

execute triggers a single server-side run outside the tick loop - and it runs even if the system is disabled. With --dry-run it resolves the matching entities without recording the run:

snug --output json ecs system execute -x game1 --name MovementSystem --dry-run
{
  "name": "MovementSystem",
  "tick": 0,
  "entity_count": 1,
  "execution_time_ms": 2,
  "mutations_applied": 0,
  "dry_run": true
}

system stats accumulates per-run counters (execution_count, avg_entity_count, avg_execution_time_ms, error_count); dry runs are not counted.

The coordinator

One coordinator per index, running a tick loop:

snug ecs coordinator start -x game1 --tick-rate 100   # milliseconds
snug --output json ecs coordinator status -x game1
snug ecs coordinator stop -x game1
{
  "index": "game1",
  "running": true,
  "tick": 11,
  "tick_rate_ms": 100,
  "last_tick_at": "2026-08-28T04:36:05.495462+00:00",
  "connected_systems": 0
}

tick and last_tick_at advance while running (captured one second after a 100 ms start - eleven ticks in). connected_systems counts workers currently attached over WebSocket.

The worker loop

Workers attach to GET /api/v1/ecs/{index}/coordinator/ws, authenticating with a Bearer header or ?token=. Verified handshake behavior: the upgrade succeeds (101) only while the coordinator is running - a stopped coordinator rejects with 400, a missing token with 401. Once attached, a worker registers under its system name, receives a payload each tick with the matching entities and any include_resources values, and replies with mutations for that tick. The message schemas are in the Coordinator API reference.

Submitting mutations over REST

Mutations can also be submitted outside the WebSocket loop - useful for batch fix-ups and offline workers. Each mutation sets and/or removes components on one entity, optionally guarded by expected_version:

snug --output json ecs coordinator mutations -x game1 \
  --mutations '[{"entity_id":"player1","set_components":{"Health":{"current":50,"max":100}}}]'
{
  "results": [
    { "entity_id": "player1", "success": true, "error": null, "new_version": 5 }
  ],
  "succeeded": 1,
  "failed": 0
}

Results are per entity: a stale expected_version fails only that entry ("error": "Version conflict: expected 1, found 7") while the rest apply. The endpoint works whether or not the coordinator is running.

Caution: unlike entity create and component set, the mutation path does not validate component data against schemas - a payload that violates its schema is stored as-is (reproduced live). Validate in the worker.

On this page