Every Activity has a supervisory Defect channel—even when its modeled error type is impossible.
The channel belongs to one mounted occurrence and reports that its implementation ended with an
unexpected failure.
Supervise the occurrence that failed.
A bound Activity Effect can die even when its domain contract declares no modeled error. Activity.Defect reports that terminal outcome to the active topology without pretending it was expected.
1 · Observe in place
A defect settles the occurrence, not its owner.
Rendering consumes Render.Defect without a target. The state remains active, but the terminal occurrence is released and does not silently start again.
import { Activity, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const Render = Activity.make("Render", {
input: Schema.Struct({ orderId: Schema.String }),
success: Schema.Struct({ url: Schema.String }),
});
export class Billing extends State.Compound<Billing>()("Billing") {
static states = States.make(() => [Invoice, Halted]);
}
class Invoice extends State.Compound<Invoice>()("Invoice") {
static states = States.make(() => [Rendering, Ready]);
}
class Rendering extends State.Atomic<Rendering>()("Rendering") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(Render, () => ({ orderId: "order-123" })),
on(Render.Done, Ready),
on(Render.Defect),
]);
}
class Ready extends State.Atomic<Ready>()("Ready") {}
class Halted extends State.Atomic<Halted>()("Halted") {}
export const BillingChart = Statechart.make(Billing);
2 · Supervise from above
An ancestor can own the response.
Billing is active whenever Invoice.Rendering and its Render occurrence are active. The ancestor listener can therefore isolate the whole invoice in Halted, leaving no Activity slot beneath the exited branch.
import { Activity, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const Render = Activity.make("Render", {
input: Schema.Struct({ orderId: Schema.String }),
success: Schema.Struct({ url: Schema.String }),
});
export class Billing extends State.Compound<Billing>()("Billing") {
static states = States.make(() => [Invoice, Halted]);
static transitions = Transitions.make(this, ({ on }) => [
on(Render.Defect, Halted),
]);
}
class Invoice extends State.Compound<Invoice>()("Invoice") {
static states = States.make(() => [Rendering, Ready]);
}
class Rendering extends State.Atomic<Rendering>()("Rendering") {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(Render, () => ({ orderId: "order-123" })),
on(Render.Done, Ready),
]);
}
class Ready extends State.Atomic<Ready>()("Ready") {}
class Halted extends State.Atomic<Halted>()("Halted") {}
export const BillingChart = Statechart.make(Billing);
Defect belongs to the invoked Effect
Render declares input and success Schemas but no modeled error Schema:
const Render = Activity.make("Render", {
input: Schema.Struct({ orderId: Schema.String }),
success: Schema.Struct({ url: Schema.String }),
});
The contract says there is no expected failure value for the domain to interpret. It does not say the implementation is physically incapable of dying.
If the bound Effect ends with a Cause containing a defect, Motive records an immutable incident and
the mounted occurrence produces Render.Defect. The public event carries event.incident, the
reference to that record, and event.defect, one squashed representative for ordinary control
flow. Motive does not invent an Error.error value that the Activity never declared.
Defect is not a catch-all
Render.Defect describes the terminal outcome of the Effect bound to Render. It
does not catch every defect that could happen while the chart is running.
A defect thrown while resolving invocation input, executing a transition action, or committing an engine step belongs to that different boundary. Keeping those failures separate preserves which operation failed and which authority can respond.
A targetless listener still handles the event
The first frame listens without changing topology:
on(Render.Defect);
The Rendering state remains active, but the Activity occurrence is no longer running. Its slot
is released after the terminal outcome while its owner stays active. A targetless listener does not
restart the Effect, turn the defect into success, or make the occurrence available for another
attempt.
Reentering Rendering would retire that slot and mount a fresh occurrence. Retry policy is
different: it keeps one occurrence alive across eligible modeled failures. A defect is not eligible
for that modeled retry path.
The listener may live on an active ancestor
Completion events fold through the same active ancestry as ordinary events. Billing is active
while its Invoice.Rendering descendant owns the invocation, so the second frame can place
supervision on that outer compound state:
class Billing extends State.Compound<Billing>()("Billing") {
static transitions = Transitions.make(this, ({ on }) => [
on(Render.Defect, Halted),
]);
}
This is useful when several working states share one isolation or escalation policy. The listener must still be structurally capable of being active with the occurrence; an unrelated sibling is not a supervisor merely because it names the same trigger.
Leaving the owner retires its topology
The ancestor listener moves Billing to Halted. The defective occurrence has settled, and exiting
the Invoice branch removes the invocation site from active topology. No Render slot
remains beneath the new state.
The transition can read the incident reference and representative defect while it is selected. The complete normalized Cause, ownership coordinates, and provenance remain in the immutable incident record, available through the storage-backed incident service after the Activity slot is retired. State should retain the reference when later domain control flow needs to name that incident; it does not need to copy the operational record into chart data.
Supervision remains application policy
Activity.Defect gives topology a source-qualified event; it does not prescribe a universal
response. A chart can observe in place, isolate a branch, escalate an incident, or terminate a
larger boundary according to its own failure model.
That response should say something true about the unexpected failure. Routing a defect into an ordinary domain recovery state only to avoid supervisory handling erases the distinction Motive has preserved.
Test the occurrence boundary
- Defect both frames and compare the released occurrence under an active owner with the absent site after owner exit.
- Send another event while the first frame remains in
Renderingand confirm that the defective occurrence does not run again. - Move the ancestor listener to an exclusive sibling and follow the authoring error back to the missing coactivity proof.
Next, Child.Defect carries the same incident-backed supervisory evidence across an independently
running child-chart boundary.