Getting Started
Let's build your first AI agent with Helix Agents. We'll create a simple research assistant that can search the web and summarize findings.
Prerequisites
- Node.js 22+ and npm 11+
- An OpenAI API key (or another LLM provider)
Installation
Install the SDK (which bundles core, memory stores, and JS runtime) plus the LLM adapter:
npm install @helix-agents/sdk @helix-agents/llm-vercel @ai-sdk/openai zodUnderstanding What We're Building
Before writing code, let's understand the pieces:
- Output Schema - What the agent produces (a research summary)
- Tools - Actions the agent can take (searching, taking notes)
- Agent Definition - Configuration combining everything
- Executor - The runtime that executes the agent
- Execution - Starting the agent and streaming results
Step 1: Define the Output Schema
Our research assistant will produce a structured summary. Define this with Zod:
import { z } from 'zod';
// What the agent will return when finished
const ResearchOutputSchema = z.object({
topic: z.string().describe('The topic that was researched'),
summary: z.string().describe('A comprehensive summary of findings'),
keyPoints: z.array(z.string()).describe('Key takeaways'),
sources: z.array(z.string()).describe('URLs of sources used'),
});
type ResearchOutput = z.infer<typeof ResearchOutputSchema>;The outputSchema tells the framework to inject a special __finish__ tool. When the LLM calls this tool with valid data, the agent completes.
Step 2: Define a Tool
Tools give agents capabilities. Let's create a search tool:
import { defineTool } from '@helix-agents/sdk';
const searchTool = defineTool({
name: 'search',
description: 'Search the web for information on a topic',
// What the LLM provides when calling this tool
inputSchema: z.object({
query: z.string().describe('The search query'),
}),
// What the tool returns
outputSchema: z.object({
results: z.array(
z.object({
title: z.string(),
url: z.string(),
snippet: z.string(),
})
),
}),
// The actual implementation
execute: async ({ query }, context) => {
// In a real app, you'd call a search API
// For now, we'll return mock results
console.log(`Searching for: ${query}`);
return {
results: [
{
title: `${query} - Wikipedia`,
url: `https://en.wikipedia.org/wiki/${encodeURIComponent(query)}`,
snippet: `Learn about ${query} and its various aspects...`,
},
{
title: `Understanding ${query}`,
url: `https://example.com/${encodeURIComponent(query)}`,
snippet: `A comprehensive guide to ${query}...`,
},
],
};
},
});The context parameter provides access to state, custom events, and more. We'll explore this in the Tools guide.
Step 3: Define the Agent
Now combine everything into an agent definition:
import { defineAgent } from '@helix-agents/sdk';
import { openai } from '@ai-sdk/openai';
const ResearchAgent = defineAgent({
// Unique identifier for this agent type
name: 'research-assistant',
// Instructions for the LLM
systemPrompt: `You are a helpful research assistant. When given a topic:
1. Search for relevant information using the search tool
2. Analyze the results carefully
3. Provide a comprehensive summary with key points
4. Always cite your sources
Be thorough but concise. Focus on factual, well-sourced information.`,
// Tools the agent can use
tools: [searchTool],
// The structured output schema
outputSchema: ResearchOutputSchema,
// LLM configuration
llmConfig: {
model: openai('gpt-4o-mini'),
temperature: 0.7,
},
// Safety limit: maximum LLM calls before stopping
maxSteps: 10,
});Agent Definition is Just Data
defineAgent() doesn't execute anything - it returns a configuration object. This is intentional: you can define agents without any runtime, then execute them with different runtimes in different environments.
Step 4: Create the Executor
The executor runs agents. For development, use the JS runtime with in-memory stores:
import { JSAgentExecutor, InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/sdk';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
// State store: where agent state persists between steps
const stateStore = new InMemoryStateStore();
// Stream manager: handles real-time event streaming
const streamManager = new InMemoryStreamManager();
// LLM adapter: bridges to the Vercel AI SDK
const llmAdapter = new VercelAIAdapter();
// Create the executor
const executor = new JSAgentExecutor(stateStore, streamManager, llmAdapter);Swappable Components
In production, you'd swap InMemoryStateStore for RedisStateStore and potentially use a different runtime like Temporal. The agent definition stays the same.
Step 5: Execute and Stream Results
Now let's run the agent:
async function main() {
// Make sure you have OPENAI_API_KEY set
if (!process.env.OPENAI_API_KEY) {
console.error('Please set OPENAI_API_KEY environment variable');
process.exit(1);
}
console.log('Starting research on AI agents...\n');
// Start execution
const handle = await executor.execute(
ResearchAgent,
'Research the benefits and challenges of AI agents in software development'
);
// Get the stream
const stream = await handle.stream();
if (stream) {
// Process streaming events
for await (const chunk of stream) {
switch (chunk.type) {
case 'text_delta':
// Incremental text from the LLM
process.stdout.write(chunk.delta);
break;
case 'tool_start':
// Tool is about to execute
console.log(`\n[Tool: ${chunk.toolName}]`);
break;
case 'tool_end':
// Tool finished
console.log(`[Result: ${JSON.stringify(chunk.result).slice(0, 100)}...]`);
break;
case 'error':
// Something went wrong
console.error(`\n[Error: ${chunk.error}]`);
break;
}
}
}
// Get the final result
const result = await handle.result();
console.log('\n\n=== Research Complete ===');
console.log('Status:', result.status);
if (result.output) {
console.log('\nTopic:', result.output.topic);
console.log('\nSummary:', result.output.summary);
console.log('\nKey Points:');
result.output.keyPoints.forEach((point, i) => {
console.log(` ${i + 1}. ${point}`);
});
console.log('\nSources:', result.output.sources.join(', '));
}
}
main().catch(console.error);Complete Example
Here's the full code in one file:
// research-agent.ts
import { defineAgent, defineTool } from '@helix-agents/sdk';
import { JSAgentExecutor, InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/sdk';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// Output schema
const ResearchOutputSchema = z.object({
topic: z.string(),
summary: z.string(),
keyPoints: z.array(z.string()),
sources: z.array(z.string()),
});
// Search tool
const searchTool = defineTool({
name: 'search',
description: 'Search the web for information',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.object({
results: z.array(
z.object({
title: z.string(),
url: z.string(),
snippet: z.string(),
})
),
}),
execute: async ({ query }) => ({
results: [
{
title: `${query} - Wikipedia`,
url: `https://wikipedia.org`,
snippet: `Info about ${query}`,
},
],
}),
});
// Agent definition
const ResearchAgent = defineAgent({
name: 'research-assistant',
systemPrompt: 'You are a research assistant. Search for info and provide summaries.',
tools: [searchTool],
outputSchema: ResearchOutputSchema,
llmConfig: { model: openai('gpt-4o-mini') },
maxSteps: 10,
});
// Execute
async function main() {
const executor = new JSAgentExecutor(
new InMemoryStateStore(),
new InMemoryStreamManager(),
new VercelAIAdapter()
);
const handle = await executor.execute(ResearchAgent, 'Benefits of TypeScript');
for await (const chunk of (await handle.stream()) ?? []) {
if (chunk.type === 'text_delta') process.stdout.write(chunk.delta);
}
const result = await handle.result();
console.log('\n\nOutput:', JSON.stringify(result.output, null, 2));
}
main();Run it:
OPENAI_API_KEY=sk-your-key npx tsx research-agent.tsUnderstanding What Happened
Let's trace through the execution:
Initialization: The executor created a new agent run with a unique
runIdandstreamIdFirst LLM Call: The agent's system prompt plus the user message were sent to GPT-4o-mini
Tool Call: The LLM decided to use the search tool, returning
{"name": "search", "arguments": {"query": "..."}}Tool Execution: The search tool ran, and its result was added to the conversation
Second LLM Call: The LLM saw the search results and decided what to do next
Finish: Eventually, the LLM called the
__finish__tool with structured output matching our schemaResult: The agent completed with status
completedand typed output
Next Steps
You now have a working agent! Here's where to go next:
- Defining Agents - All configuration options in depth
- Defining Tools - Tool context, state mutation, custom events
- State Management - Custom state schemas and patterns
- Streaming - All event types and filtering
- Sub-Agents - Multi-agent orchestration
- Runtimes - Choosing JS, Temporal, or Cloudflare