Search
Search documentation
Outputs
Updated Oct 21, 2026
.output(schema) defines the shape of whatever the handler
returns. There's no f.argument() / f.option() equivalent for outputs —
outputs aren't CLI inputs, so commander has no per-field surface to
configure. The schema is pure data shape.
.output(z.object({
greeting: z.string(),
iterations: z.number(),
}))Any zod schema works: z.array(z.string()) writes a bare JSON array under
--json, and a scalar writes a scalar. An undefined result is written as
null. Prefer an object when the output may grow fields later — and when
the CLI is exposed to MCP clients, whose outputSchema must be an object.
Where the output schema is used
- Validation — the handler's return value is parsed through the schema
on every invocation. Type mismatches throw at runtime through
commander's
parseAsync. Zod's transforms apply, soz.string().transform(s => s.toUpperCase())post-processes the return. --jsonserialization — when--jsonis set, the validated value is serialized viaJSON.stringify(value, null, 2)and written to commander'swriteOut. See JSON mode.- Human rendering — without
--json, the validated value goes to.render(...)when the command has one. See Commands. --llmsmanifest — the schema is run throughz.toJSONSchema(...)and surfaced as the tool'soutputSchema. See LLMs mode.
Serialization
JSON.stringify does most of the work. Native types that need attention:
| Type | What JSON.stringify does | Recommendation |
|---|---|---|
Date | calls .toJSON() → ISO 8601 string | use z.iso.datetime() to document the wire shape (and accept the same on input) |
BigInt | throws (TypeError) | use z.coerce.bigint() and stringify yourself, or store as string in the schema |
Map/Set | become {} / [] (lossy) | model as z.record(...) / z.array(...) |
undefined | property dropped | use .optional() deliberately; null if you want presence |
Function/Symbol | property dropped | don't return these from a handler |
If a richer wire format is needed (preserve Date/BigInt round-trip
faithfully), the right place is not the output schema — it's a
serialization layer like superjson. fireargs deliberately stays plain-JSON
so the wire is consumable by any LLM, shell, or non-fireargs client.
Round-trip caveat for Date
z.date() makes round-trip with --json awkward and is unrepresentable
in z.toJSONSchema(...), so --llms shows it as {}. Prefer:
.output(z.object({
createdAt: z.iso.datetime(), // ISO string in, ISO string out
}))…or transform on output:
.output(z.object({
createdAt: z.string(),
}))
.handler(() => ({ createdAt: new Date().toISOString() }))Tracked in issue #1.
What flows to --llms
Per-property, the JSON Schema in the manifest carries:
type,format,pattern— derived from the zod constructor.description— from.describe(...)and.meta({ description }).enum— fromz.enum([...]).required— derived from non-optionalfields; a defaulted output field is always present, so it is required.{}— for a type JSON Schema cannot express, such asz.date()or the result of a.transform().
There's no CLI-side metadata to strip on outputs — f.argument() /
f.option() are inputs-only — so every key zod emits passes through.
Why no separate slots like f.argument()/f.option()?
Commander's Command has no output abstraction. CLI users see whatever
.render prints (or what --json serializes); there's nothing for
fireargs to "wire" beyond running the validation. Keeping outputs as plain
zod schemas matches commander's surface and avoids inventing concepts the
runtime can't honor.
Created with ❤ and Livemark