Skip to main content
Version: Next

FScript Extensions

Terrabuild custom extensions are FScript (.fss) programs. FScript provides the language; Terrabuild provides a small host protocol that binds target arguments to exported functions and turns their results into executable operations.

FScript language documentation

If records, options, pipelines, pattern matching, or [<export>] are unfamiliar, begin with the FScript tutorial. The versioned FScript manual documents the language embedded by Terrabuild.

Legacy compiled F# scripts such as .fsx are not supported.

Your First Extension

Create a script inside the workspace:

type ShellOperation =
{ Command: string
Arguments: string
ErrorLevel: int }

type CommandResult =
{ Batchable: bool
Operations: ShellOperation list }

[<export>] let install (context: {| Directory: string |}) (args: string option) : CommandResult =
let arguments = args |> Option.defaultValue ""
let command =
match arguments with
| "" -> "ci"
| value -> $"ci {value}"

{ Batchable = false
Operations =
[ { Command = "npm"
Arguments = command
ErrorLevel = 0 } ] }

type ExportFlag =
| Dispatch
| Default
| Never
| Local
| External
| Remote

{ [nameof install] = [Local] }

Register it in the workspace:

extension npm_ci {
script = "tools/extensions/npm-ci.fss"
}

Then call the exported function as an action:

target install {
npm_ci install { args = "--ignore-scripts" }
}

The action name install resolves to the exported FScript function named install. Terrabuild supplies context; the target supplies args.

Handler Binding

Every extension entrypoint follows the same rules:

  1. Declare it with [<export>] let.
  2. Name its first parameter context.
  3. Name remaining parameters exactly like target arguments; matching is case-sensitive.
  4. Use option<_> for arguments that callers may omit.
  5. Return a CommandResult from command handlers.

Context records are structurally typed. Request only the fields the handler uses:

[<export>] let dispatch (context: {| Command: string |}) (args: string option) =
// ...

An omitted optional parameter becomes None. An omitted required parameter is a configuration error. Extra target arguments that are not present in the function signature are ignored.

Exact Actions and Dispatch

Terrabuild first looks for an exported function whose name matches the requested action. If none exists, it uses the single function marked Dispatch.

[<export>] let dispatch (context: {| Command: string |}) (args: string option) : CommandResult =
let arguments = args |> Option.defaultValue ""
{ Batchable = false
Operations =
[ { Command = "your-tool"
Arguments = $"{context.Command} {arguments}"
ErrorLevel = 0 } ] }

{ [nameof dispatch] = [Dispatch; Never] }

With this fallback, my_extension lint { } invokes dispatch with context.Command = "lint". A script may define at most one dispatch handler.

The Descriptor

The script's final expression must be a map from exported function names to lists of discriminated-union flags. Flags are union cases, not strings.

type ExportFlag =
| Dispatch
| Default
| Never
| Local
| External
| Remote

{ [nameof defaults] = [Default]
[nameof dispatch] = [Dispatch; Never]
[nameof build] = [Remote] }

Dispatch and Default select special handlers. Every command handler also needs exactly one cacheability behavior:

FlagArtifact behavior
NeverDo not cache outputs.
LocalStore outputs in the local workspace cache.
RemoteUse managed caching, including Insights when connected.
ExternalThe command manages artifacts externally; Terrabuild stores its summary.

Batching is not a descriptor flag. Each invocation reports whether it supports batching through CommandResult.Batchable.

Optional Project Defaults

A handler marked Default can discover project identity, outputs, and dependencies while Terrabuild constructs the project graph:

type DependencyResolution =
| Path
| Scope

type ProjectInfo =
{ Id: string option
DependencyResolution: DependencyResolution option
Outputs: string list
Dependencies: string list }

[<export>] let defaults (context: {| Directory: string |}) : ProjectInfo =
{ Id = None
DependencyResolution = Some Path
Outputs = ["dist/**"]
Dependencies = [] }

{ [nameof defaults] = [Default] }

A script may define at most one default handler. See ProjectInfo for identity and dependency-resolution semantics.

Batching

Set Batchable = true only when the handler can produce one correct operation for the supplied batch. In that case, request Batch from the action context and use its project paths, commands, hash, or temporary directory.

Returning Batchable = false keeps the action as an individual operation. Terrabuild still applies its normal graph and phase rules.

Local and Remote Scripts

A local script path is resolved from the directory containing the WORKSPACE or PROJECT file that declares it and must remain inside the workspace:

extension my_extension {
script = "tools/extensions/my-extension.fss"
}

Remote scripts must use HTTPS:

extension my_extension {
script = "https://example.org/terrabuild/my-extension.fss"
}

Imports in local scripts are resolved to workspace files. Imports in remote scripts are resolved relative to the script URL. Terrabuild confines filesystem host functions to the workspace and applies workspace.deny, but returned shell operations execute with the build's authority; review remote extension code as carefully as other build tooling.

Where to Continue