Guide

Activity.Done

Consume typed successful completion and decide where its value belongs.

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

  1. Complete ReviewOrder in both frames and compare Done.value with retained Accepted data.
  2. Change the success Schema and follow the resulting type error into the target input producer.
  3. Keep Checking active with a targetless Done handler and decide whether the completed value needs a longer-lived owner.