Quickstart
Build a real processor step by step — each section adds one concept.
1. The simplest processor
A processor needs a name and a goal. That's it.
import { Baleybot } from '@baleybots/core';
const responder = Baleybot.create({
name: 'responder',
goal: 'Answer user questions clearly and concisely',
});
const response = await responder.process('What is the capital of France?');
console.log(response);
// "The capital of France is Paris."
process() sends your input to the LLM and returns the response as a string. The goal becomes the system prompt.
2. Add structured output
Raw text is fine for chat, but most applications need typed data. Use Output.object() with a Zod schema:
import { Baleybot, Output } from '@baleybots/core';
import { z } from 'zod';
const analyzer = Baleybot.create({
name: 'sentiment-analyzer',
goal: 'Analyze the sentiment of text',
output: Output.object({
schema: z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number().min(0).max(1),
keywords: z.array(z.string()),
}),
}),
});
const result = await analyzer.process(
'I absolutely love this product!'
);
console.log(result.sentiment); // 'positive'
console.log(result.confidence); // 0.95
console.log(result.keywords); // ['love', 'product']
The return type is fully inferred from the schema — TypeScript knows result.sentiment is 'positive' | 'negative' | 'neutral'. If the LLM returns data that doesn't match the schema, Baleybots catches it and retries automatically.
3. Add tools
Processors become powerful when they can do things. Define a tool with tool():
import { Baleybot, tool, stepCountIs } from '@baleybots/core';
import { z } from 'zod';
const weatherTool = tool(
'get_weather',
'Get current weather for a city',
z.object({ city: z.string().describe('City name') }),
async ({ city }) => {
// In a real app, call a weather API here
return { temp: 72, condition: 'sunny', city };
},
);
const helper = Baleybot.create({
name: 'weather-helper',
goal: 'Help users with weather information',
tools: { get_weather: weatherTool },
// Optional — default is stepCountIs(50)
stopWhen: stepCountIs(10),
});
const answer = await helper.process(
"What's the weather in Tokyo?"
);
console.log(answer);
// "The weather in Tokyo is 72°F and sunny."
Here's what happens under the hood:
- The LLM sees the tool definition and decides to call
get_weather - Baleybots executes the tool function with the LLM's arguments
- The tool result is sent back to the LLM
- The LLM formulates a natural language response
This tool loop runs automatically — you don't write the loop yourself. By default it stops after 50 steps (stepCountIs(50)). Override with stopWhen or combine conditions with combineConditions(). See the Tools guide for stop conditions, approval gates, and more.
4. Compose processors
Here's the key idea: every processor is a Processable — an object with a .process() method. Pipelines, parallel compositions, and even other processors passed as tools all accept any Processable. Processors compose like functions.
Chain with pipelines
import { Baleybot, pipeline } from '@baleybots/core';
const researcher = Baleybot.create({
name: 'researcher',
goal: 'Research a topic and gather key facts',
});
const writer = Baleybot.create({
name: 'writer',
goal: 'Write a clear, engaging article from research notes',
});
const workflow = pipeline(researcher, writer);
const article = await workflow.process('AI agents in 2025');
// researcher runs first, its output feeds into writer
Run in parallel
import { Baleybot, parallel } from '@baleybots/core';
const sentimentBot = Baleybot.create({
name: 'sentiment',
goal: 'Classify sentiment as positive, negative, or neutral',
});
const summaryBot = Baleybot.create({
name: 'summary',
goal: 'Write a one-sentence summary',
});
const analysis = parallel({
sentiment: sentimentBot,
summary: summaryBot,
});
const results = await analysis.process('Your text here...');
// { sentiment: '...', summary: '...' }
// Both processors run simultaneously
Processors as tools
For typed specialists (fixed goal + Output.object), wrap the child with subagentTool so the parent gets a real Zod tool with a typed return:
import { Baleybot, Output, subagentTool } from '@baleybots/core';
import { z } from 'zod';
const Findings = z.object({
summary: z.string(),
sources: z.array(z.string()),
confidence: z.number().min(0).max(1),
});
const researcher = Baleybot.create({
name: 'researcher',
goal: 'Research a topic and return structured findings',
model: 'gpt-4.1-mini',
output: Output.object({ schema: Findings }),
});
const coordinator = Baleybot.create({
name: 'coordinator',
goal: 'Answer the user by delegating research when needed',
model: 'gpt-4.1-mini',
tools: {
research: subagentTool({
name: 'research',
bot: researcher,
inputSchema: z.object({
query: z.string().describe('What to research'),
}),
}),
},
});
const answer = await coordinator.process('What changed in AI SDK v7 agents?');
You can still pass a raw Processable into tools for quick demos, but prefer subagentTool when you need typed input/output, abortSignal, or toModelOutput.
What's next?
Now that you have the mental model — processors, structured output, tools, composition — go deeper:
- Packages overview —
@baleybots/chat,@baleybots/orchestration,@baleybots/tools, and more - Custom models — pass any AI SDK
LanguageModelfor community providers and custom endpoints - Provider setup — OpenAI, Anthropic, Ollama, env auto-detect
- Orchestration — ticket-based teams and coordinator routing
- Building a chat app? React Hooks guide —
useChathandles the full lifecycle - Need finer tool control? Tools guide + Streaming guide — approval gates, stop conditions, real-time output
- Designing multi-processor systems? Composition guide — pipelines, parallel, routing, and loops