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.JavaScriptWhy Flow?
Flow<'env, 'error, 'value> is a type for application work that runs asynchronously, can fail in expected ways, can
be cancelled, and uses services. Eight things change when that work returns Flow instead of Task. Each section
shows the difference in code; the last one is what they add up to.
The examples share these types:
Shared setup
type Cart = { Id: int; Total: decimal }
type Reservation = { CartId: int }
type Payment = { Reference: string }
type Receipt = { Reservation: Reservation; Payment: Payment }
type OrderError =
| CartNotFound
| OutOfStock
| CardDeclined
let receipt reservation payment = { Reservation = reservation; Payment = payment }
FsLiveDocsGeneratedPage0_4556FC416C2B.CartId: intintAn abbreviation for the CLI type . Basic Types
Total: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage0_4556FC416C2B.ReservationCartId: intFsLiveDocsGeneratedPage0_4556FC416C2B.PaymentReference: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage0_4556FC416C2B.ReceiptReservation: ReservationPayment: PaymentFsLiveDocsGeneratedPage0_4556FC416C2B.OrderErrorCartNotFoundOutOfStockCardDeclinedreceipt: Reservation -> Payment -> Receiptreservation: Reservationpayment: Payment1. Async work and expected errors in one type
With Task, an operation that can fail returns Task<Result<_, _>>, and every step has to match the result before
the next step can run:
let loadCartTask (id: int) : Task<Result<Cart, OrderError>> =
Task.FromResult(Ok { Id = id; Total = 40m })
let reserveTask (cart: Cart) : Task<Result<Reservation, OrderError>> =
Task.FromResult(Ok { CartId = cart.Id })
let chargeTask (cart: Cart) : Task<Result<Payment, OrderError>> =
Task.FromResult(Ok { Reference = "p-1" })
let checkoutTask (id: int) : Task<Result<Receipt, OrderError>> =
task {
match! loadCartTask id with
| Error error -> return Error error
| Ok cart ->
match! reserveTask cart with
| Error error -> return Error error
| Ok reservation ->
match! chargeTask cart with
| Error error -> return Error error
| Ok payment -> return Ok(receipt reservation payment)
}
loadCartTask: int -> Task<Result<Cart,OrderError>>id: intintAn abbreviation for the CLI type . Basic Types
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
FsLiveDocsGeneratedPage0_4556FC416C2B.CartFsLiveDocsGeneratedPage0_4556FC416C2B.OrderErrorSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
Id: intTotal: decimalreserveTask: Cart -> Task<Result<Reservation,OrderError>>cart: CartFsLiveDocsGeneratedPage0_4556FC416C2B.ReservationCartId: intchargeTask: Cart -> Task<Result<Payment,OrderError>>FsLiveDocsGeneratedPage0_4556FC416C2B.PaymentReference: stringcheckoutTask: int -> Task<Result<Receipt,OrderError>>FsLiveDocsGeneratedPage0_4556FC416C2B.Receipttask: TaskBuilderBuilds a task using computation expression syntax.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
error: OrderErrorreservation: Reservationpayment: Paymentreceipt: Reservation -> Payment -> ReceiptIn flow { }, let! waits for the work and stops at the first expected error:
let loadCart (id: int) : Flow<OrderError, Cart> = Flow.ok { Id = id; Total = 40m }
let reserve (cart: Cart) : Flow<OrderError, Reservation> = Flow.ok { CartId = cart.Id }
let charge (cart: Cart) : Flow<OrderError, Payment> = Flow.ok { Reference = "p-1" }
let checkout (id: int) : Flow<OrderError, Receipt> =
flow {
let! cart = loadCart id
let! reservation = reserve cart
let! payment = charge cart
return receipt reservation payment
}
loadCart: int -> Flow<OrderError,Cart>id: intintAn abbreviation for the CLI type . Basic Types
FlowA flow that requires no environment and can fail with a typed error.
FsLiveDocsGeneratedPage0_4556FC416C2B.OrderErrorFsLiveDocsGeneratedPage0_4556FC416C2B.CartAxial.Flowok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
Id: intTotal: decimalreserve: Cart -> Flow<OrderError,Reservation>cart: CartFsLiveDocsGeneratedPage0_4556FC416C2B.ReservationCartId: intcharge: Cart -> Flow<OrderError,Payment>FsLiveDocsGeneratedPage0_4556FC416C2B.PaymentReference: stringcheckout: int -> Flow<OrderError,Receipt>FsLiveDocsGeneratedPage0_4556FC416C2B.Receiptflow: FlowBuilderThe universal flow { } computation expression.
reservation: Reservationpayment: Paymentreceipt: Reservation -> Payment -> ReceiptAn exception thrown inside the work does not escape as a faulted task. It becomes a defect in the flow's outcome, kept
apart from the expected errors in OrderError.
FsToolkit.ErrorHandling's taskResult { } removes the
same nesting, and flow { } reads the same way. The result is still Task code, though: cancellation tokens,
failures in parallel work, and cleanup are still handled by hand at every call site. The next three sections cover
those. The FsToolkit.ErrorHandling comparison shows how
to use the two together.
2. Cancellation is handled the same way everywhere
With Task, cancellation is a CancellationToken argument that every function must accept and pass on. Running two
loads in parallel with a time limit needs a linked token source, and a failure in one load does not stop the other:
type Profile = { Name: string }
type Order = { Number: int }
type DashboardError =
| ProfileUnavailable
| DashboardTimedOut
let loadProfileTask (user: int) (token: CancellationToken) : Task<Profile> =
task {
do! Task.Delay(10, token)
return { Name = "Ada" }
}
let loadOrdersTask (user: int) (token: CancellationToken) : Task<Order list> =
task {
do! Task.Delay(10, token)
return [ { Number = 1 } ]
}
let dashboardTask (user: int) (token: CancellationToken) : Task<Profile * Order list> =
task {
use limit = CancellationTokenSource.CreateLinkedTokenSource token
limit.CancelAfter(TimeSpan.FromSeconds 2.0)
let profile = loadProfileTask user limit.Token
let orders = loadOrdersTask user limit.Token
let! _ = Task.WhenAll(profile :> Task, orders :> Task)
return profile.Result, orders.Result
}
FsLiveDocsGeneratedPage0_4556FC416C2B.ProfileName: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage0_4556FC416C2B.OrderNumber: intintAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage0_4556FC416C2B.DashboardErrorProfileUnavailableDashboardTimedOutloadProfileTask: int -> CancellationToken -> Task<Profile>user: inttoken: CancellationTokenSystem.Threading.CancellationTokenPropagates notification that operations should be canceled.
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
task: TaskBuilderBuilds a task using computation expression syntax.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
Delay: int * CancellationToken -> TaskCreates a cancellable task that completes after a specified number of milliseconds. The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely. A cancellation token to observe while waiting for the task to complete. A task that represents the time delay. The argument is less than -1. The task has been canceled. The provided has already been disposed.
loadOrdersTask: int -> CancellationToken -> Task<Order list>listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
dashboardTask: int -> CancellationToken -> Task<Profile * Order list>limit: CancellationTokenSourceSystem.Threading.CancellationTokenSourceSignals to a that it should be canceled.
CreateLinkedTokenSource: CancellationToken array -> CancellationTokenSourceCreates a that will be in the canceled state when any of the source tokens in the specified array are in the canceled state. An array that contains the cancellation token instances to observe. A that is linked to the source tokens. A associated with one of the source tokens has been disposed. is . is empty.
CancelAfter: TimeSpan -> unitSchedules a cancel operation on this after the specified time span. The time span to wait before canceling this . The exception thrown when this has been disposed. The exception that is thrown when is less than -1 or greater than Int32.MaxValue.
System.TimeSpanRepresents a time interval.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
profile: Task<Profile>Token: CancellationTokenGets the associated with this . The associated with this . The token source has been disposed.
orders: Task<Order list>WhenAll: Task array -> TaskCreates a task that will complete when all of the objects in an array have completed. The tasks to wait on for completion. A task that represents the completion of all of the supplied tasks. The argument was . The array contained a task.
Result: ProfileGets the result value of this . The result value of this , which is of the same type as the task's type parameter. The task was canceled. The collection contains a object. -or- An exception was thrown during the execution of the task. The collection contains information about the exception or exceptions.
Result: Order listGets the result value of this . The result value of this , which is of the same type as the task's type parameter. The task was canceled. The collection contains a object. -or- An exception was thrown during the execution of the task. The collection contains information about the exception or exceptions.
A flow takes no token. The runtime passes cancellation to all the work it starts:
let loadProfile (user: int) : Flow<ClockEnvironment, DashboardError, Profile> =
Flow.sleep (TimeSpan.FromMilliseconds 10.0) |> Flow.map (fun () -> { Name = "Ada" })
let loadOrders (user: int) : Flow<ClockEnvironment, DashboardError, Order list> =
Flow.sleep (TimeSpan.FromMilliseconds 10.0) |> Flow.map (fun () -> [ { Number = 1 } ])
let dashboard (user: int) : Flow<ClockEnvironment, DashboardError, Profile * Order list> =
Flow.zipPar (loadProfile user) (loadOrders user)
|> Flow.timeout (TimeSpan.FromSeconds 2.0) DashboardTimedOut
loadProfile: int -> Flow<ClockEnvironment,DashboardError,Profile>user: intintAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
FsLiveDocsGeneratedPage0_4556FC416C2B.DashboardErrorFsLiveDocsGeneratedPage0_4556FC416C2B.ProfileAxial.Flowsleep: TimeSpan -> Flow<'env,'error,unit>Suspends the flow for the specified duration, observing cancellation. The duration to sleep. A flow that completes after the specified delay, or is interrupted if cancelled first.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
map: ('value -> 'next) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Transforms the successful value of a flow. If the source fails, the is not executed. The original failure cause is preserved, including typed failures, interruption, and defects. Use map for pure value transformations after an effect has succeeded. A function of type 'value -> 'next to transform the successful value. The source flow of type to transform. A new with the transformed success value of type 'next. let flow = Flow.succeed 1 |> Flow.map (fun x -> x + 1)
Name: stringloadOrders: int -> Flow<ClockEnvironment,DashboardError,Order list>FsLiveDocsGeneratedPage0_4556FC416C2B.OrderlistThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
Number: intdashboard: int -> Flow<ClockEnvironment,DashboardError,(Profile * Order list)>zipPar: Flow<'env,'error,'left> -> Flow<'env,'error,'right> -> Flow<'env,'error,('left * 'right)>Combines two flows into a tuple of their values, running them concurrently. If either flow fails, the other is interrupted immediately. The first flow to combine. The second flow to combine. A flow that returns a tuple of both successful values. let combined = Flow.zipPar (Flow.succeed 1) (Flow.succeed 2) combined |> Flow.run ()
timeout: TimeSpan -> 'error -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Fails with the supplied typed error when the flow does not complete before the timeout. The timed-out flow is interrupted. The timeout owns that interruption, so it may report it as a typed error. The timeout duration. The typed error returned when the timeout wins. The source flow. A flow that returns the source outcome or the timeout error.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
DashboardTimedOutIf loadOrders fails, loadProfile is interrupted. If the time limit passes, both are interrupted, and whatever they
acquired has been released before dashboard returns DashboardTimedOut. The same holds for work started with
Flow.fork: it runs inside the scope of the flow that started it, and it is interrupted and awaited when that flow
ends. No work outlives the operation that started it, so a request that is cancelled or times out leaves nothing
running.
Retries and timeouts are functions applied to a flow, such as Flow.retry (Schedule.recurs 3). Because a flow is a
description of work rather than work that has started, a retry runs it again from the beginning.
3. Dependencies in the signature mark the boundaries
With Task, services usually arrive through a constructor or a dependency-injection container. The signature of
checkoutTask above does not say that it reserves stock or charges a card.
A flow's first type parameter names the services it uses. Each area of the application declares its own:
type Inventory = { Reserve: Cart -> Result<Reservation, OrderError> }
type Billing = { Charge: Cart -> Result<Payment, OrderError> }
type Shop = { Inventory: Inventory; Billing: Billing }
let reserveStock (cart: Cart) : Flow<Inventory, OrderError, Reservation> =
Flow.envWith (fun inventory -> inventory.Reserve cart) |> Flow.bind Flow.fromResult
let chargeCard (cart: Cart) : Flow<Billing, OrderError, Payment> =
Flow.envWith (fun billing -> billing.Charge cart) |> Flow.bind Flow.fromResult
let placeOrder (cart: Cart) : Flow<Shop, OrderError, Receipt> =
flow {
let! reservation = reserveStock cart |> Flow.localEnv _.Inventory
let! payment = chargeCard cart |> Flow.localEnv _.Billing
return receipt reservation payment
}
FsLiveDocsGeneratedPage0_4556FC416C2B.InventoryReserve: Cart -> Result<Reservation,OrderError>FsLiveDocsGeneratedPage0_4556FC416C2B.CartMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
FsLiveDocsGeneratedPage0_4556FC416C2B.ReservationFsLiveDocsGeneratedPage0_4556FC416C2B.OrderErrorFsLiveDocsGeneratedPage0_4556FC416C2B.BillingCharge: Cart -> Result<Payment,OrderError>FsLiveDocsGeneratedPage0_4556FC416C2B.PaymentFsLiveDocsGeneratedPage0_4556FC416C2B.ShopInventory: InventoryBilling: BillingreserveStock: Cart -> Flow<Inventory,OrderError,Reservation>cart: CartAxial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.FlowenvWith: ('env -> 'value) -> Flow<'env,'error,'value>Projects one value from the current environment. This is the primary way to access app dependencies, configuration, or request metadata stored in env. The projection runs only when the flow is executed, so constructing the flow is still pure and side-effect free. Prefer small projections over passing a large environment deeper into reusable helpers. A function that extracts a value from the environment. A containing the projected value. let currentTime () = Flow.envWith (fun (environment: BaseRuntime) -> environment.Clock.UtcNow())
inventory: Inventory(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
bind: ('value -> Flow<'env,'error,'next>) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Sequences a dependent flow after a successful value. This is the flatmap operation for . The continuation only runs when the source flow succeeds, and it receives the successful value. Use bind when the next effect depends on the previous result; use map when the next step is pure. A function that takes the successful value and returns a new flow. The source flow to sequence. A representing the combined workflow. let flow = Flow.succeed 1 |> Flow.bind (fun x -> Flow.succeed (x + 1))
fromResult: Result<'value,'error> -> Flow<'env,'error,'value>Lifts a into a synchronous flow. The result value to lift. A flow that succeeds or fails based on the result. Flow.fromResult (Ok "success") |> Flow.run ()
chargeCard: Cart -> Flow<Billing,OrderError,Payment>billing: BillingplaceOrder: Cart -> Flow<Shop,OrderError,Receipt>FsLiveDocsGeneratedPage0_4556FC416C2B.Receiptflow: FlowBuilderThe universal flow { } computation expression.
reservation: ReservationlocalEnv: ('outerEnvironment -> 'innerEnvironment) -> Flow<'innerEnvironment,'error,'value> -> Flow<'outerEnvironment,'error,'value>Runs a flow against an environment derived from the outer environment. Use this to embed a smaller workflow inside a larger application environment without changing the smaller workflow's type. The mapping is applied at execution time. This is useful for preserving narrow helper signatures while still running everything from one app boundary. A function that maps the outer environment to the inner environment. The flow to run with the inner environment. A flow that expects the outer environment. let flow = Flow.succeed 1 |> Flow.localEnv (fun outer -> outer)
_arg1: Shoppayment: Payment_arg3: Shopreceipt: Reservation -> Payment -> ReceiptreserveStock can only reach Inventory, and the compiler rejects a call to billing from inside it.
placeOrder is where the two areas meet, and Flow.localEnv marks each crossing. A test of reserveStock supplies an
Inventory and nothing else.
4. Time, randomness, and IDs are services too
Code that reads DateTimeOffset.UtcNow or calls Guid.NewGuid() gives a different result on every run, and nothing in
its signature says so. Testing it means waiting, or replacing a static.
In a flow, the clock and the GUID generator are services like any other. Reading them adds their requirement to the flow's type:
type PlacedOrder = { Id: Guid; PlacedAt: DateTimeOffset }
let stamp<'env when 'env :> IHasClock and 'env :> IHasGuid> : Flow<'env, Never, PlacedOrder> =
flow {
let! placedAt = Clock.now
let! id = Guid.newGuid
return { Id = id; PlacedAt = placedAt }
}
FsLiveDocsGeneratedPage0_4556FC416C2B.PlacedOrderId: GuidSystem.GuidRepresents a globally unique identifier (GUID).
PlacedAt: DateTimeOffsetSystem.DateTimeOffsetRepresents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).
stamp: Flow<'env,Never,PlacedOrder>envAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
Axial.PlatformService.IHasGuidDeclares that an environment supplies the GUID service.
Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.NeverRepresents an error channel that cannot occur.
flow: FlowBuilderThe universal flow { } computation expression.
placedAt: DateTimeOffsetAxial.PlatformService.ClockHelpers for the clock service.
now: Flow<'env,'error,DateTimeOffset>Reads the current UTC timestamp from an explicit clock service.
id: GuidAxial.PlatformService.GuidHelpers for the GUID service.
newGuid: Flow<'env,'error,Guid>Reads a GUID from an explicit GUID service.
Clock.now requires an environment that has a clock, and the requirement carries up to every flow that calls
stamp, whether or not their types are written out. The application supplies the live services; a test supplies fixed
ones and gets the same result every time:
type Fixed =
{ Clock: IClock
Guid: IGuid }
interface IHasClock with
member this.Clock = this.Clock
interface IHasGuid with
member this.Guid = this.Guid
let fixedServices =
{ Clock = Clock.fromValue (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero))
Guid = Guid.fromValue Guid.Empty }
let placed = stamp |> Flow.run fixedServices
FsLiveDocsGeneratedPage0_4556FC416C2B.FixedClock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
Guid: IGuidAxial.PlatformService.IGuidProvides synchronous GUID generation.
Axial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: FixedClock: Fixed -> unit -> IClockAxial.PlatformService.IHasGuidDeclares that an environment supplies the GUID service.
Guid: Fixed -> unit -> IGuidfixedServices: FixedAxial.PlatformService.ClockHelpers for the clock service.
fromValue: DateTimeOffset -> IClockCreates a deterministic clock that always returns the supplied instant; measured durations are zero.
``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than or later than .
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
Axial.PlatformService.GuidHelpers for the GUID service.
fromValue: Guid -> IGuidCreates a deterministic GUID service that always returns the supplied value.
System.GuidRepresents a globally unique identifier (GUID).
Empty: GuidA read-only instance of the structure whose value is all zeros.
placed: Exit<PlacedOrder,Never>stamp: Flow<'env,Never,PlacedOrder>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
This is what keeps a workflow pure in the useful sense: everything it depends on arrives through its environment, so the same environment gives the same result. With Axial.Guardrails installed, the build also warns when code reads the clock, randomness, or environment variables directly instead of through a service.
5. Resources belong to the flow that acquired them
With Task, use releases a resource when the function that opened it returns. A helper that opens a connection
cannot hand it to its caller and still guarantee it is closed, and a resource opened by background work has no owner.
A flow registers each resource with its scope. A helper can acquire one and return it, and the resource stays open until the scope that ran the helper closes:
type Connection = { Name: string }
let closed = ResizeArray<string>()
let connect (name: string) : Flow<Connection> =
Flow.scopeAcquireRelease (Flow.ok { Name = name }) (fun connection _ ->
closed.Add connection.Name
Task.CompletedTask)
let report : Flow<string> =
flow {
let! orders = connect "orders"
let! billing = connect "billing"
return $"{orders.Name} and {billing.Name} are open"
}
|> Flow.scoped
FsLiveDocsGeneratedPage0_4556FC416C2B.ConnectionName: stringstringAn abbreviation for the CLI type . Basic Types
closed: ResizeArray<string>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
connect: string -> Flow<Connection>name: stringFlowA flow that requires no environment and cannot fail with a typed error.
Axial.FlowscopeAcquireRelease: Flow<'env,'error,'resource> -> ('resource -> CancellationToken -> Task) -> Flow<'env,'error,'resource>Acquires a value and registers its release with the current runtime scope. The flow that acquires the value. The release action run when the current scope closes. A flow that succeeds with the acquired value.
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
connection: ConnectionAdd: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
CompletedTask: TaskGets a task that has already completed successfully. The successfully completed task.
report: Flow<string>flow: FlowBuilderThe universal flow { } computation expression.
orders: Connectionbilling: Connection(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
Flow.scoped closes both connections when report ends, in reverse order, whether it succeeds, fails, or is
interrupted. Work forked inside the scope is interrupted and awaited before its resources are released. The
scopes guide compares this with use in more detail.
6. Streams keep the same rules
FlowStream is a stream of values produced by flows. Its operators run under the same cancellation and scope rules,
so a pipeline with parallel workers stops all of them, and releases what they acquired, as soon as the consumer has
enough:
let fetchPage (id: int) : Flow<ClockEnvironment, Never, string> =
Flow.sleep (TimeSpan.FromMilliseconds 5.0) |> Flow.map (fun () -> $"page {id}")
let firstTen : Flow<ClockEnvironment, Never, string list> =
FlowStream.fromSeq [ 1..1000 ]
|> FlowStream.mapFlowPar (Parallelism.bounded 4) fetchPage
|> FlowStream.take 10
|> FlowStream.runCollect
fetchPage: int -> Flow<ClockEnvironment,Never,string>id: intintAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
Axial.NeverRepresents an error channel that cannot occur.
stringAn abbreviation for the CLI type . Basic Types
Axial.Flowsleep: TimeSpan -> Flow<'env,'error,unit>Suspends the flow for the specified duration, observing cancellation. The duration to sleep. A flow that completes after the specified delay, or is interrupted if cancelled first.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
map: ('value -> 'next) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Transforms the successful value of a flow. If the source fails, the is not executed. The original failure cause is preserved, including typed failures, interruption, and defects. Use map for pure value transformations after an effect has succeeded. A function of type 'value -> 'next to transform the successful value. The source flow of type to transform. A new with the transformed success value of type 'next. let flow = Flow.succeed 1 |> Flow.map (fun x -> x + 1)
firstTen: Flow<ClockEnvironment,Never,string list>listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
Axial.FlowStreamModulefromSeq: 'value seq -> FlowStream<'env,'error,'value>Creates a stream from a synchronous sequence of values. The sequence of values to be emitted by the stream. A that yields each value from the sequence. FlowStream.fromSeq [1..10] |> FlowStream.runCollect |> Flow.run ()
(..): ^T -> ^T -> ^T seqThe standard overloaded range operator, e.g. [n..m] for lists, seq {n..m} for sequences The start value of the range. The end value of the range. The sequence spanning the range. [1..4] // Evaluates to [1; 2; 3; 4] [1.5..4.4] // Evaluates to [1.5; 2.5; 3.5] ['a'..'d'] // Evaluates to ['a'; 'b'; 'c'; 'd'] [|1..4|] // Evaluates to an array [|1; 2; 3; 4|] { 1..4 } // Evaluates to a sequence [1; 2; 3; 4])
mapFlowPar: Parallelism -> ('a -> Flow<'b,'c,'d>) -> FlowStream<'b,'c,'a> -> FlowStream<'b,'c,'d>Maps values with a continuously replenished, bounded set of child fibers. Results are emitted in completion order. After each result is consumed, the next upstream value starts, so there are no strict batch barriers. At most the configured number of mappings are active or retained. The first failure observed stops the stream; the terminal consumer's child scope interrupts and awaits all remaining mappings before returning.
Axial.ParallelismModuleCreates bounds for parallel Flow and stream operators.
bounded: int -> ParallelismCreates a positive concurrency bound. Thrown when is not positive.
take: int -> FlowStream<'a,'b,'c> -> FlowStream<'a,'b,'c>Emits at most values. stream |> FlowStream.take 10
runCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
At most four pages are fetched at a time, and once ten have arrived the remaining fetches are interrupted. The same
pipeline written with IAsyncEnumerable and SemaphoreSlim has to cancel and await its own workers. The
streams guide covers batching, time-based operators, and connecting streams to queues.
7. You can see what is running
A Task does not know which operation started it, and .NET cannot list the tasks that are still running. Every forked
flow is a fiber with an id, a parent, and an optional name. A FiberRegistry lists the live ones as a tree, and
annotations travel to every fiber the flow starts:
let registry = FiberRegistry(100)
let poller : Flow<ClockEnvironment, Never, unit> =
flow {
let! _ = Flow.sleep (TimeSpan.FromMinutes 1.0) |> Flow.forkNamed "outbox-poller"
return ()
}
|> Flow.annotate "tenant" "acme"
|> Flow.withFiberRegistry registry
registry: FiberRegistry``.ctor``: int -> FiberRegistrypoller: Flow<ClockEnvironment,Never,unit>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
Axial.NeverRepresents an error channel that cannot occur.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
Axial.Flowsleep: TimeSpan -> Flow<'env,'error,unit>Suspends the flow for the specified duration, observing cancellation. The duration to sleep. A flow that completes after the specified delay, or is interrupted if cancelled first.
System.TimeSpanRepresents a time interval.
FromMinutes: float -> TimeSpanReturns a that represents a specified number of minutes, where the specification is accurate to the nearest millisecond. A number of minutes, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
forkNamed: string -> Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber carrying a diagnostic name. The name appears in FiberDump snapshots, FiberRegistry dumps, and telemetry fiber spans, so long-lived background fibers are recognizable in diagnostics instead of showing as bare ids. The diagnostic name recorded in the fiber's metadata. The flow to fork. A flow that produces a handle.
annotate: string -> string -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Adds a runtime annotation for the duration of the supplied flow. Annotations are runtime metadata for diagnostics, logging, metrics, and tracing. Nested annotations with the same key override the outer value for the nested flow only. The annotation key. The annotation value. The source flow. A flow that runs with the supplied annotation in the ambient runtime context.
withFiberRegistry: FiberRegistry -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Tracks every fiber forked inside the flow in . The registry's observer is composed with any observer already installed, so telemetry hooks and the registry can coexist from separate installs. Install once at the application edge, keep the registry, and call registry.DumpAt(clock) (or registry.Snapshot()) whenever a live fiber tree is needed. The registry that receives fiber lifecycle events. The source flow. A flow whose forked fibers are tracked in the registry.
registry.DumpAt(clock) prints each live fiber with its name, status, age, and annotations, which is what a diagnostics
endpoint or a stuck shutdown needs. Axial.Telemetry turns the same information into OpenTelemetry spans and metrics,
including a count of failures in background work that nothing awaited. See observability.
8. Fewer states to reason about
The first seven points shrink what a reader, a reviewer, or a test has to consider.
A flow ends in one of three ways: its value, one of the errors named in its type, or an unexpected outcome (a defect or
an interruption). There is no fourth case such as an exception that escapes past a Result, or a task still running
after its caller has returned.
Concurrency follows fixed rules instead of per-call-site choices. Every forked fiber belongs to the scope that started it. Cancellation always arrives as an interruption, never as an error value that one layer handles and the next ignores. A scope releases its resources in reverse order however it ends. An interrupted take from a queue never loses a value. Code that uses these pieces does not need to be checked for each interleaving by hand, and the torture tests check that the rules hold under random interruption.
The build enforces the conventions the types cannot. Axial.Guardrails reports direct calls
to the clock, randomness, and other ambient effects (AXG001), exceptions raised inside flow { } (AXG003), task
adapters that drop their cancellation token (AXG005), and formatting that breaks under NativeAOT (AXG006).
This matters most when the code is written by an LLM coding assistant, or by someone new to the codebase. The
signature says which services exist and which errors are expected, so the assistant has less to guess. The mistakes it
tends to make are harder to make or show up in the build: forked work is owned by its scope even if nobody awaits it,
there is no token to forget to pass, and reading DateTime.Now or dropping a token in a task adapter produces a
build warning. Review can then focus on the business logic.
When to use something else
Validation and other pure transformations need none of this: write them as ordinary functions returning Result.
Return Flow from application
operations: the ones that call services, can be cancelled, need a timeout or retry, own a resource, or start
background work. A Task-based service can be called from a flow directly, so you can adopt Axial one module at a time; see
Add Axial to an existing Task application.
Related guides
- Task vs Flow: seven scenarios compares ownership, cancellation, retries, and background work in longer examples.
- Flow compared with Effect-TS explains the shared model and where F# leads to a different API.
- Compiler-directed, AOT, and Fable describes the supported runtime targets.

