InlayDocs

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

Ensure 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
--agentWhich deployed agent to read.
--versionPin to a version. Omit to read the published one.
--nameBase name for the generated symbols (KitchenBindings, useKitchenAgent).
--outWhere to write. Commit the result.
--targetreact (default) or types for a framework-free contract.
--checkVerify instead of write; requires --version. Use the same generation flags.
--strictFail 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>() and agent.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 metadataagent.useTable("…") checked against the agent's declared collections (<Base>Tables), and <Base>Fields: every public field's format, durability, and streams as 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; EDGES still 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.

On this page