Skip to main content

Build A Task Tracker

This example creates a small task tracker using the EAI CLI, EAI App Template, Object Types, and ResourceAPI hooks.

1. Scaffold

eai init task-tracker
cd task-tracker
npm install

2. Connect A Tenant

eai login
eai tenant list --format json
eai tenant select <tenant-slug>
eai whoami

3. Configure The Tenant

Create src/eai.config/tenants/tracker.config.ts:

import { defineConfig } from "@enterpriseaigroup/core/config/server";

export const trackerConfig = defineConfig({
tenantId: "task-tracker",
workflowId: "task-workflow",
defaultEmail: "support@example.com",
meta: {
title: "Task Tracker",
description: "AI-powered task management",
},
store: {
user: { initialState: { name: null }, persist: true },
tasks: {
initialState: { items: [], isLoading: false, selectedId: null },
persist: true,
},
},
layout: {
header: {
components: [{ component: "Header", priority: 1 }],
},
middlePane: {
components: [
{
component: "Dashboard",
priority: 1,
storeBindings: [
{ prop: "tasks", storePath: "tasks.items" },
{ prop: "isLoading", storePath: "tasks.isLoading" },
],
},
],
},
},
});

Register it in src/eai.config/index.ts:

import templateConfig from "./default";
import { trackerConfig } from "./tenants/tracker.config";

export const tenantConfigs = {
default: trackerConfig,
template: templateConfig,
"task-tracker": trackerConfig,
};

4. Define The Object Type

Add a Task object type in src/eai.config/object-types.ts:

export const objectTypes = {
"task-tracker": [
{
name: "Task",
slug: "task",
displayName: "Task",
description: "A trackable unit of work",
storageBackend: "postgresql",
status: "published",
properties: [
{ name: "title", type: "text", required: true, indexed: true },
{ name: "description", type: "text", required: false },
{
name: "status",
type: "select",
required: true,
defaultValue: "draft",
options: [
{ label: "Draft", value: "draft" },
{ label: "In Progress", value: "in-progress" },
{ label: "Done", value: "done" },
],
},
{
name: "priority",
type: "select",
required: true,
defaultValue: "medium",
options: [
{ label: "Low", value: "low" },
{ label: "Medium", value: "medium" },
{ label: "High", value: "high" },
],
},
{ name: "dueDate", type: "date", required: false },
],
linkTypes: [],
actions: [
{
name: "start",
displayName: "Start Work",
requiredRole: "tenant-viewer",
validationRules: { requiredStatus: "draft" },
sideEffects: [
{ type: "set_field", field: "status", value: "in-progress" },
{ type: "set_timestamp", field: "startedAt" },
],
},
{
name: "complete",
displayName: "Mark Done",
requiredRole: "tenant-viewer",
validationRules: { requiredStatus: "in-progress" },
sideEffects: [
{ type: "set_field", field: "status", value: "done" },
{ type: "set_timestamp", field: "completedAt" },
],
},
],
},
],
};

5. Publish And Verify

eai types validate
eai types diff --tenant-key task-tracker --tenant-id <tenant-id>
eai types seed --tenant-key task-tracker --tenant-id <tenant-id> --format json
eai resources schema --tenant-id <tenant-id> --format json
eai verify calls --tenant-id <tenant-id> --resource-type task

Do not continue until the diff is clean.

Task is the model name used in configuration and TypeScript. task is the exact stored slug used by resource commands, routes, relationship targets, and runtime requests. Do not derive a different slug at the call site.

6. Use The Resource Hook

"use client";

import { useEffect, useState } from "react";
import { useResources } from "@/hooks/useResources";

interface TaskData {
title: string;
description?: string;
status: string;
priority: string;
dueDate?: string;
}

export function TaskList() {
const { list, create, update } = useResources<TaskData>("Task");
const [tasks, setTasks] = useState<Array<{ id: string; data: TaskData }>>([]);

useEffect(() => {
list({ page: 1, limit: 50, sort: "-created_at" }).then((res) => {
setTasks(res.docs);
});
}, [list]);

async function addTask(title: string) {
await create({ title, status: "draft", priority: "medium" });
const refreshed = await list({ page: 1, limit: 50, sort: "-created_at" });
setTasks(refreshed.docs);
}

async function startTask(task: { id: string; version: number }) {
await update(task.id, { status: "in-progress" }, task.version);
}

return (
<section>
<button type="button" onClick={() => addTask("New task")}>
Add task
</button>
{tasks.map((task) => (
<article key={task.id}>{task.data.title}</article>
))}
</section>
);
}