Invoke
State machines can “invoke” one or many actors within a given state. The invoked actor will start when the state is entered, and stop when the state is exited. Any XState actor can be invoked, including simple Promise-based actors, or even complex machine-based actors.
Invoking an actor is useful for managing synchronous or asynchronous work that the state machine needs to orchestrate and communicate with at a high level, but doesn't need to know about in detail.
Actors can be invoked within any state except for the top-level final state. In the following example, the loading
state invokes a Promise-based actor:
import { setup, createActor, fromPromise, assign } from 'xstate';
const fetchUser = (userId: string) =>
fetch(`https://example.com/${userId}`).then((response) => response.text());
const userMachine = setup({
types: {
context: {} as {
userId: string;
user: object | undefined;
error: unknown;
},
},
actors: {
fetchUser: fromPromise(async ({ input }) => {
const user = await fetchUser(input.userId);
return user;
}),
},
}).createMachine({
id: 'user',
initial: 'idle',
context: {
userId: '42',
user: undefined,
error: undefined,
},
states: {
idle: {
on: {
FETCH: { target: 'loading' },
},
},
loading: {
invoke: {
id: 'getUser',
src: 'fetchUser',
input: ({ context: { userId } }) => ({ userId }),
onDone: {
target: 'success',
actions: assign({ user: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error }),
},
},
},
success: {},
failure: {
on: {
RETRY: { target: 'loading' },
},
},
},
});
Actors can also be invoked on the root of the machine, and they will be active for the lifetime of their parent machine actor:
import { fromEventObservable, fromEvent } from 'rxjs';
const interactiveMachine = createMachine({
invoke: {
src: fromEventObservable(
() => fromEvent(document.body, 'click') as Subscribable<EventObject>,
),
},
on: {
click: {
actions: ({ event }) => console.log(event),
},
},
});
And invoke
can be an array, to invoke multiple actors:
const vitalsWorkflow = createMachine({
states: {
CheckVitals: {
invoke: [
{ src: 'checkTirePressure' },
{ src: 'checkOilPressure' },
{ src: 'checkCoolantLevel' },
{ src: 'checkBattery' },
],
},
},
});
For further examples, see:
- Reusing function and event definitions workflow
- Check inbox periodically (cron-based workflow)
- Car vitals checks (SubFlow Repeat) workflow
How are actors different from actions?
Actions are “fire-and-forget”; as soon as their execution starts, the state machine running the actions forgets about them. If you specify an action as async
, the action won’t be awaited before moving to the next state. Remember: transitions are always zero-time (states transition synchronously).
Invoked actors can do asynchronous work and communicate with their parent machine actor. They can send and receive events. Invoked machine actors can even invoke or spawn their own child actors.
Unlike actions, errors thrown by invoked actors can be handled directly:
invoke: {
src: 'fetchUser',
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error })
}
}
Whereas errors thrown by actions can only be handled globally by a subscriber of their parent state machine:
actor.subscribe({
error: (err) => {
console.error(err);
},
});
Lifecycle
Invoked actors have a lifecycle that is managed by the state they are invoked in. They are created and started when the state is entered, and stopped when the state is exited.
If a state is entered and then immediately exited, e.g. due to an eventless ("always") transition, then no actors will be invoked on that state.
Re-entering
By default, when a state machine transitions from a parent state to the same parent state or a descendent (child or deeper), it will not re-enter the parent state. Because the transition is not re-entering, the parent state's existing invoked actors will not be stopped and new invoked actors will not be started.
However, if you want a transition to re-enter the parent state, set the transition's reenter
property to true
. Transitions that re-enter the state will stop existing invoked actors and start new invoked actors.
Read more about re-entering states.
The invoke
property API
An invocation is defined in a state node's configuration with the invoke
property, whose value is an object that contains:
src
- The source of the actor logic to invoke when creating the actor, or a string referring to actor logic defined in the machine's provided implementation.id
- A string identifying the actor, unique within its parent machine.input
- The input to pass to the actor.onDone
- Transition that occurs when the actor is complete.onError
- Transition that occurs when the actor throws an error.onSnapshot
- Transition that occurs when the actor emits a new value.systemId
- A string identifing the actor, unique system-wide.
Source
The src
represents the actor logic the machine should use when creating the actor. There are several actor logic creators available in XState:
- State machine logic (
createMachine
) - Promise logic (
fromPromise
), where invoke will take theonDone
transition onresolve
, or theonError
transition onreject
- Transition function logic (
fromTransition
), which follows the reducer pattern - Observable logic (
fromObservable
), which can send events to the parent machine, and where invoke will take anonDone
transition when completed - Event observable logic (
fromEventObservable
), like Observable logic but for streams of event objects - Callback logic (
fromCallback
), which can send events to and receive events from the parent machine
The invoke src
can be inline or provided.
Inline src
Either directly inline:
invoke: {
src: fromPromise(…)
}
Or from some logic in the same scope as the machine:
const logic = fromPromise(…)
const machine = createMachine({
// …
invoke: {
src: logic
}
});
Provided src
The src
can be provided in the machine implementation and referenced using a string or an object.
const machine = createMachine({
// …
invoke: {
src: 'workflow', // string reference
},
});
const actor = createActor(
machine.provide({
actors: {
workflow: fromPromise(/* ... */), // provided
},
}),
);
onDone
- Transitions when invoked actor is complete
- Event object
output
property is provided with actor's output data - Not available for callback actors
The onError
transition can be an object:
{
invoke: {
src: 'fetchItem',
onDone: {
target: 'success',
actions: ({ event }) => {
console.log(event.output);
}
},
onError: {
target: 'error',
actions: ({ event }) => {
console.error(event.error);
}
}
}
}
Or, for simplicity, target-only transitions can be strings:
{
invoke: {
src: 'fetchItem',
onDone: 'success',
onError: 'error'
}
}
onError
- Transitions when invoked actor throws an error, or (for Promise-based actors) when the promise rejects
- Event object
error
property is provided with actor’s error data
invoke: {
src: 'getUser',
onError: {
target: 'failure',
actions: ({ event }) => {
console.error(event.error);
}
}
}
Or, for simplicity, target-only transitions can be strings:
{
invoke: {
src: 'getUser',
onError: 'failure'
}
}
onSnapshot
- Transitions when invoked actor emits a new snapshot
- Event gets
snapshot
with actor's snapshot - Not available for callback actors
invoke: {
src: 'getUser',
onSnapshot: {
actions: ({ event }) => console.log(event.snapshot)
}
}
Read more about actor snapshots.
Input
To define input to an invoked actor, use input
.
The input
property can be a static input value, or a function that returns the input value. The function will be passed an object that contains the current context
and event
.
Input from a static value
invoke: {
src: 'liveFeedback',
input: {
domain: 'stately.ai'
}
}
Input from a function
invoke: {
src: fromPromise(({ input: { endpoint, userId } }) => {
return fetch(`${endpoint}/${userId}`).then((res) => res.json());
}),
input: ({ context, event }) => ({
endpoint: context.endpoint,
userId: event.userId
})
}
See Input for more.
Invoking Promises
The most common type of actors you’ll invoke are promise actors. Promise actors allow you to await the result of a promise before deciding what to do next.
XState can invoke Promises as actors using the fromPromise
actor logic creator. Promises can:
resolve()
, which will take theonDone
transitionreject()
(or throw an error), which will take theonError
transition
If the state where the invoked promise is active is exited before the promise settles, the result of the promise is discarded.
import { setup, createActor, fromPromise, assign } from 'xstate';
// Function that returns a Promise
// which resolves with some useful value
// e.g.: { name: 'David', location: 'Florida' }
const fetchUser = (userId: string) =>
fetch(`/api/users/${userId}`).then((response) => response.json());
const userMachine = setup({
types: {
context: {} as {
userId: string;
user: object | undefined;
error: unknown;
},
},
}).createMachine({
id: 'user',
initial: 'idle',
context: {
userId: '42',
user: undefined,
error: undefined,
},
states: {
idle: {
on: {
FETCH: { target: 'loading' },
},
},
loading: {
invoke: {
id: 'getUser',
src: fromPromise(({ input }) => fetchUser(input.userId)),
input: ({ context: { userId } }) => ({ userId }),
onDone: {
target: 'success',
actions: assign({ user: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error }),
},
},
},
success: {},
failure: {
on: {
RETRY: { target: 'loading' },
},
},
},
});
The resolved output is placed into a 'xstate.done.actor.<id>'
event, under the output
property, e.g.:
{
type: 'xstate.done.actor.getUser',
output: {
name: 'David',
location: 'Florida'
}
}
Promise Rejection
If a Promise rejects, the onError
transition will be taken with a { type: 'xstate.error.actor.<id>' }
event. The error data is available on the event's error
property:
import { setup, createActor, fromPromise, assign } from 'xstate';
const search = (query: string) =>
new Promise((resolve, reject) => {
if (!query.length) {
return reject('No query specified');
// or:
// throw new Error('No query specified');
}
return resolve(getSearchResults(query));
});
// ...
const searchMachine = setup({
types: {
context: {} as {
results: object | undefined;
errorMessage: unknown;
},
},
}).createMachine({
id: 'search',
initial: 'idle',
context: {
results: undefined,
errorMessage: undefined,
},
states: {
idle: {
on: {
SEARCH: { target: 'searching' },
},
},
searching: {
invoke: {
id: 'search',
src: fromPromise(({ input: { query } }) => search(query)),
input: ({ event }) => ({ query: event.query }),
onError: {
target: 'failure',
actions: assign({
errorMessage: ({ context, event }) => {
// event is:
// { type: 'xstate.error.actor.<id>', error: 'No query specified' }
return event.error;
},
}),
},
onDone: {
target: 'success',
actions: assign({ results: ({ event }) => event.output }),
},
},
},
success: {},
failure: {},
},
});
If the onError
transition is missing, and the Promise is rejected, the error will throw. However, you can handle all thrown errors for an actor by subscribing an observer object with an error
function:
actor.subscribe({
error: (err) => { ... }
})
Invoking Callbacks
You can invoke callback actor logic by:
- Setting up the callback actor logic in the
actors
object of thesetup({ actors: { ... } })
call - Invoking the callback actor logic by its source name (
src
) in theinvoke
property of a state
import { setup, fromCallback } from 'xstate';
const machine = setup({
actors: {
someCallback: fromCallback(({ input, sendBack, receive }) => {
// ...
}),
},
}).createMachine({
// ...
invoke: {
src: 'someCallback',
input: {
/* ... */
},
},
});
Read callback actor logic for more information on callback actors.
Invoking Observables
You can invoke observable logic by:
- Setting up the observable logic in the
actors
object of thesetup({ actors: { ... } })
call - Invoking the observable logic by its source name (
src
) in theinvoke
property of a state
import { setup, fromObservable } from 'xstate';
import { interval } from 'rxjs';
const machine = setup({
actors: {
someObservable: fromObservable(({ input }: { input: number }) => {
return interval(input.ms);
}),
},
}).createMachine({
// ...
invoke: {
src: 'someObservable',
input: { ms: 1000 },
onSnapshot: {
actions: ({ event }) => {
console.log(event.snapshot.context); // 1, 2, 3, ...
},
},
},
});
Read observable actor logic for more information on observable actors.
Invoking Event Observables
You can invoke event observables by using the fromEventObservable(...)
actor logic creator. Event observable logic is similar to observable logic in that the parent actor subscribes to the event observable; however, the emitted values of an event observable are expected to be events that are sent to the invoking (parent) actor directly.
import { setup, fromEventObservable } from 'xstate';
const mouseClicks = fromEventObservable(/* ... */);
const machine = setup({
actors: {
mouseClicks,
},
}).createMachine({
// ...
invoke: {
src: 'mouseClicks',
// No `onSnapshot` or `onDone` needed; events are sent directly to
// the machine actor
},
on: {
// Sent by the event observable actor
click: {
// ...
},
},
});
Invoking Transitions
You can invoke transition actor logic by:
- Setting up the transition actor logic in the
actors
object of thesetup({ actors: { ... } })
call - Invoking the transition actor logic by its source name (
src
) in theinvoke
property of a state
import { setup, fromTransition } from 'xstate';
const machine = setup({
actors: {
someTransition: fromTransition((state, event, { input }) => {
// ...
return state;
}),
},
}).createMachine({
// ...
invoke: {
src: 'someTransition',
input: {
/* ... */
},
onSnapshot: {
actions: ({ event }) => {
console.log(event.context);
},
},
},
});
Read transition actor logic for more information on transition actors.
Invoking Machines
You can invoke state machine actor logic by:
- Setting up the state machine actor logic in the
actors
object of thesetup({ actors: { ... } })
call - Invoking the state machine actor logic by its source name (
src
) in theinvoke
property of a state
import { setup } from 'xstate';
const childMachine = setup({
/* ... */
}).createMachine({
context: ({ input }) => ({
// ...
}),
// ...
});
const machine = setup({
actors: {
someMachine: childMachine,
},
}).createMachine({
// ...
invoke: {
src: 'someMachine',
input: {
/* ... */
},
},
});
Read state machine actor logic for more information on state machine actors.
Sending Responses
An invoked actor (or spawned actor) can respond to another actor; i.e., it can send an event in response to an event sent by another actor. To do so, provide a reference to the sending actor as a custom property on the event object being sent. In the following example, we use event.sender
, but any name works.
// Parent
actions: sendTo('childActor', ({ self }) => ({
type: 'ping',
sender: self,
}));
// Child
actions: sendTo(
({ event }) => event.sender,
{ type: 'pong' },
);
In the following example, the 'client'
machine below sends the 'CODE'
event to the invoked 'auth-server'
actor, which then responds with a 'TOKEN'
event after 1 second.
import { createActor, createMachine, sendTo } from 'xstate';
const authServerMachine = createMachine({
id: 'server',
initial: 'waitingForCode',
states: {
waitingForCode: {
on: {
CODE: {
actions: sendTo(
({ event }) => event.sender,
{ type: 'TOKEN' },
{ delay: 1000 },
),
},
},
},
},
});
const authClientMachine = createMachine({
id: 'client',
initial: 'idle',
states: {
idle: {
on: {
AUTH: { target: 'authorizing' },
},
},
authorizing: {
invoke: {
id: 'auth-server',
src: authServerMachine,
},
entry: sendTo('auth-server', ({ self }) => ({
type: 'CODE',
sender: self,
})),
on: {
TOKEN: { target: 'authorized' },
},
},
authorized: {
type: 'final',
},
},
});
Note that by default sendTo
will send events anonymously, in which case the receiver will not know the source of the event.
Multiple Actors
You can invoke multiple actors by specifying each in an array:
invoke: [
{ id: 'actor1', src: 'someActor' },
{ id: 'actor2', src: 'someActor' },
{ id: 'logActor', src: 'logActor' },
];
Each invocation will create a new instance of that actor, so even if the src
of multiple actors are the same (e.g., someActor
above), multiple instances of someActor
will be invoked.
Testing
You can test invoked actors by asserting that the parent actor receives expected events from the invoked actor.
const machine = setup({
actors: {
countLogic,
},
}).createMachine({
invoke: {
src: 'countLogic',
},
});
Referencing Invoked Actors
Actors can be read on snapshot.children.<actorId>
. The returned value is an ActorRef
object, with properties like:
id
- the ID of the actorsend()
getSnapshot()
actor.subscribe({
next(snapshot) {
console.log(Object.keys(snapshot.children));
},
});
snapshot.children
is a key-value object where the keys are the actor ID and the value is the ActorRef
.
Invoke and TypeScript
You should the setup({ ... })
API to properly infer types for invoked actor logic.
import { setup, fromPromise, assign } from 'xstate';
interface User {
id: string;
name: string;
}
const machine = setup({
actors: {
fetchUser: fromPromise<User, { userId: string }>(async ({ input }) => {
const response = await fetch(`https://example.com/${input.userId}`);
return response.json();
}),
},
}).createMachine({
// ...
context: {
user: null,
userId: 42,
},
initial: 'idle',
states: {
idle: {
on: {
editUserDetails: { target: 'loadingUser' },
},
},
loadingUser: {
invoke: {
src: 'fetchUser',
input: ({ context }) => ({
userId: context.userId, // Type enforced to be string
}),
onDone: {
actions: assign({
user: ({ event }) => event.output, // Strongly typed as User
}),
},
},
},
},
});
Read the documentation on setting up state machines for more information.
Invoke cheatsheet
Cheatsheet: invoke an actor
import { setup, createActor, fromPromise, assign } from 'xstate';
const fetchUser = (userId: string) =>
fetch(`https://example.com/${userId}`).then((response) => response.text());
const userMachine = setup({
actors: {
getUser: fromPromise(async ({ input }: { input: { userId: string } }) => {
const data = await fetchUser(input.userId);
return data;
}),
},
}).createMachine({
// …
states: {
idle: {
on: {
FETCH: { target: 'loading' },
},
},
loading: {
invoke: {
id: 'getUser',
src: 'getUser',
input: ({ context: { userId } }) => ({ userId }),
onDone: {
target: 'success',
actions: assign({ user: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error }),
},
},
},
success: {},
failure: {
on: {
RETRY: { target: 'loading' },
},
},
},
});
Cheatsheet: invoke an actor on the root of the machine
import { createMachine } from 'xstate';
import { fromEventObservable, fromEvent } from 'rxjs';
const interactiveMachine = createMachine({
invoke: {
src: fromEventObservable(
() => fromEvent(document.body, 'click') as Subscribable<EventObject>,
),
},
on: {
click: {
actions: ({ event }) => console.log(event),
},
},
});
Cheatsheet: invoke multiple actors as an array
import { createMachine } from 'xstate';
const vitalsWorkflow = createMachine({
states: {
CheckVitals: {
invoke: [
{ src: 'checkTirePressure' /* ... */ },
{ src: 'checkOilPressure' /* ... */ },
{ src: 'checkCoolantLevel' /* ... */ },
{ src: 'checkBattery' /* ... */ },
],
},
},
});