AI features in your applications

Aha! Builder

Aha! Builder applications can make AI and LLM calls from their own server code — summarizing text, classifying input, analyzing images and documents, and streaming generated responses back to the screen. Every request runs server-side, and both the input and the output are automatically checked for unsafe content, so moderation is built in. Elle wires this into your application when you describe an AI-powered feature; you do not write the code yourself.

This reference documents the ai API your application uses: models, reasoning levels, structured output, file attachments, and streaming. Read it to see what your applications can do with AI, to review the code Elle generates, or to ground an AI assistant in the specifics — paste this article into Elle or any LLM so it knows exactly how AI works inside an Aha! Builder application.

Click any of the following links to skip ahead:

Overview

Use ai.singleResponse() inside any server function to make AI/LLM calls. It runs server-side — the client never calls AI directly.

Both inputs and outputs are automatically checked for safety (UnsafeContentError). Moderation is built in — you do not need to add your own content filtering.

Without structuredOutput, ai.singleResponse() returns a plain string. Always wrap the result in an object before returning from a server function — returning a raw string can cause JSON serialization errors when the response contains special characters like quotes or newlines.

import { server, ai } from '@aha-app/builder-core';

server.data('summarize', async ({ text }) => {
const summary = await ai.singleResponse({
systemPrompt: 'Summarize the following text in one sentence.',
userPrompt: text,
});
return { summary };
});

server.data('categorize', async ({ text }) => {
return await ai.singleResponse({
systemPrompt: 'Categorize the input into one of: bug, feature, question.',
userPrompt: text,
reasoning: 'medium',
structuredOutput: [
{ name: 'category', type: 'string', desc: 'The category', required: true },
{
name: 'confidence',
type: 'string',
desc: 'Confidence level: high, medium, low',
required: true,
},
],
});
});

Top

Options

type AIMODEL = 'fast' | 'balanced' | 'strong';
// 'minimal' (no reasoning), 'low' (fast/cheap), 'medium' (balanced), 'high' (strongest)
type AIReasoning = 'minimal' | 'low' | 'medium' | 'high';
type AIAttributeType = 'string' | 'integer' | 'float' | 'boolean';

interface SingleResponseOptions {
systemPrompt: string;
userPrompt: string;
model?: AIMODEL; // default: 'fast'
reasoning?: AIReasoning; // default: 'minimal'
temperature?: number; // default: 0.7
structuredOutput?: Array<{
name: string;
type?: AIAttributeType;
desc?: string;
required?: boolean;
}>;
attachments?: string[]; // FileBlob IDs for file inputs (images, documents)
}

Top

model

Controls which LLM backs the request. Default: 'fast'.

Value

Best for

Trade-off

'fast'

High-volume, low-latency tasks: summarization, extraction, classification, simple Q&A

Cheapest and fastest, but less capable on nuanced or multi-step reasoning

'balanced'

Most use cases: content generation, analysis, code review, moderate reasoning

Good balance of quality, speed, and cost

'strong'

Tasks where quality matters most: complex reasoning, long-form writing, subtle instructions

Slowest and most expensive, but highest quality output

Start with 'fast' and move up only if output quality is insufficient.

Top

reasoning

Controls how much the model "thinks" before responding. Default: 'minimal'.

Value

Best for

'minimal'

Direct tasks with clear instructions — summarization, extraction, reformatting

'low'

Light reasoning — classification, simple comparisons

'medium'

Multi-step analysis — weighing trade-offs, drawing conclusions from data

'high'

Complex reasoning — math, logic puzzles, nuanced judgment, multi-constraint problems

Higher reasoning increases latency and cost. Most tasks work well with 'minimal' or 'low'.

Top

temperature

Sampling temperature (0–2). Default: 0.7. Lower values (0–0.3) give more deterministic output, useful for classification or extraction. Higher values (0.8–1.5) give more creative output, useful for writing or brainstorming. Most relevant when using the 'fast' model; 'balanced' and 'strong' models rely more on reasoning than sampling.

Top

structuredOutput

Use structuredOutput to get back a typed object instead of free-form text. This is the recommended approach for any RPC-style AI call where you need to parse or act on the response programmatically. Define the fields you want and the response will contain exactly those keys.

Each field takes:

Property

Required

Description

name

yes

The key name in the returned object

type

no

'string' (default), 'integer', 'float', 'boolean'

desc

no

Description to guide the LLM on what to produce

required

no

When true, validates that the LLM returns a non-empty value

server.data('analyzeSentiment', async ({ text }) => {
const result = await ai.singleResponse({
systemPrompt: 'Analyze the sentiment of the given text.',
userPrompt: text,
structuredOutput: [
{
name: 'sentiment',
type: 'string',
desc: 'positive, negative, or neutral',
required: true,
},
{ name: 'score', type: 'float', desc: 'Confidence score from 0 to 1' },
{ name: 'summary', type: 'string', desc: 'One sentence explanation' },
],
});

// result is { sentiment: 'positive', score: 0.95, summary: 'The text expresses enthusiasm...' }
return result;
});

Top

attachments

Pass an array of FileBlob IDs to include files in the AI request. The model will see the files alongside the text prompts.

Supported file types: images (PNG, JPEG) and documents (PDF, DOCX, XLSX, TXT, CSV, MD).

File size limits: Images must be under 10 MB each. Documents must be under 10 MB each.

Validation errors: If a file has an unsupported type, the AI call throws an error. For non-streaming calls, the error is returned as a thrown exception in the server function. For streaming calls, an error SSE event is emitted with the message.

Upload a file using uploadFile on the client, then pass the blob ID to a server function:

import { uploadFile } from '@aha-app/builder-core';

const blobId = await uploadFile(file);
await analyzeFile({ fileId: blobId });

On the server, pass the blob ID in the attachments array:

import { server, ai } from '@aha-app/builder-core';

server.data('analyzeFile', async ({ fileId }) => {
const text = await ai.singleResponse({
systemPrompt: 'Analyze the contents of the attached file.',
userPrompt: 'What do you see?',
model: 'balanced',
attachments: [fileId],
});
return { text };
});

You can combine attachments with structuredOutput for typed analysis:

server.data('classifyImage', async ({ imageId }) => {
return await ai.singleResponse({
systemPrompt: 'Classify the image.',
userPrompt: 'What category does this image belong to?',
attachments: [imageId],
structuredOutput: [
{ name: 'category', type: 'string', desc: 'The image category', required: true },
{ name: 'confidence', type: 'float', desc: 'Confidence score from 0 to 1' },
],
});
});

Top

Streaming AI

For streaming text to the client (e.g., typing effect), use ai.streamedSingleResponse() to get a stream, then streamToResponse() to return it as an SSE response.

Server side:

import { server, ai, streamToResponse } from '@aha-app/builder-core';

server.data('storyTeller', async ({ topic }) => {
const stream = ai.streamedSingleResponse({
systemPrompt: 'You are a creative storyteller.',
userPrompt: topic,
});
return streamToResponse(stream);
});

Client side — use invokeStreamingFunction which handles SSE parsing:

import { invokeStreamingFunction } from '@aha-app/builder-core';

const [text, setText] = useState('');

const handleGenerate = async () => {
setText('');
await invokeStreamingFunction('storyTeller', [{ topic: prompt }], chunk => {
setText(prev => prev + chunk);
});
};

streamedSingleResponse accepts the same options as singleResponse except structuredOutput, which is not supported with streaming.

Top

Build it with Elle

Describe the capability you want in plain language, and Elle, the Aha! Builder AI assistant, builds it into your application using the framework above.

New to this? Open Elle, ask what it can do, and try a capability in your application today.

Top

Feedback received!

Error submitting feedback, please try again later