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.HttpClient
open Axial.PlatformService
open Axial.Process
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript

Tutorial: Runtime Operations

This tutorial focuses on the operational helpers that sit around a workflow: timeout, retry, cancellation, annotations, and exception translation.

Use these helpers at the application boundary. They are not substitutes for domain rules.

A Small Workflow To Wrap

open System
open System.Threading

type CheckoutError =
    | GatewayUnavailable
    | CheckoutTimedOut
    | CheckoutCancelled
    | ReceiptStoreFailed
    | UnexpectedGatewayFailure of string

let authorizeCard : Flow<unit, CheckoutError, string> =
    flow {
        do! Flow.Runtime.sleep (TimeSpan.FromMilliseconds 50)
        return "receipt-123"
    }

let storeReceipt (receiptId: string) : Flow<unit, CheckoutError, unit> =
    flow {
        do! Flow.Runtime.sleep (TimeSpan.FromMilliseconds 20)
        return ()
    }

let notifyCustomer (receiptId: string) : Flow<unit, CheckoutError, unit> =
    flow {
        do! Flow.Runtime.sleep (TimeSpan.FromMilliseconds 20)
        return ()
    }

let checkout : Flow<unit, CheckoutError, string> =
    flow {
        let! receiptId = authorizeCard
        do! storeReceipt receiptId
        do! notifyCustomer receiptId
        return receiptId
    }
Even in this tiny example there are already several composed steps. Runtime helpers answer how this execution should behave when one of those steps is slow, flaky, canceled, or throws.

Timeout

let checkoutWithTimeout =
    checkout
    |> Flow.Runtime.timeoutToError (TimeSpan.FromMilliseconds 10) CheckoutTimedOut
`timeout`, `timeoutToError`, `timeoutToOk`, and `timeoutWith` are boundary tools. They answer "what should this workflow do if it takes too long?"

Retry

let retryingCheckout =
    checkout
    |> Flow.Runtime.retry (function
        | GatewayUnavailable -> Some (TimeSpan.FromMilliseconds 100)
        | ReceiptStoreFailed -> Some (TimeSpan.FromMilliseconds 50)
        | _ -> None)
Use `Flow.Runtime.retry` when the retry decision depends on the actual typed error. Use `Schedule` when you want a reusable retry policy value.

Exceptions

let rawGatewayCall : Flow<unit, CheckoutError, string> =
    flow {
        if DateTime.UtcNow.Second % 2 = 0 then
            return raise (InvalidOperationException "gateway client exploded")

        return "receipt-123"
    }

let safeGatewayCall =
    rawGatewayCall
    |> Flow.catch (fun ex -> UnexpectedGatewayFailure ex.Message)
Use `Flow.catch` when you are deliberately translating technical exceptions into your typed error channel. If you do not catch them, they surface as `Cause.Die` in the final `Exit`.

Cancellation

let runCancellable (cancellationToken: CancellationToken) =
    task {
        let! exit = checkoutWithTimeout.StartAsTask((), cancellationToken = cancellationToken)

        match exit with
        | Exit.Success receipt -> printfn "Receipt %s" receipt
        | Exit.Failure Cause.Interrupt -> printfn "Cancelled"
        | Exit.Failure cause -> printfn "%s" (Cause.prettyPrint string cause)
    }
If the host cancels the token, the flow finishes with `Exit.Failure Cause.Interrupt`.

Annotations

let annotatedCharge =
    flow {
        let! annotations = Flow.Runtime.annotations
        let! traceId = Flow.Runtime.traceId
        return annotations, traceId
    }
Annotations are useful for observability and correlation. They belong to runtime mechanics, not to your domain model.

Pulling It Together

let guardedCheckout =
    safeGatewayCall
    |> Flow.bind (fun receiptId ->
        flow {
            do! storeReceipt receiptId
            do! notifyCustomer receiptId
            return receiptId
        })
    |> Flow.Runtime.timeoutToError (TimeSpan.FromSeconds 2) CheckoutTimedOut
    |> Flow.Runtime.retry (function
        | GatewayUnavailable -> Some (TimeSpan.FromMilliseconds 200)
        | ReceiptStoreFailed -> Some (TimeSpan.FromMilliseconds 100)
        | _ -> None)
Keep the mental split clear:
  • domain validation decides whether the operation should happen at all
  • runtime helpers decide how the host should run that operation
  • Flow.catch decides which technical exceptions should become typed failures