HOW-TO

Getting Started with FireFoundry: Your First Agent

January 13, 2026 12 min read

FireFoundry Team

Developer Relations

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:

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:

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:

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:

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:

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.

FireFoundry Team

Developer Relations

The FireFoundry Developer Relations team creates tutorials, guides, and resources to help developers build production AI agents. We work closely with beta partners to understand real-world challenges and translate them into practical, actionable documentation.

Related Posts

Request Beta Access

FireFoundry is now in private beta. Join the teams already building production AI agents on the Agent-as-a-Service platform.