diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/langgraph.json b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/langgraph.json
new file mode 100644
index 000000000..fa46b9f8f
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/langgraph.json
@@ -0,0 +1,8 @@
+{
+ "node_version": "20",
+ "dependencies": ["."],
+ "graphs": {
+ "agent": "./src/agent/agent.ts:graph"
+ },
+ "env": ".env"
+}
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/manifest.json b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/manifest.json
new file mode 100644
index 000000000..350ad76ab
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/manifest.json
@@ -0,0 +1,23 @@
+{
+ "packageJson": {
+ "dependencies": {
+ "@langchain/core": "^1.2.5",
+ "@langchain/langgraph": "^1.4.9",
+ "@langchain/openai": "^1.5.6",
+ "@openuidev/langchain": "latest",
+ "langchain": "^1.5.5",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@langchain/langgraph-cli": "^1.4.4",
+ "npm-run-all2": "^9.0.2"
+ },
+ "scripts": {
+ "dev": "run-p dev:langgraph dev:next",
+ "dev:langgraph": "langgraphjs dev",
+ "dev:next": "next dev"
+ },
+ "removeDependencies": ["openai"]
+ },
+ "gettingStarted": "The generated LangGraph backend is a standalone Agent Server. `{{packageManager}} run dev` starts both the Agent Server and Next.js. Deploy the Next.js frontend to Vercel and point LANGGRAPH_API_URL at wherever the Agent Server runs.\nAsk \"What's the weather in Berlin?\" to exercise its native tool loop."
+}
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/agent/agent.ts b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/agent/agent.ts
new file mode 100644
index 000000000..15d9eefe5
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/agent/agent.ts
@@ -0,0 +1,38 @@
+import { tool } from "@langchain/core/tools";
+import { ChatOpenAI } from "@langchain/openai";
+import { generateSystemPrompt } from "@openuidev/lang-core";
+import { openUIStreamTransformer } from "@openuidev/langchain/transformer";
+import { createAgent } from "langchain";
+import { z } from "zod";
+import librarySpec from "../generated/spec.json";
+import { promptOptions } from "../lib/prompt-options";
+import { getWeather, WEATHER_TOOL_DESCRIPTION } from "../lib/tools/get-weather";
+
+const getWeatherTool = tool(
+ async ({ location }, config) =>
+ JSON.stringify(await getWeather(location, { signal: config.signal })),
+ {
+ name: "get_weather",
+ description: WEATHER_TOOL_DESCRIPTION,
+ schema: z.object({
+ location: z.string().trim().min(1).describe("City or place name, e.g. Berlin."),
+ }),
+ },
+);
+
+const model = new ChatOpenAI({
+ model: process.env.OPENAI_MODEL ?? "gpt-5.2",
+ streaming: true,
+ configuration: process.env.OPENAI_BASE_URL ? { baseURL: process.env.OPENAI_BASE_URL } : undefined,
+});
+
+/**
+ * A standalone LangGraph agent: LangGraph owns orchestration and tool execution,
+ * while the application supplies its OpenAI-compatible model provider.
+ */
+export const graph = createAgent({
+ model,
+ tools: [getWeatherTool],
+ systemPrompt: generateSystemPrompt({ library: librarySpec, promptOptions }),
+ streamTransformers: [openUIStreamTransformer],
+});
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/app/api/chat/route.ts b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/app/api/chat/route.ts
new file mode 100644
index 000000000..82207adfc
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/app/api/chat/route.ts
@@ -0,0 +1,19 @@
+import { createLangChainStreamResponse } from "@openuidev/langchain";
+
+export const runtime = "nodejs";
+
+const API_URL = process.env.LANGGRAPH_API_URL || "http://localhost:2024";
+const ASSISTANT_ID = process.env.LANGGRAPH_ASSISTANT_ID || "agent";
+
+/**
+ * Browser-to-Agent-Server proxy. The agent itself lives in src/agent/agent.ts
+ * and can be run locally or deployed independently.
+ */
+export async function POST(request: Request) {
+ return createLangChainStreamResponse(request, {
+ apiUrl: API_URL,
+ assistantId: ASSISTANT_ID,
+ apiKey: process.env.LANGSMITH_API_KEY,
+ debug: process.env.NODE_ENV !== "production",
+ });
+}
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/app/page.tsx b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/app/page.tsx
new file mode 100644
index 000000000..4cb904e45
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/langgraph/src/app/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+import "@openuidev/react-ui/components.css";
+import "@openuidev/react-ui/styles/index.css";
+
+import { AgentInterface, agUIAdapter, fetchLLM } from "@openuidev/react-ui";
+import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
+
+const llm = fetchLLM({
+ url: "/api/chat",
+ streamAdapter: agUIAdapter(),
+});
+
+export default function Home() {
+ return (
+
+ );
+}
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/manifest.json b/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/manifest.json
new file mode 100644
index 000000000..57ba335ec
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/manifest.json
@@ -0,0 +1,11 @@
+{
+ "packageJson": {
+ "dependencies": {
+ "@ai-sdk/openai": "3.0.90",
+ "ai": "6.0.244",
+ "zod": "^4.4.3"
+ },
+ "removeDependencies": ["openai"]
+ },
+ "gettingStarted": "The generated API route uses the Vercel AI SDK.\nAsk \"What's the weather in Berlin?\" to exercise its native tool loop."
+}
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/src/app/api/chat/route.ts b/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/src/app/api/chat/route.ts
new file mode 100644
index 000000000..da856d41c
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/src/app/api/chat/route.ts
@@ -0,0 +1,44 @@
+import librarySpec from "@/generated/spec.json";
+import { promptOptions } from "@/lib/prompt-options";
+import { getWeather, WEATHER_TOOL_DESCRIPTION } from "@/lib/tools/get-weather";
+import { createOpenAI } from "@ai-sdk/openai";
+import { generateSystemPrompt } from "@openuidev/lang-core";
+import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from "ai";
+import { z } from "zod";
+
+export const runtime = "nodejs";
+
+const openai = createOpenAI({
+ baseURL: process.env.OPENAI_BASE_URL,
+ apiKey: process.env.OPENAI_API_KEY,
+});
+
+const tools = {
+ get_weather: tool({
+ description: WEATHER_TOOL_DESCRIPTION,
+ inputSchema: z.object({
+ location: z.string().trim().min(1).describe("City or place name, e.g. Berlin."),
+ }),
+ execute: ({ location }, { abortSignal }) => getWeather(location, { signal: abortSignal }),
+ }),
+};
+
+export async function POST(req: Request) {
+ const payload = (await req.json()) as { messages?: UIMessage[] };
+ if (!Array.isArray(payload.messages)) {
+ return Response.json({ error: "messages must be an array" }, { status: 400 });
+ }
+
+ const result = streamText({
+ model: openai.chat(process.env.OPENAI_MODEL ?? "gpt-5.2"),
+ system: generateSystemPrompt({ library: librarySpec, promptOptions }),
+ messages: await convertToModelMessages(payload.messages),
+ tools,
+ stopWhen: stepCountIs(5),
+ abortSignal: req.signal,
+ });
+
+ // Preserve the AI SDK's native UIMessage stream. The frontend adapter uses
+ // the AI SDK itself to decode it before mapping chunks into OpenUI events.
+ return result.toUIMessageStreamResponse();
+}
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/src/app/page.tsx b/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/src/app/page.tsx
new file mode 100644
index 000000000..5f5b9cb55
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/backends/vercel-ai-sdk/src/app/page.tsx
@@ -0,0 +1,25 @@
+"use client";
+import "@openuidev/react-ui/components.css";
+import "@openuidev/react-ui/styles/index.css";
+
+import {
+ AgentInterface,
+ fetchLLM,
+ vercelAIAdapter,
+ vercelAIMessageFormat,
+} from "@openuidev/react-ui";
+import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
+
+const llm = fetchLLM({
+ url: "/api/chat",
+ streamAdapter: vercelAIAdapter(),
+ messageFormat: vercelAIMessageFormat,
+});
+
+export default function Home() {
+ return (
+
+ );
+}
diff --git a/packages/openui-cli/src/templates/openui-self-hosted/src/lib/tools/get-weather.ts b/packages/openui-cli/src/templates/openui-self-hosted/src/lib/tools/get-weather.ts
new file mode 100644
index 000000000..08a38246b
--- /dev/null
+++ b/packages/openui-cli/src/templates/openui-self-hosted/src/lib/tools/get-weather.ts
@@ -0,0 +1,74 @@
+/** Example app-owned tool: current weather via Open-Meteo (free, no API key). */
+
+export const WEATHER_TOOL_DESCRIPTION =
+ "Get the current weather for a city or place name. Use whenever the user " +
+ "asks about weather, temperature, rain, or what to wear.";
+
+export type WeatherResult =
+ | {
+ place: string;
+ temperature_c: number;
+ conditions: string;
+ wind_kmh: number;
+ }
+ | { error: string };
+
+function describeWeather(code: number): string {
+ if (code === 0) return "clear sky";
+ if (code <= 3) return "partly cloudy";
+ if (code <= 48) return "fog";
+ if (code <= 57) return "drizzle";
+ if (code <= 67) return "rain";
+ if (code <= 77) return "snow";
+ if (code <= 82) return "rain showers";
+ if (code <= 86) return "snow showers";
+ return "thunderstorm";
+}
+
+async function fetchJson(url: URL, signal?: AbortSignal): Promise {
+ const response = await fetch(url, { signal });
+ if (!response.ok) {
+ throw new Error(`Open-Meteo returned ${response.status}`);
+ }
+ return (await response.json()) as T;
+}
+
+export async function getWeather(
+ locationInput: string,
+ options: { signal?: AbortSignal } = {},
+): Promise {
+ const location = locationInput.trim();
+ if (!location) return { error: "location is required" };
+
+ try {
+ const geoUrl = new URL("https://geocoding-api.open-meteo.com/v1/search");
+ geoUrl.searchParams.set("name", location);
+ geoUrl.searchParams.set("count", "1");
+ const geo = await fetchJson<{
+ results?: Array<{ name: string; country?: string; latitude: number; longitude: number }>;
+ }>(geoUrl, options.signal);
+ const place = geo.results?.[0];
+ if (!place) return { error: `No place found for "${location}"` };
+
+ const weatherUrl = new URL("https://api.open-meteo.com/v1/forecast");
+ weatherUrl.searchParams.set("latitude", String(place.latitude));
+ weatherUrl.searchParams.set("longitude", String(place.longitude));
+ weatherUrl.searchParams.set("current", "temperature_2m,weather_code,wind_speed_10m");
+ const weather = await fetchJson<{
+ current?: { temperature_2m: number; weather_code: number; wind_speed_10m: number };
+ }>(weatherUrl, options.signal);
+ if (!weather.current) return { error: "No weather data returned" };
+
+ return {
+ place: `${place.name}${place.country ? `, ${place.country}` : ""}`,
+ temperature_c: weather.current.temperature_2m,
+ conditions: describeWeather(weather.current.weather_code),
+ wind_kmh: weather.current.wind_speed_10m,
+ };
+ } catch (error) {
+ if (options.signal?.aborted) throw error;
+ return {
+ error: `Weather lookup failed: ${error instanceof Error ? error.message : String(error)}`,
+ };
+ }
+}