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.JavaScriptAdd Axial to an existing Task application
Convert one module to Flow and turn it back into a Task at the boundary. Callers above it can stay on Task, and the host need not change.
This page assumes you have read Get started and repeats its checkout workflow so the example below
compiles on its own.
Choose the boundary
Put the boundary where a request, job, or message begins. For example, use a controller action, endpoint handler, hosted service loop, or message consumer.
Above the boundary, callers continue to use Task. Below it, the module uses workflows.
Do not scatter boundaries through the call tree. Each boundary starts its own runtime with its own scope, so converting a leaf function first gives you the costs of Flow and none of its cancellation or cleanup guarantees.
Start the workflow and translate the exit
StartAsTask supplies the environment, accepts the caller's cancellation token, and returns a
Task<Exit<'value, 'error>>. Match on the exit to turn typed failures into whatever your host already returns:
Show page setup
// Setup for the checked examples on this page.
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
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actualopen System.Threading
open System.Threading.Tasks
open Axial
type CheckoutError =
| OrderNotFound of orderId: int
| PaymentDeclined of reason: string
type Receipt = { OrderId: int; Total: decimal; Reference: string }
type CheckoutEnv =
{ FindTotal: int -> CancellationToken -> Task<Result<decimal, CheckoutError>>
Charge: decimal -> CancellationToken -> Task<Result<string, CheckoutError>> }
let checkout orderId : Flow<CheckoutEnv, CheckoutError, Receipt> =
flow {
let! findTotal = Flow.envWith _.FindTotal
let! charge = Flow.envWith _.Charge
let! total = ColdTask(fun cancellationToken -> findTotal orderId cancellationToken)
let! reference = ColdTask(fun cancellationToken -> charge total cancellationToken)
return { OrderId = orderId; Total = total; Reference = reference }
}
let handleCheckout (env: CheckoutEnv) (orderId: int) (cancellationToken: CancellationToken) : Task<string> =
task {
let running = (checkout orderId).StartAsTask(env, cancellationToken = cancellationToken)
match! running with
| Exit.Success receipt -> return $"200 {receipt.Reference}"
| Exit.Failure(Cause.Fail(OrderNotFound id)) -> return $"404 order {id} not found"
| Exit.Failure(Cause.Fail(PaymentDeclined reason)) -> return $"402 {reason}"
| Exit.Failure Cause.Interrupt -> return raise (OperationCanceledException(cancellationToken))
| Exit.Failure cause -> return raise (InvalidOperationException(Cause.prettyPrint string cause))
}
SystemThreadingTasksAxialFsLiveDocsGeneratedPage2_2D306DEB1823.CheckoutErrorOrderNotFoundorderId: intintAn abbreviation for the CLI type . Basic Types
PaymentDeclinedreason: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage2_2D306DEB1823.ReceiptOrderId: intTotal: decimaldecimalAn abbreviation for the CLI type . Basic Types
Reference: stringFsLiveDocsGeneratedPage2_2D306DEB1823.CheckoutEnvFindTotal: int -> CancellationToken -> Task<Result<decimal,CheckoutError>>System.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 .
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
Charge: decimal -> CancellationToken -> Task<Result<string,CheckoutError>>checkout: int -> Flow<CheckoutEnv,CheckoutError,Receipt>Axial.Flow`3flow: FlowBuilderThe universal flow { } computation expression.
findTotal: int -> CancellationToken -> Task<Result<decimal,CheckoutError>>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())
_arg1: CheckoutEnvcharge: decimal -> CancellationToken -> Task<Result<string,CheckoutError>>_arg3: CheckoutEnvtotal: decimalColdTaskcancellationToken: CancellationTokenreference: stringhandleCheckout: CheckoutEnv -> int -> CancellationToken -> Task<string>env: CheckoutEnvtask: TaskBuilderBuilds a task using computation expression syntax.
running: Task<Exit<Receipt,CheckoutError>>StartAsTask: CheckoutEnv * CancellationToken option -> Task<Exit<Receipt,CheckoutError>>Starts the workflow immediately and returns a task handle for its final exit. The work is already in flight when this returns. Use ToAsync for a cold handle. The environment used by the workflow. The optional cancellation token. Defaults to . A task that completes with the workflow exit. .NET only
cancellationTokenAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
receipt: ReceiptFailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
id: intInterruptAn administrative signal to stop the workflow (e.g., cancellation).
raise: Exception -> 'TRaises an exception The exception to raise. The result value. open System.IO exception FileNotFoundException of string let readFile (fileName: string) = if not (File.Exists(fileName)) then raise(FileNotFoundException(fileName)) File.ReadAllText(fileName) readFile "/this-file-doest-exist" When executed, raises a FileNotFoundException.
``.ctor``: CancellationToken -> unitInitializes a new instance of the class with a cancellation token. A cancellation token associated with the operation that was canceled.
cause: Cause<CheckoutError>``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
Axial.CauseprettyPrint: ('error -> string) -> Cause<'error> -> stringPretty prints a cause tree for diagnostics.
string: 'T -> stringConverts the argument to a string using ToString. For standard integer and floating point values and any type that implements IFormattable, ToString conversion uses CultureInfo.InvariantCulture. The input value. The converted string. string 'A' // evaluates to "A" string 0xff // evaluates to "255" string -10 // evaluates to "-10"
With an environment whose order 42 exists and whose payments succeed, the handler returns the host's usual responses:
let sample =
{ FindTotal = fun orderId _ -> Task.FromResult(if orderId = 42 then Ok 19.95m else Error(OrderNotFound orderId))
Charge = fun _ _ -> Task.FromResult(Ok "ch_1a2b3c") }
(handleCheckout sample 42 CancellationToken.None).Result |> shouldEqual "200 ch_1a2b3c"
(handleCheckout sample 7 CancellationToken.None).Result |> shouldEqual "404 order 7 not found"
sample: CheckoutEnvFindTotal: int -> CancellationToken -> Task<Result<decimal,CheckoutError>>orderId: intSystem.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.
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
OrderNotFoundCharge: decimal -> CancellationToken -> Task<Result<string,CheckoutError>>Result: stringGets 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.
handleCheckout: CheckoutEnv -> int -> CancellationToken -> Task<string>System.Threading.CancellationTokenPropagates notification that operations should be canceled.
None: CancellationTokenReturns an empty value. An empty cancellation token.
(|>): '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
shouldEqual: 'a -> 'a -> unitThe match handles expected errors, defects, and cancellation:
- The two
Cause.Failarms map the expected errors. The final catch-all also matches any newCheckoutErrorcase, so adding one does not make this match incomplete; review boundary mappings whenever the error union changes. Cause.Interruptbecomes a cancelled task at this host boundary. The final arm reports other causes, including defects and compound failures, as an exception with the cause text. Adapt that arm to your host's exception policy; it does not preserve the original defect exception.- The host's cancellation token flows in, so cancelling the request cancels every inner call and closes the
workflow's scope.
ColdTasksupplies that token to each task dependency without adding it to the workflow's public parameters.
Keep the environment where the host already keeps services
The environment is a record, so build it once from whatever your host already resolves:
open Microsoft.Extensions.DependencyInjection
type IOrderRepository =
abstract FindTotalAsync: orderId: int * cancellationToken: CancellationToken -> Task<Result<decimal, CheckoutError>>
type IPaymentGateway =
abstract ChargeAsync: total: decimal * cancellationToken: CancellationToken -> Task<Result<string, CheckoutError>>
let checkoutEnv (provider: IServiceProvider) : CheckoutEnv =
// GetRequiredService throws when a registration is missing, so a missing service fails here
// instead of surfacing as a null dereference later.
{ FindTotal = fun orderId cancellationToken -> provider.GetRequiredService<IOrderRepository>().FindTotalAsync(orderId, cancellationToken)
Charge = fun total cancellationToken -> provider.GetRequiredService<IPaymentGateway>().ChargeAsync(total, cancellationToken) }Register that function with your container and resolve CheckoutEnv in the handler. The workflows below the boundary
still take a plain record and stay testable without the container.
open Microsoft.Extensions.DependencyInjection
// Register the concrete services, then the environment factory that resolves them.
let configureServices (services: IServiceCollection) =
services.AddSingleton<IOrderRepository, SqlOrderRepository>() |> ignore
services.AddSingleton<IPaymentGateway, StripePaymentGateway>() |> ignore
services.AddSingleton<CheckoutEnv>(checkoutEnv) |> ignorecheckoutEnv is itself the factory IServiceProvider -> CheckoutEnv, so AddSingleton<CheckoutEnv>(checkoutEnv)
registers it directly. The handler resolves the record and hands it to the same boundary function the checked example
above used:
let handleOrder (provider: IServiceProvider) (orderId: int) (token: CancellationToken) : Task<string> =
let env = provider.GetRequiredService<CheckoutEnv>()
handleCheckout env orderId tokenConvert leaf functions last
Before binding a CancellationToken -> Task<_> function, wrap it in ColdTask. The wrapper keeps task creation cold
and receives Flow's runtime cancellation token.
When the task returns Result<'value,'error>, the builder sends Error to the expected-error channel.
Use Flow.fromTask or Flow.fromTaskResult when you compose without flow { }. Use Flow.awaitStartedTask only when
a host API has already started the operation.
Some legacy APIs do not accept a cancellation token. You can adapt one with ColdTask(fun _ -> legacyCall ()), but
Flow cancellation cannot stop the underlying operation.
Prefer cancellation-aware overloads. Add cancellation support to adapters that you control.
Check effect boundaries during migration
Add Axial.Guardrails while you migrate modules to find dependencies that the existing signatures do not expose.
The analyzer detects ambient access to clocks, randomness, the console, and other operational effects.
The default warning severity does not fail an existing build. Resolve findings in each module as you migrate it, then configure the analyzer to report errors when the project is clean.
For installation and configuration, see Installation.
Related guides
- Task and async interop covers the full set of conversions in both directions.
- Expected errors and defects explains which failures belong in
'errorand which belong in the defect channel. - Your first application replaces the host entirely when a Flow is the application root.
- Effect-boundary guardrails covers what the analyzer checks and how to mark an intended boundary.

