Run an Effect when a state becomes active
Invoke application services from a machine and route typed outcomes.
This guide shows you how to let a machine state own an Effect, provide its service through a Layer, and route success or typed failure into the next state.
Define the service and failure
Use an ordinary Effect service. Its requirement will be inferred from the machine definition:
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import * as Machine from "effect-state-machine/Machine"
import * as MachineEngine from "effect-state-machine/MachineEngine"
class LoadFailed extends Schema.TaggedError<LoadFailed>()("LoadFailed", {
message: Schema.String,
}) {}
class Profiles extends Context.Service<
Profiles,
Readonly<{ load: (id: string) => Effect.Effect<string, LoadFailed> }>
>()("app/Profiles") {}Make the active state an invocation node
Define states for the active operation and both typed outcomes, then use invoke for the active
state:
const Input = Schema.Struct({ id: Schema.String })
const State = Machine.taggedUnion({
Loading: { fields: { id: Schema.String } },
Loaded: { fields: { name: Schema.String } },
Failed: { fields: { message: Schema.String } },
})
const Event = Machine.taggedUnion({
Cancel: { fields: {} },
})
const profile = Machine.builder({ input: Input, state: State, event: Event })
const definition = profile.define(
{
id: "profile",
idempotencyKey: ({ id }) => id,
initial: ({ id }) => ({ _tag: "Loading", id }),
},
{
Loading: profile.invoke({
name: "Profiles.load",
success: Schema.String,
error: LoadFailed,
effect: (state) => Effect.flatMap(Profiles, ({ load }) => load(state.id)),
onSuccess: {
target: "Loaded",
reduce: ({ value }) => ({ name: value }),
},
onFailure: {
target: "Failed",
reduce: ({ error }) => ({ message: error.message }),
},
}),
Loaded: profile.final(),
Failed: profile.final(),
},
)The name is stable metadata for inspection and graph tooling. The required success and error
Schemas define the replayable outcome contract and determine the values available to
onSuccess.reduce and onFailure.reduce. Use Schema.Never when typed failure is impossible.
The effect callback receives the narrowed Loading state and a required stable work execution
context. A callback may omit the second parameter when it does not need to coordinate an external
idempotency boundary.
For several named operations, use profile.invoke.all(...) to join every lane or
profile.invoke.race(...) to select the first successful lane. all accepts an optional
concurrency limit. Each named lane is an object containing success, error, and effect;
direct function lanes are not accepted. A race success reducer receives correlated winner and
value fields.
Provide the service when the machine runs
Provide the Layer at the application boundary:
const ProfilesLive = Layer.succeed(
Profiles,
Profiles.of({ load: (id) => Effect.succeed(`profile-${id}`) }),
)
const program = Effect.scoped(
definition.run({ id: "42" }).pipe(
Effect.flatMap((handle) => handle.completion),
Effect.provide(ProfilesLive),
),
).pipe(Effect.provide(MachineEngine.layerMemory()))
const result = await Effect.runPromise(program)
// result: { _tag: "Loaded", name: "profile-42" }Inspect the invocation
The live example starts Profiles.load on entry. Wait for it to reach Loaded, then select
Load again to watch the invocation lifecycle repeat. Select Cancel load while Loading is
active to see state-owned work interrupted before the cancellation transition commits.
The goal is complete when the invocation reaches either declared final state. Leaving Loading
through an event would interrupt the Effect owned by that state. Defects are not sent through
onFailure; refer to the generated Machine API for termination
semantics.