An Activity describes Effect work that belongs to the lifetime of an active state. invoke mounts
one typed occurrence; the runtime Layer supplies the implementation that actually performs it.
Mount effectful work where its lifetime belongs.
An Activity is typed Effect work with an authored owner. invoke mounts one occurrence while that state is active and builds its input from the chart's current data.
1 · Name the phase without the work
A state does not start an Effect by itself.
Checking describes the order's phase, but no asynchronous computation exists because that state exists. Work started beside the chart would have no authored owner or exit lifetime.
import { State, Statechart, States } from "@motive/motive";
class Review extends State.Compound<Review>()("Review") {
static states = States.make(() => [Checking, Accepted]);
}
class Checking extends State.Atomic<Checking>()("Checking") {}
class Accepted extends State.Atomic<Accepted>()("Accepted") {}
export const ReviewChart = Statechart.make(Review);
2 · Define, invoke, and bind
ReviewOrder exists exactly while Checking does.
Activity.make declares the contract, invoke derives its orderId from active state data, and toLayer supplies the Effect implementation. Settling the mounted occurrence previews its typed Done boundary.
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 }),
});
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);
A state does not run an Effect
The first frame names Checking, but entering that state starts no asynchronous work. A meaningful
state name is still only part of the model.
Starting a Promise beside the chart would create a second, unmodeled lifetime. It could continue
after Checking exits, and its result would have no typed route back into the statechart.
Define the Activity contract
The second frame declares ReviewOrder before any state invokes it:
const ReviewOrder = Activity.make("ReviewOrder", {
input: Schema.Struct({ orderId: Schema.String }),
success: Schema.Struct({ approvalCode: Schema.String }),
});
The definition names the work and the Schemas at its boundary. It does not choose a state owner, start an Effect, or capture a particular order instance.
Invoke gives the work an owner
Checking mounts the definition in its transition declaration:
invoke(ReviewOrder, ({ state }) => ({ orderId: state.orderId }));
The invocation site is part of the authored topology. Entering Checking creates one mounted
ReviewOrder occurrence; leaving Checking ends that occurrence's lifetime and interrupts an
implementation that is still running.
The input is a Query
The second argument is a Query over the active chart world. The inline form receives state
because Checking owns this transition declaration, and it constructs the exact input required by
ReviewOrder.
Use Query.gen, Query.fn, or composed Queries when the input depends on several coactive facets.
The same strict and optional read rules apply: an Activity cannot mount with data whose owner is not
active.
The Layer binds the Effect
The chart owns the Activity definition and invocation site. ReviewChart.toLayer supplies its
implementation:
const ReviewLive = ReviewChart.toLayer({
activities: {
ReviewOrder: ({ input }) => reviewOrder(input.orderId),
},
});
The handler receives decoded input and may require ordinary Effect services. Those requirements remain visible on the Layer, where the application composes databases, HTTP clients, queues, and other runtime capabilities.
One active site owns one occurrence
This form of invoke is singular: one active site owns one mounted occurrence. Its identity
includes the chart instance and authored site, so the engine can start, interrupt, settle, and
recover the right work without relying on an ad hoc task registry.
Use an Activity family when one state owns a fixed or keyed set of concurrent occurrences. That is a different topology and has its own completion rules.
Outcomes return to the model
Completing the live ReviewOrder produces its typed Done event, and the existing listener moves
the chart to Accepted. This page uses that control only to expose the complete boundary: the
implementation reports an outcome; the chart decides what the outcome means.
The next pages treat Activity.Done and Activity.Error individually, including their payloads,
required dispositions, and the difference between modeled failure and a defect.
Test the lifetime
- Complete the mounted Activity and compare its Effect witness with the resulting chart state.
- Log
input.orderIdinside the binding and verify that it came from the activeCheckingfacet. - Add another transition out of
Checking, leave before completion, and observe interruption of the bound Effect.