Build A Task Tracker
This example creates a small task tracker using the EAI CLI, EAI App Template, Object Types, and ResourceAPI hooks.
The template keeps tenantId, tenants, and tenantConfigs as code contract
names. Their values and configurations represent EAI workspaces.
1. Scaffold
eai init task-tracker
cd task-tracker
npm install
2. Connect A Workspace
eai login
eai workspace list --format json
eai workspace select <workspace-slug>
eai whoami
3. Configure The Workspace
Create src/eai.config/tenants/tracker.config.ts:
import { defineConfig } from "@enterpriseaigroup/core/config/server";
export const trackerConfig = defineConfig({
tenantId: "task-tracker", // Stable field name; this value identifies the EAI workspace.
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 <workspace-id>
eai types seed --tenant-key task-tracker --tenant-id <workspace-id> --format json
eai resources schema --tenant-id <workspace-id> --format json
eai verify calls --tenant-id <workspace-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>
);
}