This tutorial walks you through building and deploying your first AI agent with FireFoundry. By the end, you will have a working agent that accepts tasks via a REST API, uses an LLM to analyze them, persists results to the entity graph, and runs in a deployed environment. The whole process takes under 30 minutes.
We will build a Task Analyzer -- a simple agent that receives task descriptions, uses AI to generate structured analysis and summaries, and stores everything as persistent entities. It is straightforward enough to follow as a tutorial, but it demonstrates the core patterns you will use in every FireFoundry project: entities for state, bots for AI logic, and API endpoints for external access.
Prerequisites
Before you begin, make sure you have the following installed and configured:
- Node.js 20+ -- FireFoundry's Agent SDK is built on Node.js. Verify your version with
node --version. - pnpm -- FireFoundry projects use pnpm workspaces for monorepo management. Install it globally with
npm install -g pnpm. - FireFoundry CLI (ff-cli) -- The command-line tool for scaffolding projects, managing deployments, and running local environments. Download the latest release from the developer portal and ensure
ff-cli --versionreturns a valid version. - A running FireFoundry environment -- Either a local development setup (minikube with FireFoundry Core Services) or access to a shared development cluster. If you do not have one yet, request beta access and we will help you get set up.
If you have all of those ready, let's build something.
Step 1: Scaffold Your Project
FireFoundry uses a monorepo structure powered by Turborepo and pnpm workspaces. The CLI handles all the scaffolding for you. Open your terminal and run:
ff-cli project create my-first-agent --agent-name task-analyzer
cd my-first-agent
pnpm install
This creates a complete project structure. Here is what you get:
my-first-agent/
├── apps/
│ └── task-analyzer/ # Your agent bundle
│ ├── src/
│ │ ├── index.ts # Server entry point
│ │ ├── agent-bundle.ts # Main agent bundle class
│ │ ├── constructors.ts # Entity registry
│ │ ├── entities/ # Domain entities (empty)
│ │ ├── bots/ # LLM orchestration (empty)
│ │ └── prompts/ # Prompt templates (empty)
│ ├── helm/ # Kubernetes deployment charts
│ ├── Dockerfile
│ ├── firefoundry.json # Agent bundle metadata
│ └── package.json
├── packages/ # Shared libraries
├── docker-compose.yml # Local dev environment
├── turbo.json # Build orchestration
├── pnpm-workspace.yaml # Workspace config
└── package.json # Root package
The key concept here is the agent bundle. An agent bundle is the deployable unit in FireFoundry -- it is an independent HTTP service that integrates with the platform's runtime services (LLM broker, entity service, context service) and defines custom entities, bots, and API endpoints for your domain. Everything inside apps/task-analyzer/ is your agent bundle.
Step 2: Define an Entity
Entities are the foundation of state management in FireFoundry. They are persistent business objects stored in the entity graph -- a structured data layer that your agents read from and write to. Unlike ephemeral conversation state, entities survive across sessions and can be queried, related to other entities, and shared between agents.
Let's create a TaskEntity that represents a task our agent will analyze. Create the file apps/task-analyzer/src/entities/TaskEntity.ts:
import {
RunnableEntity,
EntityMixin,
} from '@firebrandanalytics/ff-agent-sdk';
@EntityMixin({
specificType: 'TaskEntity',
generalType: 'TaskEntity',
allowedConnections: {},
})
export class TaskEntity extends RunnableEntity {
title: string;
description: string;
status: 'pending' | 'analyzing' | 'done' | 'error';
priority: 'low' | 'medium' | 'high' | 'critical';
summary: string;
estimatedHours: number;
tags: string[];
analyzedAt: string;
}
The @EntityMixin decorator registers this class with the FireFoundry runtime, defining its specificType and generalType for the entity graph. Extending RunnableEntity gives the class persistence, lifecycle management, and integration with the platform runtime. Properties declared on the class are persisted to the entity graph automatically.
Code examples are simplified for clarity. See the full SDK tutorials for production-ready patterns with complete type definitions.
Notice that some fields (title, description) will be provided by the user, while others (summary, priority, estimatedHours, tags) will be populated by our bot's AI analysis. This separation of input from AI-generated output is a common pattern in FireFoundry agents.
Now register the entity in your constructors file. Open apps/task-analyzer/src/constructors.ts and add:
import { FFConstructors } from '@firebrandanalytics/ff-agent-sdk';
import { TaskEntity } from './entities/TaskEntity.js';
export const TaskAnalyzerConstructors = {
...FFConstructors,
TaskEntity,
} as const;
Step 3: Create a Bot
Bots are where the AI logic lives. A bot wraps an LLM interaction with a system prompt, structured output handling, and integration with the entity graph. In FireFoundry, bots are separate from entities by design -- entities handle structure and state, bots handle behavior and intelligence.
Create apps/task-analyzer/src/bots/TaskAnalysisBot.ts:
import {
MixinBot,
StructuredOutputBotMixin,
RegisterBot,
ComposeMixins,
MixinBotConfig,
StructuredPromptGroup,
PromptGroup,
PromptInputText,
logger,
withSchemaMetadata,
} from '@firebrandanalytics/ff-agent-sdk';
import { z } from 'zod';
import { TaskEntity } from '../entities/TaskEntity.js';
const TaskAnalysisSchema = withSchemaMetadata(
z.object({
summary: z.string().describe('A concise 2-3 sentence summary of the task'),
priority: z.enum(['low', 'medium', 'high', 'critical'])
.describe('Priority level based on urgency and impact'),
estimatedHours: z.number()
.describe('Estimated hours to complete the task'),
tags: z.array(z.string())
.describe('Relevant tags for categorization (3-5 tags)'),
}),
'TaskAnalysisOutput',
'AI analysis of a task'
);
type TaskAnalysis = z.infer<typeof TaskAnalysisSchema>;
const botConfig: MixinBotConfig = {
name: 'TaskAnalysisBot',
base_prompt_group: 'task-analysis',
model_pool_name: 'default',
};
@RegisterBot('TaskAnalysisBot')
export class TaskAnalysisBot extends ComposeMixins(
MixinBot,
StructuredOutputBotMixin
) {
async analyzeTask(task: TaskEntity): Promise<void> {
logger.info(`Analyzing task: ${task.title}`);
task.status = 'analyzing';
await task.save();
try {
const prompts: StructuredPromptGroup = {
system: new PromptGroup([
new PromptInputText(`You are a task analysis assistant for a
software development team. Given a task title and description,
you provide a summary, priority, time estimate, and tags.
Be practical and specific.`),
]),
user: new PromptGroup([
new PromptInputText(
`Analyze this task:\n\nTitle: ${task.title}\nDescription: ${task.description}`
),
]),
};
const response = await this.runStructuredOutput<TaskAnalysis>(
prompts,
TaskAnalysisSchema
);
task.summary = response.summary;
task.priority = response.priority;
task.estimatedHours = response.estimatedHours;
task.tags = response.tags;
task.status = 'done';
task.analyzedAt = new Date().toISOString();
await task.save();
logger.info(`Task analyzed successfully: ${task.title} [${task.priority}]`);
} catch (error) {
logger.error(`Failed to analyze task: ${error}`);
task.status = 'error';
await task.save();
}
}
}
Let's break down what is happening here:
@RegisterBot('TaskAnalysisBot')registers this bot with the FireFoundry runtime, making it available for dependency injection and lifecycle management.ComposeMixins(MixinBot, StructuredOutputBotMixin)composes the base bot functionality with structured output support. TheStructuredOutputBotMixinprovides therunStructuredOutputmethod for schema-validated LLM responses.withSchemaMetadatawraps a Zod schema with a name and description for the FireFoundry runtime, enabling schema registry and validation features.runStructuredOutputsends prompts to the LLM through FireFoundry's Broker Service and parses the response according to the Zod schema. The Broker handles model routing, failover, and retry logic automatically -- your bot code never needs to worry about which provider is being used.task.save()persists the entity to the entity graph after each state change. This means you have a complete audit trail of the task's lifecycle, from creation through analysis to completion.
Step 4: Add an API Endpoint
Your agent needs a way to receive tasks from the outside world. FireFoundry agent bundles expose API endpoints through the @ApiEndpoint decorator on the main agent bundle class. Open apps/task-analyzer/src/agent-bundle.ts and replace its contents:
import {
FFAgentBundle,
app_provider,
ApiEndpoint,
logger,
} from '@firebrandanalytics/ff-agent-sdk';
import { TaskAnalyzerConstructors } from './constructors.js';
import { TaskEntity } from './entities/TaskEntity.js';
import { TaskAnalysisBot } from './bots/TaskAnalysisBot.js';
export class TaskAnalyzerBundle extends FFAgentBundle<any> {
private analysisBot: TaskAnalysisBot;
constructor() {
super(
{
id: 'task-analyzer-001',
name: 'TaskAnalyzer',
description: 'Analyzes tasks using AI and persists results',
},
TaskAnalyzerConstructors,
app_provider
);
}
override async init() {
await super.init();
this.analysisBot = new TaskAnalysisBot();
logger.info('TaskAnalyzer agent bundle initialized');
}
@ApiEndpoint({ method: 'POST', route: 'tasks' })
async createTask(req: any): Promise<any> {
const { title, description } = req.body;
if (!title || !description) {
return { error: 'Title and description are required', status: 400 };
}
const task = await TaskEntity.create({
title,
description,
status: 'pending',
});
// Run analysis asynchronously
this.analysisBot.analyzeTask(task).catch((err) => {
logger.error(`Background analysis failed: ${err}`);
});
return {
id: task.id,
title: task.title,
status: task.status,
message: 'Task created. Analysis in progress.',
};
}
@ApiEndpoint({ method: 'GET', route: 'tasks/:id' })
async getTask(req: any): Promise<any> {
const task = await TaskEntity.findById(req.params.id);
if (!task) {
return { error: 'Task not found', status: 404 };
}
return {
id: task.id,
title: task.title,
description: task.description,
status: task.status,
priority: task.priority,
summary: task.summary,
estimatedHours: task.estimatedHours,
tags: task.tags,
analyzedAt: task.analyzedAt,
};
}
@ApiEndpoint({ method: 'GET', route: 'tasks' })
async listTasks(): Promise<any> {
const tasks = await TaskEntity.findAll();
return {
count: tasks.length,
tasks: tasks.map((t: TaskEntity) => ({
id: t.id,
title: t.title,
status: t.status,
priority: t.priority,
})),
};
}
}
The @ApiEndpoint decorator exposes methods as REST endpoints through the agent bundle's HTTP server. The FireFoundry runtime handles routing, request parsing, and response serialization. Your POST /tasks endpoint creates a new entity and kicks off AI analysis in the background, so the API responds immediately while the bot does its work asynchronously.
Step 5: Run Locally
FireFoundry provides a local development experience through Docker Compose, which spins up the core runtime services your agent needs: the Entity Graph database, the LLM Broker proxy, and the agent bundle itself.
From the project root, start the development environment:
# Set required environment variables
export PG_SERVER="localhost"
export PG_DATABASE="firefoundry"
export PG_PASSWORD="localdev"
export LLM_BROKER_HOST="localhost"
export LLM_BROKER_PORT="8080"
# Start all services
pnpm run dev
Your agent bundle will start on port 3000. You can verify it is running with a health check:
curl http://localhost:3000/health
Now let's create a task and see the agent analyze it:
# Create a task
curl -X POST http://localhost:3000/invoke/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "Migrate user auth to OAuth 2.0",
"description": "Replace our custom JWT-based authentication with a standard OAuth 2.0 flow using Auth0 as the identity provider. Need to update login, token refresh, and session management. Must maintain backward compatibility with existing API keys for 90 days."
}'
You will get an immediate response with the task ID and a status of pending. The AI analysis runs asynchronously. Wait a few seconds, then fetch the task to see the results:
# Retrieve the analyzed task (replace TASK_ID with the actual ID)
curl http://localhost:3000/invoke/tasks/TASK_ID
You should see the full analysis -- a summary, priority level, estimated hours, and relevant tags -- all generated by the LLM and persisted to the entity graph. Because the analysis is stored as entity properties, it is queryable, auditable, and available to other agents or services in your system.
Step 6: Deploy
Once your agent is working locally, deploying to a FireFoundry cluster is a single command. The CLI handles building the Docker container, pushing it to the registry, and deploying via the Helm chart that was scaffolded with your project.
# Build and deploy in one step
ff-cli ops deploy task-analyzer -y --namespace ff-dev
Here is what happens behind the scenes:
- Build: The CLI runs
docker buildusing the Dockerfile in your agent bundle, producing a production-optimized container image. - Push: The image is pushed to the container registry configured in your active ff-cli profile (Harbor for local/minikube, or ACR/ECR for cloud environments).
- Deploy: The CLI runs
helm upgrade --installusing the Helm chart inhelm/, which creates or updates the Kubernetes deployment, service, and any associated resources.
After deployment, your agent is running in the cluster with access to the full FireFoundry runtime: the Broker Service for LLM routing with automatic failover, the Entity Service for persistent state, the Context Service for conversation memory, and the Management Console for monitoring and observability.
You can verify the deployment:
# Check pod status
kubectl get pods -n ff-dev -l app=task-analyzer
# View logs
kubectl logs -n ff-dev -l app=task-analyzer --tail=50
What You Built
Let's step back and appreciate what you have accomplished in just a few steps. You built an AI agent that:
- Accepts structured input through a REST API
- Persists state in the entity graph as first-class business objects
- Uses AI reasoning through a bot with structured output validation
- Handles errors gracefully with status tracking and logging
- Runs in a production environment with Kubernetes deployment, health checks, and access to the full FireFoundry runtime
This is not a prototype. This is the same architecture used by production agents handling real workloads. The patterns you learned here -- entities for state, bots for intelligence, API endpoints for access -- scale from this simple tutorial to complex multi-agent systems with dozens of entity types, multiple bots, and sophisticated workflow orchestration.
What's Next
You have the foundation. Here is where to go from here:
- Entity Graph Deep Dive -- Learn about entity relationships, collections, and runnable entities that connect entity state to bot behavior. Explore how to model complex domains with interconnected business objects.
- Advanced Bot Patterns -- Explore multi-step reasoning, tool use, and structured prompts with Zod schemas. Build bots that chain multiple LLM calls and orchestrate complex workflows.
- Tutorials -- Work through the News Article Impact Analyzer (structured analysis across multiple verticals), the Document-to-Report Pipeline (document ingestion, processing, and report generation), and the Illustrated Story Generator (multi-modal agent combining text and image generation).
- Management Console -- Monitor your deployed agents in real time. View request traces, search logs with natural language queries, and track performance metrics across your agent fleet.
- Marketplace -- Explore pre-built applications like FireIQ (intelligent Q&A) and the AI Training Portal that you can deploy and customize.
Visit the developer documentation for comprehensive SDK reference, architecture guides, and more tutorials. And if you have questions, reach out to us at firefoundry@firebrand.ai -- we are building this platform alongside our beta partners and your feedback shapes the product.
Happy building.