Warning
Axial 0.7.0 is the first planned release under the Axial name as split packages, renamed from monolithic FsFlow. The new package line continues in Axial.Flow, Axial.ErrorHandling, Axial.Refined, Axial.Schema, Axial.Codec, Axial.Validation, Axial.Validation.Schema, and the umbrella Axial package. The direction is designed to keep parts usable independently and reduce cognitive load.
Axial provides structured composition over normal F#/.NET code. It is two tools that share one vocabulary:
Parse, don't validate. For domain models, declare a Schema once — parsing raw input (HTTP form-like, CLI,
JSON-like, configuration), validation, redisplay with path-aware field errors, contextual rules, and metadata
interpreters (JSON Schema, docs, UI) all fall out of that one declaration. An invalid model is never constructed.
For simple code without a domain model, plain F# Result with your own error union is the whole story — Check,
Validation, and Refined are the machinery behind those two doors, there when you need them directly.
Effects in Flow. A cold, environment-aware Reader-Async-Result workflow model in the ZIO tradition: explicit
dependencies in 'env, direct Task/ValueTask/Async interop, cancellation, layers, scoped resources, fibers,
STM, and scheduling. Policy and Flow.verify are where the two sides meet — a parsed model enters a workflow with
the environment injected. Flow is optional; the parse-don't-validate side works without it.
Everything is zero-reflection, AOT- and trimming-safe, and Fable-compatible.
The same vocabulary carries from pure checks into effectful workflows.
- Composition:
flow {}bindsResult,Option,Async,Task, andColdTaskdirectly. - Explicit dependencies: Keep dependencies visible in
'env, withIServiceProviderintegration at the host boundary. - Execution outcomes: Keep typed domain failures, cancellations, and unhandled defects separate.
Start with a reusable check and a fail-fast result:
open Axial.Flow
open Axial.ErrorHandling
type RegistrationError =
| EmailMissing
| SaveFailed of string
let validateEmail (email: string) : Result<string, RegistrationError> =
email
|> Result.notBlank
|> Result.mapError (fun _ -> EmailMissing)Use the same validation logic directly inside a task-oriented workflow:
type User =
{ Email: string }
type RegistrationEnv =
{ LoadUser: int -> Task<Result<User, RegistrationError>>
SaveUser: User -> Task<Result<unit, RegistrationError>> }
let registerUser userId : Flow<RegistrationEnv, RegistrationError, unit> =
flow {
let! loadUser = Flow.read _.LoadUser
let! saveUser = Flow.read _.SaveUser
let! user = loadUser userId
do! validateEmail user.Email
return! saveUser user
}validateEmail is just a plain Result<string, RegistrationError>.
flow lifts it directly with do!.
The same builder also binds Async, Task, ValueTask, and ColdTask directly.
Axial stays close to standard F# and .NET:
flow { ... }binds toResultandOptionflow { ... }also binds toAsync,Async<Option<_>>,Async<ValueOption<_>>, andAsync<Result<_,_>>- On .Net,
flow { ... }also binds toTask,ValueTask,Task<_>,ValueTask<_>, andColdTask result {}keeps fail-fast pure code readablevalidate {}keeps sibling validation accumulation explicitSchema+Input.parseturn raw boundary input into trusted models or path-aware diagnostics — invalid models are never constructedJson.compile(Axial.Codec) compiles the same schema into a reflection-free JSON codec for trusted payloads, andJsonSchema.generatepublishes the matching contract
Because tasks are hot, Axial includes ColdTask: a small wrapper around CancellationToken -> Task.
flow handles token passing for you and keeps reruns explicit.
The full runnable example is in examples/Axial.ReadmeExample/Program.fs.
dotnet run --project examples/Axial.ReadmeExample/Axial.ReadmeExample.fsproj// ReadmeEnv = { Root: string }
// FileReadError = NotFound
let readTextFile (path: string) : Flow<ReadmeEnv, FileReadError, string> =
flow {
// In production, map access and path exceptions separately at the boundary.
do! File.Exists path |> Result.checkOr () |> Bind.error (NotFound path)
// Wrap in ColdTask for later exeuction
return! ColdTask(fun ct -> File.ReadAllTextAsync(path, ct))
}
let program : Flow<ReadmeEnv, FileReadError, string * string> =
flow {
let! root = Flow.read _.Root // ReadmeEnv.Root -> string
let settingsFile = Path.Combine(root, "settings.json")
let featureFlagsFile = Path.Combine(root, "feature-flags.json")
let! settings = readTextFile settingsFile // Flow<ReadmeEnv, FileReadError, string>
let! featureFlags = readTextFile featureFlagsFile // Flow<ReadmeEnv, FileReadError, string>
return settings, featureFlags // Flow<ReadmeEnv, FileReadError, string * string>
}It reads Root from 'env, performs two file reads in one flow {}, and keeps failure typed at the boundary.
- Preview the docs locally with
bash scripts/preview-docs.sh - Build and validate the docs with
bash scripts/validate-docs.sh examples/for runnable repo examples