Recursion is a cycle between chart definitions. It is not a State containing itself, and it is not an unusually deep Compound State.
The same definition may create another instance of itself, but every instance remains an ordinary durable process with its own identity, snapshot, mailbox, children, and lifecycle:
WorkNodeChildrenrootdepth 0depth-1depth 1depth-2depth 2depth-3depth 3
Declare the interface before the implementation
Ordinary child charts are assembled before their parent refers to them. Direct recursion reverses that timing: the implementation needs a child site for the final chart before the final chart can exist.
Statechart.contract creates the lawful early reference:
const Self = Statechart.contract({
name: "WorkNode",
output: Instance.Ancestry,
error: Schema.Never,
events: [Begin],
recursive: { maximumDepth: 3 },
});
This value carries the complete child contract: name, optional input, output, modeled error, events, emits, slots, open Resource requirements, and maximum depth. It may mint child sites and project their structural outcomes. It cannot create a client, build a Layer, register with an engine, or pretend to have an executable chart hash.
The early contract is not compatibility metadata. Closing the definition later must prove that the implemented root has exactly the promised boundaries.
Self-reference still uses ordinary child grammar
The unresolved reference is itself the default singular occurrence, and it can mint the familiar named forms:
const Primary = Self.as("Primary");
const Replicas = Self.batch("Replicas");
const Children = Self.each("Children");
This page uses each so every discovered key owns one child incarnation. Reusing the definition
does not merge those children. Their keys, generations, Refs, InstanceIds, snapshots, and ancestry
frames remain exact in the same way repeated Component placements remain exact.
Close the fixed point exactly once
Author the root and its States as ordinary top-level values, then close the recursive definition:
export const WorkChart = Self.make(WorkNode);
make resolves every self edge to one completed chart authority and verifies the early contract.
The unresolved Self reference never becomes a second registrable chart, and a second closure
is refused.
Now build that definition from the early interface to a running recursive population:
Author a finite cycle between chart definitions
One definition can create a population of exact instances.
A recursive chart declares its child contract before its implementation exists, then closes that fixed point into one executable authority. The cycle is authored once; every runtime node is still a separate durable instance.
1 · Declare the recursive contract
The future child must be knowable before the chart exists.
Statechart.contract creates an unresolved, Schema-owned reference. Its input, output, error, event, Resource, and depth boundaries are available early enough for ordinary child authoring, but it is not yet an executable chart.
import { Instance, Statechart } from "@motive/motive";
import { Schema } from "effect";
class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}
const Self = Statechart.contract({
name: "WorkNode",
output: Instance.Ancestry,
error: Schema.Never,
events: [Begin],
recursive: { maximumDepth: 3 },
});
2 · Name a self-child site
Children points back to the same definition, not the same instance.
Self.each("Children") is an ordinary keyed child site. Every admitted key creates a separate durable instance with its own identity, mailbox, snapshot, lifecycle, and ancestry frame—even though every child runs WorkNode again.
import { Instance, Statechart } from "@motive/motive";
import { Schema } from "effect";
class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}
const Self = Statechart.contract({
name: "WorkNode",
output: Instance.Ancestry,
error: Schema.Never,
events: [Begin],
recursive: { maximumDepth: 3 },
});
const Children = Self.each("Children");
3 · Author an explicit base case
Depth is queried from ancestry before another birth is requested.
Each instance reads Self.depth. Depths zero through two request one descendant; depth three returns its exact ancestry. Child Done then carries that same value back through each parent. The authored base case completes before the hard limit is touched.
import {
Instance,
Query,
State,
Statechart,
States,
Transitions,
} from "@motive/motive";
import { Schema } from "effect";
class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}
const Self = Statechart.contract({
name: "WorkNode",
output: Instance.Ancestry,
error: Schema.Never,
events: [Begin],
recursive: { maximumDepth: 3 },
});
const Children = Self.each("Children");
class WorkNode extends State.Compound<WorkNode>()("WorkNode") {
static states = States.make(() => [Waiting, Expanding, Complete]);
}
class Waiting extends State.Atomic<Waiting>()("Waiting") {
static transitions = Transitions.make(this, ({ always, on }) => [
always(Expanding).when(
Query.gen(function* () {
return (yield* Self.depth) > 0;
}),
),
on(Begin, Expanding),
]);
}
class Expanding extends State.Atomic<Expanding>()("Expanding") {
static transitions = Transitions.make(this, ({ always, on, spawn }) => [
spawn(
Children,
Query.gen(function* () {
const depth = yield* Self.depth;
return depth < 3 ? [`depth-${depth + 1}`] : [];
}),
),
always(
Complete,
Query.gen(function* () {
return yield* Query.ancestry;
}),
).when(
Query.gen(function* () {
return (yield* Self.depth) === 3;
}),
),
on(Children.MemberDone, Complete, ({ event }) => event.value),
on(Children.Empty),
on(Children.MemberDefect),
]);
}
class Complete extends State.Done<Complete>()("Complete", Instance.Ancestry) {}
4 · Close the fixed point once
Self.make(WorkNode) creates the sole executable chart authority.
Closure proves the implementation matches the promised recursive contract, resolves every self reference, and computes canonical group identity. Run it to birth four distinct instances and settle the leaf's four-frame ancestry at the root.
import {
Instance,
Query,
State,
Statechart,
States,
Transitions,
} from "@motive/motive";
import { Schema } from "effect";
class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}
const Self = Statechart.contract({
name: "WorkNode",
output: Instance.Ancestry,
error: Schema.Never,
events: [Begin],
recursive: { maximumDepth: 3 },
});
const Children = Self.each("Children");
class WorkNode extends State.Compound<WorkNode>()("WorkNode") {
static states = States.make(() => [Waiting, Expanding, Complete]);
}
class Waiting extends State.Atomic<Waiting>()("Waiting") {
static transitions = Transitions.make(this, ({ always, on }) => [
always(Expanding).when(
Query.gen(function* () {
return (yield* Self.depth) > 0;
}),
),
on(Begin, Expanding),
]);
}
class Expanding extends State.Atomic<Expanding>()("Expanding") {
static transitions = Transitions.make(this, ({ always, on, spawn }) => [
spawn(
Children,
Query.gen(function* () {
const depth = yield* Self.depth;
return depth < 3 ? [`depth-${depth + 1}`] : [];
}),
),
always(
Complete,
Query.gen(function* () {
return yield* Query.ancestry;
}),
).when(
Query.gen(function* () {
return (yield* Self.depth) === 3;
}),
),
on(Children.MemberDone, Complete, ({ event }) => event.value),
on(Children.Empty),
on(Children.MemberDefect),
]);
}
export class Complete extends State.Done<Complete>()("Complete", Instance.Ancestry) {}
export const WorkChart = Self.make(WorkNode);
The base case belongs in the model
Self.depth is a Query derived from engine-owned ancestry. The first instance in the recursive
group is depth zero; every child edge back into the same group increments it. Unrelated parents do
not consume this chart's recursive depth.
The example stops asking for children at depth three and returns Query.ancestry. That is normal
completion. maximumDepth is a hard safety boundary, not an alternate spelling for the base case.
If an implementation requests depth four, the engine refuses the birth before allocating a child
generation, Ref, durable Birth, mailbox, runner, or partial parent plan. It records a typed
RecursiveDepthExceeded incident and delivers the exact child site's ordinary Defect outcome.
Handled supervision may recover locally; otherwise the incident escalates and can Park the root.
The engine never silently omits the requested child.
Depth limits lineage, Resources limit authority and capacity
A maximum depth bounds how long one recursive lineage may become. It does not bound the width of an
each family, concurrent model calls, corpus access, token budgets, or other scarce authority.
Those remain Resource concerns. A recursive child receives only the Resources explicitly inherited, bound, or attenuated to its site. Ancestry proves where an instance came from; it does not grant the instance access to its ancestors' data. This is why Resource Boundaries and Capacity & Settlement precede recursive composition in the learning path.
Use recursion when the node deserves a lifecycle
A recursive function, Activity, or one chart with an explicit work graph is often enough. Give each recursive node its own chart instance when it needs several of these together:
- durable identity and checkpointing;
- independent retry, recovery, inspection, or interruption;
- child supervision and subtree cancellation;
- Resource acquisition, attenuation, capacity, or acknowledgement;
- recursive delegation of its own;
- a compact result that settles back to its parent.
Recursive charts add structural ownership, not computational power.
Direct and mutual cycles share one identity law
Direct recursion forms one chart cycle: WorkNode → WorkNode. Indirect recursion forms a group such
as Planner → Executor → Planner. Statechart.contract closes that strongly connected
group atomically so definition order cannot become chart identity and no partial group may start.
For either form, changing the recursive contract, topology, or maximumDepth changes canonical
identity. Repeated chart names in ancestry are lawful evidence of separate instances, never an
identity collision.
Test recursion at the birth boundary
- Prove the authored base case completes without touching the hard bound.
- Prove every recursive child has a distinct InstanceId, Ref, generation, and ancestry frame.
- Restart an activation and prove it re-enacts the exact retained child incarnation without a duplicate birth.
- Request one child beyond
maximumDepthand prove no child identity or durable Birth exists. - Assert the refusal carries
RecursiveDepthExceededthrough the exact site'sDefectchannel. - Bound breadth and scarce work independently with Resource capacity.
Next, Depth & Recursion Queries separates the modeled base case from the engine's hard refusal boundary.