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.JavaScriptTutorial: Composing Built-in Services
Every page in this section shows one service in isolation, constrained by its own IHasX interface. A real
application wants several of them at once, alongside its own dependencies, and it should build that combined
environment without repeating itself. This tutorial builds that environment.
The problem
BaseRuntime bundles the five platform services and already implements
IHasClock, IHasLog, IHasRandom, IHasGuid, and IHasEnvironmentVariables. Embedding it as a field of your
own record does not carry those interface implementations with it: F# has no mechanism for one type to forward
another type's interfaces automatically. Your own environment record has to state, once per service, where that
service lives:
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.
open Axial.PlatformService
type AppEnv =
{ Runtime: BaseRuntime }
interface IHasClock with
member this.Clock = this.Runtime.Clock
interface IHasLog with
member this.Log = this.Runtime.Log
interface IHasEnvironmentVariables with
member this.EnvironmentVariables = this.Runtime.EnvironmentVariables
AxialPlatformServiceFsLiveDocsGeneratedPage4_65D5665AADA9.AppEnvRuntime: BaseRuntimeAxial.PlatformService.BaseRuntimeGroups the standard operational services commonly used by workflow hosts.
Axial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: AppEnvClock: AppEnv -> unit -> IClockClock: IClockAxial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
Log: AppEnv -> unit -> ILogLog: ILogAxial.PlatformService.IHasEnvironmentVariablesDeclares that an environment supplies the environment-variable service.
EnvironmentVariables: AppEnv -> unit -> IEnvironmentVariablesEnvironmentVariables: IEnvironmentVariablesEach line is a delegation: member this.Clock = this.Runtime.Clock tells the compiler
which field satisfies which contract. Declare an interface member for every service the application actually uses;
skip the ones it does not, the same way you would skip a field it does not need. This is boilerplate, but it is
boilerplate you write once, at the boundary, rather than something that spreads through the workflow code.
Use the services
Nothing about calling a service changes because it arrived through Runtime instead of a top-level field. The
workflow names the contract, not the storage:
let loadMode : Flow<AppEnv, EnvironmentVariableError, string> =
flow {
let! now = Clock.utcDateTime
let! mode = EnvironmentVariable.get "APP_MODE"
do! Log.info $"[{now:O}] starting in mode {mode}"
return mode
}
loadMode: Flow<AppEnv,EnvironmentVariableError,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.
FsLiveDocsGeneratedPage4_65D5665AADA9.AppEnvAxial.PlatformService.EnvironmentVariableErrorstringAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
now: DateTimeAxial.PlatformService.ClockHelpers for the clock service.
utcDateTime: Flow<'env,'error,DateTime>Reads the current UTC date/time from an explicit clock service.
mode: stringAxial.PlatformService.EnvironmentVariableHelpers for reading and parsing environment variables through an explicit service.
get: string -> Flow<'env,EnvironmentVariableError,string>Reads a raw string environment variable through an explicit service.
Axial.PlatformService.LogHelpers for the logging service.
info: string -> Flow<'env,'error,unit>Writes an informational log message through an explicit logging service.
loadMode does not know that Clock, Log, and EnvironmentVariable all come from the same Runtime field; it
only knows the three interfaces. Swap AppEnv for any other type that implements them and the
workflow is unchanged.
Add your own dependencies alongside it
AppEnv is an ordinary record, so extending it with an application-specific dependency is the same pattern as
the app record tutorial: add a field, add an interface if other
helpers should depend on the contract rather than the field name directly:
type IOrderRepository =
abstract Count: unit -> int
type ShopEnv =
{ Runtime: BaseRuntime
Orders: IOrderRepository }
interface IHasClock with
member this.Clock = this.Runtime.Clock
interface IHasLog with
member this.Log = this.Runtime.Log
interface IHasEnvironmentVariables with
member this.EnvironmentVariables = this.Runtime.EnvironmentVariables
FsLiveDocsGeneratedPage4_65D5665AADA9.IOrderRepositoryCount: IOrderRepository -> 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
FsLiveDocsGeneratedPage4_65D5665AADA9.ShopEnvRuntime: BaseRuntimeAxial.PlatformService.BaseRuntimeGroups the standard operational services commonly used by workflow hosts.
Orders: IOrderRepositoryAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: ShopEnvClock: ShopEnv -> unit -> IClockClock: IClockAxial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
Log: ShopEnv -> unit -> ILogLog: ILogAxial.PlatformService.IHasEnvironmentVariablesDeclares that an environment supplies the environment-variable service.
EnvironmentVariables: ShopEnv -> unit -> IEnvironmentVariablesEnvironmentVariables: IEnvironmentVariablesA workflow that reads Orders directly (Flow.envWith _.Orders) is coupled to this record's field name. If you
want Orders reusable behind a named contract instead, the way Clock and Log are, see
Tutorial: Creating Reusable Services.
Run it
In production the environment holds the live services:
let runLive () : Task<Exit<string, EnvironmentVariableError>> =
loadMode |> Flow.startTask { Runtime = BaseRuntime.liveValue }
runLive: unit -> Task<Exit<string,EnvironmentVariableError>>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
stringAn abbreviation for the CLI type . Basic Types
Axial.PlatformService.EnvironmentVariableErrorloadMode: Flow<AppEnv,EnvironmentVariableError,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.FlowstartTask: 'env -> Flow<'env,'error,'value> -> Task<Exit<'value,'error>>Starts the workflow immediately and returns a task handle for its final exit. The work is already in flight when this returns. Use Flow.toAsync for a cold handle. The environment used by the workflow. The workflow to start. A task that completes with the workflow exit. let running = workflow |> Flow.startTask environment
Runtime: BaseRuntimeAxial.PlatformService.BaseRuntimeModuleHelpers for constructing the standard explicit service bundle used by workflow hosts.
liveValue: BaseRuntimeCreates the standard live base runtime as an explicit service bundle.
Test it
Nothing changes about substituting test doubles either. BaseRuntime's fields each accept the fixed value shown in
deterministic implementations, so a test builds the
whole environment as one value literal, with no interface to reimplement:
let logged = ResizeArray<string>()
let testEnv =
{ Runtime =
{ Clock = Clock.fromValue (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero))
Log = Log.fromSink (fun _ message -> logged.Add message)
Random = Random.fromValue 7
Guid = Guid.fromValue (Guid.Parse "00000000-0000-0000-0000-000000000001")
EnvironmentVariables = EnvironmentVariables.fromPairs [ "APP_MODE", "diagnostic" ] } }
loadMode |> Flow.run testEnv |> shouldEqual (Exit.Success "diagnostic")
List.ofSeq logged |> shouldEqual [ "[2026-01-01T00:00:00.0000000Z] starting in mode diagnostic" ]
logged: ResizeArray<string>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
stringAn abbreviation for the CLI type . Basic Types
testEnv: AppEnvRuntime: BaseRuntimeClock: 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.
Log: ILogAxial.PlatformService.LogHelpers for the logging service.
fromSink: (LogLevel -> string -> unit) -> ILogCreates a logger from a synchronous sink function. Exceptions are appended to the message text.
message: stringAdd: 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.
Random: IRandomAxial.PlatformService.RandomHelpers for the random-number service.
fromValue: int -> IRandomCreates a deterministic random generator that always returns the supplied value.
Guid: IGuidAxial.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).
Parse: string -> GuidConverts the string representation of a GUID to the equivalent structure. The string to convert. A structure that contains the value that was parsed. is . is not in a recognized format.
EnvironmentVariables: IEnvironmentVariablesAxial.PlatformService.EnvironmentVariablesHelpers for the environment-variable service.
fromPairs: (string * string) seq -> IEnvironmentVariablesCreates a deterministic provider from a fixed set of name/value pairs.
loadMode: Flow<AppEnv,EnvironmentVariableError,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.
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.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
If you already have several standard services in play, wrapping them once in an app environment like this is usually the cleanest boundary. Continue with Tutorial: Creating Reusable Services when you need your own service contract alongside the built-in ones, or with Layers when building the environment itself becomes effectful.

