This feature is in active development and doesn't work yet. This information was written by an LLM and may be out of date.
Schema parameter injection
Agency lets you write functions that take a Schema<...> parameter and then automatically passes a Zod schema in for that argument, generated from the call's expected type. This is the mechanism that makes calls like
const numbers: number[] = llm("Return the first 5 Fibonacci numbers")work without the caller having to write schema(number[]) themselves.
You will probably not need this feature day-to-day — it's mostly useful when you're writing a structured-output wrapper of your own (a custom llm-style function, a typed JSON parser, etc.). When you do need it, here's how it works.
The rule
When the compiler sees a call to a function that has a Schema<...> parameter, and:
- The caller did not supply a value for that parameter, and
- The call appears in a position where the compiler knows the expected type,
then the compiler synthesizes a schema(T) expression — where T is the expected type — and inserts it as the missing argument.
The two positions that count as "the compiler knows the expected type" are:
- The right-hand side of a typed
constorletdeclaration.agencyconst xs: number[] = parseValue("[1,2,3]") // → parseValue("[1,2,3]", s: schema(number[])) - A
returnstatement inside a function with a declared return type.agencydef wrapper(): number[] { return parseValue("[1,2,3]") // → return parseValue("[1,2,3]", s: schema(number[])) }
If neither position applies — for example, a bare expression statement like parseValue("[1,2,3]") with no assignment and no return — no injection happens. The function receives undefined for the schema parameter, which will usually fail at runtime when the body tries to use it. The compiler does not currently produce a special error for this case; the assumption is that anyone defining a Schema-using function knows what the parameter is for.
Defining a Schema-using function
def parseValue(input: string, s: Schema<any>): any {
return s.parseJSON(input)
}A few constraints worth knowing:
- At most one
Schema<...>parameter per function. The injection mechanism only has one expected-type slot to draw from, so a function with two Schema parameters has no sensible meaning. Declaring two will produce a compile-time error. - Schema parameters are optional from the type checker's point of view. Even if you don't write
= <default>, the type checker will not flag a call that omits the schema argument — the injection pass is expected to fill it in. (If neither injection nor a default supplies a value, the runtime seesundefined.) - The return type of the function is independent of the LHS type. A Schema-using function commonly returns
anyso that any LHS annotation is accepted. If your function returns something more specific (e.g.Result<any, any>), the LHS annotation still must be assignable from that return type.
Overriding the injection
Pass the schema explicitly — by position or by name — to suppress injection:
// Positional
const x = parseValue("[1,2,3]", schema(any))
// Named
const x = parseValue("[1,2,3]", s: schema(string))Defaults
A Schema parameter can have a default value:
def parseString(input: string, s: Schema<any> = schema(string)): any {
return s.parseJSON(input)
}The precedence at the call site is:
- Explicit argument wins — if the caller passes the schema, it's used.
- LHS hint wins over the default — if the caller omits the schema and an LHS / return-position hint is available, the hint is injected.
- Default wins — if there's no explicit argument and no hint, the default runs at function entry.
Interaction with ! validation
const x: number[]! = parseValue("[1,2,3]") is fine. The injected schema is built from number[]; the ! triggers a separate runtime validation pass at the assignment site, also built from number[]. The two schemas agree, so validation passes whenever the value matches the schema.
Known limitations
These are intentional v1 limitations and may be relaxed in future versions:
- No argument-position injection. Only LHS annotations and enclosing return types drive injection. A call like
foo(parseValue("hi"))will not pick up a hint fromfoo's parameter type. - No injection through partial application.
parseValue.partial(input: "fixed")followed byconst x: number[] = partial()does not inject. Pass the schema at thepartial(...)call instead. - No injection through pipe chains.
"[1,2,3]" |> parseValuedoes not inject. Write the assignment directly when you want injection. - No injection through type aliases that name
Schema<T>. A parameter declared ass: MySchemaAlias(whereMySchemaAlias = Schema<any>) is not currently recognized as a Schema parameter. Declare it with theSchema<...>form directly.
When NOT to use this
Most Agency code should not declare Schema<...> parameters at all. The feature exists for a small population of structured-output wrappers — primarily llm() and similar — and is documented here mostly so you know what's happening when you read those functions' source.