Start with a normal Astro app. Agentuity adds service clients, local development support, and deploy validation around the file-based routing and build output you already use.
Create a Starter
npm create agentuity -- --name my-astro-app --framework astro
cd my-astro-appThe starter calls create-astro under the hood, then adds @agentuity/cli, installs @astrojs/node, adds a deploy script, and overlays an AI example using src/pages/api/translate.ts.
Key files in the AI example:
| File | Purpose |
|---|---|
src/pages/index.astro | Page UI that calls the API route |
src/pages/api/translate.ts | API route that calls AI Gateway through the starter helper |
astro.config.mjs | Adapter and output mode config |
package.json | dev, build, and deploy scripts pre-wired |
Import an Existing App
For an existing Astro app, add the CLI and register the project:
npm install -D @agentuity/cli
npx agentuity project import --validate-only # dry run: checks config only
npx agentuity project import # registers the project and writes agentuity.jsonnpm install -D installs the CLI as a project-local dev dependency, not a global install. Use npx to run that local CLI. For Bun, pnpm, and Yarn equivalents, see Local versus global CLI.
agentuity project import writes the local project metadata that agentuity deploy expects. Run it once before the first deploy. After that, agentuity dev runs the Astro dev script with the Agentuity environment values your app needs.
See Add Agentuity to an existing app for validation, import, the Invalid project folder deploy case, and first deploy.
What Agentuity adds
| Area | Astro keeps | Agentuity adds |
|---|---|---|
| routing | src/pages/api/*.ts endpoint files | service clients imported in server endpoints |
| local dev | astro dev | agentuity dev supplies service credentials |
| build | Astro server output and adapter config | agentuity build runs the build and packages launch metadata |
| deploy | the Node adapter server entry | agentuity deploy ships the packaged app |
Add an API Route
Astro endpoint files in src/pages/api/ export named functions per HTTP method. Put Agentuity service clients here; never import them from client-side <script> blocks.
The route below uses AIGatewayClient for the model call. The project SDK key authenticates the request, and the model ID is app configuration or request input.
npm install @agentuity/aigateway @agentuity/keyvalue arktypeimport { AIGatewayClient } from '@agentuity/aigateway';
import { KeyValueClient } from '@agentuity/keyvalue';
import { type } from 'arktype';
import type { APIRoute } from 'astro';
const DEFAULT_MODEL = 'openai/gpt-5.4-mini';
const HISTORY_NAMESPACE = 'translation-history';
const SESSION_COOKIE = 'agentuity_session';
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30;
interface HistoryEntry {
readonly model: string;
readonly sessionId: string;
readonly text: string;
readonly timestamp: string;
readonly toLanguage: string;
readonly translation: string;
}
interface HistoryState {
readonly history: readonly HistoryEntry[];
readonly translationCount: number;
}
const requestSchema = type({
'model?': 'string',
text: 'string',
toLanguage: 'string',
});
// Instantiated once at module level; reads AGENTUITY_SDK_KEY from env
const kv = new KeyValueClient();
const gateway = new AIGatewayClient();
function getSessionId(cookies: Parameters<APIRoute>[0]['cookies']): string {
const existing = cookies.get(SESSION_COOKIE)?.value;
const sessionId = existing ?? crypto.randomUUID();
// Astro.cookies.set writes the Set-Cookie header for the response
cookies.set(SESSION_COOKIE, sessionId, {
httpOnly: true,
maxAge: SESSION_TTL_SECONDS,
path: '/',
sameSite: 'lax',
secure: import.meta.env.PROD,
});
return sessionId;
}
export const POST: APIRoute = async ({ cookies, request }) => {
const body: unknown = await request.json();
const parsed = requestSchema(body);
if (parsed instanceof type.errors) {
return new Response(JSON.stringify({ error: 'text and toLanguage are required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const input = parsed;
const model = input.model ?? DEFAULT_MODEL;
const sessionId = getSessionId(cookies);
const result = await gateway.completeText({
model,
messages: [
{
role: 'user',
content: `Translate to ${input.toLanguage}:\n\n${input.text}`,
},
],
});
if (!result.hasText) {
return new Response(JSON.stringify({ error: 'model returned no text' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
}
const translation = result.text;
// Append to the last 5 entries stored for this session
const previous = await kv.get<HistoryState>(HISTORY_NAMESPACE, sessionId);
const history = [
...(previous.exists ? previous.data.history : []),
{
model,
sessionId,
text: input.text,
timestamp: new Date().toISOString(),
toLanguage: input.toLanguage,
translation,
},
].slice(-5);
const translationCount = (previous.exists ? previous.data.translationCount : 0) + 1;
await kv.set(HISTORY_NAMESPACE, sessionId, { history, translationCount }, { ttl: SESSION_TTL_SECONDS });
return new Response(JSON.stringify({ translation, history, model, translationCount }), {
headers: { 'Content-Type': 'application/json' },
});
};KeyValueClient reads AGENTUITY_SDK_KEY from the environment. Run the app via agentuity dev so the linked project key is available to endpoints.
Use the model id exactly as it appears in the AI Gateway model catalog. Use a provider SDK directly only when the endpoint depends on provider-specific APIs.
Call the Route from a Page
The .astro page calls the server route from a client-side <script> block:
---
// Server-side frontmatter: runs at request time (or build time for static pages)
const title = 'Agentuity + Astro';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<textarea id="text" rows="4">Hello</textarea>
<input id="lang" value="Spanish" placeholder="Target language" />
<button id="btn" type="button">Translate</button>
<p id="result"></p>
<ul id="history"></ul>
<script>
// Typed lookups: querySelector with a generic narrows the result and
// returns null if missing, so we early-return instead of asserting
const btn = document.querySelector<HTMLButtonElement>('#btn');
const result = document.querySelector<HTMLParagraphElement>('#result');
const historyList = document.querySelector<HTMLUListElement>('#history');
const textInput = document.querySelector<HTMLTextAreaElement>('#text');
const langInput = document.querySelector<HTMLInputElement>('#lang');
if (!btn || !result || !historyList || !textInput || !langInput) {
throw new Error('Translate UI is missing required elements');
}
btn.addEventListener('click', async () => {
const response = await fetch('/api/translate', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text: textInput.value, toLanguage: langInput.value }),
});
const data: {
translation: string;
history: Array<{ text: string; translation: string; toLanguage: string }>;
} = await response.json();
result.textContent = data.translation;
historyList.replaceChildren(
...data.history.map((entry) => {
const li = document.createElement('li');
li.textContent = `${entry.text} โ ${entry.translation}`;
return li;
})
);
});
</script>
</body>
</html>For richer client-side behavior, use an Astro island with client:load and a React or Svelte component instead of the inline <script> block above. The API call pattern stays the same.
Run Locally
Run through Agentuity. When the linked project key is available, the CLI supplies AGENTUITY_SDK_KEY to the Astro process:
agentuity devSmoke-test the API route:
curl http://localhost:4321/api/translate \
-H "content-type: application/json" \
-d '{"text":"Hello","toLanguage":"Spanish"}'Deployed Environment Variables
Configure the variables your deployed app needs. Direct AIGatewayClient calls use AGENTUITY_SDK_KEY; the model ID in this example is normal app configuration or request input.
| Variable | Used by |
|---|---|
AGENTUITY_SDK_KEY | Agentuity service clients and @agentuity/aigateway |
DATABASE_URL | database endpoints |
AWS_* | object storage endpoints; see Object Storage for exact variables |
Validate Before Deploy
Astro requires a server adapter for routes that must render on demand. The starter uses @astrojs/node in standalone mode. Install it either via the Astro CLI or directly:
# Option A: automated (recommended for new projects)
npx astro add node
# Option B: manual
npm install @astrojs/nodeThe explicit astro.config.mjs the starter ships with:
// @ts-check
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // all routes render on demand; opt individual pages out with export const prerender = true
adapter: node({
mode: 'standalone', // produces dist/server/entry.mjs, a self-contained Node server
}),
});Then build and inspect the launch metadata. Run npm run build when you want Astro-only feedback; agentuity build runs the framework build during packaging and writes .agentuity/launch.json.
npm run build
npx agentuity build
cat .agentuity/launch.json # verify processes[0].command runs dist/server/entry.mjsDeploy only after the framework build, Agentuity build, and a local packaged validation of / and one endpoint pass:
agentuity deploy --confirmAstro 6 and official server adapters require Node 22.12.0 or newer. Pin that version in local development, CI, and the deployment environment before upgrading an Agentuity-hosted Astro app.
Without output: 'server' (or an adapter with output: 'hybrid' and export const prerender = false), Astro attempts to prerender API routes as static files at build time. The route will be missing or return the wrong content type when deployed.
Common Gotchas
| Symptom | Check |
|---|---|
| API route works in dev but 404s after deploy | Confirm output: 'server' is set and the Node adapter is installed |
| Endpoint returns static HTML, not JSON | API route files need output: 'server'; without it, Astro prerenders them at build time. See on-demand rendering |
launch.json points at _serve.js | The packaged output is static-only and will not serve src/pages/api/*.ts endpoints |
| Service client returns auth errors | Run with agentuity dev or set AGENTUITY_SDK_KEY manually |
| Model call returns an auth error | Verify AGENTUITY_SDK_KEY is available to the endpoint process. See AI Gateway |
| Service client code appears in the browser bundle | Keep KeyValueClient and AIGatewayClient in src/pages/api/*.ts, never in client <script> blocks |
| Cookies do not persist across requests | Pass path: '/' when setting cookies; Astro's cookies.set defaults differ from browser document.cookie |
Framework Docs
- Astro Endpoints:
src/pages/api/*.tshandler shape, HTTP method exports, andAPIRoutetype - Astro On-Demand Rendering:
output: 'server'vs'hybrid', and theprerenderexport flag - Astro Node Adapter:
mode: 'standalone'vs'middleware', and thedist/server/entry.mjsentry point
Next Steps
- Key-Value Storage: TTL semantics, namespaces, and type-safe reads
- AI Gateway: supported providers and how environment injection works
- Deployment: CLI deploy flags and lifecycle hooks
- App Configuration: project IDs, domains, and environment files