You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/features/sagas.md
+73-10Lines changed: 73 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,28 +12,91 @@ Sagas are an established design pattern for managing complex, long-running opera
12
12
- The saga pattern assures that all operations are either completed successfully or the corresponding compensation activities are run to undo any completed work.
By default, compensations execute sequentially in the reverse order they were added. To run them in parallel, use `$this->setParallelCompensation(true)`. To ignore exceptions that occur inside compensation activities while still running them sequentially, use `$this->setContinueWithError(true)` instead.
44
+
When the workflow catches an exception, `$this->compensate()` runs every registered compensation in **reverse order**. In the example above, if `BookRentalCarActivity` fails, the engine cancels the hotel first and then the flight — unwinding the saga from the most recent step backward.
45
+
46
+
## Compensation ordering
47
+
48
+
By default, compensations execute **sequentially in reverse registration order**. This is the safest default because later steps may depend on earlier ones.
49
+
50
+
## Parallel compensation
51
+
52
+
To run compensations in parallel, use `setParallelCompensation(true)`. When parallel compensation is enabled, each compensation closure should return a started (but not awaited) activity call so the engine can execute them concurrently:
Use `startActivity()` (not `activity()`) inside parallel compensation closures. `startActivity()` returns a pending call that the engine collects and runs through `all()`, while `activity()` would block inline and defeat the purpose of parallelism.
85
+
86
+
## Continue with error
87
+
88
+
By default, if a compensation activity throws an exception, the remaining compensations are skipped and the error propagates. To run all compensations regardless of individual failures, use `setContinueWithError(true)`:
89
+
90
+
```php
91
+
$this->setContinueWithError(true);
92
+
```
93
+
94
+
When enabled, the engine catches and discards exceptions from each compensation closure and continues to the next one. This is useful when compensations are independent and you want a best-effort cleanup even if some steps fail.
95
+
96
+
## How it works
97
+
98
+
-`addCompensation()` registers a callable that will be invoked during `compensate()`
99
+
-`compensate()` iterates the registered compensations in reverse order
100
+
- each compensation closure is a normal V2 workflow step — the activities it calls produce durable history events just like any other activity
101
+
- compensation activities are visible in Waterline's timeline and history export
102
+
- if the workflow succeeds, compensation closures are never invoked and produce no history
Copy file name to clipboardExpand all lines: docs/features/side-effects.md
+63-2Lines changed: 63 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -27,11 +27,72 @@ class MyWorkflow extends Workflow
27
27
28
28
The workflow will only call `random_int()` once and save the result, even if the workflow later fails and is retried.
29
29
30
-
**Important:** The code inside a side effect should never fail because it will not be retried. Code that can possibly fail and therefore need to be retried should be moved to an activity instead.
30
+
## When to use side effects
31
31
32
-
## How It Works
32
+
Use `sideEffect()` when you need a non-deterministic value that:
33
+
34
+
- is computed locally without external I/O (random numbers, UUIDs, timestamps)
35
+
- should never change once recorded, even across replays
36
+
- does not need retry semantics — the closure runs exactly once
37
+
38
+
```php
39
+
// Generate a correlation token for downstream systems.
If the code can fail, talks to an external service, or needs retry/timeout semantics, use an [activity](../defining-workflows/activities.md) instead of a side effect:
49
+
50
+
| Scenario | Use |
51
+
|---|---|
52
+
| Generate a random token |`sideEffect()`|
53
+
| Read a config value at decision time |`sideEffect()`|
54
+
| Call an external API |`activity()`|
55
+
| Write to a database |`activity()`|
56
+
| Send an email or notification |`activity()`|
57
+
| Compute an expensive value that can throw |`activity()`|
58
+
59
+
The rule of thumb: if the closure can throw an exception that you would want to retry, it belongs in an activity.
60
+
61
+
## How it works
33
62
34
63
- each `sideEffect()` call appends a typed `SideEffectRecorded` history event with the workflow step sequence
35
64
- workflow replay and query replay both reuse that committed value instead of re-running the closure
36
65
- Waterline surfaces the side-effect snapshot as a typed history entry in the selected run timeline
37
66
- side effects are still for replay-safe snapshots only, not for work that can fail or that needs retry semantics
67
+
68
+
## Anti-patterns
69
+
70
+
**Do not call external services inside a side effect.** If the service call fails, the side effect will not be retried and the workflow will fail permanently:
71
+
72
+
```php
73
+
// BAD: HTTP calls can fail and side effects do not retry.
**Do not put slow or blocking operations inside a side effect.** The closure runs on the workflow task thread. Long-running work delays the entire workflow task:
81
+
82
+
```php
83
+
// BAD: Expensive computation blocks the workflow task.
**Do not rely on mutable external state.** The closure is executed exactly once. If you read a value that changes over time, the snapshot is frozen at the moment of first execution — not at replay time:
91
+
92
+
```php
93
+
// The cached value is whatever it was during the first execution.
94
+
// If the cache changes later, this workflow still sees the old value.
This is by design — the snapshot is intentionally frozen for determinism. If you need a value that updates over the lifetime of the workflow, use a signal or an activity.
0 commit comments