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

Application Lifecycle

App runs one root Flow as an owned application. Use it when the workflow represents the lifetime of a CLI, desktop process, browser mount, Node process, worker, or another application rather than one request or operation.

Application code remains an ordinary Flow value. Provision its environment before handing it to App:

Show page setup
// Setup for the checked examples on this page.
open System.Threading
open Axial
open Axial.Layers
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 AppError =
    | ConfigurationError of string
    | OrderError of string

    static member describe(error: AppError) =
        match error with
        | ConfigurationError message
        | OrderError message -> message

type IOrderRepository =
    abstract ProcessPending: unit -> Result<unit, AppError>

type AppEnv =
    { Orders: IOrderRepository
      Log: ILog }

type StartupInputs = { PendingOrders: int }

let program : Flow<AppEnv, AppError, unit> =
    flow {
        let! orders = Flow.envWith _.Orders
        return! orders.ProcessPending()
    }

let appLayer : Layer<StartupInputs, AppError, AppEnv> =
    Layer.envWith (fun inputs ->
        { Orders =
            { new IOrderRepository with
                member _.ProcessPending() =
                    if inputs.PendingOrders >= 0 then Ok() else Error(OrderError "negative backlog") }
          Log = Log.live })

let root : Flow<StartupInputs, AppError, unit> =
    program
    |> Layer.provide appLayer
Requires & types
Requires: open Axial; open Axial.Layers; open Axial.PlatformService (packages: Axial, Axial.Layers, Axial.PlatformService)
val program: Flow<AppEnv,AppError,unit>
val appLayer: Layer<StartupInputs,AppError,AppEnv>
val root: Flow<StartupInputs,AppError,unit>

root takes startup inputs and returns typed application failures. Layer.provide runs appLayer inside a fresh provisioning scope that finalizes any acquired resources when root completes; the appLayer shown here only projects a record and acquires nothing, so there is no resource to release. See Layers for a resource-acquiring layer.

Run a Finite Application

Use App.run when the caller only needs the final outcome:

Verified
let run inputs = async {
    let! exit = App.run inputs root

    match exit with
    | Exit.Success () -> return 0
    | Exit.Failure cause ->
        eprintfn "%s" (Cause.prettyPrint AppError.describe cause)
        return 1
}
Requires & types
Requires: open Axial (packages: Axial)
val run: StartupInputs -> Async<int>
Verified
run { PendingOrders = 3 } |> Async.RunSynchronously |> shouldEqual 0
run { PendingOrders = -1 } |> Async.RunSynchronously |> shouldEqual 1

App.run uses the caller's F# async cancellation token. It waits until the root scope closes, so layer and Flow finalizers have finished when the returned Exit becomes available.

Own a Long-Running Application

Use App.start when another module controls when the application stops:

Verified
let service : Flow<StartupInputs, AppError, unit> = Flow.never
Requires & types
Requires: open Axial (packages: Axial)
val service: Flow<StartupInputs,AppError,unit>
Verified
let running = App.start { PendingOrders = 0 } service
running.Status |> shouldEqual AppStatus.Running

// Called later by a signal handler, window close event, or UI unmount:
let finalExit = running.Stop() |> Async.RunSynchronously

(match finalExit with Exit.Failure cause -> Cause.isInterrupted cause | _ -> false) |> shouldEqual true
running.Status |> shouldEqual AppStatus.Completed
Requires & types
Requires: open Axial (packages: Axial)
val running: AppHandle<AppError,unit>
val finalExit: Exit<unit,AppError>

An AppHandle<'error,'value> exposes:

  • Status: Running, Stopping, or Completed.
  • Completion: the one final Exit, available to any number of observers.
  • Stop(): requests cooperative interruption and waits for cleanup.

Calling Stop() several times is safe. Every caller observes the same final exit. Disposing the handle requests stop but cannot await asynchronous finalizers; application shutdown code should await Stop() or Completion.

On .NET, App.start starts the root on the thread pool, away from the caller's SynchronizationContext. A UI thread can call running.StopAndWait(timeout) to request stop and block until finalizers finish; it returns false if the timeout expires. Use Stop() when the caller can await asynchronously. StopAndWait is not available on Fable.

Publish Scoped Services

Use App.startWith env build when a desktop app opens a repository: build creates its cache, queues, or Layer.fromFlow resources and returns the services the UI needs. The returned Async<Result<AppHandle<_,unit> * _,_>> resolves after construction, while the root scope stays open. On repository switch, call StopAndWait on .NET (or await Stop()); only then open the next repository. A failed build returns Error after cleanup, with no handle. Unlike App.start, startWith publishes built services; unlike ElmishRuntime.startLayer, it does not own an Elmish program.

External Cancellation

Use App.startWithCancellation or App.runWithCancellation when an existing owner already supplies a CancellationToken:

Verified
let startWithHost (hostStopping: CancellationToken) =
    App.startWithCancellation hostStopping { PendingOrders = 0 } service
Requires & types
Requires: open Axial; open System.Threading (packages: Axial)
val startWithHost: CancellationToken -> AppHandle<AppError,unit>

Cancellation is administrative interruption. It becomes Cause.Interrupt; it is not mapped into the application's typed error channel.

App and Direct Flow Execution

Flow.run, Flow.startTask, and Flow.toAsync remain the direct execution interface for individual workflows and interop boundaries. App adds ownership around a root workflow:

Use Entry point
Execute one operation workflow |> Flow.run env or workflow |> Flow.startTask env
Run a finite root application App.run env application
Start and later stop a root application App.start env application
Publish services with a scoped lifetime App.startWith env build
Integrate with .NET Generic Host Axial.Hosting
Run under Node signals Node hosting
Tie lifetime to a browser owner Browser hosting

App does not render errors, choose process exit codes, or subscribe to platform lifecycle events. Those decisions belong to the application edge or one of the platform hosting packages.