Skip to main content

Effect Types

Workflows4s is effect-polymorphic — your workflows can run with any effect type (IO, Future, Try, etc.). The effect type appears in three places, each with its own constraints:

Where effects are declared

1. WorkflowContext — workflow logic

The Effect type in your WorkflowContext controls what effects your workflow steps can use (e.g., in runIO).

import workflows4s.wio.WorkflowContext

object MyCtx extends WorkflowContext {
type Effect[T] = cats.effect.IO[T] // or Future[T], Try[T], etc.
type State = MyState
type Event = MyEvent
}

Requirements: None by default. Some convenience builders (like handleSignal(...).purely or retry) need Applicative[Effect], but this is optional and required only on builder level.

2. WorkflowRuntime — execution environment

Each runtime has its own effect type F[_], dictated by its execution model:

RuntimeEffect typeConstraint on F
InMemorySynchronizedRuntimeAny FMonadThrow[F]
InMemoryConcurrentRuntimeAny FAsync[F] (cats-effect)
PekkoRuntimeFuture (hardcoded)
DatabaseRuntimeAny FAsync[F] (cats-effect)
SqliteRuntimeAny FAsync[F] (cats-effect)

3. WorkflowInstanceEngine — the bridge

WorkflowInstanceEngine[F, Ctx] sits between the workflow logic and the runtime. Its F must match the runtime's effect type, and it carries a WCEffectLift[Ctx, F] that transforms the context's Effect into the runtime's F.

When Effect and F are the same (the common case), WCEffectLift is derived automatically — you don't need to provide anything:

import cats.effect.IO
import workflows4s.runtime.instanceengine.WorkflowInstanceEngine

// Context uses IO, engine uses IO — WCEffectLift is derived automatically
val engine = WorkflowInstanceEngine.basic[IO, MyCtx.Ctx]()

When they differ (e.g., workflow uses IO but Pekko needs Future), use engine.mapK to transform:

import scala.concurrent.Future
import cats.effect.unsafe.implicits.global
import scala.concurrent.ExecutionContext.Implicits.global as ec

// Build engine in your workflow's effect type, then transform for the runtime
val ioEngine: WorkflowInstanceEngine[IO, MyCtx.Ctx] = WorkflowInstanceEngine.basic[IO, MyCtx.Ctx]()
val futureEngine: WorkflowInstanceEngine[Future, MyCtx.Ctx] =
ioEngine.mapK([A] => (fa: IO[A]) => fa.unsafeToFuture())

The base engine (via WorkflowInstanceEngine.basic or .default) requires MonadThrow[F]. Additional wrappers can add their own constraints.

Examples

scala.util.Try

No extra dependencies — MonadThrow[Try] is provided by cats.

object TryExample {
import scala.util.Try
import workflows4s.wio.WorkflowContext
import workflows4s.runtime.InMemorySynchronizedRuntime
import workflows4s.runtime.instanceengine.WorkflowInstanceEngine

object Ctx extends WorkflowContext {
type Effect[T] = Try[T]
type State = String
sealed trait Event
case class Greeting(msg: String) extends Event
}
import Ctx.*

val greet: WIO[Any, Nothing, String] =
WIO
.runIO[Any](_ => Try(Greeting("Hello from Try!")))
.handleEvent((_, evt) => evt.msg)
.autoNamed()

val engine = WorkflowInstanceEngine.basic[Try, Ctx.Ctx]()
val runtime = InMemorySynchronizedRuntime.create[Try, Ctx.Ctx](greet, "", engine)
val instance = runtime.createInstance("id")
// instance: Try[InMemorySynchronizedWorkflowInstance[Try, Ctx]]
}

scala.concurrent.Future (with Pekko)

object FutureExample {
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global as ec
import workflows4s.wio.WorkflowContext
import workflows4s.runtime.pekko.PekkoRuntime
import workflows4s.runtime.instanceengine.WorkflowInstanceEngine
import org.apache.pekko.actor.typed.ActorSystem

object Ctx extends WorkflowContext {
type Effect[T] = Future[T]
type State = String
sealed trait Event
case class Greeting(msg: String) extends Event
}
import Ctx.*

val greet: WIO[Any, Nothing, String] =
WIO
.runIO[Any](_ => Future.successful(Greeting("Hello from Future!")))
.handleEvent((_, evt) => evt.msg)
.autoNamed()

given ActorSystem[?] = ???
val engine: WorkflowInstanceEngine[Future, Ctx.Ctx] = ???
val runtime: PekkoRuntime[Ctx.Ctx] = PekkoRuntime.create[Ctx.Ctx]("greeting", greet, "", engine)
// runtime: PekkoRuntime[Ctx] — backed by Pekko Persistence
}

cats.effect.IO

object IOExample {
import cats.effect.IO
import workflows4s.wio.WorkflowContext
import workflows4s.runtime.cats.effect.InMemoryConcurrentRuntime
import workflows4s.runtime.instanceengine.WorkflowInstanceEngine

object Ctx extends WorkflowContext {
type Effect[T] = IO[T]
type State = String
sealed trait Event
case class Greeting(msg: String) extends Event
}
import Ctx.*

val greet: WIO[Any, Nothing, String] =
WIO
.runIO[Any](_ => IO(Greeting("Hello from IO!")))
.handleEvent((_, evt) => evt.msg)
.autoNamed()

val engine = WorkflowInstanceEngine.basic[IO, Ctx.Ctx]()
val runtime = InMemoryConcurrentRuntime.default[IO, Ctx.Ctx](greet, "", engine)
// runtime: IO[InMemoryConcurrentRuntime[IO, Ctx]]
}

zio.Task

Requires zio-interop-cats for cats typeclasses.

object ZIOExample {
import zio.{Task, ZIO}
import zio.interop.catz.*
import workflows4s.wio.WorkflowContext
import workflows4s.runtime.InMemorySynchronizedRuntime
import workflows4s.runtime.instanceengine.WorkflowInstanceEngine

object Ctx extends WorkflowContext {
type Effect[T] = Task[T]
type State = String
sealed trait Event
case class Greeting(msg: String) extends Event
}
import Ctx.*

val greet: WIO[Any, Nothing, String] =
WIO
.runIO[Any](_ => ZIO.attempt(Greeting("Hello from ZIO!")))
.handleEvent((_, evt) => evt.msg)
.autoNamed()

val engine = WorkflowInstanceEngine.basic[Task, Ctx.Ctx]()
val runtime = InMemorySynchronizedRuntime.create[Task, Ctx.Ctx](greet, "", engine)
// runtime: InMemorySynchronizedRuntime[Task, Ctx]
}