Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ee5052a
note
linux-root Aug 1, 2025
c8522b5
draft for WIO.Fork
linux-root Aug 3, 2025
6acefc6
draft for WIO.Parallel
linux-root Aug 4, 2025
582734a
draft doc for signal
linux-root Aug 6, 2025
3774889
draft doc for timer
linux-root Aug 6, 2025
835b31e
draft doc for loop
linux-root Aug 7, 2025
e5e0dac
draft doc for fork
linux-root Aug 7, 2025
7d01613
draft doc for parallel
linux-root Aug 7, 2025
2d6c495
add draft-signal example with OperationOutputs
linux-root Aug 8, 2025
785bfb3
add draft-timer example with OperationOutputs
linux-root Aug 8, 2025
3d2484f
add draft-loop example with OperationOutputs
linux-root Aug 8, 2025
48d8548
add draft-parallel example with OperationOutputs
linux-root Aug 8, 2025
fad5870
rm
linux-root Aug 10, 2025
8038dd3
update drafting support doc for handle errors
linux-root Aug 10, 2025
d627af1
add drafting support note for Sequencing Operations
linux-root Aug 10, 2025
ec9ddbc
add drafting support notes for operations
linux-root Aug 10, 2025
2443617
add drafting support for interuption
linux-root Aug 18, 2025
2fe5249
Update website/docs/operations/02.1-timers.mdx
linux-root Aug 20, 2025
88c3811
Update website/docs/operations/05-loops.mdx
linux-root Aug 21, 2025
1e41577
Update website/docs/operations/05-loops.mdx
linux-root Aug 21, 2025
1e52e57
remove redundant tips
linux-root Aug 21, 2025
f345425
update fork draft
linux-root Aug 24, 2025
fadcc55
add missing draft-interruption.json
linux-root Aug 24, 2025
3096632
add draft-interruption.bpmn
linux-root Aug 24, 2025
89e18bd
add draft-interruption.mermaid
linux-root Aug 24, 2025
df15e2f
simplify example
linux-root Aug 28, 2025
f5f1a48
simplify example
linux-root Aug 28, 2025
111ef8a
simplify example
linux-root Aug 28, 2025
ab51add
make use of existing DraftWorkflowContext
linux-root Sep 1, 2025
fb5ae39
draft support for checkpoint wio
linux-root Sep 11, 2025
b413bb4
draft support for retry
linux-root Sep 12, 2025
0723506
scalafmt
linux-root Sep 12, 2025
e759d0d
scalafmt
linux-root Sep 12, 2025
c7eb205
simplify docs, improve checkpoints, recovery and retries support
Krever Sep 24, 2025
659415d
missing files
Krever Sep 24, 2025
64fccc1
fix tests
Krever Sep 24, 2025
351b077
scalafmt
Krever Sep 24, 2025
65a0308
Merge branch 'main' into complete-draft-api
Krever Sep 24, 2025
0701c9f
bring back ability to do .toInterruption
Krever Sep 24, 2025
0a41c13
scalafmt
Krever Sep 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion website/docs/operations/01-run-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ These operations need special handling because they shouldn't be re-executed dur

:::info

Workflows4s currently supports onyl cats-effect IO. If you're interested in other effect systems, please check [here](https://github.com/business4s/workflows4s/issues/59).
Workflows4s currently supports only cats-effect IO. If you're interested in other effect systems, please check [here](https://github.com/business4s/workflows4s/issues/59).

:::

Expand Down Expand Up @@ -69,3 +69,10 @@ You can add descriptions to RunIO operations to provide additional context in di
```

<OperationOutputs name="run-io-description"/>

## Drafting support

Executing logic comes with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftStepExample.scala start=start_doc end=end_doc
```
9 changes: 9 additions & 0 deletions website/docs/operations/02-await-signal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ Signal handling is essential for workflows that need to pause and wait for exter

<OperationOutputs name="handle-signal"/>

## Drafting Support

Awaiting signals come with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftSignalExample.scala start=start_draft end=end_draft
```

<OperationOutputs name="draft-signal"/>

# Unhandled Signals

The Workflows4s API allows arbitrary signals to be sent to a workflow instance. While this provides flexibility, it also
Expand Down
9 changes: 9 additions & 0 deletions website/docs/operations/02.1-timers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ This operation enables time-based coordination, allowing workflows to pause for
```

<OperationOutputs name="timer"/>

## Drafting Support

Awaiting time comes with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftTimerExample.scala start=start_draft end=end_draft
```

<OperationOutputs name="draft-timer"/>
4 changes: 4 additions & 0 deletions website/docs/operations/03-sequence-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ Dynamic steps cannot be rendered statically. Consider using [Forks](/docs/operat
```

<OperationOutputs name="flat-map"/>

## Drafting Support

For Sequencing Operations, no dedicated draft API is required. Drafting can be done using the standard API.
4 changes: 4 additions & 0 deletions website/docs/operations/04-handle-errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ Errors defined and handled should be domain errors and not technical ones. Domai
```

<OperationOutputs name="handle-error-with"/>

## Drafting Support

For Error Handling, no dedicated draft API is required. Drafting can be done using the standard API.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9.1
sidebar_position: 5.1
---
import OperationOutputs from '@site/src/components/OperationOutputs';

Expand All @@ -23,9 +23,9 @@ This element is one of the most complicated ones and requires you to configure q
<OperationOutputs name="for-each"/>


### Draft Mode
## Drafting Support

For quick prototyping, you can use the draft API:
Iterating comes with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/ForEachExample.scala start=draft_start end=draft_end
```
Expand Down
9 changes: 9 additions & 0 deletions website/docs/operations/05-loops.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ In the future this will be detected by the [linter](https://github.com/business4
```

<OperationOutputs name="loop"/>

## Drafting Support

Loops come with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftLoopExample.scala start=start_draft end=end_draft
```

<OperationOutputs name="draft-loop"/>
9 changes: 9 additions & 0 deletions website/docs/operations/06-fork.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ It's equivalent to `if` instructions but allow for static rendering of the workf
```

<OperationOutputs name="fork"/>

## Drafting Support

Branching comes with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftForkExample.scala start=start_draft end=end_draft
```

<OperationOutputs name="draft-choice"/>
9 changes: 9 additions & 0 deletions website/docs/operations/07-Interrupting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ This allows selecting an alternative path based on such a signal.
```

<OperationOutputs name="interruption-signal"/>

## Drafting Support

Interruptions come with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftInterruptionExample.scala start=start_draft end=end_draft
```

<OperationOutputs name="draft-interruption"/>
11 changes: 10 additions & 1 deletion website/docs/operations/08-parallel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ Workflow's state is continously updated after each step completion (doesn't wait
```scala file=./main/scala/workflows4s/example/docs/ParallelExample.scala start=start_doc end=end_doc
```

<OperationOutputs name="parallel"/>
<OperationOutputs name="parallel"/>

## Drafting Support

Parallel flows come with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftParallelExample.scala start=start_draft end=end_draft
```

<OperationOutputs name="draft-parallel"/>
16 changes: 15 additions & 1 deletion website/docs/operations/10-checkpoints.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,18 @@ In practice, checkpointing and recovery are often used together to enable workfl

1. Version 1 of a workflow includes a segment that is checkpointed
2. Version 2 of the workflow removes that segment but includes a recovery mechanism
3. When a workflow instance created with version 1 is resumed using version 2, the recovery mechanism processes the checkpoint event, allowing the workflow to continue without the removed segment
3. When a workflow instance created with version 1 is resumed using version 2, the recovery mechanism processes the checkpoint event, allowing the workflow to continue without the removed segment

## Drafting Support

Checkpointing and recovery come with [drafting support](20-drafting.mdx).

### Draft Checkpoint
```scala file=./main/scala/workflows4s/example/docs/draft/DraftCheckpointExample.scala start=start_draft_checkpoint end=end_draft_checkpoint
```
<OperationOutputs name="draft-checkpoint" showBpmn={false}/>

### Draft Checkpoint
```scala file=./main/scala/workflows4s/example/docs/draft/DraftCheckpointExample.scala start=start_draft_recovery end=end_draft_recovery
```
<OperationOutputs name="draft-recovery" showBpmn={false}/>
10 changes: 9 additions & 1 deletion website/docs/operations/11-retry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,12 @@ If you see it as a major limitation, please reach out.

Use workflow-level retries for retry schedules spanning **minutes to hours or days**

For short-lived retries (e.g., retrying within milliseconds or seconds), prefer handling them directly inside the `IO` operation using libraries like [`cats-retry`](https://github.com/cats-effect/cats-retry).
For short-lived retries (e.g., retrying within milliseconds or seconds), prefer handling them directly inside the `IO` operation using libraries like [`cats-retry`](https://github.com/cats-effect/cats-retry).

## Drafting Support

Retries come with [drafting support](20-drafting.mdx).

```scala file=./main/scala/workflows4s/example/docs/draft/DraftRetryExample.scala start=start_draft end=end_draft
```
<OperationOutputs name="draft-retry"/>
20 changes: 20 additions & 0 deletions website/docs/operations/20-drafting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Drafting

Workflows4s comes with a dedicated API for creating renderable but NOT runnable workflow definitions.
Its goal is to allow defining the structure of a workflow that can be discussed with the team or stakeholders,
before committing more time to the actual implementation.

The entire drafting API is exposed under `WIO.draft` and documented alongside specific operations.

## Details

Drafts use exactly the same model under the hood as real definitions, but filled with dummy logic.

The drafting support simplifies the API in the following ways:

* Not requiring type parameters
* Not requiring event handling
* Not requiring run logic
* Automatically generating names from the context if not provided explicitly
* Making as many parameters as possible optional

Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ object DraftBuilder {

trait Step0[Ctx <: WorkflowContext]() {

def draft: DraftBuilderStep1 = DraftBuilderStep1()
val draft: DraftBuilderStep1.type = DraftBuilderStep1

class DraftBuilderStep1 {
def signal(name: String = null, error: String = null)(using autoName: sourcecode.Name): WIO.Draft[Ctx] = WIO.HandleSignal(
draftSignal,
SignalHandler[Unit, Unit, Any]((_, _) => ???),
dummyEventHandler,
WIO.HandleSignal.Meta(
Option(error).map(ErrorMeta.Present(_)).getOrElse(ErrorMeta.noError),
getEffectiveName(name, autoName),
None,
),
)
object DraftBuilderStep1 {
def signal(name: String = null, error: String = null)(using autoName: sourcecode.Name): WIO.Draft[Ctx] =
WIO.HandleSignal(
draftSignal,
SignalHandler[Unit, Unit, Any]((_, _) => ???),
dummyEventHandler,
WIO.HandleSignal.Meta(
Option(error).map(ErrorMeta.Present(_)).getOrElse(ErrorMeta.noError),
getEffectiveName(name, autoName),
None,
),
)
def timer(name: String = null, duration: FiniteDuration = null)(using autoName: sourcecode.Name): WIO.Timer[Ctx, Any, Nothing, Nothing] =
WIO.Timer(
Option(duration) match {
Expand All @@ -47,6 +48,13 @@ object DraftBuilder {
),
)

def choice(name: String = null)(branches: (String, WIO.Draft[Ctx])*)(using autoName: sourcecode.Name): WIO.Draft[Ctx] = {
val branchWios = branches.map { case (branchName, wio) =>
WIO.Branch(_ => None, wio, Some(branchName))
}
WIO.Fork(branchWios.toVector, getEffectiveName(name, autoName).some, None)
}

def forEach(forEach: WIO.Draft[Ctx], name: String = null)(using autoName: sourcecode.Name): WIO.Draft[Ctx] = {
val effName = getEffectiveName(name, autoName).some
WIO.ForEach(_ => ???, forEach, () => ???, null, _ => ???, (_, _, _) => ???, (_, _) => ???, None, null, WIOMeta.ForEach(effName))
Expand All @@ -63,6 +71,80 @@ object DraftBuilder {
base.transformInput((_: Any) => ???).map(_ => ???)
}

def parallel(elements: WIO.Draft[Ctx]*): WIO.Draft[Ctx] = {
val parallelElements = elements.map { element =>
WIO.Parallel.Element(element.map(_ => ???), (interimState: WCState[Ctx], _: WCState[Ctx]) => interimState)
}
WIO
.Parallel[Ctx, Any, Nothing, WCState[Ctx], WCState[Ctx]](
elements = parallelElements,
formResult = _ => ???,
initialInterimState = (_: Any) => ???,
)
.transformInput((_: Any) => ???)
.map(_ => ???)
}

def recovery: WIO.Draft[Ctx] = WIO.Recovery(dummyEventHandler)

def interruptionSignal(
signalName: String = null,
operationName: String = null,
error: String = null,
)(using autoName: sourcecode.Name): WIO.Interruption[Ctx, Nothing, Nothing] = {
val draftSignalHandling = WIO
.HandleSignal(
draftSignal,
SignalHandler[Unit, Unit, WCState[Ctx]]((_, _) => ???),
dummyEventHandler[WCEvent[Ctx], Unit],
WIO.HandleSignal.Meta(
Option(error).map(ErrorMeta.Present(_)).getOrElse(ErrorMeta.noError),
Option(signalName).getOrElse(getEffectiveName(null, autoName)),
Option(operationName),
),
)
.transformInput((_: WCState[Ctx]) => ???)
.map(_ => ???)
WIO.Interruption(draftSignalHandling, WIO.HandleInterruption.InterruptionType.Signal)
}

def interruptionTimeout(
timerName: String = null,
duration: FiniteDuration = null,
)(using autoName: sourcecode.Name): WIO.Interruption[Ctx, Nothing, Nothing] = {
val draftTimer = WIO
.Timer(
Option(duration) match {
case Some(value) => WIO.Timer.DurationSource.Static(value.toJava)
case None => WIO.Timer.DurationSource.Dynamic(_ => ???)
},
dummyEventHandler[WCEvent[Ctx], WIO.Timer.Started],
Option(timerName).orElse(getEffectiveName(null, autoName).some),
dummyEventHandler[WCEvent[Ctx], WIO.Timer.Released],
)
.transformInput((_: WCState[Ctx]) => ???)
.map(_ => ???)
WIO.Interruption(draftTimer, WIO.HandleInterruption.InterruptionType.Timer)
}

def retry(base: WIO.Draft[Ctx]): WIO.Draft[Ctx] = {
WIO
.Retry(
base,
(_: Throwable, _: WCState[Ctx], _: java.time.Instant) => ???,
)
.transformInput((_: Any) => ???)
.map(_ => ???)
}
def checkpoint(base: WIO.Draft[Ctx]): WIO.Draft[Ctx] = WIO.Checkpoint(base, (_, _) => ???, dummyEventHandler)

object syntax {
extension (base: WIO.Draft[Ctx]) {
def draftCheckpointed: WIO.Draft[Ctx] = checkpoint(base)
def draftRetry: WIO.Draft[Ctx] = retry(base)
}
}

}

}
Expand Down
37 changes: 37 additions & 0 deletions workflows4s-core/src/test/scala/workflows4s/wio/WIODraftTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,42 @@ class WIODraftTest extends AnyFreeSpec with Matchers with OptionValues with Eith
case _ => fail("Expected Sequence model")
}
}

"should create a fork with correct branches" in {
val approve = WIO.draft.step("Approve")
val reject = WIO.draft.step("Reject")
val wio = WIO.draft.choice("Review")(
"Approved" -> approve,
"Rejected" -> reject,
)
val model = wio.toProgress.toModel

model match {
case WIOModel.Fork(branches, meta) =>
meta.name shouldBe Some("Review")
branches.length shouldBe 2

branches.head shouldBe WIOModel.RunIO(WIOMeta.RunIO(Some("Approve"), None, None))
branches(1) shouldBe WIOModel.RunIO(WIOMeta.RunIO(Some("Reject"), None, None))
case _ => fail("Expected Fork model")
}
}

"should create a parallel step with multiple elements" in {
val step1: Draft[Ctx] = WIO.draft.step("task1")
val step2: Draft[Ctx] = WIO.draft.step("task2")
val step3: Draft[Ctx] = WIO.draft.step("task3")
val parallel: Draft[Ctx] = WIO.draft.parallel(step1, step2, step3)
val model = parallel.toProgress.toModel

model match {
case WIOModel.Parallel(elements) =>
elements.length shouldBe 3
elements(0) shouldBe WIOModel.RunIO(WIOMeta.RunIO(Some("task1"), None, None))
elements(1) shouldBe WIOModel.RunIO(WIOMeta.RunIO(Some("task2"), None, None))
elements(2) shouldBe WIOModel.RunIO(WIOMeta.RunIO(Some("task3"), None, None))
case _ => fail("Expected Parallel model")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package workflows4s.example.docs.draft

import workflows4s.wio.DraftWorkflowContext.*

object DraftCheckpointExample {

// start_draft_checkpoint
val base = WIO.draft.step()

val checkpointed = WIO.draft.checkpoint(base)

// or with a postfix application
import WIO.draft.syntax.*
val checkpointed2 = base.draftCheckpointed
// end_draft_checkpoint

// start_draft_recovery
val recovery = WIO.draft.recovery
// end_draft_recovery
}
Loading