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.JavaScript

A layer builds an environment using flow capabilities when construction must await a connection, read configuration, fail with a typed startup error, or acquire a resource that must be released. Most applications do not need the separate Axial.Layers package.

Use a record when you can. Construct the environment directly and hand it to the workflow:

Show page setup
// Setup for the checked examples on this page.
open System
open Axial
open Axial.Layers
open Axial.Console
open Axial.PlatformService
/// 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
Verified
type RegionEnv = { Clock: IClock; Region: string }

let describeRegion : Flow<RegionEnv, Never, string> =
    flow {
        let! clock = Flow.envWith _.Clock
        let! region = Flow.envWith _.Region
        return $"{region} at {clock.UtcNow().Year}"
    }
Requires & types
Requires: open Axial (packages: Axial)
val describeRegion: Flow<RegionEnv,Never,string>
Verified
let env = { Clock = Clock.fromValue (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); Region = "eu" }
describeRegion |> Flow.run env |> shouldEqual (Exit.Success "eu at 2026")
Requires & types
Requires: open Axial; open Axial.PlatformService; open System (packages: Axial, Axial.PlatformService)
val env: RegionEnv

Direct construction needs no package beyond the services themselves. See dependencies.

Use a layer when construction is itself effectful:

  • provisioning can fail, and the failure should be a typed startup error rather than an exception
  • a service must be acquired and released for the duration of the flow Layer.provide runs: acquired during provisioning and released when that flow ends, whether it succeeds, fails, or is interrupted
  • independent parts of the environment should be built in parallel
  • a service needs another service in order to be constructed
Verified
type Connection(name: string, closed: ResizeArray<string>) =
    member _.Name = name
    member _.Close() = closed.Add name

type AppEnv = { Clock: IClock; Connection: Connection }
type AppError = QueryFailed of string

let connectionLayer (closed: ResizeArray<string>) : Layer<unit, Never, Connection> =
    Layer.acquireRelease
        (Layer.succeed (Connection("orders-db", closed)))
        (fun connection ->
            Flow.delay (fun () ->
                connection.Close()
                Flow.ok ()))

let runtime (closed: ResizeArray<string>) : Layer<unit, Never, AppEnv> =
    Layer.merge Clock.layer (connectionLayer closed)
    |> Layer.map (fun (clock, connection) -> { Clock = clock; Connection = connection })

let query : Flow<AppEnv, AppError, string> =
    Flow.envWith (fun env -> env.Connection.Name)

let program (closed: ResizeArray<string>) : Flow<unit, AppError, string> =
    Layer.provide (Layer.widenError (runtime closed)) query
Requires & types
Requires: open Axial; open Axial.Layers; open Axial.PlatformService (packages: Axial, Axial.Layers, Axial.PlatformService)
val connectionLayer: ResizeArray<string> -> Layer<unit,Never,Connection>
val runtime: ResizeArray<string> -> Layer<unit,Never,AppEnv>
val query: Flow<AppEnv,AppError,string>
val program: ResizeArray<string> -> Flow<unit,AppError,string>

runtime cannot fail, so its error type is Never; Layer.widenError lets it provide a workflow that fails with AppError. The connection is closed when program ends:

Verified
let closed = ResizeArray<string>()
program closed |> Flow.run () |> shouldEqual (Exit.Success "orders-db")
List.ofSeq closed |> shouldEqual [ "orders-db" ]
Requires & types
Requires: open Axial (packages: Axial)
val closed: ResizeArray<string>

Layer.provide is the boundary: it opens a scope, builds the layer inside it, runs the downstream flow with the result, and closes the scope afterwards whether the flow succeeds, fails, or is interrupted.

Two real examples

Axial packages use layers for typed provisioning errors and service construction from other services.

Provisioning that can fail with a typed error. Axial.PlatformService builds the five standard services from a host container:

Fragment
let servicesFromServiceProvider
    : Layer<IServiceProvider, BaseRuntimeError, IClock * ILog * IRandom * IGuid * IEnvironmentVariables> =
    Layer.fromFlow (
        Flow.envWith (fun provider ->
            match tryService<IClock> provider, tryService<ILog> provider, tryService<IRandom> provider,
                  tryService<IGuid> provider, tryService<IEnvironmentVariables> provider with
            | Ok clock, Ok log, Ok random, Ok guid, Ok environmentVariables ->
                Ok(clock, log, random, guid, environmentVariables)
            | Error name, _, _, _, _ | _, Error name, _, _, _ | _, _, Error name, _, _
            | _, _, _, Error name, _ | _, _, _, _, Error name ->
                Error(BaseRuntimeError.MissingService name))
        |> Flow.bind Flow.fromResult
    )

The error channel is BaseRuntimeError, not Never. Construction can fail with a typed error naming the missing service. A factory returning Result<'env, BaseRuntimeError> could model that typed failure in a record, but a layer keeps the failure inside the provisioning step: Layer.provide runs the workflow only after every step succeeds, releases resources acquired by earlier steps when one fails, and lets independent steps build in parallel with merge. A missing service surfaces as a typed startup failure before any workflow runs.

A service built from another service. Axial.Hosting turns what the host container has into what workflows need:

Fragment
let layer (categoryName: string) : Layer<ILoggerFactory, Never, ILog> =
    Layer.envWith (fromFactory categoryName)

The layer consumes the host's ILoggerFactory and produces an ILog named by the category. The factory arrives only after the host starts, so Layer.envWith (fromFactory categoryName) adapts it at the provide boundary. This demonstrates adapting one dependency into another — not a limitation of records, since a record built after startup could hold the factory or the resulting logger directly.

An already-built value that cannot fail needs no provisioning. Layer.succeed wraps it:

Verified
Layer.succeed Console.live
Requires & types
Requires: open Axial.Console; open Axial.Layers (packages: Axial.Console, Axial.Layers)

Console, FileSystem, HttpClient and Process are all of this shape, which is why none of them depends on this package.

Scopes are not part of this package

Scopes and acquireRelease are core, and work without layers. See scopes and resources.

In this section

  1. Layers: construction, composition, and provisioning failure.
  2. Tutorial: the same material worked end to end.