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.JavaScriptElmish programs
The Axial.ElmishFlows package exposes the Axial.ElmishFlows namespace and connects an Elmish 5 program to Axial
workflows. Create one runtime per program, passing an environment and a Post function that delivers callbacks on the
host's dispatch thread. When already on that thread, Post should run inline; otherwise it should post to it. Do not
call Elmish's dispatch directly from workers. Connect OnDefect to the host's crash reporting path; typed failures
are messages, but defects are not.
Install
dotnet add package Axial.ElmishFlows
Axial.ElmishFlows brings Elmish 5 with it. Reference Elmish directly when your program uses Program.mkProgram and
dispatch. Open Axial.ElmishFlows for the Axial API, and Axial.PlatformService when your environment needs
Clock.live.
A minimal runtime supplies an environment, a Post function, a defect handler, and a history capacity:
open Axial
open Axial.ElmishFlows
open Axial.PlatformService
// In an Elmish host, `Post` is usually the program's dispatch, so callbacks run on the host's dispatch thread.
// Here the code is already on that thread, so it runs the callback inline.
let runtime =
ElmishRuntime.start
{ Post = (fun callback -> callback ())
OnDefect = (fun defect -> failwith defect.Rendered)
HistoryCapacity = 32 }
(ClockEnvironment Clock.live)When Post is not already on the dispatch thread it must post to it (for example Application.Post in WPF or
dispatching back to a UI loop); never call Elmish's dispatch directly from a worker.
First-use lifecycle
ElmishRuntime requires an environment implementing IHasClock. Keep the runtime's clock environment stable, and
supply a command's current dependencies with Flow.localEnv. For example, a git client can open another repository
without recreating its Elmish runtime:
open Axial
open Axial.ElmishFlows
open Axial.PlatformService
type RepositoryEnvironment = { Clock: IClock; Repository: string } with
interface IHasClock with
member this.Clock = this.Clock
let runtime =
ElmishRuntime.start
{ Post = (fun callback -> callback ())
OnDefect = (fun defect -> failwith defect.Rendered)
HistoryCapacity = 32 }
(ClockEnvironment Clock.live)
let currentEnv = { Clock = Clock.live; Repository = "/repos/new" }
let repositoryName : Flow<RepositoryEnvironment, string, string> =
Flow.envWith (fun env -> env.Repository)
let command =
Cmd.OfFlow.either "repository" runtime
(repositoryName |> Flow.localEnv (fun _ -> currentEnv))
id
(fun _ -> "unavailable")Flow.localEnv selects the environment when the command runs; capture the selected environment for each command when
constructing it so later repository switches do not change already-issued requests.
Provisioning with startLayer
When a runtime needs resources that must live for the whole program — a Cache.make* cache, queues, hubs, or a
Layer.fromFlow resource — build them with a layer and start the runtime with ElmishRuntime.startLayer. The layer
is provisioned inside the runtime's root scope, so Stop/StopAndWait releases what the layer acquired. A typed
provisioning failure returns Error and leaves nothing running.
open Axial
open Axial.Layers
open Axial.ElmishFlows
open Axial.PlatformService
type CacheEnvironment = { Clock: IClock; Cache: Cache<int, Never, string> } with
interface IHasClock with
member this.Clock = this.Clock
let layer : Layer<ClockEnvironment, string, CacheEnvironment> =
Layer.fromFlow (flow {
let! cache =
Cache.make (fun key -> Flow.succeed $"value-{key}" : Flow<ClockEnvironment, Never, string>)
let! clock = Flow.envWith (fun env -> env.Clock)
return { Clock = clock; Cache = cache }
})
let runtime =
match
ElmishRuntime.startLayer
{ Post = (fun callback -> callback ())
OnDefect = (fun defect -> failwith defect.Rendered)
HistoryCapacity = 32 }
layer
(ClockEnvironment Clock.live)
|> Async.RunSynchronously
with
| Ok runtime -> runtime
| Error error -> failwith errorLong-lived services and fire-and-forget
A service that lives as long as the program — a search pipeline, a poller, a cache warmer — belongs in a stream
subscription or in a service the runtime provisions, not in a never-completing command. Cmd.OfFlow.perform needs a
success message, so running Flow.never through it forces an unreachable onSuccess; use
Sub.OfFlowStream.subscribe instead, and let Elmish dispose the subscription
(interrupting the stream and releasing its resources) when the program unmounts:
open Axial
open Axial.ElmishFlows
open Axial.PlatformService
// A search pipeline backed by an inbox: post queries, subscribe to results for the program's lifetime.
let searchPipeline (runtime: ElmishRuntime<ClockEnvironment>) =
let inbox = Inbox.create 8 runtime
let queries : FlowStream<ClockEnvironment, string, string> =
Inbox.stream inbox |> FlowStream.map (fun query -> $"result:{query}")
inbox, Sub.OfFlowStream.subscribe [ "search" ] runtime queries (fun error -> $"search-error:{error}")When the service also needs resources provisioned once for the whole program, ElmishRuntime.startLayer
above is the alternative: it builds the cache, queue, or hub inside the runtime's root scope and releases it on
Stop/StopAndWait.
Cmd.OfFlow.start is the right tool for fire-and-forget work, but it accepts only Flow<'env, Never, unit>. A flow
with a typed failure channel must handle that failure first — log it, then recover to unit:
Show page setup
open System
open Axial
open Axial.PlatformService
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actualtype SearchEnvironment =
{ Clock: IClock
Log: ILog }
interface IHasClock with member this.Clock = this.Clock
interface IHasLog with member this.Log = this.Log
let indexDocument (query: string) : Flow<SearchEnvironment, string, unit> =
flow {
if String.IsNullOrWhiteSpace query then
return! Flow.fail "empty query"
}
// Handle the typed failure before Cmd.OfFlow.start, which accepts only Flow<'env, Never, unit>.
let indexSafely (query: string) : Flow<SearchEnvironment, Never, unit> =
indexDocument query
|> Flow.recover (fun error ->
flow {
do! Log.error $"indexing skipped: {error}"
})
FsLiveDocsGeneratedPage12_10B692608C6D.SearchEnvironmentClock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
Log: ILogAxial.PlatformService.ILogProvides synchronous access to workflow logging as an explicit service.
Axial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: SearchEnvironmentClock: SearchEnvironment -> unit -> IClockAxial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
Log: SearchEnvironment -> unit -> ILogindexDocument: string -> Flow<SearchEnvironment,string,unit>query: stringstringAn abbreviation for the CLI type . Basic Types
Axial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
System.StringRepresents text as a sequence of UTF-16 code units.
IsNullOrWhiteSpace: string -> boolIndicates whether a specified string is , empty, or consists only of white-space characters. The string to test. if the parameter is or , or if consists exclusively of white-space characters.
Axial.Flowfail: 'error -> Flow<'env,'error,'value>Same as error. The error value to wrap in a failing flow. A flow that always fails with the provided error. let result = Flow.fail "error" |> Flow.run () // result = Failure (Cause.Fail "error")
indexSafely: string -> Flow<SearchEnvironment,Never,unit>Axial.NeverRepresents an error channel that cannot occur.
(|>): '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
recover: ('error -> Flow<'env,'nextError,'value>) -> Flow<'env,'error,'value> -> Flow<'env,'nextError,'value>error: stringAxial.PlatformService.LogHelpers for the logging service.
error: string -> Flow<'env,'error,unit>Writes an error log message through an explicit logging service.
let log = Log.capturing ()
let env = { Clock = ManualClock(DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); Log = log }
indexSafely "" |> Flow.run env |> shouldEqual (Exit.Success ())
log.Entries |> Seq.toList |> List.map _.Message |> shouldEqual [ "indexing skipped: empty query" ]
log: TestLogAxial.PlatformService.LogHelpers for the logging service.
capturing: unit -> TestLogCreates a recording logger whose entries are exposed for assertions.
env: SearchEnvironmentClock: IClock``.ctor``: DateTimeOffset -> ManualClock``.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: ILogindexSafely: string -> Flow<SearchEnvironment,Never,unit>(|>): '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.
Entries: Collections.Generic.IReadOnlyList<LogEntry>The recorded entries, in the order they were written.
Microsoft.FSharp.Collections.SeqModuleContains operations for working with values of type .
toList: 'T seq -> 'T listBuilds a list from the given collection. The input sequence. The result list. Thrown when the input sequence is null. let inputs = seq { 1; 2; 5 } inputs |> Seq.toList Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
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.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
_arg1: LogEntryMessage: stringopen Axial
open Axial.ElmishFlows
// indexSafely already handled its typed failure, so its type is Flow<'env, Never, unit>.
let backgroundIndex runtime query =
Cmd.OfFlow.start "index" runtime (indexSafely query)Command parameter order
Axial's commands keep Elmish's "work first" idea, but the runtime owns the command's root scope, so it precedes the flow. Pass the flow positionally, not piped:
let command =
Cmd.OfFlow.either "increment" runtime
(Flow.succeed 2)
id
(fun (_: string) -> 0)The order is name runtime workflow onSuccess onError for either; perform drops onError, attempt drops
onSuccess, and Cmd.OfFlow.start name runtime workflow is fire-and-forget: it runs a
Flow<'env, Never, unit> as a named fiber in the root scope, dispatches nothing, and reports defects to OnDefect.
Operation reference
The exhaustive operation inventory lives in the API reference:
- Cmd:
Cmd.OfFlow.either/perform/attempt/startand the latest-winsCmd.OfFlow.Latestvariants. - Sub:
Sub.OfFlowStream.subscribefor running a stream under a subscription id. - Inbox:
Inbox.create/offer/post/dropped/streamfor synchronous UI input and search-as-you-type. - ElmishRuntime:
start,startLayer,test, and theStop/StopAndWaitshutdown methods. - TestRuntime:
run,settle,drain,dispatched, anddefectsfor tests with a manual clock.

