Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript
Typed asynchronous workflows for F#
Axial Axial

Make the safe path the easy path.

Flow is one type for work that is async, can fail, can be cancelled, and uses services. One runtime cancels, cleans up, and retries it by the same rules everywhere.

Run your first workflow

1

Async work and expected errors in one type

let! stops at the first expected error. An exception becomes a defect in the outcome instead of escaping.

Task
Fragment
task {
    match! loadCart id with
    | Error e -> return Error e
    | Ok cart ->
        match! charge cart with
        | Error e -> return Error e
        | Ok payment ->
            return Ok(receipt cart payment)
}
Flow
Fragment
flow {
    let! cart = loadCart id
    let! payment = charge cart
    return receipt cart payment
}
2

Cancellation is handled the same way everywhere

No cancellation token to pass. If one parallel load fails, the other is interrupted. On timeout, both are, and their resources are released first.

Task
Fragment
use cts =
    CancellationTokenSource
        .CreateLinkedTokenSource token
cts.CancelAfter(TimeSpan.FromSeconds 2.0)
let profile = loadProfile user cts.Token
let orders = loadOrders user cts.Token
// one failing does not stop the other
do! Task.WhenAll(profile :> Task, orders)
Flow
Fragment
Flow.zipPar
    (loadProfile user)
    (loadOrders user)
|> Flow.timeout
    (TimeSpan.FromSeconds 2.0)
    DashboardTimedOut
3

Dependencies in the signature mark the boundaries

The first type parameter names the services a flow uses. reserveStock reaches inventory and nothing else, and a test supplies only what the flow names.

Task
Fragment
// Which services does it use?
// The signature does not say.
val checkout:
    Cart -> CancellationToken
    -> Task<Result<Receipt, OrderError>>
Flow
Fragment
val reserveStock: Cart ->
    Flow<Inventory, OrderError, Reservation>
val chargeCard: Cart ->
    Flow<Billing, OrderError, Payment>
val placeOrder: Cart ->
    Flow<Shop, OrderError, Receipt>
4

Time, randomness, and IDs are services too

Reading the clock or making a GUID adds that service to the type. Production supplies live ones; a test supplies fixed ones and gets the same result every run.

Task
Fragment
// A different result on every run,
// and nothing in the type says why.
{ Id = Guid.NewGuid()
  PlacedAt = DateTimeOffset.UtcNow }
Flow
Fragment
flow {
    let! placedAt = Clock.now
    let! id = Guid.newGuid
    return { Id = id; PlacedAt = placedAt }
}
// needs: 'env :> IHasClock and IHasGuid
5

Fast underneath

A running flow is a ValueTask on .NET, so work that completes synchronously avoids a heap allocation. On Fable JS it is an Async.

6

Resources belong to their scope

A helper opens a connection and returns it. It closes when the caller's scope ends.

Fragment
flow {
    let! orders = connect "orders"
    let! billing = connect "billing"
    return summarise orders billing
}
|> Flow.scoped
7

Streams keep the same rules

Four workers at a time. After ten pages, the rest are interrupted and released.

Fragment
FlowStream.fromSeq pageIds
|> FlowStream.mapFlowPar
    (Parallelism.bounded 4) fetchPage
|> FlowStream.take 10
|> FlowStream.runCollect
8

You can see what is running

Every fiber has an id, a parent, and a name. A registry lists the live ones.

Fragment
poll
|> Flow.forkNamed "outbox-poller"
|> Flow.annotate "tenant" "acme"
|> Flow.withFiberRegistry registry
// registry.DumpAt(clock) lists live fibers
9

Fewer states to reason about, for people and for LLMs

Every flow ends one of three ways, and concurrency follows fixed rules. Reviewers and LLM assistants have fewer cases to consider.

Value Expected error, named in the type Defect or interruption
  • Forked work belongs to the scope that started it.
  • Cancellation always arrives as an interruption.
  • Resources are released in reverse order, however the scope ends.
  • Axial.Guardrails flags direct clock and randomness calls, exceptions in flow { }, and dropped cancellation tokens.

Read the full Why Flow page →

How it fits together
Typed environment
Application record
Clock
HTTP
File system
Process
Your services
Flow<'env, 'error, 'value>
Supplied at the edge
Live services
Test doubles
.NET
Browser
Node

A flow names the environment it needs. The caller supplies it at run time.

Packages

  • Axial: typed async workflows with explicit dependencies, concurrency, resources, and streams.
  • Axial.Process: run external commands and pipelines as flows, with streamed output and cancellation.
  • Axial.HttpClient: an HTTP client with typed requests, responses, and retry policies.

View all packages →