Skip to content

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 value with 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.

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}`
})
)
})

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")

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.

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.

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.

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"

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 fails

Parses 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.14

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 => false

Parses 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)

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"

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)

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)

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"

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 string

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)

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)

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 => true

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, ...)).

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"

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)

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 -v

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")
)

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 SECONDS

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 --help

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 error

Transforms 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"

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 bytes

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: ..."

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)"

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"

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)

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)

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 fails

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 fails

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 fails

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"]

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 value

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" interactively

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.