Arguments & Flags
A command’s input is described by a config object: a record whose keys are
the field names your handler receives, and whose values are Params. There are
two kinds of Param:
Flag— a named option, written--name value(or-n valuewith an alias). Order-independent.Argument— a positional operand, matched left to right by position.
They share most constructors (string, integer, choice, file, …) and
combinators (withDefault, optional, withSchema, map, …), so once you know
one you mostly know the other. A few are specific to one kind — for example
boolean and keyValuePair are Flag-only, withAlias is Flag-only, and
variadic is Argument-only.
import { Console, Effect } from "effect"import { Argument, Command, Flag } from "effect/unstable/cli"
const deploy = Command.make( "deploy", { // Required positional argument. Without it, parsing fails with usage help. service: Argument.string("service").pipe( Argument.withDescription("Name of the service to deploy") ),
// A choice flag restricts the value to a fixed set. The result type is the // union "staging" | "production", not just string. env: Flag.choice("env", ["staging", "production"]).pipe( Flag.withAlias("e"), Flag.withDescription("Target environment") ),
// Optional flag with a default. Because of the default the field is always // present, so the handler never has to deal with "missing". replicas: Flag.integer("replicas").pipe( Flag.withDefault(1), Flag.withDescription("Number of replicas to run") ),
// A boolean flag is true when present (`--force`) and false otherwise. // It also supports `--no-force` for explicit negation. force: Flag.boolean("force").pipe( Flag.withDescription("Skip the production confirmation prompt") ) }, Effect.fn(function*({ service, env, replicas, force }) { if (env === "production" && !force) { // Handlers are Effects, so failing is just `Effect.fail`. return yield* Effect.fail("Refusing to deploy to production without --force") } yield* Console.log(`Deploying ${service} to ${env} (${replicas} replicas)`) }))Invoked as deploy api --env production --replicas 3 --force, the handler
receives { service: "api", env: "production", replicas: 3, force: true },
fully typed.
Required vs. optional
Section titled “Required vs. optional”A bare Flag or Argument is required — if it is missing, parsing fails and
the user sees usage help. You make an input non-required in one of two ways:
import { Console, Effect, Option } from "effect"import { Flag } from "effect/unstable/cli"
// 1. `withDefault` supplies a value when the flag is absent. The field type is// just the value type — the handler never sees the absence.const port = Flag.integer("port").pipe(Flag.withDefault(8080))
// 2. `optional` wraps the value in an Option, so the handler can distinguish// "not provided" (Option.none) from any concrete value.const tag = Flag.string("tag").pipe(Flag.optional)
const handler = Effect.fn(function*(config: { readonly port: number readonly tag: Option.Option<string>}) { yield* Console.log(`port=${config.port}`) yield* Console.log( Option.match(config.tag, { onNone: () => "no tag supplied", onSome: (t) => `tag=${t}` }) )})Validating and transforming values
Section titled “Validating and transforming values”The built-in constructors already parse and validate primitive types (integer
rejects non-numbers, choice rejects values outside its set, file can require
existence). When you need more, use withSchema to run a
Schema over the parsed value, or map to transform it.
import { Schema } from "effect"import { Flag } from "effect/unstable/cli"
// Validate with a Schema: parsing fails with a clear error if the port is out// of range. Schema lives in core `effect`.const port = Flag.integer("port").pipe( Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))))
// Transform a parsed value into a richer shape.const endpoint = Flag.integer("port").pipe( Flag.map((p) => ({ port: p, url: `http://localhost:${p}` })))
// Read a secret as a Redacted value so it never prints in logs or errors.const apiKey = Flag.redacted("api-key")Repeated and variadic inputs
Section titled “Repeated and variadic inputs”Use variadic to collect a positional argument that may appear multiple times
into a ReadonlyArray. Pass min/max to bound how many are accepted.
import { Console, Effect } from "effect"import { Argument, Command, Flag } from "effect/unstable/cli"
const cat = Command.make( "cat", { // `cat a.txt b.txt c.txt` -> files: ["a.txt", "b.txt", "c.txt"] // `min: 1` makes at least one file mandatory. files: Argument.string("file").pipe( Argument.withDescription("Files to print"), Argument.variadic({ min: 1 }) ),
// A key=value flag merges repeated occurrences into one record: // `--define A=1 --define B=2` -> define: { A: "1", B: "2" } define: Flag.keyValuePair("define").pipe( Flag.withDescription("Template variables, repeatable") ) }, Effect.fn(function*({ files, define }) { for (const file of files) { yield* Console.log(`reading ${file}`) } yield* Console.log(JSON.stringify(define)) }))For flags, repetition is expressed with the counting combinators
atLeast / atMost / between instead of variadic (see the reference
below) — each makes the flag repeatable and collects every occurrence into a
ReadonlyArray.
Adding metadata
Section titled “Adding metadata”These combinators improve the generated --help output and ergonomics. They
work on any Param (and recursively find the underlying leaf, so they compose
after optional, map, etc.):
withDescription(text)— help text shown next to the flag/argument.withAlias("x")(Flag only) — a short alias, e.g.-x.withMetavar("NAME")— the placeholder shown in usage, e.g.<NAME>.withHidden(Flag only) — omit from help and completions but still parse.withFallbackConfig(config)— fall back to a value from Configuration (an env var, for instance) when the flag is absent.
Constructor reference
Section titled “Constructor reference”Every constructor below produces a leaf Param. Constructors marked (Flag
only) have no Argument counterpart. The Argument module deliberately omits
boolean (a positional true/false is ambiguous — use Flag.boolean, or
Argument.choice with explicit "true" / "false" strings) and
keyValuePair.
string
Section titled “string”Accepts any text value, with no validation. Available as Flag.string(name) and
Argument.string(name). Result type: string.
import { Flag, Argument } from "effect/unstable/cli"
const name = Flag.string("name") // --name "John Doe" => "John Doe"const filename = Argument.string("file") // myfile.txt => "myfile.txt"integer
Section titled “integer”Parses a whole number and rejects non-integer input. Flag.integer(name) /
Argument.integer(name). Result type: number.
import { Flag } from "effect/unstable/cli"
const port = Flag.integer("port") // --port 8080 => 8080 ; --port 8.5 failsParses a finite decimal number. Flag.float(name) / Argument.float(name).
Result type: number.
import { Flag } from "effect/unstable/cli"
const rate = Flag.float("rate") // --rate 3.14 => 3.14boolean (Flag only)
Section titled “boolean (Flag only)”A switch that is true when the flag is present and false otherwise; it also
accepts the negated form --no-<name>. Result type: boolean.
import { Flag } from "effect/unstable/cli"
const verbose = Flag.boolean("verbose") // --verbose => true ; --no-verbose => false ; absent => falseParses a string into a JavaScript Date (ISO dates and other forms the
underlying primitive accepts). Flag.date(name) / Argument.date(name). Result
type: Date.
import { Flag } from "effect/unstable/cli"
const start = Flag.date("start-date") // --start-date 2023-12-25 => Date(2023-12-25)choice
Section titled “choice”Restricts input to a fixed set of strings; the result type is the literal union,
not string. Flag.choice(name, choices) / Argument.choice(name, choices).
import { Flag } from "effect/unstable/cli"
const color = Flag.choice("color", ["red", "green", "blue"])// --color red => "red" (typed as "red" | "green" | "blue")// --color purple fails: Expected "red" | "green" | "blue", got "purple"choiceWithValue
Section titled “choiceWithValue”Like choice, but maps each accepted string key to an arbitrary typed value.
Flag.choiceWithValue(name, pairs) / Argument.choiceWithValue(name, pairs).
Result type: the union of the second tuple elements.
import { Flag } from "effect/unstable/cli"
const logLevel = Flag.choiceWithValue("log-level", [ ["debug", 0], ["info", 1], ["error", 3]] as const)// --log-level info => 1 (typed as 0 | 1 | 3)A filesystem path resolved to an absolute path. The options control what kind of
entry is allowed and whether it must already exist. Flag.path(name, options?) /
Argument.path(name, options?). Result type: string.
import { Flag } from "effect/unstable/cli"
// options: { pathType?: "file" | "directory" | "either"; mustExist?: boolean }// Flag.path additionally accepts { typeName?: string } for help output.const cfg = Flag.path("config-path", { pathType: "file", mustExist: true })// --config-path ./app.json => "/abs/path/app.json" (fails if it does not exist or is a directory)Convenience for path with pathType: "file". Flag.file(name, options?) /
Argument.file(name, options?), where options is { mustExist?: boolean }.
Result type: string (the resolved path).
import { Flag } from "effect/unstable/cli"
const input = Flag.file("input", { mustExist: true })// --input ./data.json => "/abs/path/data.json" (fails if missing or not a file)directory
Section titled “directory”Convenience for path with pathType: "directory". Flag.directory(name, options?) / Argument.directory(name, options?), with { mustExist?: boolean }. Result type: string.
import { Argument } from "effect/unstable/cli"
const workspace = Argument.directory("workspace", { mustExist: true })// ./project => "/abs/path/project" (fails if it is not an existing directory)redacted
Section titled “redacted”Wraps the parsed string in Redacted.Redacted<string> so it is masked in logs,
errors, and String(...). Read the underlying value with Redacted.value.
Flag.redacted(name) / Argument.redacted(name).
import { Redacted } from "effect"import { Flag } from "effect/unstable/cli"
const password = Flag.redacted("password")// --password abc123 => Redacted; String(value) => "<redacted>"; Redacted.value(value) => "abc123"fileText
Section titled “fileText”Reads the referenced file and returns its full contents as a string (failing if
the path is missing or not a file). Flag.fileText(name) /
Argument.fileText(name). Result type: string.
import { Flag } from "effect/unstable/cli"
const config = Flag.fileText("config-file")// --config-file ./app.json => the file's text content as a stringfileParse
Section titled “fileParse”Reads a file and parses it into structured data. The parser is chosen from
options.format or, when omitted, the file extension. Supported formats: ini,
json, toml, yaml / yml. Flag.fileParse(name, options?) /
Argument.fileParse(name, options?). Result type: unknown.
import { Flag } from "effect/unstable/cli"
// Format inferred from the extension:const config = Flag.fileParse("config")// Force the JSON parser regardless of extension:const json = Flag.fileParse("json-config", { format: "json" })// --config ./app.toml => parsed object (typed as unknown)fileSchema
Section titled “fileSchema”Reads and parses a file (like fileParse) and then decodes the result with a
Schema, giving a fully typed value. Flag.fileSchema(name, schema, options?) / Argument.fileSchema(name, schema, options?). Result type: the
schema’s decoded type.
import { Schema } from "effect"import { Flag } from "effect/unstable/cli"
const Config = Schema.Struct({ port: Schema.Number, host: Schema.String })const config = Flag.fileSchema("config", Config, { format: "json" })// --config ./app.json => { port: number; host: string } (decode failures become CLI errors)keyValuePair (Flag only)
Section titled “keyValuePair (Flag only)”Parses one or more key=value occurrences and merges them into a single record.
Requires at least one pair. Flag.keyValuePair(name). Result type:
Record<string, string>.
import { Flag } from "effect/unstable/cli"
const env = Flag.keyValuePair("env")// --env FOO=bar --env BAZ=qux => { FOO: "bar", BAZ: "qux" }A sentinel Param that always fails to parse, used as a placeholder in
conditional builders. Flag.none / Argument.none. Result type: never.
import { Flag } from "effect/unstable/cli"
const makeValueFlag = (include: boolean) => include ? Flag.string("value") : Flag.none// makeValueFlag(false) === Flag.none => trueCombinator reference
Section titled “Combinator reference”These combinators are shared by Flag and Argument unless noted. They are
pipeable (param.pipe(Flag.withDefault(...))) and also accept the
data-first form (Flag.withDefault(param, ...)).
withDefault
Section titled “withDefault”Supplies a fallback used only when the parameter is absent; provided values
parse normally. The fallback may be a plain value or an Effect. Widens the
result type to A | B.
import { Flag } from "effect/unstable/cli"
const host = Flag.string("host").pipe(Flag.withDefault("localhost"))// absent => "localhost" ; --host db => "db"optional
Section titled “optional”Turns a missing parameter into Option.none() and a present one into
Option.some(value). Result type: Option.Option<A>.
import { Option } from "effect"import { Flag } from "effect/unstable/cli"
const port = Flag.integer("port").pipe(Flag.optional)// absent => Option.none() ; --port 4000 => Option.some(4000)withAlias (Flag only)
Section titled “withAlias (Flag only)”Adds an alternative name for a flag, typically a single-character shortcut. Any leading dashes are stripped. Chain it to add several aliases.
import { Flag } from "effect/unstable/cli"
const verbose = Flag.boolean("verbose").pipe(Flag.withAlias("v"))// matches --verbose or -vwithDescription
Section titled “withDescription”Attaches help text shown next to the flag/argument in generated --help.
import { Flag } from "effect/unstable/cli"
const port = Flag.integer("port").pipe( Flag.withDescription("The port number to listen on"))withMetavar
Section titled “withMetavar”Sets the placeholder shown for the value in usage text (for example --timeout SECONDS instead of the default --timeout integer).
import { Flag } from "effect/unstable/cli"
const timeout = Flag.integer("timeout").pipe(Flag.withMetavar("SECONDS"))// In help: --timeout SECONDSwithHidden (Flag only)
Section titled “withHidden (Flag only)”Removes the flag from generated help and shell completions while keeping it fully parseable — useful for experimental or internal toggles.
import { Flag } from "effect/unstable/cli"
const experimental = Flag.boolean("experimental-foo").pipe(Flag.withHidden)// still parses --experimental-foo, but does not appear in --helpwithSchema
Section titled “withSchema”Validates and transforms the parsed value through a Schema codec. The schema’s input type must match the parameter’s current type; its output type becomes the new result type.
import { Schema } from "effect"import { Flag } from "effect/unstable/cli"
const email = Flag.string("email").pipe( Flag.withSchema( Schema.String.pipe( Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ) ))// --email a@b.com => "a@b.com" ; --email nope fails with a validation errorTransforms the parsed value with a pure function.
import { Flag } from "effect/unstable/cli"
const upper = Flag.string("name").pipe(Flag.map((n) => n.toUpperCase()))// --name john => "JOHN"mapEffect
Section titled “mapEffect”Transforms the parsed value with an Effect, allowing IO (filesystem, network)
or validation that fails with a CliError.
import { Effect, FileSystem } from "effect"import { Flag } from "effect/unstable/cli"
const fileSize = Flag.file("input").pipe( Flag.mapEffect(Effect.fnUntraced(function*(path) { const fs = yield* FileSystem.FileSystem const stats = yield* Effect.orDie(fs.stat(path)) return stats.size })))// --input ./data.json => the file size in bytesmapTryCatch
Section titled “mapTryCatch”Transforms with a function that may throw, converting thrown errors into a validation message.
import { Flag } from "effect/unstable/cli"
const json = Flag.string("config").pipe( Flag.mapTryCatch( (s) => JSON.parse(s), (error) => `Invalid JSON: ${error}` ))// --config '{"a":1}' => { a: 1 } ; --config '{bad' fails with "Invalid JSON: ..."filter
Section titled “filter”Keeps the value only if a predicate holds, failing with a custom message otherwise.
import { Flag } from "effect/unstable/cli"
const port = Flag.integer("port").pipe( Flag.filter( (p) => p >= 1 && p <= 65535, (p) => `Port ${p} is out of range (1-65535)` ))// --port 8080 => 8080 ; --port 0 fails with "Port 0 is out of range (1-65535)"filterMap
Section titled “filterMap”Transforms and filters in one step: return Option.some(b) to keep the mapped
value or Option.none() to reject with a custom message.
import { Option } from "effect"import { Flag } from "effect/unstable/cli"
const positive = Flag.integer("count").pipe( Flag.filterMap( (n) => (n > 0 ? Option.some(n) : Option.none()), (n) => `Expected positive integer, got ${n}` ))// --count 5 => 5 ; --count -1 fails with "Expected positive integer, got -1"orElse
Section titled “orElse”Falls back to a second parameter (built lazily) if the first fails to parse.
Result type: A | B.
import { Flag } from "effect/unstable/cli"
const value = Flag.integer("value").pipe( Flag.orElse(() => Flag.string("value")))// --value 42 => 42 (number) ; --value abc => "abc" (string)orElseResult
Section titled “orElseResult”Like orElse, but wraps the outcome in a Result so the handler can tell which
side succeeded. Result type: Result.Result<A, B>.
import { Result } from "effect"import { Flag } from "effect/unstable/cli"
const source = Flag.file("source").pipe( Flag.orElseResult(() => Flag.string("source-url")))// existing file => Result.succeed(path) ; otherwise => Result.fail(url string)atLeast
Section titled “atLeast”Makes the parameter repeatable and requires at least min occurrences,
collecting them into a ReadonlyArray. Works on both flags and arguments.
import { Flag } from "effect/unstable/cli"
const source = Flag.file("source").pipe(Flag.atLeast(2))// --source a.ts --source b.ts => ["a.ts", "b.ts"] ; fewer than 2 failsatMost
Section titled “atMost”Makes the parameter repeatable and allows at most max occurrences (0 to max).
import { Flag } from "effect/unstable/cli"
const warning = Flag.string("warning").pipe(Flag.atMost(3))// up to 3 --warning flags => ReadonlyArray<string> ; a 4th failsbetween
Section titled “between”Makes the parameter repeatable and bounds occurrences to the min..max range
(inclusive).
import { Flag } from "effect/unstable/cli"
const host = Flag.string("host").pipe(Flag.between(1, 3))// --host h1 --host h2 => ["h1", "h2"] ; 0 or more than 3 failsvariadic (Argument only)
Section titled “variadic (Argument only)”Consumes multiple consecutive positional values into a ReadonlyArray. With no
options it accepts zero or more; pass { min, max } to bound the count. Because
it greedily consumes the remaining positional input, place a variadic argument
last.
import { Argument } from "effect/unstable/cli"
const anyFiles = Argument.string("files").pipe(Argument.variadic())const atLeastOne = Argument.string("files").pipe(Argument.variadic({ min: 1 }))const upToFive = Argument.string("files").pipe(Argument.variadic({ min: 1, max: 5 }))// a.txt b.txt c.txt => ["a.txt", "b.txt", "c.txt"]withFallbackConfig
Section titled “withFallbackConfig”Loads the value from Configuration when the parameter is
missing from the command line. CLI values always win; a missing config preserves
the original missing-parameter error, and a config parse failure becomes an
invalid-value error. Result type: A | B.
import { Config } from "effect"import { Flag } from "effect/unstable/cli"
const verbose = Flag.boolean("verbose").pipe( Flag.withFallbackConfig(Config.boolean("VERBOSE")))// --verbose wins ; otherwise reads the VERBOSE config/env valuewithFallbackPrompt
Section titled “withFallbackPrompt”Shows an interactive Prompt when a required parameter is missing,
so the CLI can ask the user for the value. Accepts a Prompt or an Effect that
builds one; invalid values do not prompt, and cancelling re-fails with the
original missing error. Result type: A | B.
import { Flag, Prompt } from "effect/unstable/cli"
const name = Flag.string("name").pipe( Flag.withFallbackPrompt(Prompt.text({ message: "Name" })))// --name given => used directly ; absent => prompts "Name" interactivelyThe Primitive layer
Section titled “The Primitive layer”Underneath every Flag and Argument constructor is a Primitive<A> — the
low-level parser that turns a single raw string token into a typed value (with no
knowledge of names, aliases, or repetition). Most users never touch Primitive
directly; reach for it only when building a custom parameter shape.
The module exposes the parsers that back the constructors above:
Primitive.string, Primitive.boolean, Primitive.integer, Primitive.float,
Primitive.date, Primitive.choice(pairs), Primitive.path(pathType, mustExist?), Primitive.redacted, Primitive.fileText,
Primitive.fileParse(options?), Primitive.fileSchema(schema, options?),
Primitive.keyValuePair, and the Primitive.none sentinel. Each exposes a
parse(value: string) effect, and Primitive.getTypeName returns the
help-friendly type name.
import { Effect } from "effect"import { Primitive } from "effect/unstable/cli"
const program = Effect.gen(function*() { const port = yield* Primitive.integer.parse("8080") // => 8080 const mode = yield* Primitive.choice([ ["dev", "development"], ["prod", "production"] ] as const).parse("prod") // => "production" return { port, mode }})
Primitive.getTypeName(Primitive.integer) // => "integer"Primitive.getTypeName(Primitive.keyValuePair) // => "key=value"Next, see how to combine commands into a tree on the Subcommands page.