Guide

Activities and invoke

Define typed Effect work and mount one occurrence on its owning state.

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

  1. Complete the mounted Activity and compare its Effect witness with the resulting chart state.
  2. Log input.orderId inside the binding and verify that it came from the active Checking facet.
  3. Add another transition out of Checking, leave before completion, and observe interruption of the bound Effect.