FsFlow 0.2.0
Prefix ReservedSee the version list below for details.
dotnet add package FsFlow --version 0.2.0
NuGet\Install-Package FsFlow -Version 0.2.0
<PackageReference Include="FsFlow" Version="0.2.0" />
<PackageVersion Include="FsFlow" Version="0.2.0" />
<PackageReference Include="FsFlow" />
paket add FsFlow --version 0.2.0
#r "nuget: FsFlow, 0.2.0"
#:package FsFlow@0.2.0
#addin nuget:?package=FsFlow&version=0.2.0
#tool nuget:?package=FsFlow&version=0.2.0
FsFlow
<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/content/img/fsflow-readme-dark.svg"> <source media="(prefers-color-scheme: light)" srcset="docs/content/img/fsflow-readme-light.svg"> <img alt="FsFlow" src="docs/content/img/fsflow-readme-light.svg" width="160"> </picture>
F# application workflows that compose with normal Result, Async, and .NET Task.
Docs: adz.github.io/FsFlow
When one F# use case starts mixing Result, async {}, .NET Task, and dependency management,
the code often stops reading like the happy path.
Why This Exists In F#
Most real F# application code ends up mixing:
- dependencies passed through several layers, whether as one app environment or explicit feature dependencies
Resultfor expected business errorsAsyncor.NET Taskfor IO
That often turns into one of these shapes:
AppEnv -> Async<Result<'value, 'error>>
or:
Deps -> Input -> Async<Result<'value, 'error>>
plus helper modules, adapters, and wrapper-specific boilerplate.
FsFlow is a minimal, idiomatic way to represent that shape directly in F#.
It gives that use case one shape:
Flow<'env, 'error, 'value>
and one workflow:
flow { ... }
so dependency access, typed failures, Async, and Task stay in one place instead of spreading
across helper modules, adapters, and wrapper-specific CEs.
Before And After
Before:
let handle (deps: UserDeps) userId =
async {
let! loaded = deps.LoadName userId |> Async.AwaitTask
match loaded with
| Error error ->
return Error (GatewayFailed error)
| Ok loadedName ->
match validateName loadedName with
| Error error ->
return Error error
| Ok validName ->
return Ok $"{deps.Prefix} {validName}"
}
After:
let handle (deps: UserDeps) userId : Flow<RequestContext, AppError, string> =
flow {
let! loadedName =
deps.LoadName userId
|> Flow.mapError GatewayFailed
let! validName = validateName loadedName
return $"{deps.Prefix} {validName}"
}
This is the same application flow without the plumbing taking over the happy path. If your codebase prefers a single booted app environment, that style works too. FsFlow supports both.
What It Actually Is
FsFlow is a small, focused F# library built around composable flows:
- explicit environment requirements
- typed failures via Result
- cancellation-aware execution without passing tokens through every step
- direct
Asyncand.NET Taskinterop - helpers for retry, timeout, logging, and scoped cleanup
The point is to keep that code in one place, with one workflow type, while staying in ordinary F#:
- one computation expression:
flow {} - one CE that binds
Result,Async,Task, and env access in one place - explicit environment access through
Flow.readandFlow.env - explicit execution through
Flow.toAsync env cancellationToken flow
It does not replace F# Async, .NET Task, or Result.
It gives you a smaller, more consistent way to compose them in application code.
Cancellation stays explicit at the runtime boundary and in cold task signatures, but usually disappears inside flow {} itself.
What It Is Not
FsFlow is not trying to become a new runtime platform.
- it does not reimplement
AsyncorTask - it does not introduce its own concurrency system
- it does not hide when effects run
- it stays explicit at the execution boundary with
Flow.toAsync
The library is intentionally narrow:
- better DX for mixed application workflows
- better readability across
Result/Async/Taskcode - less wrapper and adapter noise around the happy path
Reader-style composition can feel imported in F# when it arrives as a larger FP stack. FsFlow keeps the same practical benefits in plain F# terms:
- one computation expression
- plain functions
- explicit environment access
- no transformer stacks or typeclass machinery
Flows are cold by default. Building a flow does not run it.
Full Code
type AppEnv =
{ Prefix: string
LoadName: int -> Task<Result<string, AppError>> }
type AppError =
| MissingName
| GatewayFailed of string
let validateName (name: string) =
if System.String.IsNullOrWhiteSpace name then
Error MissingName
else
Ok name
let greet userId : Flow<AppEnv, AppError, string> =
flow {
let! loadName = Flow.read _.LoadName
let! loadedName =
loadName userId
|> Flow.mapError GatewayFailed
let! validName = validateName loadedName
let! prefix = Flow.read _.Prefix
return $"{prefix} {validName}"
}
let result =
greet 42
|> Flow.toAsync
{ Prefix = "Hello"
LoadName = fun _ -> Task.FromResult(Ok "Ada") }
System.Threading.CancellationToken.None
|> Async.RunSynchronously
This full example shows the intended shape in one place:
- one env dependency through
Flow.read - one plain
Resultfunction - one
Task<Result<_,_>>boundary - one happy-path workflow in
flow {}
Where To Use It
Use FsFlow at the effectful application boundary:
- handlers
- use cases
- service orchestration
- infrastructure-facing application services
Keep the domain plain F# where possible:
- plain functions
- plain domain types
- plain
Resultwhen that already reads well
Keep domain code plain. Use flow {} by default in the application layer.
Supported Architectural Styles
FsFlow supports three valid architectural styles:
- Booted App Environment
- Explicit Dependencies + Context
- Standard
.NETAppHost + DI
The library does not require one application shape. Choose the style that fits your codebase and team:
- use Booted App Environment when app composition simplicity matters most
- use Explicit Dependencies + Context when feature locality and testability matter most
- use standard
.NETAppHost + DI when familiarity and incremental adoption matter most
Read docs/ARCHITECTURAL_STYLES.md for examples and trade-offs.
When FsFlow Fits Well
FsFlow is a good fit when:
- a workflow needs 2 to 5 dependencies
- validation, IO, and error translation all belong in one use case
- your code touches both
Asyncand.NET Task - you want expected failures in the type rather than scattered exception handling
- retry, timeout, and cleanup belong close to the business flow
FsFlow is usually not worth it when:
- the code is mostly pure
- plain
Resultalready reads well - a direct
Task<'T>boundary is the clearest option
Learn The Library In This Order
docs/GETTING_STARTED.mddocs/TINY_EXAMPLES.mddocs/ARCHITECTURAL_STYLES.mddocs/WHY_FSFLOW.mddocs/TASK_ASYNC_INTEROP.mddocs/FSTOOLKIT_MIGRATION.mddocs/ENV_SLICING.mddocs/SEMANTICS.mdexamples/README.mddocs/TROUBLESHOOTING_TYPES.mdsrc/FsFlow/Flow.fs
Compatibility
AOT Verified
NativeAOT is verified in this repo through a small publish-and-run probe application.
.NET only - no Fable story
The design is .NET-first. Cancellation is explicit in the Flow execution model, and task interop is part of the first-class public surface.
This means we don't have a Fable story (yet).
Existing Shapes
FsFlow builds on existing F# and .NET primitives rather than replacing them.
If a direct Result, Async<'T>, or Task<'T> boundary is already the clearest shape,
use that shape directly.
If you already have Async<Result<_,_>> or FsToolkit-style workflows, you can adopt Flow
per use case and interoperate through adapters like:
Flow.fromAsyncResultFlow.toAsyncResult
Run The Repo
Run the examples:
# Longer main example
dotnet run --project examples/FsFlow.Examples/FsFlow.Examples.fsproj
# Maintenance example:
dotnet run --project examples/FsFlow.MaintenanceExamples/FsFlow.MaintenanceExamples.fsproj
# Minimal playground example:
dotnet run --project examples/FsFlow.Playground/FsFlow.Playground.fsproj
Run the test suite:
dotnet run --project tests/FsFlow.Tests/FsFlow.Tests.fsproj
Run the NativeAOT probe:
bash scripts/run-aot-probe.sh
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- FSharp.Core (>= 10.1.201)
-
net8.0
- FSharp.Core (>= 10.1.201)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on FsFlow:
| Package | Downloads |
|---|---|
|
FsFlow.Net
.NET task-oriented workflows, task interop, and task-specific runtime helpers for FsFlow. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Second public preview release of FsFlow with completed package rename alignment, refreshed docs and branding, updated solution and project layout, improved docs site presentation, and release pipeline cleanup after the initial 0.1.0 package.