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
Make the safe path the easy path.
Flow is one type for work that is async, can fail, can be cancelled, and uses services. One runtime cancels, cleans up, and retries it by the same rules everywhere.
Async work and expected errors in one type
let! stops at the first expected error. An exception becomes a defect in the outcome instead of escaping.
task {
match! loadCart id with
| Error e -> return Error e
| Ok cart ->
match! charge cart with
| Error e -> return Error e
| Ok payment ->
return Ok(receipt cart payment)
}flow {
let! cart = loadCart id
let! payment = charge cart
return receipt cart payment
}Cancellation is handled the same way everywhere
No cancellation token to pass. If one parallel load fails, the other is interrupted. On timeout, both are, and their resources are released first.
use cts =
CancellationTokenSource
.CreateLinkedTokenSource token
cts.CancelAfter(TimeSpan.FromSeconds 2.0)
let profile = loadProfile user cts.Token
let orders = loadOrders user cts.Token
// one failing does not stop the other
do! Task.WhenAll(profile :> Task, orders)Flow.zipPar
(loadProfile user)
(loadOrders user)
|> Flow.timeout
(TimeSpan.FromSeconds 2.0)
DashboardTimedOutDependencies in the signature mark the boundaries
The first type parameter names the services a flow uses. reserveStock reaches inventory and nothing else, and a test supplies only what the flow names.
// Which services does it use?
// The signature does not say.
val checkout:
Cart -> CancellationToken
-> Task<Result<Receipt, OrderError>>val reserveStock: Cart ->
Flow<Inventory, OrderError, Reservation>
val chargeCard: Cart ->
Flow<Billing, OrderError, Payment>
val placeOrder: Cart ->
Flow<Shop, OrderError, Receipt>Time, randomness, and IDs are services too
Reading the clock or making a GUID adds that service to the type. Production supplies live ones; a test supplies fixed ones and gets the same result every run.
// A different result on every run,
// and nothing in the type says why.
{ Id = Guid.NewGuid()
PlacedAt = DateTimeOffset.UtcNow }flow {
let! placedAt = Clock.now
let! id = Guid.newGuid
return { Id = id; PlacedAt = placedAt }
}
// needs: 'env :> IHasClock and IHasGuidFast underneath
A running flow is a ValueTask on .NET, so work that completes synchronously avoids a heap allocation. On Fable JS it is an Async.
Resources belong to their scope
A helper opens a connection and returns it. It closes when the caller's scope ends.
flow {
let! orders = connect "orders"
let! billing = connect "billing"
return summarise orders billing
}
|> Flow.scopedStreams keep the same rules
Four workers at a time. After ten pages, the rest are interrupted and released.
FlowStream.fromSeq pageIds
|> FlowStream.mapFlowPar
(Parallelism.bounded 4) fetchPage
|> FlowStream.take 10
|> FlowStream.runCollectYou can see what is running
Every fiber has an id, a parent, and a name. A registry lists the live ones.
poll
|> Flow.forkNamed "outbox-poller"
|> Flow.annotate "tenant" "acme"
|> Flow.withFiberRegistry registry
// registry.DumpAt(clock) lists live fibersFewer states to reason about, for people and for LLMs
Every flow ends one of three ways, and concurrency follows fixed rules. Reviewers and LLM assistants have fewer cases to consider.
- Forked work belongs to the scope that started it.
- Cancellation always arrives as an interruption.
- Resources are released in reverse order, however the scope ends.
- Axial.Guardrails flags direct clock and randomness calls, exceptions in
flow { }, and dropped cancellation tokens.
A flow names the environment it needs. The caller supplies it at run time.
Packages
- Axial: typed async workflows with explicit dependencies, concurrency, resources, and streams.
- Axial.Process: run external commands and pipelines as flows, with streamed output and cancellation.
- Axial.HttpClient: an HTTP client with typed requests, responses, and retry policies.

