FsFlow 0.2.0

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package FsFlow --version 0.2.0
                    
NuGet\Install-Package FsFlow -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FsFlow" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FsFlow" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="FsFlow" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FsFlow --version 0.2.0
                    
#r "nuget: FsFlow, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FsFlow@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FsFlow&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=FsFlow&version=0.2.0
                    
Install as a Cake Tool

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.

ci NuGet License

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
  • Result for expected business errors
  • Async or .NET Task for 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 Async and .NET Task interop
  • 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.read and Flow.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 Async or Task
  • 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 / Task code
  • 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 Result function
  • 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 Result when 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 .NET AppHost + 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 .NET AppHost + 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 Async and .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 Result already reads well
  • a direct Task<'T> boundary is the clearest option

Learn The Library In This Order

  1. docs/GETTING_STARTED.md
  2. docs/TINY_EXAMPLES.md
  3. docs/ARCHITECTURAL_STYLES.md
  4. docs/WHY_FSFLOW.md
  5. docs/TASK_ASYNC_INTEROP.md
  6. docs/FSTOOLKIT_MIGRATION.md
  7. docs/ENV_SLICING.md
  8. docs/SEMANTICS.md
  9. examples/README.md
  10. docs/TROUBLESHOOTING_TYPES.md
  11. src/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.fromAsyncResult
  • Flow.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
0.3.0 111 5/1/2026
0.2.0 83 4/27/2026

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.