Skip to content

Actor paths

Every actor in the system has a path — a hierarchical identifier built from its position in the supervisor tree:

actor-ts://my-app/user/api/sessions/user-42
│ │ │ │ │
│ │ │ │ └── leaf actor name
│ │ │ └── parent (a SessionManager)
│ │ └── grandparent (an Api manager)
│ └── user-guardian (every user actor is under /user)
└── system name from ActorSystem.create('my-app')

The path identifies an actor uniquely within its system, the way a filesystem path identifies a file: at any moment, the path either resolves to a live actor or doesn’t.

You’ll see paths in three places:

  • Log lines — every actor’s log is bound to its path, so log messages carry the full hierarchy.
  • Terminated.actor.path — when an actor stops, watchers can inspect the path to dispatch on which actor it was.
  • ActorSelection — looking up actors by path string rather than holding a ref.

context.path and ref.path both return an ActorPath:

import { Actor } from 'actor-ts';
class Logger extends Actor<Msg> {
override preStart(): void {
this.log.info(`starting; my path is ${this.context.path}`);
// → "starting; my path is actor-ts://my-app/user/logger"
this.log.info(`depth = ${this.context.path.depth()}`);
// → "depth = 2" (root is 0, /user is 1, /user/logger is 2)
this.log.info(`elements = ${this.context.path.elements().join('')}`);
// → "elements = / → user → logger"
}
}

The full surface:

MemberWhat it gives you
path.nameThe leaf segment ('logger' above).
path.parentThe parent’s ActorPath, or null for the root.
path.systemNameThe system name ('my-app' above).
path.elements()Array of segment names from root to leaf.
path.depth()Distance from root (root = 0).
path.isAncestorOf(other)Does this path enclose other?
path.equals(other)Lexical equality (compares toString() output).
path.toString()Canonical URI form: actor-ts://<sys>/<segments>.

Paths are immutable values — calling path.child('thing') returns a new path; it doesn’t mutate the original.

You can pass a name when spawning:

const ref = system.actorOf(Props.create(() => new Foo()), 'my-foo');
// ref.path.toString() === 'actor-ts://my-app/user/my-foo'
const child = this.context.actorOf(Props.create(() => new Bar()), 'bar');
// child.path.toString() === 'actor-ts://my-app/user/my-foo/bar'

If you omit the name, the framework synthesizes one ('$1', '$2', …). Two rules govern naming:

  1. Names must be unique among siblings. context.actorOf(props, 'bar') followed by another context.actorOf(props, 'bar') on the same parent throws — that path is already in use.
  2. The name “looks like a path segment.” No slashes, no leading dots, no whitespace. The framework validates this.

The uid field on ActorPath distinguishes successive incarnations of the same path — if my-foo stops and is then re-spawned at the same path, the second one has a different uid. Most code never looks at uid; the framework uses it internally to make sure messages aimed at the old incarnation don’t accidentally land in the new one.

import { ActorSystem } from 'actor-ts';
const system = ActorSystem.create('my-app');
const selection = system.actorSelection('/user/api/sessions/user-42');
const ref = await selection.resolveOne(5_000);
ref.tell({ kind: 'whatever' });

actorSelection(pathString) builds a description of where to look. resolveOne(timeoutMs) walks the actor tree to find a match; it retries every 10 ms until the deadline, useful when the caller races with the actor’s spawn.

Two ways to send to a selection:

  • selection.tell(msg) — resolve once, deliver immediately if found, dead-letter if not. No retry. Useful when you have a clear expectation the actor exists; missing it is an error.
  • (await selection.resolveOne(timeout)).tell(msg) — wait until the actor exists (or time out). Useful for spawn-race scenarios.

actorSelection parses three input shapes:

system.actorSelection('actor-ts://my-app/user/api'); // absolute URI
system.actorSelection('/user/api'); // absolute path
system.actorSelection('user/api'); // absolute path, no leading slash

URI form is what ActorPath.toString() produces, so round-tripping a path through string form works. The system-name in URI form is checked — selecting actor-ts://other-app/user/api from a system named my-app returns null (a different system entirely).

Refs are the default way to address actors. Hold the ActorRef you got at spawn time, pass it around, store it in fields. The compiler knows the actor’s message type; selection returns unknown-typed refs that you lose typing on.

Reach for an ActorSelection in three situations:

  1. Cross-tree lookup by convention — the actor you want lives at a well-known path (/user/sharding/region, /system/pubsub), and there’s no clean way to thread the ref through.
  2. Spawn-race resolution — caller doesn’t know exactly when the target spawns; resolveOne(timeout) waits.
  3. Looking up actors received as strings — e.g. an HTTP request body contains "/user/sessions/user-42" and you want to forward to that actor. Parse + select; never trust the string without re-checking that the actor exists.

For routine in-actor wiring, prefer passing refs through constructors / messages / parent-child relationships:

// ✗ awkward
class Worker extends Actor<...> {
override onReceive(msg) {
const cache = await this.context.actorSelection('/user/cache').resolveOne();
cache.tell({ kind: 'put', ... });
}
}
// ✓ direct
class Worker extends Actor<...> {
constructor(private readonly cache: ActorRef<CacheMsg>) { super(); }
override onReceive(msg) {
this.cache.tell({ kind: 'put', ... });
}
}
const cache = system.actorOf(Props.create(() => new Cache()), 'cache');
const worker = system.actorOf(Props.create(() => new Worker(cache)));

The constructor-injection version is type-safe, doesn’t depend on path-naming conventions, and survives renames trivially.

Three top-level guardian paths exist in every system:

PathWhat lives there
/userYour application’s top-level actors (everything system.actorOf(...) creates).
/systemFramework internals (event-stream listeners, cluster gossipers, the dead-letter actor itself, …).
/deadLettersThe synthetic recipient for messages with no live target.

You rarely interact with /system paths directly — the extensions (cluster, pubsub, sharding) expose their own typed APIs that shield you from the path scheme. But knowing the paths exist is useful when reading log output or debugging “where did this message go?”

When the cluster extension is active, paths get a host-port fragment:

actor-ts://my-app@10.0.0.5:2552/user/api/sessions/user-42
│ │ │
│ └── node-host:port
└── system name

That fragment tells the runtime which node this actor lives on. A bare path like actor-ts://my-app/user/foo (no host) is local to the resolving system. The cluster transport handles the host-routed delivery transparently — your tell looks the same in both cases.

See Refs across nodes for how the host-aware path is wire-encoded.

  • Props — the configuration bundle that includes the optional name for the actor’s path.
  • Actor system — the guardian hierarchy (/user, /system, /deadLetters).
  • Refs across nodes — how paths and refs work across cluster nodes.
  • Discovery — for actors whose location isn’t known by path but by service-registry semantics.

The ActorPath and ActorSelection API references cover the full method set.