elsa-workflows / elsa-workflows/elsa-foundation

Core activity library: port Elsa 3 control-flow & primitive activities (Section 1)

Open
#255 2 comments 0 reactions 0 assignees View on GitHub
ready-for-agent
Dominant language
C#
Stars
5
Forks
1
Avg merge
3h 52m
Merged PRs (30d)
212

Description

> **Depends on #254** (server-side execution / post-commit outbox). These activities build and unit-test in-process today; they only run on the server once #254 lands. Server end-to-end execution is part of this PRD's acceptance and is gated on #254.
>
> Source inventory: `docs/reports/elsa-4-activity-gaps.md` §1. Scope here is **Section 1 only** (core control-flow + primitive activities). Sections 2–4 are out of scope (see below).

## Problem Statement

Elsa 4 ships only three usable activities — `WriteLine`, `Sequence`, and `Flowchart`. As a workflow author I cannot build anything beyond a linear list of writes: there is no way to branch on a condition, loop over a collection, run branches in parallel, assign or shape workflow data, raise a fault, or run inline code. Every real workflow I want to author — "if the order total is over X, do A else B", "for each item, send a notification", "while not approved, wait and retry" — is impossible. The designer palette is effectively empty, so the product cannot be used for its core purpose.

## Solution

A **core activity library** that ports the essential Elsa 3 control-flow and primitive activities to Elsa 4, so I can author real workflows:

- **Branch:** `If` (boolean), `Switch` (multi-way on an expression).
- **Loop:** `ForEach` (over a collection), `For` (counted), `While` / `Do` (conditional).
- **Parallelism:** `Parallel` (fork branches, join on completion).
- **Data:** `SetVariable` / `SetVariables`, `SetName`, `SetOutput`.
- **Control:** `Break`, `Complete` / `Finish`, `Fault` / `Throw`.
- **Code & I/O:** `Inline` (inline C#/expression), `WriteLines`, `ReadLine`.

Each behaves like its Elsa 3 counterpart, evaluates its inputs through the existing expression/input-binding path, and executes through the runtime scheduler — so authored workflows actually run (in-process now; on the server once #254 lands).

## User Stories

1. As a workflow author, I want an `If` activity, so that I can run one branch when a condition is true and another when it is false.
2. As a workflow author, I want the `If` activity to expose `True`/`False` outcomes, so that I can connect each branch in a flowchart.
3. As a workflow author, I want a `Switch` activity, so that I can route execution to one of several branches based on an expression value.
4. As a workflow author, I want `Switch` to support a default/no-match branch, so that unexpected values still have a defined path.
5. As a workflow author, I want a `ForEach` activity, so that I can run a body activity once per item in a collection.
6. As a workflow author, I want `ForEach` to expose the current item (and optionally index) to the body, so that the body can act on each element.
7. As a workflow author, I want a `For` activity, so that I can run a body a fixed number of times over a numeric range.
8. As a workflow author, I want a `While` activity, so that I can repeat a body while a condition holds.
9. As a workflow author, I want a `Do`/`DoWhile` activity, so that I can run a body at least once and repeat while a condition holds.
10. As a workflow author, I want loops to use the engine's iteration scope, so that each pass is a distinct activity execution and outputs do not collide across iterations.
11. As a workflow author, I want a `Parallel` activity, so that I can run multiple branches concurrently.
12. As a workflow author, I want `Parallel` to join — completing only after all (or a configured subset of) branches finish — so that downstream work waits for the branches.
13. As a workflow author, I want a `SetVariable` activity, so that I can assign a value to a workflow variable.
14. As a workflow author, I want a `SetVariables` activity, so that I can assign several variables at once.
15. As a workflow author, I want variables set by `SetVariable` to be visible to later activities' input expressions, so that data flows through the workflow.
16. As a workflow author, I want a `SetName` activity, so that I can give the running instance a meaningful name.
17. As a workflow author, I want a `SetOutput` activity, so that I can set the workflow's output value.
18. As a workflow author, I want a `Fault`/`Throw` activity, so that I can deliberately raise a fault/incident with a message.
19. As a workflow author, I want a `Finish`/`Complete` activity, so that I can end the workflow early with a defined outcome.
20. As a workflow author, I want a `Break` activity, so that I can exit a loop early.
21. As a workflow author, I want a `Correlate` activity, so that I can set the instance's correlation id from workflow data.
22. As a workflow author, I want an `Inline` activity, so that I can run a small inline code/expression step without a custom activity.
23. As a workflow author, I want `WriteLines` and `ReadLine`, so that I have console I/O parity with `WriteLine`.
24. As a workflow author, I want each new activity to evaluate its inputs (conditions, collections, values) through the same expression engine as existing activities, so that Literal/JavaScript/Variable inputs all work consistently.
25. As a workflow author, I want these activities to appear in the designer palette with their inputs and outcomes described, so that I can drag and configure them.
26. As a workflow author, I want a flowchart mixing `If`, `ForEach`, and `SetVariable` to run to completion, so that realistic compositions work, not just single activities.
27. As a platform engineer, I want leaf activities to be plain CLR types resolved by the existing CLR activity constructor, so that adding a leaf activity does not require new construction plumbing.
28. As a platform engineer, I want composite activities to schedule children through the existing runtime child-scheduling API, so that they reuse the proven Sequence/Flowchart seam rather than inventing a new one.
29. As a platform engineer, I want each composite to declare its authored→executable structure via a structure handler, so that the compiler produces correct child slots in the executable artifact.
30. As a platform engineer, I want the new runtime activity modules to take no dependency on any Design project, so that the runtime/design separation gate (§E2.2) holds.
31. As a platform engineer, I want each activity registered via a shell feature and the reconciliation catalog, so that both the runtime can construct it and the design side can surface it.
32. As an agent/contributor, I want each activity covered by focused in-process execution tests, so that its behavior is proven the way `FlowchartRuntimeTests` proves the flowchart.
33. As an operator, I want a workflow using these activities to run on the server (once #254 lands), so that authored logic executes in production, not just in tests.
34. As a workflow author, I want activity outcomes to follow a consistent vocabulary (e.g. `Done`, `True`/`False`, case names), so that connecting activities in a flowchart is predictable.
35. As a workflow author, I want a `Fault` raised inside a branch to surface as an incident on the instance, so that failures are visible and follow the engine's incident model rather than crashing the host.

## Implementation Decisions

**This PRD uses only existing seams** — the activity authoring surfaces are already proven by `WriteLine`, `Sequence`, and `Flowchart`. No new cross-cutting seam is introduced.

**Leaf-activity seam (data, control, I/O activities).**
- Implement as CLR types deriving from `ActivityBase` / `CodeActivity`, overriding `Execute(IActivityExecutionContext)`. Read inputs via `context.Get(InputArgument)`, write results via `context.Set(OutputArgument, value)`, branch via `context.SetOutcomes(string[])`, and (where a wait is needed) `context.CreateBookmark(...)`.
- These need **no new constructor**: the existing `ClrActivityConstructor` (descriptor `TypeInformation`) loads and activates the type and binds authored arguments via `ActivityArgumentBinder`.
- Covers: `SetVariable(s)`, `SetName`, `SetOutput`, `Fault`/`Throw`, `Finish`/`Complete`, `Break`, `Correlate`, `Inline`, `WriteLines`, `ReadLine`.

**Composite/control-flow seam (branch and loop activities).**
- Implement as activities that hold child structure and implement `IActivityChildCompletionHandler`. In `Execute` and `OnChildCompletedAsync`, cast to `IRuntimeActivityExecutionContext` and drive children via `ScheduleChildActivity(nodeId, schedulingActivityExecutionId, metadata)` and finish via `CompleteCompositeActivity(outcomes)` — exactly as `Sequence` does.
- Each composite declares a **structure handler** (authored structure → executable child slots), mirroring `SequenceStructureHandler` / the flowchart structure handler, so the compiler emits the right child nodes in the executable artifact.
- Branch selection (`If`/`Switch`) evaluates its condition/value input, then schedules the matching child branch. Loops (`ForEach`/`For`/`While`/`Do`) re-schedule the body on each completion using the engine's **iteration scope** (`IterationId`) so each pass is a distinct activity execution. `Parallel` schedules multiple branches and joins on the recorded branch completions using the engine's **branch scope** (`BranchId`), completing only when the join condition is met.
- Covers: `If`, `Switch`, `ForEach`, `For`, `While`, `Do`, `Parallel`.

**Cross-cutting decisions.**
- Input evaluation reuses the existing input-binding/materializer path (`InputArgument` + `RuntimeInputBinding` Literal/Expression/Variable); no new expression system.
- Outcome vocabulary is consistent and authored-connectable: composites complete with named outcomes via the existing `ActivityOutcomes` (`Done`, plus `True`/`False` for `If`, case names for `Switch`).
- `Fault`/`Throw` records an incident through the engine's incident model (gap-analysis 1.6), not by throwing to the host.
- Module placement: extend `Elsa.Activities.Primitives` for leaf activities; add composite activities in modules shaped like `Elsa.Activities.Sequence` / `Elsa.Activities.Flowchart`. New runtime activity modules reference **no** `Elsa.*.Design.*` project (§E2.2); they contribute constructors/structure handlers via DI and register types in the reconciliation catalog so the design side can describe them.
- **Dependency on #254 / variable persistence:** `SetVariable(s)` and any activity whose inputs read `variables.*` rely on the runtime persisting workflow variables across checkpoints (Seam C of #254). Until that lands, variable values are only reliable within a single in-memory pass. This PRD assumes #254 provides durable variable state.

## Testing Decisions

Good tests assert **observable execution behavior**, never internal scheduling mechanics: given inputs, which branch ran, how many iterations occurred, the variable's value after assignment, that a join waited for all branches, that a `Fault` produced an incident. They run through the real agent/scheduler loop, not by calling `Execute` directly.

- **Test seam / prior art:** the `FlowchartRuntimeFixture` + `ProbeActivityConstructor` harness used by `FlowchartRuntimeTests`, `FlowchartLoopIterationTests`, and `FlowchartImplicitJoinTests` — build a small executable, run it through the in-process agent, then assert `ActivityExecutionState` status/outcomes and which nodes ran. `WriteLineExpressionInputExecutionTests` is the prior art for input-expression evaluation.
- **Per activity (constitution §2.23):** each new activity gets focused unit/execution tests — e.g. `If` takes the true branch when the condition holds and the false branch otherwise; `ForEach` runs the body once per item with the right per-iteration item; `While` stops when the condition flips; `SetVariable` makes the value readable by a downstream activity's input expression; `Fault` records an incident; `Parallel` completes only after all branches complete.
- **Composition test:** a flowchart mixing `If` + `ForEach` + `SetVariable` runs to `Completed`, proving the activities compose, not just run in isolation.
- **Server end-to-end (gated on #254):** once the outbox fix ships, add a test that runs a representative workflow through the Groundwork-backed provider to completion, so server execution of the new activities is asserted, not assumed.

## Out of Scope

- **Section 2 composites:** `StateMachine` (gap-analysis 3.1) and sub-workflow execution (`WorkflowDefinitionActivity.Execute`, gap 1.10) — each is its own larger work item.
- **Section 3 timing/triggers:** `Delay`, `Timer`, `Cron`, `StartAt`, `Event`/signals — these need a scheduling infrastructure that does not exist yet (gap-analysis §7) and a trigger/bookmark-creating surface; separate PRD(s).
- **Section 4 integration activities:** HTTP, email, messaging, SQL, file, CSV, Slack/Telnyx/GitHub, etc. — destined for a separate `elsa-foundation-extensions` workspace, not this repo.
- **Studio designer UX:** palette icons, property editors, and authoring affordances live in `elsa-foundation-studio`; this PRD covers the backend activities and their catalog descriptors only.
- **The server-execution fix itself:** owned by #254; this PRD depends on it but does not re-implement it.

## Further Notes

- The engine already supports the execution scopes these activities need — iteration (`IterationId`) and parallel branches (`BranchId`) — so this is pure activity work on existing seams, not engine work.
- Suggested sequencing for the implementing agent: (1) leaf activities (`SetVariable`, `Fault`, `Finish`, `WriteLines`, `Inline`) as quick wins on the CLR-constructor seam; (2) `If` then `Switch` (single-branch selection); (3) the loops (`ForEach`/`For`/`While`/`Do`) on the iteration scope; (4) `Parallel` last, since the join is the most involved. Each lands with its own tests before the next.
- Acceptance is in-process execution for every activity; the server end-to-end assertion is added when #254 closes.

---

## Implementation tracking (sub-issues)

**Phase 0 — pathfinder**
- [ ] #257 — `If` + `Fault` (locks the porting pattern; everything else follows it)

**Cross-cutting**
- [ ] #258 — Reusable composite/leaf execution-test harness
- [ ] #259 — Loop iteration-variable scope (prereq for Phase 3)

**Phase 1 — leaves**
- [ ] #260 — Data: `SetVariable`, `SetVariables`, `SetName`, `SetOutput` _(durability gated on #254)_
- [ ] #261 — Control: `Finish`/`Complete`, `Break`, `Correlate`
- [ ] #262 — Code & I/O: `Inline`, `WriteLines`, `ReadLine`

**Phase 2 — branch**
- [ ] #263 — `Switch`

**Phase 3 — loops**
- [ ] #264 — `ForEach`
- [ ] #265 — `For`
- [ ] #266 — `While`
- [ ] #267 — `Do`/`DoWhile`

**Phase 4 — parallel**
- [ ] #268 — `Parallel` (Fork + Join)

**Acceptance**
- [ ] #269 — Section 1 acceptance + server end-to-end _(gated on #254)_

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with docs/reports/elsa-4-activity-gaps.md §1 and the FlowchartRuntimeFixture, ProbeActivityConstructor, FlowchartRuntimeTests, FlowchartLoopIterationTests, FlowchartImplicitJoinTests, and WriteLineExpressionInputExecutionTests. Use the existing primitive and composite activity seams, then verify focused execution tests, the If/ForEach/SetVariable composition, and server execution once #254 is available.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.