Machine.spawn allocates an unstarted actor. Start it and stop it, or attach it to an Effect scope.
const program = Effect.scoped(
Machine.scoped(
Effect.gen(function* () {
const actor = yield* Machine.spawn(machine);
yield* actor.start;
yield* actor.send(Event.Start);
return yield* actor.awaitOutput;
}),
),
);Use Machine.run for an autonomous actor. It starts the actor, waits for output, and stops the actor on success, failure, or interruption.
Use ActorSystemService for named lookup and multi-actor coordination. system.spawn starts the actor.
const system = yield * ActorSystemService;
const actor = yield * system.spawn("checkout", checkoutMachine);
const maybeActor = yield * system.get("checkout");Terminal actors leave the registry. A stopped actor keeps its final snapshot and lifecycle value through its existing ActorRef.
Use a typed key when lookup callers must keep the machine state, event, and output types.
const CheckoutActor = actorSystemKey<CheckoutState, CheckoutEvent>("checkout");
const actor = yield * system.spawn(CheckoutActor, checkoutMachine);
const maybeActor = yield * system.get(CheckoutActor);
const generations = system.watch(CheckoutActor);watch starts with the current actor or Option.none(). It then emits only when that ID starts or stops. A later actor generation with the same key emits its new ActorRef.
Spawn a child from a state-owned Effect:
machine.spawn(State.Menu, ({ self }) =>
self.spawn("menu", menuMachine).pipe(Effect.asVoid, Effect.orDie),
);The child uses the same actor system. The child stops when the parent exits the owning state. A child from .background lives until the parent actor stops.
This pattern replaces a root router that invokes one screen actor for each route. See actor-system.ts.
Use ActorHost when consumers request a child but the parent state must own its lifetime. Construct the host in a scoped service layer. Keep session validation and authorization in that service.
const menuHost =
yield *
ActorHost.make({
identity: (input: { sessionId: string }) => input.sessionId,
spawn: (input) => Machine.spawn(menuMachine, { input }),
});
parent.spawn(State.Menu, ({ state }) =>
menuHost.host({ sessionId: state.sessionId }).pipe(Effect.asVoid, Effect.orDie),
);
const menu = yield * menuHost.acquire({ sessionId });host registers one generation in the current state scope. The first matching acquire supplies the factory input. Identity values match with Object.is. Concurrent consumers share startup and its result. Consumer cancellation does not cancel startup or stop the actor. ActorHost starts the factory result, so both Machine.spawn and system.spawn work.
When the parent owns startup data, give spawn a typed second argument. Pass that data to host; consumers still provide only the request input.
const screenHost =
yield *
ActorHost.make({
identity: (request: { sessionId: string }) => request.sessionId,
spawn: (request, screen: { destination: string }) =>
Machine.spawn(screenMachine, { input: { ...request, ...screen } }),
});
parent.spawn(State.Screen, ({ state }) =>
screenHost
.host({ sessionId: state.sessionId }, { destination: state.destination })
.pipe(Effect.asVoid, Effect.orDie),
);
const screen = yield * screenHost.acquire({ sessionId });The second argument belongs to the registered generation. A consumer cannot replace it. Reentry supplies new host data. Host data is passed by reference, so use immutable values. Existing one-argument factories need no host data.
The factory uses the services captured when the host was made. Its Scope and ActorScope belong to the hosting generation. State exit closes the actor. Closing the host service also closes the current generation and ends consumers waiting for a future generation.
Consumers receive ActorHostClosedError before actor cleanup starts. An acquisition belongs to the generation it observed. Acquire again after reentry to get the new actor. The registered host wait is interrupted when its generation closes. Calls made after the host service closes fail with ActorHostClosedError. An overlapping host fails with ActorHostOccupiedError, including while the previous actor is still being cleaned up.
Factory failures stay in the typed error channel and remain shared until the hosting scope closes. Handle expected factory failures in the parent's spawn handler with a state transition or event. The example uses Effect.orDie because it treats the remaining host errors as wiring failures. A defect stops the parent; it cannot reenter to retry. Both host error classes are exported from the package root.
sendqueues an event and returns.callwaits for event processing and returnsProcessEventResult.askreturns the schema-checked reply for anEvent.replyevent.- A transition can return
Machine.deferReply(nextState)when state-owned work must produce the reply later. The work callsself.reply(value)once it has the value. The value is decoded with the event's reply schema before the pendingaskis settled. A decode mismatch is a defect and also settles the caller with that defect. If state exit interrupts decoding, the pending caller is settled with the same interruption and the actor can continue. A secondself.replyreturnsfalse. waitForwaits for a state constructor or predicate. Its predicate sees the current snapshot and then every later state write, each once, in write order, and one call at a time. The wait returns the first match, and the predicate is not called after it matches. A predicate that keeps state (counts calls, remembers an earlier state) therefore gets the same answer as a replay of the written states. A predicate that sends an event does not see the resulting states inside its own call: it sees them in later calls.awaitFinalreturns the retained final state.sendAndWaitstarts to observe, judges the snapshot from before the event, sends the event, then waits likewaitFor, or likeawaitFinalwithout a target. A brief state that the event causes is not missed. The event is sent even when the snapshot already matches. It is not sent when the predicate throws on the snapshot.- These waits end when a state matches or the actor terminates. When the actor terminates (final, stop, drain, or defect) and no state from the start of the wait matches, they fail with
ActorStoppedError. A state counts as reached once it is written, even when its entry crashes later, and even when a stop comes before its notification. A predicate that throws fails the wait with that defect. A wait releases its listener and its subscription when it ends, also when it is interrupted at any point. - A sleeping wait wakes at the notification that follows a state write, or at the terminal exit. Code that holds the actor between a write and its notification (for example, holds the
latestTransitionpermit) keeps the wait asleep until that notification or the terminal exit runs. The wait then judges every state written in between, in write order. - A supervised restart keeps this order: the waits see a state written by an entry that crashed, then the restart state, then the states of the new generation, in write order.
- A state restored by recovery reaches waiters and
subscribelisteners like any other state change. subscribe(andactor.client.subscribe, andself.client.subscribeinside the machine) calls the listener with every state written after the call, each once, in write order. It does not call it with the state that is current when it subscribes; readgetSnapshotfor that. Each call is its own registration: the same function subscribed twice gets each write twice, and each unsubscribe ends one registration. A listener added inside another listener's callback gets only later writes too. A listener is not called again while its own callback runs: when the callback sends an event, the listener gets the resulting states after it returns, and every other listener still gets the older state first. A supervised restart keeps write order for listeners as it does for waits. When a stop comes before the notification of the last write, the listeners still get that write beforeawaitExitcompletes. After the terminal exit, and after unsubscribe returns, a listener gets nothing more.changesstreams the same order, starting with the state current at subscription.awaitOutputreturns the final output.awaitExitreturnsFinal,Stopped, orDefect.stopandawaitExitcomplete after the publiclifecyclereports the same terminal result.stop,Machine.scoped, andawaitExitshare one shutdown completion. Each caller waits for runtime, child, and scope finalizers. An interrupted waiter does not cancel the shutdown owner. A cleanup failure returns aDefectwith phasecleanupand remains visible to later stop callers.clientexposes send, stop, snapshot, matching, subscription, and Promise-based capability checks outside Effect.client.canSyncsupports Boolean transition predicates only.- React and Solid should use Actor Atoms for state and capability subscriptions.