Repeated placements are separate authored locations built from the same definition. They share the state tree you wrote, but not state, facets, mounted work, or runtime progress.
The distinction becomes visible as the chart qualifies each reusable local name:
IndicatorIndicatorLeftRightLeft.WaitingRight.WaitingDashboard.Left.Waiting.PulseDashboard.Right.Waiting.PulseOnly the first row is shared. Every row beneath it names a different part of the assembled model.
One definition, two authored locations
Place the same Component twice, then put both placements under a Parallel host:
const Indicator = Component.make(() => {
class Indicator extends State.Compound<Indicator>()("Indicator") {
static states = States.make(() => [Waiting, Complete]);
}
class Waiting extends State.Atomic<Waiting>()("Waiting") {}
class Complete extends State.Atomic<Complete>()("Complete") {}
return Indicator;
});
class Left extends Indicator.as<Left>()("Left") {}
class Right extends Indicator.as<Right>()("Right") {}
class Dashboard extends State.Parallel<Dashboard>()("Dashboard") {
static states = States.make(() => [Left, Right]);
}
Left and Right are ordinary placement classes. Each lowers the captured Indicator tree at a
different path; there is no hidden Indicator segment and no shared running singleton behind the
two classes. The placement tags also contribute to chart hashing, so renaming a placement changes
model identity rather than display copy.
Sibling placement tags must be unique. The same local names remain legal inside different placements because the surrounding path makes them distinct.
Qualification is structural
Inside the definition, Waiting arms one Timer site named Pulse. Placement derives typed
references for each authored location:
Left.Waiting;
Right.Waiting;
Left.Waiting.timers.Pulse;
Right.Waiting.timers.Pulse;
The state nodes can be used as exact transition targets and Query subjects. The Timer references
can be used by tools such as migration to inspect or dispose the left and right occurrences
independently. Authors keep the readable local name Pulse; Component supplies the qualification.
Three names are doing different jobs:
PulseTimer- The Timer definition whose duration is bound by the Simulator or runtime Layer.
PulseThe local named site authored once beneath
Waiting.Dashboard.Left.Waiting.Pulse- The complete mounted coordinate for one placement's currently armed Timer.
Now follow that identity from one placement to two, then address only one occurrence:
Reuse definitions; qualify every occurrence
Two placements share behavior, never runtime identity.
Repeated placements share one component definition while keeping every active node and mounted citizen distinct. Placement paths make local names reusable without letting one occurrence complete another.
1 · Carry citizens through one placement
A placement qualifies the component's mounted work.
Indicator owns a local Pulse Timer beneath Waiting. Placing the component as Player preserves that authored citizen while qualifying it by the placement path, so its durable identity is no longer merely the local name Pulse.
import { Component, State, Statechart, States, Timer, Transitions } from "@motive/motive";
const PulseTimer = Timer.relative("PulseTimer");
const Pulse = PulseTimer.as("Pulse");
const Indicator = Component.make(() => {
class Indicator extends State.Compound<Indicator>()("Indicator") {
static states = States.make(() => [Waiting, Complete]);
}
class Waiting extends State.Atomic<Waiting>()("Waiting") {
static transitions = Transitions.make(this, ({ arm, on }) => [
arm(Pulse),
on(Pulse.Done, Complete),
]);
}
class Complete extends State.Atomic<Complete>()("Complete") {}
return Indicator;
});
export class Panel extends State.Compound<Panel>()("Panel") {
static states = States.make(() => [Player]);
}
class Player extends Indicator.as<Player>()("Player") {}
export const IndicatorChart = Statechart.make(Panel);
2 · Reuse the definition twice
Left and Right share a template, not an occurrence.
Two placements of Indicator are legal in parallel because every state node and mounted citizen is qualified by its placement. Dashboard.Left.Waiting.Pulse and Dashboard.Right.Waiting.Pulse can be armed together without colliding even though their template source is identical.
import { Component, State, Statechart, States, Timer, Transitions } from "@motive/motive";
const PulseTimer = Timer.relative("PulseTimer");
const Pulse = PulseTimer.as("Pulse");
const Indicator = Component.make(() => {
class Indicator extends State.Compound<Indicator>()("Indicator") {
static states = States.make(() => [Waiting, Complete]);
}
class Waiting extends State.Atomic<Waiting>()("Waiting") {
static transitions = Transitions.make(this, ({ arm, on }) => [
arm(Pulse),
on(Pulse.Done, Complete),
]);
}
class Complete extends State.Atomic<Complete>()("Complete") {}
return Indicator;
});
export class Dashboard extends State.Parallel<Dashboard>()("Dashboard") {
static states = States.make(() => [Left, Right]);
}
class Left extends Indicator.as<Left>()("Left") {}
class Right extends Indicator.as<Right>()("Right") {}
export const DashboardChart = Statechart.make(Dashboard);
3 · Address one exact citizen
Firing Left leaves the sibling placement untouched.
Simulator.TimerCitizen names the complete mounted coordinate, not just Pulse. Firing Dashboard.Left.Waiting.Pulse moves only Left to Complete; Right remains Waiting with its own independently armed incarnation.
import {
Component,
Simulator,
State,
Statechart,
States,
Timer,
Transitions,
} from "@motive/motive";
import { Effect, Result } from "effect";
const PulseTimer = Timer.relative("PulseTimer");
const Pulse = PulseTimer.as("Pulse");
const Indicator = Component.make(() => {
class Indicator extends State.Compound<Indicator>()("Indicator") {
static states = States.make(() => [Waiting, Complete]);
}
class Waiting extends State.Atomic<Waiting>()("Waiting") {
static transitions = Transitions.make(this, ({ arm, on }) => [
arm(Pulse),
on(Pulse.Done, Complete),
]);
}
class Complete extends State.Atomic<Complete>()("Complete") {}
return Indicator;
});
export class Dashboard extends State.Parallel<Dashboard>()("Dashboard") {
static states = States.make(() => [Left, Right]);
}
export class Left extends Indicator.as<Left>()("Left") {}
export class Right extends Indicator.as<Right>()("Right") {}
export const DashboardChart = Statechart.make(Dashboard);
export const initial = Effect.runSync(
Simulator.make(DashboardChart, { timers: { PulseTimer: "5 seconds" } }),
);
export const leftDone = Result.getOrThrow(
initial.fireTimer(Simulator.TimerCitizen.make("Dashboard.Left.Waiting.Pulse")),
);
Equal deadlines are still separate lifecycles
The second frame advances virtual time by five seconds. Both placements reach Complete because both
independent Timer occurrences are due—not because one completion is broadcast across the shared
definition. The Simulator delivers a completion to each exact coordinate.
The final frame makes the separation observable. Firing
Dashboard.Left.Waiting.Pulse moves only Left to Complete; Right remains in Waiting with
Dashboard.Right.Waiting.Pulse still armed.
Exact addresses do not guess
Simulator.TimerCitizen.make("Dashboard.Left.Waiting.Pulse") names one currently armed Timer
occurrence. A bare Pulse or nonexistent placement path does not fall back to a local-name match.
The Simulator records the attempted delivery as Stale with reason NotArmed, leaves the
configuration unchanged, and preserves both real occurrences.
That failure direction matters: stale external facts may become harmless evidence, but they may never settle whichever repeated placement happens to look compatible.
Test repeated placement as an identity boundary
- Assemble one placement and verify the definition name does not add a hidden path segment.
- Assemble two placements and verify their state paths, Timer occurrences, and chart hash are placement-qualified.
- Advance both equal deadlines and verify two independent completions occur.
- Fire only the left coordinate and verify the right Timer remains armed.
- Fire a bare or nonexistent coordinate and verify a
Stale/NotArmedtrace with no state change. - Select
Left.Waiting.timers.PulseandRight.Waiting.timers.Pulseindependently through a migration or inspection boundary.
Next, Recursive Composition carries these same qualified identities through child-chart boundaries instead of stopping at one assembled chart.