Runs a compose typeyou defined in the dashboard - its prompt, data sources, model, and output schema all live server-side. Your code just names the type and passes the end-user it's for; Vevee assembles the prompt, calls the model, meters the AI cost against that user, and returns typed, structured output.
Signature
compose<T = unknown>(
type: string, // the compose type's name (as set in the dashboard)
userId: string, // your end-user's id
vars?: Record<string, unknown> // optional template variables
): Promise<ComposeResult<T>>Backend-only - it spends your AI budget, so it requires a sk_* secret key and will reject a pk_* key with requires_secret_key. The generic Tis the shape of your type's structured output, which you define to match the output schema configured for the type in the dashboard.
Parameters
type- the name of the compose type as configured in the dashboard. An unknown name throwsnot_found.userId- the end-user this generation is for. The AI cost is metered against this user, and any per-user data sources on the type resolve to their data.vars- optional key/value pairs injected into the type's prompt template. Use these for the bits that change per call (a topic, a tone, a product name).
Response
type ComposeResult<T> =
| { status: 'generated'; generationId: string; output: T; usage: ComposeUsage }
| { status: 'opted_out'; generationId: null; output: null; usage: null };status: 'opted_out' is returned (as a normal 200, not an error) when the end-user has opted out of AI personalization. No model call runs and nothing is persisted. Branch on status before reading output.
Examples
Basic usage with a typed output
import { createClient } from '@vevee/sdk';
const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });
const r = await vevee.compose<{ headline: string; body: string }>('onboarding-email', userId);
if (r.status === 'opted_out') return FALLBACK;
console.log(r.output.headline);Passing template variables
const r = await vevee.compose<{ summary: string }>(
'release-notes',
userId,
{ version: '2.4.0', tone: 'concise' },
);
if (r.status === 'generated') console.log(r.output.summary);Server-side (Next.js route handler)
// app/api/onboarding/route.ts
import { createClient, VeveeError } from '@vevee/sdk';
const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });
export async function POST(req: Request) {
const { userId } = await req.json();
try {
const r = await vevee.compose<{ headline: string; body: string }>(
'onboarding-email',
userId,
);
if (r.status === 'opted_out') return new Response(null, { status: 204 });
return Response.json(r.output);
} catch (e) {
if (e instanceof VeveeError && e.code === 'ai_budget_exceeded') {
return Response.json({ error: 'AI budget exhausted' }, { status: 429 });
}
throw e;
}
}Errors
Throws a typed VeveeError with one of these codes:
invalid_request(400) - missing/invalidtypeoruserId.invalid_key(401) - missing, malformed, or revoked API key.requires_secret_key(403) - called with apk_*key; compose needssk_*.not_found(404) - no compose type with that name exists for this app.ai_budget_exceeded(429) - the workspace's AI budget is exhausted; no generation runs.generation_failed(502) - the upstream model call failed.
See also
- Guide: AI compose - data sources, use cases, and patterns.
- identify() - enrich the person profile compose can draw on as a data source.
- capture() - record the behavioral events that feed a person's analytics profile.
- track() - meter consumption you generate yourself, outside compose.
- Errors & status codes