An Activity reports successful completion through its typed Done event. That event tells the
chart what the implementation produced; the chart still owns every decision that follows.
Successful work returns through the model.
An Activity's success Schema creates a typed Done event. The implementation reports the value; the chart decides what that value means and whether its next state retains it.
1 · React to successful completion
Done is an event the chart interprets.
ReviewOrder returns a decoded approvalCode and emits its typed Done event. The listener enters Accepted, but the destination retains none of the success payload yet.
import {
Activity,
State,
Statechart,
States,
Transitions,
} from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ReviewOrder = Activity.make("ReviewOrder", {
input: Schema.Struct({ orderId: Schema.String }),
success: Schema.Struct({ approvalCode: Schema.String }),
});
export class Review extends State.Compound<Review>()("Review") {
static states = States.make(() => [Checking, Accepted]);
}
class Checking extends State.Atomic<Checking>()("Checking", {
orderId: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("order-123")),
),
}) {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(ReviewOrder, ({ state }) => ({ orderId: state.orderId })),
on(ReviewOrder.Done, Accepted),
]);
}
class Accepted extends State.Atomic<Accepted>()("Accepted") {}
export const ReviewChart = Statechart.make(Review);
2 · Carry the success value forward
Construct the destination from event.value.
Accepted now owns approvalCode. Its transition input reads ReviewOrder.Done.value, so the same implementation result becomes durable state only because the chart explicitly retains it.
import {
Activity,
State,
Statechart,
States,
Transitions,
} from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ReviewOrder = Activity.make("ReviewOrder", {
input: Schema.Struct({ orderId: Schema.String }),
success: Schema.Struct({ approvalCode: Schema.String }),
});
export class Review extends State.Compound<Review>()("Review") {
static states = States.make(() => [Checking, Accepted]);
}
class Checking extends State.Atomic<Checking>()("Checking", {
orderId: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("order-123")),
),
}) {
static transitions = Transitions.make(this, ({ invoke, on }) => [
invoke(ReviewOrder, ({ state }) => ({ orderId: state.orderId })),
on(ReviewOrder.Done, Accepted, ({ event }) => ({
approvalCode: event.value.approvalCode,
})),
]);
}
export class Accepted extends State.Atomic<Accepted>()("Accepted", {
approvalCode: Schema.String,
}) {}
export const ReviewChart = Statechart.make(Review);
Success creates a typed event
ReviewOrder declares this success Schema:
success: Schema.Struct({ approvalCode: Schema.String });
That declaration creates ReviewOrder.Done. When the bound Effect succeeds, Motive encodes and
decodes its result through the Schema before the completion event reaches transition selection.
The implementation reports; the chart decides
Both frames use the same Effect result: { approvalCode: "approved-42" }. The implementation does
not enter Accepted, update chart data, or decide what approval means.
Checking owns that interpretation:
on(ReviewOrder.Done, Accepted);
The first frame therefore reaches Accepted while deliberately retaining none of the returned
value. Successful completion and state-data retention are separate authored choices.
Done is not an external command
ReviewOrder.Done is a completion descriptor tied to this Activity definition. The engine creates
it for the mounted occurrence; a client cannot send a fabricated done.invoke.* event through the
public event boundary.
That ownership preserves the connection between the implementation that settled, its exact invocation site, and the listener eligible to consume the result.
The payload lives on event.value
The successful value is available as event.value inside guards, target input producers, and
actions selected by the Done edge. Its type is inferred directly from the Activity's success
Schema.
In this example, event.value.approvalCode is a string. A misspelled field or a value that the
success Schema cannot encode is rejected at the owning boundary rather than becoming unchecked
workflow data.
Retention needs a state owner
The second frame gives Accepted an approvalCode field and constructs that state from the event:
on(ReviewOrder.Done, Accepted, ({ event }) => ({
approvalCode: event.value.approvalCode,
}));
The readout separates Done.value from Accepted data. Both show approved-42 only in the second
frame, where the topology names a durable owner for the value after Checking exits.
Completion ends the occurrence
The successful result settles this mounted ReviewOrder occurrence. Its Done event is folded
through the active chart, and the transition out of Checking removes the invocation site that
owned it.
Re-entering Checking would mount a new occurrence with a new lifecycle. It would not resume the
already settled invocation merely because the Activity definition has the same name.
Handle every possible completion
A singular Activity's declared terminal channels need an eligible disposition while its site is active. The listener may transition, update data, raise another event, or explicitly handle the completion without moving.
This structural requirement prevents a successful result from becoming an accidental unhandled edge. It does not require every success to mean the same domain transition.
Test the success boundary
- Complete
ReviewOrderin both frames and compareDone.valuewith retainedAccepteddata. - Change the success Schema and follow the resulting type error into the target input producer.
- Keep
Checkingactive with a targetlessDonehandler and decide whether the completed value needs a longer-lived owner.