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. The examples below show eight differences from Task.
The examples share these types:
1. 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:
Show page 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 }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_DBB10DAB81D5.CartFsLiveDocsGeneratedPage0_DBB10DAB81D5.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_DBB10DAB81D5.ReservationCartId: intchargeTask: Cart -> Task<Result<Payment,OrderError>>FsLiveDocsGeneratedPage0_DBB10DAB81D5.PaymentReference: stringcheckoutTask: int -> Task<Result<Receipt,OrderError>>FsLiveDocsGeneratedPage0_DBB10DAB81D5.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_DBB10DAB81D5.OrderErrorFsLiveDocsGeneratedPage0_DBB10DAB81D5.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_DBB10DAB81D5.ReservationCartId: intcharge: Cart -> Flow<OrderError,Payment>FsLiveDocsGeneratedPage0_DBB10DAB81D5.PaymentReference: stringcheckout: int -> Flow<OrderError,Receipt>FsLiveDocsGeneratedPage0_DBB10DAB81D5.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 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_DBB10DAB81D5.ProfileName: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage0_DBB10DAB81D5.OrderNumber: intintAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage0_DBB10DAB81D5.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`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
FsLiveDocsGeneratedPage0_DBB10DAB81D5.DashboardErrorFsLiveDocsGeneratedPage0_DBB10DAB81D5.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_DBB10DAB81D5.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 two flows share a scope of their own. When both succeed, the scoped resources they acquired move to the caller's scope, as if the caller had acquired them, so zipPar (scopeResource a) (scopeResource b) acquires two resources in parallel and both stay open until the caller's scope closes. When zipPar fails, both flows' scoped resources are released before it returns. 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 flow runs in its own scope: its scoped resources move to the caller's scope when it succeeds in time, and are released when it fails or times out. timeoutToOk and timeoutWith treat scoped resources the same way. 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_DBB10DAB81D5.InventoryReserve: Cart -> Result<Reservation,OrderError>FsLiveDocsGeneratedPage0_DBB10DAB81D5.CartMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
FsLiveDocsGeneratedPage0_DBB10DAB81D5.ReservationFsLiveDocsGeneratedPage0_DBB10DAB81D5.OrderErrorFsLiveDocsGeneratedPage0_DBB10DAB81D5.BillingCharge: Cart -> Result<Payment,OrderError>FsLiveDocsGeneratedPage0_DBB10DAB81D5.PaymentFsLiveDocsGeneratedPage0_DBB10DAB81D5.ShopInventory: InventoryBilling: BillingreserveStock: Cart -> Flow<Inventory,OrderError,Reservation>cart: CartAxial.Flow`3Axial.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. On .NET, a recursive return! through bind does not keep a task per call alive. Work still pending in the source, such as a use or try/finally, lives until the source completes. 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_DBB10DAB81D5.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_DBB10DAB81D5.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`3Axial.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_DBB10DAB81D5.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
With the fixed clock and GUID implementations above, repeated runs produce the same stamp. A live service can read changing external state or hold mutable state, so reusing an environment alone does not guarantee identical results. 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 ->
Flow.delay (fun () ->
closed.Add connection.Name
Flow.ok ()))
let report : Flow<string> =
flow {
let! orders = connect "orders"
let! billing = connect "billing"
return $"{orders.Name} and {billing.Name} are open"
}
|> Flow.scoped
FsLiveDocsGeneratedPage0_DBB10DAB81D5.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 -> Flow<'env,Never,unit>) -> Flow<'env,'error,'resource>Acquires a value and registers its release with the current runtime scope. Same as Resource.create acquire release |> Flow.scopeResource; see Resource.create for how the release runs. The flow that acquires the value. The release flow 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: Connectiondelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
Add: 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.
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`3Axial.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 pollerSnapshot : Flow<ClockEnvironment, Never, string> =
flow {
let! clock = Flow.envWith (fun (env: ClockEnvironment) -> env.Clock)
let! _ = Flow.sleep (TimeSpan.FromMinutes 1.0) |> Flow.forkNamed "outbox-poller"
return registry.DumpAt(clock)
}
|> Flow.annotate "tenant" "acme"
|> Flow.withFiberRegistry registry
registry: FiberRegistry``.ctor``: int -> FiberRegistrypollerSnapshot: Flow<ClockEnvironment,Never,string>Axial.Flow`3Axial.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
flow: FlowBuilderThe universal flow { } computation expression.
clock: IClockAxial.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())
env: ClockEnvironmentClock: IClocksleep: 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.
DumpAt: IClock -> stringRenders a snapshot of live fibers as a human-readable parent/child tree, timestamped at .
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.
The snapshot is taken before the parent flow ends; when it ends, it interrupts and awaits the poller. 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
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 ambient effects, exceptions raised inside flow { }, task adapters that drop their cancellation token, and
formatting that breaks under NativeAOT; the diagnostics reference lists what
each AXG code checks.
When to use something else
Write validation and other pure transformations 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.

