Side Effects

Synchronous entry effects

Use .onEntry for effects that run during send. Failures become ActionFailedError and the transition is not persisted. Good for logging, metrics, validation, and quick sync work.

Assemblies also support per-state .onEnter(state)(…) / .onExit(state)(…) for effects tied to arriving in or leaving a state regardless of which edge was taken.

{
  enum OrderState derives Finite:
    case Created, Processing, AwaitingResult, Succeeded, Failed

  enum OrderEvent derives Finite:
    case StartPayment
    case CheckPayment(orderId: String)
    case PaymentSucceeded(txnId: String)
    case PaymentFailed(message: String)

  import OrderState.*, OrderEvent.*

  ZIO.scoped {
    for
      log <- Ref.make(List.empty[String])
      machine = Machine(
        assembly[OrderState, OrderEvent](
          (Created via StartPayment to Processing)
            .onEntry { (_, target) =>
              log.update(s"entered $target" :: _)
            }
        )
      )
      fsm   <- machine.start(Created)
      _     <- fsm.send(StartPayment)
      state <- fsm.currentState
      notes <- log.get
    yield (state.toString, notes)
  }.asDoc
}
(Processing,List(entered Processing))

Producing effects

Use .producing for async work that returns another event. The effect forks as a daemon; the produced event is sent back to the FSM. Errors are logged and do not fail the original transition.

{
  enum OrderState derives Finite:
    case Created, Processing, AwaitingResult, Succeeded, Failed

  enum OrderEvent derives Finite:
    case StartPayment
    case CheckPayment(orderId: String)
    case PaymentSucceeded(txnId: String)
    case PaymentFailed(message: String)

  import OrderState.*, OrderEvent.*

  case class PaymentStatus(success: Boolean, txnId: String, message: String)

  def checkStatus(orderId: String): UIO[PaymentStatus] =
    ZIO.succeed(PaymentStatus(true, "txn-123", ""))

  val producingMachine = Machine(
    assembly[OrderState, OrderEvent](
      (Processing via event[CheckPayment] to AwaitingResult)
        .producing { (ev, _) =>
          ev match
            case CheckPayment(orderId) =>
              checkStatus(orderId).map {
                case PaymentStatus(true, txnId, _) => PaymentSucceeded(txnId)
                case PaymentStatus(false, _, msg)  => PaymentFailed(msg)
              }
            case _ => ZIO.succeed(PaymentFailed("unexpected event"))
        },
      AwaitingResult via event[PaymentSucceeded] to Succeeded,
      AwaitingResult via event[PaymentFailed] to Failed,
    )
  )

  Mermoid.diagram(
    MermaidVisualizer.flowchart(producingMachine),
    DocsDiagrams.diagramConfig,
  )
}
CheckPaymentPaymentSucceededPaymentFailed
{
  enum OrderState derives Finite:
    case Created, Processing, AwaitingResult, Succeeded, Failed

  enum OrderEvent derives Finite:
    case StartPayment
    case CheckPayment(orderId: String)
    case PaymentSucceeded(txnId: String)
    case PaymentFailed(message: String)

  import OrderState.*, OrderEvent.*

  case class PaymentStatus(success: Boolean, txnId: String, message: String)

  def checkStatus(orderId: String): UIO[PaymentStatus] =
    ZIO.succeed(PaymentStatus(true, "txn-123", ""))

  val producingMachine = Machine(
    assembly[OrderState, OrderEvent](
      (Processing via event[CheckPayment] to AwaitingResult)
        .producing { (ev, _) =>
          ev match
            case CheckPayment(orderId) =>
              checkStatus(orderId).map {
                case PaymentStatus(true, txnId, _) => PaymentSucceeded(txnId)
                case PaymentStatus(false, _, msg)  => PaymentFailed(msg)
              }
            case _ => ZIO.succeed(PaymentFailed("unexpected event"))
        },
      AwaitingResult via event[PaymentSucceeded] to Succeeded,
      AwaitingResult via event[PaymentFailed] to Failed,
    )
  )

  ZIO.scoped {
    for
      fsm   <- producingMachine.start(Processing)
      _     <- fsm.send(CheckPayment("order-1"))
      _     <- ZIO.sleep(100.millis)
      state <- fsm.currentState
    yield state
  }.asDoc
}
Succeeded

Combine .producing with @@ Aspect.timeout for self-driving heartbeats (see the Heartbeat domain page and examples/heartbeat).

Next: Running FSMs.