Skip to content

Reference | Recipe

olegz edited this page Mar 16, 2026 · 4 revisions

Recipe computation

The recipe body is a computation expression of type recipe. It is similar to F#'s async computation — both regular code and do!/let! notation can be used inside.

Defining a recipe

let compileProject config = recipe {
    do! sh "dotnet build" {
        args ["-c"; config]
    }
}

Calling another recipe

Use let! or do! to call recipes:

recipe {
    let! result = someRecipe()
    do! anotherRecipe "arg"
}

Accessing execution context

recipe {
    // Get the current target file
    let! file = getTargetFile()
    let fullPath = file.FullName

    // Shortcut for full path
    let! fullPath = getTargetFullName()

    // Get execution options
    let! opts = getCtxOptions()
    let rootPath = opts.ProjectRoot
}

Variables

Typed variables — recommended

Typed variables provide compile-time safety and --help integration. See Variables for full details.

let vars = {|
    Config = Var.string() |> withDefault "Debug"
    Version = Var.string() |> required
|}

// In a recipe:
let! config = vars.Config    // string
let! version = vars.Version  // string

Legacy: getVar and getEnv

These functions return string option and record dependencies:

recipe {
    let! ver = getVar "VERSION"
    let! user = getEnv "USERNAME"
}

When a variable or environment value changes between builds, targets depending on it will be rebuilt.

Demanding other targets

need queues targets for execution and pauses the recipe until they complete:

recipe {
    do! need ["lib.dll"; "utils.dll"]
}

This also records dependencies — the current target will be rebuilt if any demanded target changes.

Working with filesets

recipe {
    // Get files matching a pattern, recording dependency on the file list
    let! files = getFiles <| fileset {
        basedir "src"
        includes "**/*.fs"
    }

    // Demand those files — rebuild if any file changes
    do! needFiles files

    // Shortcut: getFiles + needFiles in one call
    do! dependsOn !! "src/**/*.fs"
}

Writing to log

The trace function writes messages to all log targets:

do! trace Message "Build started"
do! trace Info "Building %s version %s" name version
do! trace Error "Compilation failed"

Level values: Message, Error, Command, Warning, Info, Debug, Verbose, Never.

Messages are filtered by the configured verbosity level and prefixed with the executing task's identifier.

Async interop

Recipe supports calling F# async and Task values directly:

recipe {
    let! data = someAsyncOperation()
    let! result = someTaskReturningMethod()
}

Recipe functions

Recipe.map

Transforms the result of a recipe:

let! isDebug = getVar "DEBUG" |> Recipe.map (Option.defaultValue "0")

Recipe.Ignore

Discards the result of a recipe that returns a value:

do! someRecipeThatReturnsInt() |> Ignore

Control flow

if, for, and while are supported:

recipe {
    if condition then
        do! trace Info "Condition met"

    for file in files do
        do! trace Info "Processing %s" file

    let mutable i = 0
    while i < 3 do
        do! trace Info "Step %d" i
        i <- i + 1
}

Error handling

try/with/finally

recipe {
    try
        try
            do! riskyOperation()
        with e ->
            do! trace Error "Failed: %s" e.Message
    finally
        printfn "Cleanup"
}

Recipes with do! notation work in with blocks but not in finally blocks — this is a limitation of F# computation expressions.

WhenError

Wraps a recipe to intercept exceptions:

do! riskyRecipe |> WhenError (fun ex -> defaultValue)
do! riskyRecipe |> WhenError ignore

FailWhen

Raises an exception if the recipe's result meets a condition:

do! shell {cmd "dir"} |> FailWhen Not0 "Command failed"
// Shortcut:
do! shell {cmd "dir"} |> CheckErrorLevel

When to define a recipe vs a function

Define a recipe when you need to:

  • Access the execution context
  • Call async or task-returning functions
  • Use Xake functions like trace, need, getVar

Use a plain function for everything else.

Clone this wiki locally