Skip to main content

Overview

This guide shows you how to build an AI-powered task board where users can create, update, and organize tasks through natural language. Tasks persist across sessions and update dynamically as the AI modifies them.

Architecture

A task board with Tambo uses interactable components:
  1. Interactable Task Components - Persistent tasks that the AI can modify
  2. Task State Management - Track task status, priority, and metadata
  3. Board Layout - Organize tasks by status (todo, in-progress, done)
  4. Chat Interface - Natural language commands for task management
  5. Context Helpers - Current board state for AI awareness

Interactable Components

Interactable components persist and update as users refine their requests:
import { withInteractable } from "@tambo-ai/react";
import { z } from "zod";

// Base Task Component
function Task({ title, description, status, priority }: {
  title: string;
  description: string;
  status: "todo" | "in-progress" | "done";
  priority: "low" | "medium" | "high";
}) {
  const priorityColors = {
    low: "bg-blue-100 text-blue-800",
    medium: "bg-yellow-100 text-yellow-800",
    high: "bg-red-100 text-red-800",
  };

  return (
    <div className="border rounded-lg p-4 bg-card">
      <div className="flex items-start justify-between mb-2">
        <h3 className="font-semibold">{title}</h3>
        <span className={`text-xs px-2 py-1 rounded ${priorityColors[priority]}`}>
          {priority}
        </span>
      </div>
      <p className="text-sm text-muted-foreground">{description}</p>
    </div>
  );
}

// Interactable Task
const InteractableTask = withInteractable(Task, {
  componentName: "Task",
  description: `A task item with title, description, status, and priority.
    Use for creating, updating, and organizing tasks on a task board.
    The AI can modify any field to update the task.`,
  propsSchema: z.object({
    title: z.string().describe("Task title"),
    description: z.string().describe("Detailed task description"),
    status: z.enum(["todo", "in-progress", "done"]).describe("Current task status"),
    priority: z.enum(["low", "medium", "high"]).describe("Task priority level"),
  }),
});

export { InteractableTask };

Task Board Layout

Organize tasks into columns by status:
import { useState } from "react";
import { InteractableTask } from "./task";

type TaskData = {
  id: string;
  title: string;
  description: string;
  status: "todo" | "in-progress" | "done";
  priority: "low" | "medium" | "high";
};

function TaskBoard() {
  const [tasks, setTasks] = useState<TaskData[]>([
    {
      id: "task-1",
      title: "Design landing page",
      description: "Create mockups for the new landing page",
      status: "in-progress",
      priority: "high",
    },
    {
      id: "task-2",
      title: "Write documentation",
      description: "Document the new API endpoints",
      status: "todo",
      priority: "medium",
    },
  ]);

  const columns = [
    { id: "todo", title: "To Do" },
    { id: "in-progress", title: "In Progress" },
    { id: "done", title: "Done" },
  ];

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6">
      {columns.map((column) => (
        <div key={column.id} className="space-y-4">
          <div className="border-b pb-2">
            <h2 className="font-semibold text-lg">{column.title}</h2>
            <span className="text-sm text-muted-foreground">
              {tasks.filter((t) => t.status === column.id).length} tasks
            </span>
          </div>
          <div className="space-y-3">
            {tasks
              .filter((task) => task.status === column.id)
              .map((task) => (
                <InteractableTask
                  key={task.id}
                  id={task.id}
                  {...task}
                />
              ))}
          </div>
        </div>
      ))}
    </div>
  );
}

export default TaskBoard;

Adding AI Chat Interface

Combine the task board with a chat interface:
import { TamboProvider, useTambo } from "@tambo-ai/react";
import { MessageThreadPanel } from "@tambo-ai/ui-registry/components/message-thread-panel";
import { InteractableTask } from "./task";
import { useEffect } from "react";

function TaskBoardWithChat() {
  const { registerComponent, currentThreadId } = useTambo();

  useEffect(() => {
    registerComponent({
      name: "Task",
      description: `Task component for managing work items.
        Create new tasks or update existing ones by modifying title, description, status, or priority.
        Status options: todo, in-progress, done
        Priority options: low, medium, high`,
      component: InteractableTask,
      propsSchema: InteractableTask.propsSchema,
    });
  }, [registerComponent, currentThreadId]);

  return (
    <div className="h-screen grid grid-cols-1 lg:grid-cols-3 gap-6 p-6">
      {/* Task Board */}
      <div className="lg:col-span-2">
        <TaskBoard />
      </div>

      {/* AI Chat */}
      <div className="lg:col-span-1">
        <MessageThreadPanel className="h-full" />
      </div>
    </div>
  );
}

export default function App() {
  return (
    <TamboProvider
      apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
      userKey="user-123"
    >
      <TaskBoardWithChat />
    </TamboProvider>
  );
}

Task State Management

Track all tasks and provide context to the AI:
import { useState, useEffect } from "react";
import { TamboProvider } from "@tambo-ai/react";

function TaskBoardApp() {
  const [tasks, setTasks] = useState<TaskData[]>([]);

  // Load tasks from storage
  useEffect(() => {
    const savedTasks = localStorage.getItem("tasks");
    if (savedTasks) {
      setTasks(JSON.parse(savedTasks));
    }
  }, []);

  // Save tasks to storage
  useEffect(() => {
    localStorage.setItem("tasks", JSON.stringify(tasks));
  }, [tasks]);

  return (
    <TamboProvider
      apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
      userKey="user-123"
      contextHelpers={{
        taskSummary: () => ({
          key: "tasks",
          value: `Total tasks: ${tasks.length}. 
            Todo: ${tasks.filter((t) => t.status === "todo").length}, 
            In Progress: ${tasks.filter((t) => t.status === "in-progress").length}, 
            Done: ${tasks.filter((t) => t.status === "done").length}`,
        }),
        highPriorityTasks: () => ({
          key: "highPriority",
          value: tasks
            .filter((t) => t.priority === "high" && t.status !== "done")
            .map((t) => t.title)
            .join(", "),
        }),
      }}
    >
      <TaskBoardWithChat />
    </TamboProvider>
  );
}

Advanced Features

import { z } from "zod";
import { withInteractable } from "@tambo-ai/react";

function TaskWithDueDate({ title, description, status, priority, dueDate }: {
  title: string;
  description: string;
  status: "todo" | "in-progress" | "done";
  priority: "low" | "medium" | "high";
  dueDate?: string;
}) {
  const isOverdue = dueDate && new Date(dueDate) < new Date() && status !== "done";

  return (
    <div className="border rounded-lg p-4 bg-card">
      <h3 className="font-semibold">{title}</h3>
      <p className="text-sm text-muted-foreground mb-2">{description}</p>
      {dueDate && (
        <p className={`text-xs ${isOverdue ? "text-red-600" : "text-muted-foreground"}`}>
          Due: {new Date(dueDate).toLocaleDateString()}
        </p>
      )}
    </div>
  );
}

const InteractableTaskWithDueDate = withInteractable(TaskWithDueDate, {
  componentName: "Task",
  description: "Task with due date for deadline tracking",
  propsSchema: z.object({
    title: z.string(),
    description: z.string(),
    status: z.enum(["todo", "in-progress", "done"]),
    priority: z.enum(["low", "medium", "high"]),
    dueDate: z.string().optional().describe("Due date in YYYY-MM-DD format"),
  }),
});

Example Interactions

Here are natural language commands users can use:
  • “Create a task to review the PR”
  • “Add a high priority task for the client meeting”
  • “Make a task called ‘Fix login bug’ with due date next Friday”
  • “Move ‘Design landing page’ to done”
  • “Change the documentation task to high priority”
  • “Assign the API task to Sarah”
  • “Update the client meeting task with a reminder”
  • “Show me all high priority tasks”
  • “Move overdue tasks to the top”
  • “Group tasks by assignee”
  • “Archive completed tasks from last month”
  • “Mark all design tasks as in progress”
  • “Change all low priority tasks to medium”
  • “Move all of John’s tasks to done”

Complete Task Board Example

import {
  TamboProvider,
  useTambo,
  withInteractable,
} from "@tambo-ai/react";
import { MessageThreadPanel } from "@tambo-ai/ui-registry/components/message-thread-panel";
import { useState, useEffect } from "react";
import { z } from "zod";

// Task Component
function Task({ title, description, status, priority, dueDate, assignee }: {
  title: string;
  description: string;
  status: "todo" | "in-progress" | "done";
  priority: "low" | "medium" | "high";
  dueDate?: string;
  assignee?: string;
}) {
  const priorityColors = {
    low: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
    medium: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
    high: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
  };

  const isOverdue = dueDate && new Date(dueDate) < new Date() && status !== "done";

  return (
    <div className="border rounded-lg p-4 bg-card space-y-2">
      <div className="flex items-start justify-between">
        <h3 className="font-semibold">{title}</h3>
        <span className={`text-xs px-2 py-1 rounded ${priorityColors[priority]}`}>
          {priority}
        </span>
      </div>
      <p className="text-sm text-muted-foreground">{description}</p>
      <div className="flex items-center gap-4 text-xs text-muted-foreground">
        {dueDate && (
          <span className={isOverdue ? "text-red-600" : ""}>
            Due: {new Date(dueDate).toLocaleDateString()}
          </span>
        )}
        {assignee && <span>Assigned to: {assignee}</span>}
      </div>
    </div>
  );
}

const InteractableTask = withInteractable(Task, {
  componentName: "Task",
  description: `Task management component with full CRUD capabilities.
    Create, update, or modify tasks with title, description, status, priority, due dates, and assignees.`,
  propsSchema: z.object({
    title: z.string(),
    description: z.string(),
    status: z.enum(["todo", "in-progress", "done"]),
    priority: z.enum(["low", "medium", "high"]),
    dueDate: z.string().optional(),
    assignee: z.string().optional(),
  }),
});

function TaskBoardApp() {
  const { registerComponent, currentThreadId } = useTambo();
  const [tasks, setTasks] = useState<any[]>([]);

  useEffect(() => {
    registerComponent({
      name: "Task",
      description: InteractableTask.description,
      component: InteractableTask,
      propsSchema: InteractableTask.propsSchema,
    });
  }, [registerComponent, currentThreadId]);

  const columns = [
    { id: "todo", title: "To Do" },
    { id: "in-progress", title: "In Progress" },
    { id: "done", title: "Done" },
  ];

  return (
    <div className="h-screen flex flex-col">
      <header className="border-b p-4">
        <h1 className="text-2xl font-semibold">Task Board</h1>
      </header>

      <main className="flex-1 overflow-hidden">
        <div className="h-full grid grid-cols-1 lg:grid-cols-4 gap-6 p-6">
          {/* Task Board */}
          <div className="lg:col-span-3 grid grid-cols-1 md:grid-cols-3 gap-6 overflow-y-auto">
            {columns.map((column) => (
              <div key={column.id} className="space-y-4">
                <div className="border-b pb-2">
                  <h2 className="font-semibold text-lg">{column.title}</h2>
                </div>
                <div className="space-y-3">
                  {/* Tasks will render here */}
                </div>
              </div>
            ))}
          </div>

          {/* AI Chat */}
          <div className="lg:col-span-1">
            <MessageThreadPanel className="h-full" />
          </div>
        </div>
      </main>
    </div>
  );
}

export default function App() {
  return (
    <TamboProvider
      apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
      userKey="user-123"
    >
      <TaskBoardApp />
    </TamboProvider>
  );
}

Best Practices

  • Use interactable components for persistent UI elements
  • Provide clear prop schemas with descriptions
  • Keep component logic simple and focused
  • Handle all possible states (loading, empty, error)
  • Store task state in localStorage or database
  • Sync state changes across components
  • Use context helpers to inform AI of current state
  • Implement optimistic updates for better UX
  • Write detailed component descriptions
  • Provide context about board state
  • Support natural language commands
  • Handle ambiguous requests gracefully
  • Show visual feedback for state changes
  • Support keyboard shortcuts
  • Implement drag-and-drop for manual reordering
  • Add undo/redo functionality

Next Steps

Build docs developers (and LLMs) love