Skip to content
Version: XState v5

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:

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:

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 the onDone transition
  • reject() (or throw an error), which will take the onError 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:

  1. Setting up the callback actor logic in the actors object of the setup({ actors: { ... } }) call
  2. Invoking the callback actor logic by its source name (src) in the invoke 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:

  1. Setting up the observable logic in the actors object of the setup({ actors: { ... } }) call
  2. Invoking the observable logic by its source name (src) in the invoke 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:

  1. Setting up the transition actor logic in the actors object of the setup({ actors: { ... } }) call
  2. Invoking the transition actor logic by its source name (src) in the invoke 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:

  1. Setting up the state machine actor logic in the actors object of the setup({ actors: { ... } }) call
  2. Invoking the state machine actor logic by its source name (src) in the invoke 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 reciever 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 actor
  • send()
  • 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' /* ... */ },
],
},
},
});