Stately
XState v6 alpha

Invoke actors

Run a child actor for the lifetime of a state.

XState v6 is in alpha

APIs and behavior may change before the stable release.

Use invoke to start a child actor when a state is entered. The child stops when that state is exited.

const machine = createMachine({
  initial: 'loading',
  states: {
    loading: {
      invoke: {
        src: loadUser,
        input: ({ context }) => ({ id: context.userId }),
        onDone: { target: 'ready' },
        onError: { target: 'failed' },
        timeout: 5_000,
        onTimeout: { target: 'timedOut' }
      }
    },
    ready: {},
    failed: {},
    timedOut: {}
  }
});

Use an actor instead of an action when work is async, can be canceled, sends snapshots or affects the next state. Common examples are payment authorization and file uploads.

Invoke properties

PropertyDescription
srcRequired. The actor logic to run: a logic value, the name of logic registered in actors, or a function that returns either.
idIdentifier for the child. Defaults to a generated id based on the state node.
inputInput for the child: a value, or a function of { context, event, self }.
registryKeyRegisters the child in the actor registry under that key.
timeoutMilliseconds, or a function of { context, event } returning milliseconds.
onTimeoutTransition taken when timeout elapses. Required when timeout is set.
onDoneTransition taken when the child completes. event.output holds its output.
onErrorTransition taken when the child errors. event.error holds the error.
onSnapshotTransition taken on each child snapshot. event.snapshot holds the snapshot.

Only src is required. A state node accepts a single invoke config or an array of them.

invoke: [
  { src: heartbeat, id: 'heartbeat' },
  { src: syncLogic, id: 'sync', onError: { target: 'offline' } }
]

Actor lifecycle

Invoked actors belong to the state that starts them:

  • entering the state starts the actor
  • leaving the state stops it
  • re-entering the state stops the old actor and starts a new one

A state is re-entered by an external transition back to itself, or by a transition with reenter: true. A transition that stays inside the invoking state does not restart the child.

Invoke on the machine root runs for the whole lifetime of the actor. Root invocations survive ordinary transitions between top-level states, and restart only on a root transition with reenter: true.

const machine = createMachine({
  invoke: { src: connection, id: 'connection' },
  initial: 'idle',
  states: { idle: {}, playing: {} }
});

Children are started after the parent snapshot is committed, so a child that immediately sends an event to its parent finds the parent already in the invoking state.

Choosing logic

Invoke a machine when the child has states of its own:

checkout: {
  invoke: {
    src: paymentMachine,
    id: 'payment',
    input: ({ context }) => ({ amount: context.total }),
    onDone: { target: 'confirmed' }
  }
}

Invoke async logic for a single request. The child completes with the resolved value and errors with a rejection:

const chargeCard = createAsyncLogic({
  run: ({ input, signal }) => createCharge(input.amount, { signal })
});

Invoke callback logic for a subscription that lives as long as the state:

playing: {
  invoke: {
    src: createCallbackLogic(({ sendBack }) => {
      const id = setInterval(() => sendBack({ type: 'tick' }), 1000);
      return () => clearInterval(id);
    })
  },
  on: { tick: ({ context }) => ({ context: { time: context.time + 1 } }) }
}

Reacting to the child

onDone receives final output, onError an unhandled error, and onSnapshot each intermediate snapshot.

invoke: {
  src: uploadMachine,
  onSnapshot: ({ context, event }) => ({
    context: {
      ...context,
      progress: event.snapshot.context.progress
    }
  }),
  onDone: { target: 'complete' },
  onError: { target: 'failed' }
}

These are conveniences over lifecycle events. The child sends xstate.done.actor, xstate.error.actor and xstate.snapshot.actor events with an actorId payload; onDone, onError and onSnapshot set matches: { actorId } for you so each one only handles its own child. Handle the event directly when the transition belongs on an ancestor state:

on: {
  'xstate.done.actor': { matches: { actorId: 'payment' }, target: 'confirmed' }
}

See transitions for matches and the full list of lifecycle events.

Timing out an invocation

timeout bounds how long the invocation may run. When it elapses, onTimeout is taken and the child is stopped by exiting the state. When the child finishes first, the timeout is canceled.

authorizing: {
  invoke: {
    src: authorizePayment,
    timeout: ({ context }) => context.authTimeout,
    onTimeout: { target: 'declined' },
    onDone: { target: 'authorized' }
  }
}

Setting timeout without onTimeout throws when the machine is created. Use createAsyncLogic({ timeout }) instead when the timeout belongs to the logic rather than to this particular invocation. See timeouts.

Referencing the child

Invoked children appear on snapshot.children under their id, and transition functions receive them as children:

on: {
  cancel: ({ children }, enq) => {
    enq.sendTo(children.payment, { type: 'cancel' });
  }
}

A child sends events back through its parent argument:

const paymentMachine = createMachine({
  on: {
    approve: ({ parent }, enq) => {
      enq.sendTo(parent, { type: 'paymentApproved' });
    }
  }
});

Persistence

Invoked children are persisted with the parent snapshot and restored by createActor(machine, { snapshot }). A child restored this way keeps its context but gets a new session.

Register logic in actors and reference it by name when the persisted snapshot should stay readable and stable across refactors:

const machine = createMachine({
  actors: { chargeCard },
  initial: 'charging',
  states: {
    charging: { invoke: { src: 'chargeCard', id: 'charge' } }
  }
});

The persisted child then records src: 'chargeCard' instead of a generated source name that points back at this invoke config. Either restores, but a named source survives moving the invoke to another state. See persistence.

Common pitfalls

Invoke belongs to the state, not to the transition that led there. This is wrong:

on: {
  submit: {
    target: 'sending',
    // ❌ invoke is not a transition property
    invoke: { src: sendOrder }
  }
}

Put the invoke on the target state instead, so the actor starts when sending is entered and stops when it is left.

Invoke also is not the right tool when the child must outlive the state, or when the number of children is dynamic. Spawn those instead.

TypeScript

Provide named actor logic through setup({ actors }) or createMachine({ actors }). XState correlates src with input, so a static or mapped input is checked against that logic's input type, and onDone sees the output type of that logic.

Declare child ids with schemas.children to type children and to check that each id receives compatible logic:

setup({}).createMachine({
  schemas: {
    children: {
      payment: z.custom<ActorRefFromLogic<typeof paymentMachine>>()
    }
  },
  invoke: { id: 'payment', src: paymentMachine }
});

children.payment is then typed, children.other is a type error, and invoking incompatible logic under id: 'payment' is rejected.

Invoke cheatsheet

invoke: {
  id: 'request',
  src: requestLogic,
  input: ({ context }) => context.request,
  registryKey: 'request',
  timeout: 10_000,
  onTimeout: { target: 'timedOut' },
  onDone: { target: 'success' },
  onError: { target: 'failure' },
  onSnapshot: ({ event }) => ({ context: { snapshot: event.snapshot } })
}

On this page