Typed clients
Generate a client from the agent so typos are compile errors.
The quickstart works without code generation, with strings
for names and unknown for values. Add a generated client when you want the
compiler to check your app against a specific agent version.
inlay codegen reads a deployed agent at build time and emits a TypeScript
client for it, so output names, tool handlers, node names, and
argument shapes all become things the compiler checks.
Before you generate
You need approved access, a workspace secret key, an immutable published version, and the SDK installed in your app. Codegen runs on your development machine or CI server with the workspace key, never in the browser. Your app still mints a user-scoped session at runtime; generated code contains the agent contract, not credentials.
Use the CLI distribution and version supplied during onboarding. This guide
does not assume a public @inlayai/cli package or a downloadable release is
available. With access to the monorepo and its pinned Rust toolchain, you can
install the inlay binary from source, from the repository root:
cargo install --locked --path projects/inlay/crates/inlay-cli
inlay codegen --helpEnsure Cargo's binary directory (normally ~/.cargo/bin) is on PATH. The
crate is not published to crates.io; cargo install inlay-cli is not the
source installation command.
Set INLAY_SECRET_KEY in your shell or CI secret store to the key for the
agent's workspace. The CLI reads it automatically; --api-key is an override,
but putting secrets in command history is best avoided. INLAY_API_URL (or
--base-url) overrides the default https://app.inlayai.com for staging or
self-hosting. The key, agent UUID, and API origin must belong together.
Generate
Run from your app's root after creating the app directory. Replace
<agent-uuid> and 1 with your agent UUID and published version. Kitchen
is the example symbol base; choose one for your agent and keep it consistent:
inlay codegen \
--agent <agent-uuid> \
--version 1 \
--name Kitchen \
--out app/kitchen.generated.ts| Flag | |
|---|---|
--agent | Which deployed agent to read. |
--version | Pin to a version. Omit to read the published one. |
--name | Base name for the generated symbols (KitchenBindings, useKitchenAgent). |
--out | Where to write. Commit the result. |
--target | react (default) or types for a framework-free contract. |
--check | Verify instead of write; requires --version. Use the same generation flags. |
--strict | Fail on extraction or schema-degradation warnings, including unsupported feed regions. |
Commit the generated file. It is a build input, not a build artifact: it is the record of the contract your app was compiled against.
What you get
// Production by default — the only wiring is the session and your tools.
const agent = useKitchenAgent({
session,
tools: {
ask_question: ({ question }) => window.prompt(question) ?? "",
set_timer: ({ seconds, label, kind }) => `${label}: ${seconds}s (${kind})`,
save_to_pantry: ({ item, category }) => `${item.name} → ${category}`,
},
});
const plan = agent.useBinding("plan");The hook bakes in the agent id, version pin, tool schemas, and output schemas.
For a tool-bearing agent, tools is required and exhaustive even under an
InlayProvider: a missing handler, a mis-named one, or an argument the schema
does not declare is a compile error. A tool-less agent emits no tools option.
Connection options can come from the provider; handlers cannot. Standalone,
you supply the session as well as any required handlers.
Compare that to the bare hook, where tools is optional and args are unknown.
The guarantees are not new; what changes is that they stop being opt-in.
The provider: the connection, for every agent
The package exports an agent-agnostic <InlayProvider>. Mount it once; it holds
the connection (production by default) and the history channel for EVERY agent
— no tools, no agent, nothing that varies per agent:
// app/providers.tsx — "use client": the agent-agnostic connection provider.
"use client";
import { InlayProvider } from "@inlayai/react";
export default function Providers({ children }: { children: React.ReactNode }) {
// One provider holds the connection for EVERY agent — no tools here.
return <InlayProvider session={session}>{children}</InlayProvider>;
}
// Any component: the hook owns the tools (typed, required — a mis-named or
// missing handler is a compile error), the connection comes from the provider.
// To share ONE chat across components, lift the handle — build it in a parent
// and pass it down; each hook call otherwise builds its own client.
function Anywhere() {
const agent = useKitchenAgent({
tools: {
ask_question: ({ question }) => window.prompt(question) ?? "",
set_timer: ({ seconds, label, kind }) => `${label}: ${seconds}s (${kind})`,
save_to_pantry: ({ item, category }) => `${item.name} → ${category}`,
},
});
return <p>{agent.snapshot.rows.length}</p>;
}Tools live on the hook, not the provider — they're typed to the agent and
evolve with its contract, so a mistake is a compile error right where you use
the agent. The connection (session, baseUrl, fetch) defaults to the
provider's and overrides per-option, so useKitchenAgent({ tools }) under a
provider needs nothing else; standalone you supply at least the session.
A mounted useKitchenHistory() refetches itself when a conversation starts or
settles (the provider's channel) — no hand-wired history.refetch(). baseUrl
defaults to the production Inlay API, so the quickstart needs no env var.
Each hook call builds its own client. To share one conversation across components (a sidebar and a composer), lift the handle — build it once in a parent and pass it down (or wrap it in your own context).
The provider is a client reference, so it can be rendered from a Server
Component and the real provider mounts on the client. Only serializable props
(session, baseUrl) cross that boundary — tools are on the hook (a client
component), so they never cross. See
Server and shared components.
Names become types
const producedBy = nodeNameOf(agent.snapshot.rows[0] ?? {});
if (producedBy === "assistant") {
// narrowed to this agent's node names
}
for (const row of agent.snapshot.rows) {
if (isToolCallFor(row, "set_timer")) {
console.log(row.toolCallId, row.input);
}
}
console.log(AGENT.version);nodeNameOf(row) answers "which node produced this?" by name, so your app
never handles an opaque uuid. isToolCallFor(row, "set_timer") narrows the row
as it checks. Both reject names the agent does not have.
You also get:
agent.nodes.<name>()andagent.subagents.<name>()— the per-node and per-subagent views from Progress, by autocomplete instead of by path.- Narrowed
exitedVia— a router's view is typed to its branch labels, so comparing against a port from a different node does not compile. - Precise output values — an output that declares a JSON Schema gets its real type, validated at runtime before it is returned.
- Typed tables and field metadata —
agent.useTable("…")checked against the agent's declared collections (<Base>Tables), and<Base>Fields: every public field'sformat,durability, andstreamsas data. See State and memory. - Generated feed metadata: every React module exports
REGIONS. Accepted regions, subgraphs, and repeatable nodes determine the composite kinds in its snapshot row union;EDGESstill supports branch views. useKitchenHistory— history scoped to this agent, with the id baked in.
Staying current
After generating and committing the file, verify it with the same command
and --check appended:
inlay codegen \
--agent <agent-uuid> \
--version 1 \
--name Kitchen \
--out app/kitchen.generated.ts \
--check--check requires --version. It compares bytes without writing and fails
if the file is absent or differs from what this CLI generates. Keep --name,
--out, --target, and any other generation flags identical between write
and check. Pin the CLI and SDK versions in CI as well, and supply the API
origin and workspace key through trusted CI configuration. This is an online
check: the runner needs access to the authenticated agent API.
With the same CLI, flags, and immutable version, the generated contract stays
stable. The check does not fail just because a newer version exists.
codegen reports that separately, as advice rather than a failure: publishing
must not break a deployed consumer's build.
Regenerate older clients when adopting descriptor-based feeds. Re-run the
generation command with the updated CLI and the same agent/version pin, name,
and output path. Every React module now exports REGIONS and passes it into
the hook config, even when it is []. Older NODES/EDGES-only metadata is
not analyzed in JavaScript as a fallback. Review the regenerated diff and
compile your renderer; the fan-out union arm now follows accepted descriptors,
not wiring alone. A codegen upgrade can therefore change the file even when
the agent version is unchanged.
When you do move versions, re-run codegen and let the compiler show you what changed. That is the point of the whole exercise — a contract change becomes a list of type errors instead of a silently empty panel.
Next
The travel workspace example shows a generated client feeding an itinerary, map, and composer, with the app-owned adapters in full.
The generated module documents the full contract. Without React covers the framework-agnostic client.