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.JavaScriptProviding the Environment
Everything so far has been about reading the environment. This page is about producing the value in the first place, once at startup and again in each test.
There are three ways. Prefer them in this order.
1. Construct it
Build the record and hand it over. Nothing else is involved:
Shared 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 actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
type IUserStore =
abstract Count: unit -> int
type AppEnv = { Users: IUserStore; Clock: IClock }
let summary : Flow<AppEnv, Never, string> =
flow {
let! users = Flow.envWith _.Users
let! clock = Flow.envWith _.Clock
let today = clock.UtcNow().ToString "yyyy-MM-dd"
return $"{users.Count()} users on {today}"
}
FsLiveDocsGeneratedPage18_4556FC416C2B.IUserStoreCount: IUserStore -> unit -> intunitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
intAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage18_4556FC416C2B.AppEnvUsers: IUserStoreClock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
summary: Flow<AppEnv,Never,string>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.
stringAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
users: IUserStoreAxial.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: AppEnvclock: IClock_arg3: AppEnvtoday: stringUtcNow: unit -> DateTimeOffsetToString: string -> stringConverts the value of the current object to its equivalent string representation using the specified format. A format string. A string representation of the value of the current object, as specified by . The length of is one, and it is not one of the standard format specifier characters defined for . -or- does not contain a valid custom format pattern. The date and time is outside the range of dates supported by the calendar used by the current culture.
Production builds the record from the live services, for example { Users = SqlUserStore(connectionString); Clock = Clock.live }. A test builds it from fixed ones:
let fixedEnv =
{ Users = { new IUserStore with member _.Count() = 3 }
Clock = Clock.fromValue (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)) }
summary |> Flow.run fixedEnv |> shouldEqual (Exit.Success "3 users on 2026-01-01")
fixedEnv: AppEnvUsers: IUserStoreFsLiveDocsGeneratedPage18_4556FC416C2B.IUserStore_: IUserStoreCount: unit -> intClock: IClockAxial.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.
summary: Flow<AppEnv,Never,string>(|>): '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
shouldEqual: 'a -> 'a -> unitAxial.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.
Most applications need nothing more. The wiring is a value literal rather than a resolution process, so a reader can see where every service comes from.
For the operational services, Axial.PlatformService ships a ready-made bundle so you do not have to name all five:
> open System;;
> open Axial.PlatformService;;
> let runtime () = { BaseRuntime.liveValue with Clock = Clock.fromValue (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)) };;
> (Clock.now : Flow<BaseRuntime, Never, DateTimeOffset>) |> Flow.map _.Year |> Flow.run (runtime ());;val it: Exit<int,Never> = Success 2026BaseRuntime groups IClock, ILog, IRandom, IGuid, and IEnvironmentVariables, and implements one contract
per service, so helpers like Clock.now and EnvironmentVariable.get work against it directly. Embedding it
alongside your own services takes one interface member per service, delegating to wherever BaseRuntime ends up
living in your record; see Tutorial: Composing Built-in Services for the full
pattern.
2. Take it from a host container
.NET hosts already have an IServiceProvider. Use it to build the environment, then leave it behind:
type IOrderQueue =
abstract Flush: unit -> int
let flushOrders : Flow<IServiceProvider, unit, int> =
flow {
let! orders = ServiceProvider.get<IOrderQueue, _, _> ()
return orders.Flush()
}
FsLiveDocsGeneratedPage18_4556FC416C2B.IOrderQueueFlush: IOrderQueue -> unit -> intunitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
intAn abbreviation for the CLI type . Basic Types
flushOrders: Flow<IServiceProvider,unit,int>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.
System.IServiceProviderDefines a mechanism for retrieving a service object; that is, an object that provides custom support to other objects.
flow: FlowBuilderThe universal flow { } computation expression.
orders: IOrderQueueAxial.ServiceProviderReads services from an environment. This is the host boundary. Use it in glue and adapters where dynamic container lookup is the intended behaviour; application workflows should declare what they need instead. Missing registrations are configuration defects and fail through Cause.Die rather than the typed error channel. Build the environment with a layer when a missing registration should be a typed startup error.
get: unit -> Flow<'env,'error,'service>Resolves a service from the IServiceProvider in the environment. The service type being requested. The environment type. The workflow error type. A flow that succeeds with the requested service instance. let orders = ServiceProvider.get<IOrderRepository, _, _> ()
Flush: unit -> intA host's container supplies the IServiceProvider. The examples use a small stand-in:
let provider (services: (Type * obj) list) =
{ new IServiceProvider with
member _.GetService serviceType =
services |> List.tryFind (fst >> (=) serviceType) |> Option.map snd |> Option.defaultValue null }
provider: (Type * obj) list -> IServiceProviderservices: (Type * obj) listSystem.TypeRepresents type declarations: class types, interface types, array types, value types, enumeration types, type parameters, generic type definitions, and open or closed constructed generic types.
objAn abbreviation for the CLI type . Basic Types
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.
System.IServiceProviderDefines a mechanism for retrieving a service object; that is, an object that provides custom support to other objects.
_: IServiceProviderGetService: Type -> objGets the service object of the specified type. An object that specifies the type of service object to get. A service object of type . -or- if there is no service object of type .
serviceType: Type(|>): '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
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
tryFind: ('T -> bool) -> 'T list -> 'T optionReturns the first element for which the given function returns True. Return None if no such element exists. The function to test the input elements. The input list. The first element for which the predicate returns true, or None if every element evaluates to false. Try to find the first even number: let inputs = [1; 2; 3] inputs |> List.tryFind (fun elm -> elm % 2 = 0) Evaluates to Some 2 Try to find the first even number: let inputs = [1; 5; 3] inputs |> List.tryFind (fun elm -> elm % 2 = 0) Evaluates to None This is an O(n) operation in the worst case, where n is the length of the list.
fst: 'T1 * 'T2 -> 'T1Return the first element of a tuple, fst (a,b) = a. The input tuple. The first value. fst ("first", 2) // Evaluates to "first"
(>>): ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3Compose two functions, the function on the left being applied first The first function to apply. The second function to apply. The composition of the input functions. let addOne x = x + 1 let doubleIt x = x * 2 let addThenDouble = addOne >> doubleIt addThenDouble 3 // Evaluates to 8
(=): '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
Microsoft.FSharp.Core.OptionModuleContains operations for working with options. Options
map: ('T -> 'U) -> 'T option -> 'U optionmap f inp evaluates to match inp with None -> None | Some x -> Some (f x). A function to apply to the option value. The input option. An option of the input value after applying the mapping function, or None if the input is None. None |> Option.map (fun x -> x * 2) // evaluates to None Some 42 |> Option.map (fun x -> x * 2) // evaluates to Some 84
snd: 'T1 * 'T2 -> 'T2Return the second element of a tuple, snd (a,b) = b. The input tuple. The second value. snd ("first", 2) // Evaluates to 2
defaultValue: 'T -> 'T option -> 'TGets the value of the option if the option is Some, otherwise returns the specified default value. The specified default value. The input option. The option if the option is Some, else the default value. Identical to the built-in operator, except with the arguments swapped. (99, None) ||> Option.defaultValue // evaluates to 99 (99, Some 42) ||> Option.defaultValue // evaluates to 42
let withQueue = provider [ typeof<IOrderQueue>, box { new IOrderQueue with member _.Flush() = 5 } ]
flushOrders |> Flow.run withQueue |> shouldEqual (Exit.Success 5)
match flushOrders |> Flow.run (provider []) with
| Exit.Failure(Cause.Die _) -> ()
| other -> failwithf "expected a defect, got %A" other
withQueue: IServiceProviderprovider: (Type * obj) list -> IServiceProvidertypeof: TypeGenerate a System.Type runtime representation of a static type. let t = typeof<int> // Gets the System.Type t.FullName // Evaluates to "System.Int32"
FsLiveDocsGeneratedPage18_4556FC416C2B.IOrderQueuebox: 'T -> objnullBoxes a strongly typed value. The value to box. The boxed object. let x: int = 123 let obj1 = box x // obj1 is a generic object type unbox<int> obj1 // Evaluates to 123 (int) unbox<double> obj1 // Throws System.InvalidCastException
_: IOrderQueueFlush: unit -> intflushOrders: Flow<IServiceProvider,unit,int>(|>): '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
shouldEqual: 'a -> 'a -> unitAxial.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.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
DieAn unexpected defect or panic (e.g., an exception).
other: Exit<int,unit>failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
ServiceProvider.get treats a missing registration as a defect, not a typed error, because an unregistered
service is a configuration bug rather than something a workflow should handle.
The rule is one line: use IServiceProvider to build the world; do not make every business workflow depend on it.
A workflow typed Flow<IServiceProvider, _, _> can reach anything, which is exactly the property the environment
channel exists to remove. Convert at the edge and let the rest of the application name what it needs.
3. Provision it with a layer
When building the environment is itself effectful (it can fail with a typed startup error, needs a resource released later, or must await something), construction becomes a workflow of its own. Layers are those workflows. They live in a separate package because most applications never need them.
The signal is in the type. Layer<IServiceProvider, BaseRuntimeError, BaseRuntime> says: consumes a provider, may
fail with a typed startup error, produces a runtime. Axial.PlatformService ships exactly that as
BaseRuntime.fromServiceProvider, which turns dynamic registrations into an explicit BaseRuntime and reports
anything missing as BaseRuntimeError.MissingService before the first workflow runs.
let currentYear : Flow<BaseRuntime, BaseRuntimeError, int> = Clock.now |> Flow.map _.Year
let fromHost : Flow<IServiceProvider, BaseRuntimeError, int> =
currentYear |> Layer.provide BaseRuntime.fromServiceProvider
currentYear: Flow<BaseRuntime,BaseRuntimeError,int>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.PlatformService.BaseRuntimeGroups the standard operational services commonly used by workflow hosts.
Axial.PlatformService.BaseRuntimeErrorintAn abbreviation for the CLI type . Basic Types
Axial.PlatformService.ClockHelpers for the clock service.
now: Flow<'env,'error,DateTimeOffset>Reads the current UTC timestamp from an explicit clock service.
(|>): '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.Flowmap: ('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)
_arg1: DateTimeOffsetYear: intGets the year component of the date represented by the current object. The year component of the current object, expressed as an integer value between 0 and 9999.
fromHost: Flow<IServiceProvider,BaseRuntimeError,int>System.IServiceProviderDefines a mechanism for retrieving a service object; that is, an object that provides custom support to other objects.
Axial.Layers.LayerModuleprovide: Layer<'input,'error,'environment> -> Flow<'environment,'error,'value> -> Flow<'input,'error,'value>Builds an environment with a layer, runs a downstream flow, and always closes the layer scope. This is the provisioning boundary. It creates a fresh scope, builds the supplied layer inside that scope, runs the downstream flow with the built environment, and finalizes all acquired resources when the downstream flow completes or fails. The layer that builds the downstream environment. The flow to run with the provided environment. A flow that requires only the input environment of the layer. let program () = let runtimeLayer = Layer.succeed "production" let workflow = Flow.envWith String.length Layer.provide runtimeLayer workflow
Axial.PlatformService.BaseRuntimeModuleHelpers for constructing the standard explicit service bundle used by workflow hosts.
fromServiceProvider: Layer<IServiceProvider,BaseRuntimeError,BaseRuntime>Builds the base runtime from an .
An empty provider fails before currentYear runs, with the first service it could not find:
fromHost |> Flow.run (provider []) |> shouldEqual (Exit.Failure(Cause.Fail(BaseRuntimeError.MissingService "IClock")))
fromHost: Flow<IServiceProvider,BaseRuntimeError,int>(|>): '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
provider: (Type * obj) list -> IServiceProvidershouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
FailureThe 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.
Axial.PlatformService.BaseRuntimeErrorMissingServiceChoosing
| Situation | Use |
|---|---|
| You can build the value | Construct it and call Flow.run |
| A host container owns the implementations | ServiceProvider.get at the edge |
| Construction can fail, block, or acquire | A layer |
Tests almost always want the first row, whatever production uses.

