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:
| Runtime | Effect type | Constraint on F |
|---|---|---|
InMemorySynchronizedRuntime | Any F | MonadThrow[F] |
InMemoryConcurrentRuntime | Any F | Async[F] (cats-effect) |
PekkoRuntime | Future (hardcoded) | — |
DatabaseRuntime | Any F | Async[F] (cats-effect) |
SqliteRuntime | Any F | Async[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]
}