BaleyUI Development
1. Quick Reference
Built-in Tools
| Tool | Category | Approval | Description |
|---|---|---|---|
web_search |
information | No | Web search via Tavily API |
fetch_url |
information | No | Fetch URL content (HTML/text/JSON) |
spawn_baleybot |
orchestration | No | Execute another BB, return result |
send_notification |
notification | No | In-app notification to user |
store_memory |
storage | No | Persist key-value data across executions |
shared_storage |
storage | No | Workspace-scoped cross-BB shared data |
request_user_input |
interaction | No | Ask user a question mid-execution |
schedule_task |
orchestration | Yes | Schedule BB for future/cron execution |
create_agent |
orchestration | Yes | Create ephemeral agent for current run |
create_tool |
orchestration | Yes | Define custom NL-described tool |
Key File Locations
| Area | Path |
|---|---|
| DB Schema | packages/db/src/schema.ts |
| tRPC Routers | apps/web/src/lib/trpc/routers/ |
| Executor | apps/web/src/lib/baleybot/executor.ts |
| Built-in Tools | apps/web/src/lib/baleybot/tools/built-in/ |
| Tool Catalog | apps/web/src/lib/baleybot/tools/catalog-service.ts |
| Connection Tools | apps/web/src/lib/baleybot/tools/connection-derived/ |
| Internal BBs | apps/web/src/lib/baleybot/internal-bb/ |
| Internal BB Specs | apps/web/src/lib/baleybot/internal-bb/source/specs.json |
| Stream Events | apps/web/src/lib/streaming/types/events.ts |
| Readiness | apps/web/src/lib/baleybot/readiness.ts |
| Session Context | apps/web/src/lib/baleybot/session-context.ts |
| Creator Helpers | apps/web/src/lib/baleybot/creator-helpers.ts |
| Connections | apps/web/src/lib/connections/ |
| Components | apps/web/src/components/ |
| Pages/Routes | apps/web/src/app/ |
| API Routes | apps/web/src/app/api/ |
Core Database Tables
| Table | Purpose |
|---|---|
workspaces |
User workspaces |
baleybots |
BaleyBot definitions (BAL code, status, lifecycle) |
baleybotExecutions |
Execution records with input/output/segments |
connections |
AI providers, databases, external services |
scheduledTasks |
Cron/scheduled BB executions |
toolApprovalPatterns |
Learned auto-approval rules |
BaleyBot Lifecycle Stages
draft -> verified -> launch_prepared -> live -> paused
draft: Being built, BAL code may be incompleteverified: Tests passed, design reviewedlaunch_prepared: LaunchKit generated, triggers configuredlive: Active and accepting executionspaused: Temporarily disabled
2. BAL Language Essentials
Entity Syntax (6 supported properties)
my_entity {
"goal": "What this entity should accomplish",
"model": "anthropic:powerful",
"tools": { "web_search", "fetch_url" },
"output": {
"summary": "string",
"items": "array<object>",
"count": "number"
},
"maxTokens": 4096,
"history": "inherit"
}
Supported properties: goal, model, tools, output, maxTokens, history - that's it.
NOT supported yet: temperature, reasoning, stopWhen, retries, needsApproval, can_request
Output Types
| BAL type | Zod result | Use for |
|---|---|---|
"string" |
z.string() |
Text fields |
"number" |
z.number() |
Numeric fields |
"boolean" |
z.boolean() |
True/false |
"array" |
z.array(z.string()) |
String lists |
"array<string>" |
z.array(z.string()) |
Same as "array" |
"array<number>" |
z.array(z.number()) |
Number lists |
"array<boolean>" |
z.array(z.boolean()) |
Boolean lists |
"array<object>" |
z.array(z.record(z.string(), z.unknown())) |
Structured arrays |
"object" |
z.record(z.string(), z.unknown()) |
Nested objects |
NOT supported: array<object{...}>, enum(...), ?type, unknown
Composition Blocks
chain { a b } # Sequential
parallel { a b } # Concurrent
if ("result.score > 0.8") { a } else { b } # Conditional
loop ("until": "result.done", "max": 5) { a } # Iteration
if ("$classification.type == 'type1'") { h1 } else { h2 } # Routing pattern
if ("result.needsReview") { reviewer } # Conditional step
map result.items { enricher } # Per-item processing
select { result.data } # Data projection
Critical BAL Gotchas
Tools use BRACE syntax, NOT brackets:
"tools": { "web_search", "fetch_url" } # CORRECT tools: [ "web_search", "fetch_url" ] # WRONG - legacy array syntaxOutput fields are REQUIRED (not optional). The SDK's
buildZodSchema()produces required fields. NEVER re-add.optional()tobuildZodSchemafield loop - it breaks all internal bots with output blocks.Model strings use
provider:modelformat:"anthropic:powerful","openai:fast","anthropic:claude-sonnet-4-20250514"
3. Architecture Overview
Data Flow
User Request
-> Creator Bot (conversational architect)
-> BAL Generator (produces BAL code)
-> Connection Advisor (checks tool requirements)
-> Test Orchestrator (designs tests)
-> Deployment Advisor (evaluates readiness)
-> Saved BaleyBot (BAL code + metadata in DB)
-> Executor (parse -> compile -> execute)
-> Streaming Events -> UI
Core Abstractions
- BaleyBot: AI agent defined in BAL, stored in
baleybotstable - BAL: Baleybots Assembly Language - DSL for defining entities and compositions
- Entity: A single AI agent within BAL code (has goal, model, tools, output)
- Composition: How entities connect (chain, parallel, if/else, loop, etc.)
- Tool: Capability a BB can use (built-in, connection-derived, workspace, ephemeral)
- Connection: External service binding (AI provider, database, API)
Tool Source Hierarchy
Tools are assembled at execution time from multiple sources:
- Built-in - Always available, defined in
tools/built-in/index.ts - Connection-derived - Generated from workspace connections (e.g., postgres connection ->
query_postgrestool) - MCP tools - From connected MCP servers
- Workspace tools - User-defined tools stored in DB
- Ephemeral tools - Created at runtime via
create_tool
The catalog-service.ts assembles the full tool catalog per workspace.
4. Database Patterns
Always Use Soft Deletes
import { notDeleted, softDelete } from '@baleyui/db';
// Query - ALWAYS filter deleted records
const items = await db.query.baleybots.findMany({
where: and(eq(baleybots.workspaceId, wsId), notDeleted(baleybots))
});
// Delete
await softDelete(baleybots, itemId, userId);
Always Use Optimistic Locking
import { updateWithLock, OptimisticLockError } from '@baleyui/db';
try {
await updateWithLock(baleybots, id, currentVersion, { name: 'New' });
} catch (e) {
if (e instanceof OptimisticLockError) {
// Refresh and retry
}
}
Always Use Transactions for Multi-Table Ops
import { withTransaction } from '@baleyui/db';
await withTransaction(async (tx) => {
const [bot] = await tx.insert(baleybots).values(data).returning();
await tx.insert(auditLogs).values({ baleybotId: bot.id, ... });
});
Schema Changes
- Add table definition in
packages/db/src/schema.ts - Add relations if needed
- Export from
packages/db/src/index.ts - Run
pnpm db:push(dev) orpnpm db:generate && pnpm db:migrate(prod)
5. Execution Flow
Parse -> Compile -> Execute Pipeline
BAL Code (string)
-> tokenize() (@baleybots/tools/dsl/lexer)
-> parse() (@baleybots/tools/dsl/parser)
-> ProgramNode AST (cached via BALParseCache, 5min TTL)
-> compileBALCode() (@baleyui/sdk)
-> executeBALCode() (@baleyui/sdk)
-> ExecutionResult { executionId, status, output, segments, durationMs }
Key executor file: apps/web/src/lib/baleybot/executor.ts
Tool Approval Flow
Tools with approvalRequired: true (schedule_task, create_agent, create_tool) trigger the approval flow:
- Entity requests tool use ->
onToolCallApprovalcallback fires ApprovalRequestsent to UI with tool name, arguments, entity goal- User approves/denies/modifies ->
ApprovalResponsereturned - Approved patterns can be remembered via
toolApprovalPatternstable
Streaming Events Reference
From @baleybots/core (re-exported via @/lib/streaming/types/events):
// Text streaming - use 'content' (NOT 'delta')
{ type: 'text_delta', content: string }
{ type: 'structured_output_delta', content: string }
{ type: 'reasoning', content: string }
// Tool call streaming - use 'id' for tool call ID
{ type: 'tool_call_stream_start', id: string, toolName: string }
{ type: 'tool_call_arguments_delta', id: string, argumentsDelta: string }
{ type: 'tool_call_stream_complete', id: string, toolName: string, arguments: unknown }
// Tool execution
{ type: 'tool_execution_start', id: string, toolName: string, arguments: unknown }
{ type: 'tool_execution_output', id: string, toolName: string, result: unknown, error?: string }
{ type: 'tool_execution_stream', toolCallId: string, toolName: string, nestedEvent: BaleybotStreamEvent, childBotName?: string }
// Errors
{ type: 'tool_validation_error', toolName: string, validationErrors: unknown, receivedArguments: unknown }
{ type: 'error', error: Error | { message: string, name?: string, stack?: string } }
// Done - use 'reason' (NOT 'result')
{ type: 'done', reason: DoneReason, timestamp: number, duration_ms: number, agent_id: string, parent_agent_id?: string }
type DoneReason = 'turn_yielded' | 'out_of_iterations' | 'max_tokens_reached' | 'error' | 'interrupted' | 'no_applicable_tools' | 'max_depth_reached' | 'graceful_shutdown';
Post-Execution Services (non-blocking)
After execution completes, the executor fires (all wrapped in try/catch, failures don't affect result):
- BB Completion Triggers - fires downstream BBs configured with
other_bbtrigger - Metrics Recording - default + custom analytics metrics
- Usage Tracking - token/cost recording from execution segments
- Alert Evaluation - checks error rate thresholds on failure
6. Internal BaleyBots
BaleyUI "eats its own cooking" - internal operations use BaleyBots stored with isInternal: true.
Internal Bots
| ID | Icon | Role | Model |
|---|---|---|---|
baley |
bot |
Conversational architect, delegates to specialists | anthropic:powerful |
creator_action_advisor |
puzzle |
Suggests next creator actions based on context | anthropic:powerful |
bal_generator |
memo |
Converts descriptions to BAL code | anthropic:powerful |
pattern_learner |
brain |
Analyzes approvals, suggests auto-approval patterns | anthropic:powerful |
execution_reviewer |
search |
Reviews executions, suggests improvements | anthropic:powerful |
nl_to_sql_postgres |
elephant |
NL to PostgreSQL translation | openai:fast |
nl_to_sql_mysql |
dolphin |
NL to MySQL translation | openai:fast |
web_search_fallback |
magnifier |
AI search when no Tavily key | openai:fast |
connection_advisor |
plug |
Advises on connection requirements | anthropic:powerful |
test_orchestrator |
microscope |
Topology-aware test designer | anthropic:powerful |
test_generator |
test_tube |
Generates test cases from BB goal | anthropic:powerful |
test_validator |
check |
Validates test output semantically | anthropic:powerful |
test_results_analyzer |
chart |
Analyzes test run results | anthropic:powerful |
deployment_advisor |
rocket |
Advises on triggers/scheduling/activation | anthropic:powerful |
integration_builder |
link |
Conversational integration guide | anthropic:powerful |
test_interface_designer |
target |
Designs optimal test UI for a BB | anthropic:powerful |
tool_executor |
wrench |
Executes NL-defined workspace tools | openai:fast |
context_processor |
gear |
Processes and enriches context for BB execution | anthropic:powerful |
Calling Internal BaleyBots
import { executeInternalBaleybot } from '@/lib/baleybot/internal-baleybots';
const { output, executionId } = await executeInternalBaleybot(
'baley',
userMessage,
{
userWorkspaceId: workspace.id,
context: additionalContext, // Optional string context
}
);
All internal BB executions are tracked in baleybotExecutions with triggeredBy: 'internal'.
The resolveOutput() Pattern
Internal bot output can be: raw object, JSON string, or markdown-fenced JSON. Always normalize before parsing:
// runner.ts normalizeOutputCandidate() handles:
// 1. Object passthrough
// 2. Direct JSON.parse()
// 3. Markdown ```json ... ``` extraction
// 4. Balanced JSON segment extraction from mixed text
const resolved = normalizeOutputCandidate(output);
const result = schema.parse(resolved);
Resilient Schemas for BAL Output Consumers
BAL array<object> produces z.array(z.record(z.string(), z.unknown())) - the model doesn't know which inner fields are required. Caller schemas must use .default() for non-critical fields:
const schema = z.object({
name: z.string().min(1), // Required - unrecoverable
balCode: z.string().min(1), // Required - unrecoverable
id: z.string().min(1).default(() => crypto.randomUUID()), // Has fallback
icon: z.string().default('bot_face'), // Has fallback
tools: z.array(z.string()).default([]), // Has fallback
});
7. Creator & Readiness
Creator Pipeline
The creator flow uses a team of specialist internal BBs:
User describes what they want
-> baley (conversational architect)
-> Understands intent, asks clarifying questions
-> spawn_baleybot('bal_generator', designSpec) // Produces BAL code
-> spawn_baleybot('connection_advisor', ...) // Checks connections
-> spawn_baleybot('test_orchestrator', ...) // Designs tests
-> spawn_baleybot('deployment_advisor', ...) // Evaluates readiness
-> BaleyBot saved to DB with generated BAL + metadata
Key rule: baley NEVER generates BAL code itself. It always delegates to bal_generator via spawn_baleybot.
5 Readiness Dimensions
Tracked in readiness.ts - each dimension is incomplete | in-progress | complete | not-applicable:
| Dimension | What it checks | How it completes |
|---|---|---|
designed |
BAL code exists and has entities | hasBalCode && hasEntities |
connected |
Required connections are met | connectionsMet OR connection_advisor ran |
tested |
Tests have passed | testsPassed OR test_orchestrator ran |
integrated |
Trigger is configured | hasTrigger OR deployment_advisor ran |
monitored |
Monitoring is set up | hasMonitoring |
connected, integrated, and monitored can be not-applicable based on tool usage.
Applicability Rules
// connected: needed if BB uses schedule_task, send_notification, spawn_baleybot, or has connections
// integrated: needed if BB uses schedule_task
// monitored: needed if BB uses schedule_task
Session Context
Shared context passed to specialist BBs so they understand the current build:
interface SessionContext {
botName: string;
balCode: string;
entities: Array<{ name: string; tools: string[]; purpose: string }>;
readiness: ReadinessState;
connectedProviders: string[];
connectedDatabases: string[];
testSummary?: { total: number; passed: number; failed: number };
}
// Format for BB input:
const contextStr = formatSessionContext(ctx);
Adaptive Tabs
The detail page shows tabs based on readiness state:
- Always visible:
visual,code - After design:
test - After save:
integrate
8. Streaming UI
Performance targets: 60fps, <50ms TTFT
Core Pattern: RAF Batching
// DON'T update React state per token
const [text, setText] = useState(''); // BAD - re-renders per token
// DO use RAF batching with direct DOM manipulation
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const textNode = document.createTextNode('');
containerRef.current?.appendChild(textNode);
let buffer = '';
let rafId = 0;
const flush = () => {
textNode.textContent += buffer;
buffer = '';
rafId = 0;
};
stream.on('text_delta', (event) => {
buffer += event.content;
if (!rafId) rafId = requestAnimationFrame(flush);
});
return () => { if (rafId) cancelAnimationFrame(rafId); };
}, [stream]);
Animations: CSS Only
/* DON'T use Framer Motion for high-frequency updates */
/* DO use CSS animations */
.streaming-cursor {
animation: pulse 1s ease-in-out infinite;
}
SSE Reconnection
Always implement exponential backoff for EventSource connections.
9. React 19 & Next.js 15
No Manual Memoization
The React 19 compiler (experimental: { reactCompiler: true } in next.config.ts) handles optimization automatically.
// DON'T do this
const memoized = useMemo(() => expensive(), [deps]);
const callback = useCallback(() => {}, [deps]);
export default React.memo(Component);
// DO this - just write normal code
const result = expensive();
const handler = () => {};
export default Component;
Server Components Default
In Next.js 15 App Router, components are server components by default. Only add 'use client' when you need browser APIs, hooks, or event handlers.
eslint-disable-next-line Placement
For react-hooks/exhaustive-deps, the disable comment must go directly above the dependency array line, NOT above useEffect.
10. Common Tasks
Add a Built-in Tool
- Add JSON schema to
tools/built-in/index.ts - Add metadata to
BUILT_IN_TOOLS_METADATAarray - Add implementation to
tools/built-in/implementations.ts - Wire up in
getBuiltInRuntimeTools()
Add a tRPC Router
- Create router in
apps/web/src/lib/trpc/routers/ - Add to
apps/web/src/lib/trpc/routers/index.ts - Router is automatically available at
/api/trpc
Add a Database Table
- Add table definition in
packages/db/src/schema.ts - Add relations if needed
- Export from
packages/db/src/index.ts - Run
pnpm db:push(dev) orpnpm db:generate && pnpm db:migrate(prod)
Add an Internal BaleyBot
- Add spec to
internal-bb/source/specs.json - Add contract to
internal-bb/source/contracts.json - Add any domain skill policies to
internal-bb/skills/domain/ - Regenerate definitions: the generated-definitions are derived from specs+contracts
- Wire up caller code using
executeInternalBaleybot()
11. Testing
Commands
pnpm test # Run all tests (Vitest, 900+ tests)
pnpm test:watch # Watch mode
pnpm type-check # TypeScript checking across all packages
pnpm lint # ESLint via next lint (web app)
pnpm build # Full build with ESLint enabled
Test Patterns
- Tests live alongside source files as
__tests__/*.test.ts - Use Vitest with
vi.mock()for module mocking - Internal BB tests mock
executeInternalBaleybotto avoid real AI calls - BAL parser tests use direct
tokenize()+parse()calls - Vitest aliases needed for deep SDK imports:
@baleybots/tools/dsl/lexerand@baleybots/tools/dsl/parserpoint todist/esm/baleybots-dsl-v2/{lexer,parser}.js
SDK Submodule
After git submodule update, you must rebuild the SDK:
cd packages/baleybots/typescript/packages/tools && pnpm build
The dist/ directory is gitignored in the SDK submodule.
12. Troubleshooting
BAL Parser Errors
- "Unexpected token LBRACKET": You used
["tool"]instead of{ "tool" }for tools - "Unknown property": You used an unsupported property (e.g.,
temperature,reasoning) - "Expected STRING": Missing quotes around property values
Output Schema Bugs
- Model skipping output fields: Check that
buildZodSchema()is NOT adding.optional()to fields - "incomplete response" from internal bot: Caller schema is too strict - add
.default()to non-critical fields - Output parse failure: Use
normalizeOutputCandidate()before.parse()- handles JSON strings and markdown fences
Streaming Issues
- Choppy text rendering: You're updating React state per token - use RAF batching + direct DOM
- Missing entity name on events: Check
__entityNametag added by executor'sonEventcallback - SSE disconnects: Implement exponential backoff reconnection
Build / Deploy
GITHUB_TOKENerrors on Vercel: UseGH_PACKAGES_TOKENin.npmrc, notGITHUB_TOKEN- Cron not running: Vercel Hobby plan only supports daily crons; per-minute requires Pro
- Type errors after submodule update: Rebuild SDK (
cd packages/baleybots/typescript/packages/tools && pnpm build)
Converted and distributed by TomeVault — claim your Tome and manage your conversions.