Stately
State Machines

Eventless (always) transitions

Eventless transitions are transitions that happen without an explicit event. These transitions are always taken when the transition is enabled.

Eventless transitions are specified on the always state property and often referred to as “always” transitions.

You can easily visualize and simulated eventless transitions in Stately’s editor. Read more about eventless transitions in Stately’s editor.

import { createMachine } from 'xstate';
const machine = createMachine({
  states: {
    form: {
      initial: 'valid',
      states: {
        valid: {},
        invalid: {},
      },
      always: {
        guard: 'isValid',
        target: 'valid',
      },
    },
  },
});

Eventless transitions and guards

Eventless transitions are taken immediately after normal transitions are taken. They are only taken if enabled, for example, if their guards are true. This makes eventless transitions helpful in doing things when some condition is true.

Avoid infinite loops

Since unguarded “always” transitions always run, you should be careful not to create an infinite loop. XState will help guard against most infinite loop scenarios.

Eventless transitions with no target nor guard will cause an infinite loop. Transitions using guard and actions may run into an infinite loop if its guard keeps returning true.

You should define eventless transitions either with:

  • target
  • guard + target
  • guard + actions
  • guard + target + actions

If target is declared, the value should differ from the current state node.

When to use

Eventless transitions can be helpful when a state change is necessary, but there is no specific trigger for that change.

import { createMachine } from 'xstate';

const machine = createMachine({
  id: 'kettle',
  initial: 'lukewarm',
  context: {
    temperature: 80,
  },
  states: {
    lukewarm: {
      on: {
        boil: { target: 'heating' },
      },
    },
    heating: {
      always: {
        guard: ({ context }) => context.temperature > 100,
        target: 'boiling',
      },
    },
    boiling: {
      entry: ['turnOffLight'],
      always: {
        guard: ({ context }) => context.temperature <= 100,
        target: 'heating',
      },
    },
  },
  on: {
    'temp.update': {
      actions: ['updateTemperature'],
    },
  },
});
Click to interact

Observability and transient states

States that are only entered and left via eventless (always) transitions in the same step are transient: the machine passes through them without ever emitting a snapshot to subscribers. As a result:

  • actor.subscribe() only receives the final snapshot after all eventless transitions have been applied.
  • waitFor(actor, predicate) will never see those intermediate states, so a predicate that matches a transient state or its tags will not resolve.
  • snapshot.matches(...) and snapshot.hasTag(...) will never be true for a state that is only reached and left via always in one step.

Entry and exit actions on those states still run; only the snapshot emission is skipped.

When you need to observe or wait for such a state (for example in tests or when matching tags):

  1. Use the inspection API and listen for @xstate.microstep events. Each microstep (including eventless transitions) is reported there, so you can detect when the machine passes through a transient state.
  2. Use a zero-delay transition instead of always when observability is required: after: { 0: 'nextState' }. The behavior is the same in a single tick, but the actor emits a snapshot for that state, so subscribe, waitFor, matches, and hasTag work as expected.
// Transient: not observable via subscribe/waitFor/matches/hasTag
always: { target: 'nextState' }

// Observable: use when you need to wait for or match this state
after: { 0: 'nextState' }

Eventless transitions and TypeScript

XState v5 requires TypeScript version 5.0 or greater.

For best results, use the latest TypeScript version. Read more about XState and TypeScript

Eventless transitions can potentially be enabled by any event, so the event type is the union of all possible events.

import { createMachine } from 'xstate';

const machine = createMachine({
  types: {} as {
    events: { type: 'greet'; message: string } | { type: 'submit' };
  },
  // ...
  always: {
    actions: ({ event }) => {
      event.type; // 'greet' | 'submit'
    },
    guard: ({ event }) => {
      event.type; // 'greet' | 'submit'
      return true;
    },
  },
});

Eventless transitions cheatsheet

Cheatsheet: root eventless (always) transition

import { createMachine } from 'xstate';

const machine = createMachine({
  always: {
    guard: 'isValid',
    actions: ['doSomething'],
  },
  // ...
});

Cheatsheet: state eventless (always) transition

import { createMachine } from 'xstate';

const machine = createMachine({
  initial: 'start',
  states: {
    start: {
      always: {
        guard: 'isValid',
        target: 'otherState',
      },
    },
    otherState: {
      /* ... */
    },
  },
});

On this page