Skip to main content

AI-Assisted Workflow Patterns

The Vertical Template is designed to work seamlessly with AI-assisted development workflows. This guide covers proven patterns for leveraging AI tools to accelerate your development process while maintaining code quality and platform best practices.

Overview

AI assistants can significantly accelerate VT development by:

  • Generating boilerplate configurations from requirements
  • Creating type-safe React components
  • Designing data models with validation rules
  • Writing integration tests
  • Assisting with migrations and refactoring

Each pattern below includes when to use it, example prompts, and expected outputs.


Pattern 1: Spec-to-Config

Description

Use AI to transform product requirements or user stories into complete tenant configuration objects. This pattern is particularly useful when starting a new vertical or adding major features.

When to Use

  • Starting a new vertical application
  • Adding a new section or feature area
  • Converting wireframes or mockups into working config
  • Rapid prototyping with stakeholders

Example Prompt

Create a tenant config for a property management vertical with the following features:
- Dashboard showing property overview (occupancy rate, maintenance requests, rent collection)
- Property listing page with search and filters
- Tenant management with contact info and lease terms
- Maintenance request tracking with status workflow
- Document upload for leases and inspection reports

Use the VT tenant config format with sections, components, and Object Type references.

Expected Output

import { TenantConfig } from '@/config/types';

export const propertyManagementConfig: TenantConfig = {
id: 'property-mgmt',
name: 'Property Management Portal',
theme: {
primaryColor: '#2563eb',
logo: '/logos/property-mgmt.svg',
},
objectTypes: [
{
name: 'Property',
apiName: 'property',
fields: [
{ name: 'address', type: 'text', required: true },
{ name: 'units', type: 'number', required: true },
{ name: 'occupancyRate', type: 'number', computed: true },
{ name: 'monthlyRent', type: 'currency', required: true },
],
actions: ['view', 'edit', 'delete'],
},
{
name: 'MaintenanceRequest',
apiName: 'maintenance_request',
fields: [
{ name: 'property', type: 'reference', ref: 'property', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'status', type: 'select', options: ['open', 'in_progress', 'completed'], default: 'open' },
{ name: 'priority', type: 'select', options: ['low', 'medium', 'high'], default: 'medium' },
{ name: 'assignedTo', type: 'user', required: false },
],
},
],
sections: [
{
id: 'dashboard',
title: 'Dashboard',
path: '/',
icon: 'home',
components: [
{ type: 'metric-grid', objectType: 'property', metrics: ['occupancyRate', 'totalRent'] },
{ type: 'recent-list', objectType: 'maintenance_request', limit: 5 },
],
},
{
id: 'properties',
title: 'Properties',
path: '/properties',
icon: 'building',
components: [
{ type: 'search-bar', fields: ['address'] },
{ type: 'resource-list', objectType: 'property', columns: ['address', 'units', 'occupancyRate'] },
],
},
],
};

Tips

  • Start with high-level requirements before adding detailed validation rules
  • Ask AI to explain the config structure if unclear
  • Iterate on the output by requesting specific adjustments
  • Validate generated configs against your tenant config schema

Pattern 2: Component Generation

Description

Generate React components that integrate with VT store patterns, hooks, and the platform SDK. AI can create custom components with proper TypeScript typing and store bindings.

When to Use

  • Building custom UI components not provided by VT
  • Creating domain-specific visualizations
  • Implementing complex forms with validation
  • Building composite components from primitives

Example Prompt

Create a React component for displaying a property occupancy dashboard.

Requirements:
- Fetch properties using useResources<Property>
- Display total properties, occupied units, and occupancy percentage
- Show a bar chart of occupancy by property
- Use VT store patterns for any local state
- Include loading and error states
- TypeScript with proper types

Component name: PropertyOccupancyDashboard

Expected Output

import React from 'react';
import { useResources } from '@eai/platform-sdk/react';
import { useStoreValue } from '@/lib/store';
import { Card } from '@/components/ui/Card';
import { BarChart } from '@/components/ui/BarChart';

interface Property {
id: string;
address: string;
units: number;
occupiedUnits: number;
}

export const PropertyOccupancyDashboard: React.FC = () => {
const { resources: properties, isLoading, error } = useResources<Property>({
objectType: 'property',
filters: {},
});

const theme = useStoreValue('app.theme');

if (isLoading) {
return <div className="p-4">Loading properties...</div>;
}

if (error) {
return <div className="p-4 text-red-600">Error: {error.message}</div>;
}

const totalProperties = properties.length;
const totalUnits = properties.reduce((sum, p) => sum + p.units, 0);
const totalOccupied = properties.reduce((sum, p) => sum + p.occupiedUnits, 0);
const occupancyRate = totalUnits > 0 ? (totalOccupied / totalUnits) * 100 : 0;

const chartData = properties.map(p => ({
label: p.address,
value: p.units > 0 ? (p.occupiedUnits / p.units) * 100 : 0,
}));

return (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<Card>
<div className="text-sm text-gray-600">Total Properties</div>
<div className="text-3xl font-bold">{totalProperties}</div>
</Card>
<Card>
<div className="text-sm text-gray-600">Occupied Units</div>
<div className="text-3xl font-bold">{totalOccupied} / {totalUnits}</div>
</Card>
<Card>
<div className="text-sm text-gray-600">Occupancy Rate</div>
<div className="text-3xl font-bold">{occupancyRate.toFixed(1)}%</div>
</Card>
</div>

<Card>
<h3 className="text-lg font-semibold mb-4">Occupancy by Property</h3>
<BarChart
data={chartData}
height={300}
color={theme.primaryColor}
/>
</Card>
</div>
);
};

Tips

  • Specify the exact VT hooks and patterns to use
  • Request TypeScript interfaces for all data structures
  • Ask for loading/error states explicitly
  • Include accessibility requirements in your prompt
  • Request responsive design considerations

Pattern 3: Object Type Design

Description

Use AI to design Object Types (data models) with fields, validation rules, relationships, and actions. This pattern helps ensure your data model is well-structured and follows platform conventions.

When to Use

  • Designing a new domain model
  • Adding validation rules to existing types
  • Creating relationships between Object Types
  • Defining computed fields and business logic
  • Planning API structure before implementation

Example Prompt

Design an Object Type for tracking immigration cases in the VT platform.

Requirements:
- Case has a client (person), case type (visa category), status, filing date
- Track documents required and submitted
- Status workflow: draft -> submitted -> in_review -> approved/denied
- Validation: filing date cannot be in future, required documents must be specified
- Computed field for "days since filing"
- Actions for submitting, reviewing, and updating status
- Relationship to Document object type

Provide the full Object Type config with TypeScript types.

Expected Output

import { ObjectTypeConfig } from '@/config/types';

interface ImmigrationCase {
id: string;
caseNumber: string;
clientName: string;
clientEmail: string;
caseType: 'h1b' | 'greencard' | 'citizenship' | 'family' | 'asylum';
status: 'draft' | 'submitted' | 'in_review' | 'approved' | 'denied';
filingDate: string; // ISO date
requiredDocuments: string[];
submittedDocuments: string[]; // Document IDs
assignedAttorney?: string; // User ID
notes?: string;
daysSinceFiling: number; // computed
completionPercentage: number; // computed
createdAt: string;
updatedAt: string;
}

export const immigrationCaseObjectType: ObjectTypeConfig = {
name: 'Immigration Case',
apiName: 'immigration_case',
pluralName: 'Immigration Cases',
description: 'Track immigration case progress and documentation',

fields: [
{
name: 'caseNumber',
type: 'text',
label: 'Case Number',
required: true,
unique: true,
validation: {
pattern: '^CASE-[0-9]{6}$',
message: 'Case number must be in format CASE-XXXXXX',
},
},
{
name: 'clientName',
type: 'text',
label: 'Client Name',
required: true,
validation: {
minLength: 2,
maxLength: 100,
},
},
{
name: 'clientEmail',
type: 'email',
label: 'Client Email',
required: true,
},
{
name: 'caseType',
type: 'select',
label: 'Case Type',
required: true,
options: [
{ value: 'h1b', label: 'H-1B Work Visa' },
{ value: 'greencard', label: 'Green Card' },
{ value: 'citizenship', label: 'Citizenship' },
{ value: 'family', label: 'Family-Based' },
{ value: 'asylum', label: 'Asylum' },
],
},
{
name: 'status',
type: 'select',
label: 'Status',
required: true,
default: 'draft',
options: [
{ value: 'draft', label: 'Draft', color: 'gray' },
{ value: 'submitted', label: 'Submitted', color: 'blue' },
{ value: 'in_review', label: 'In Review', color: 'yellow' },
{ value: 'approved', label: 'Approved', color: 'green' },
{ value: 'denied', label: 'Denied', color: 'red' },
],
},
{
name: 'filingDate',
type: 'date',
label: 'Filing Date',
required: true,
validation: {
max: 'today',
message: 'Filing date cannot be in the future',
},
},
{
name: 'requiredDocuments',
type: 'multi-select',
label: 'Required Documents',
required: true,
options: [
'Passport Copy',
'Birth Certificate',
'Employment Letter',
'Financial Documents',
'Marriage Certificate',
'Police Clearance',
'Medical Exam',
],
},
{
name: 'submittedDocuments',
type: 'reference',
ref: 'document',
multiple: true,
label: 'Submitted Documents',
},
{
name: 'assignedAttorney',
type: 'user',
label: 'Assigned Attorney',
required: false,
roles: ['attorney', 'admin'],
},
{
name: 'notes',
type: 'textarea',
label: 'Case Notes',
required: false,
},
{
name: 'daysSinceFiling',
type: 'number',
label: 'Days Since Filing',
computed: true,
compute: (record) => {
const filing = new Date(record.filingDate);
const now = new Date();
return Math.floor((now.getTime() - filing.getTime()) / (1000 * 60 * 60 * 24));
},
},
{
name: 'completionPercentage',
type: 'number',
label: 'Completion %',
computed: true,
compute: (record) => {
const required = record.requiredDocuments.length;
const submitted = record.submittedDocuments.length;
return required > 0 ? Math.round((submitted / required) * 100) : 0;
},
},
],

actions: [
{
name: 'submit',
label: 'Submit Case',
icon: 'send',
condition: (record) => record.status === 'draft' && record.completionPercentage === 100,
handler: async (record, sdk) => {
await sdk.resources.update('immigration_case', record.id, {
status: 'submitted',
});
},
},
{
name: 'review',
label: 'Start Review',
icon: 'clipboard',
roles: ['attorney', 'admin'],
condition: (record) => record.status === 'submitted',
handler: async (record, sdk) => {
await sdk.resources.update('immigration_case', record.id, {
status: 'in_review',
});
},
},
{
name: 'approve',
label: 'Approve',
icon: 'check',
roles: ['attorney', 'admin'],
condition: (record) => record.status === 'in_review',
handler: async (record, sdk) => {
await sdk.resources.update('immigration_case', record.id, {
status: 'approved',
});
},
},
{
name: 'deny',
label: 'Deny',
icon: 'x',
roles: ['attorney', 'admin'],
condition: (record) => record.status === 'in_review',
handler: async (record, sdk) => {
await sdk.resources.update('immigration_case', record.id, {
status: 'denied',
});
},
},
],

views: [
{
name: 'All Cases',
default: true,
columns: ['caseNumber', 'clientName', 'caseType', 'status', 'daysSinceFiling'],
sort: { field: 'filingDate', order: 'desc' },
},
{
name: 'My Cases',
filter: { assignedAttorney: '$currentUser' },
columns: ['caseNumber', 'clientName', 'caseType', 'status', 'completionPercentage'],
},
{
name: 'In Review',
filter: { status: 'in_review' },
columns: ['caseNumber', 'clientName', 'caseType', 'assignedAttorney', 'daysSinceFiling'],
},
],
};

Tips

  • Start with core fields, then add validation and computed fields
  • Use TypeScript interfaces to clarify data structure
  • Consider workflow states and transitions
  • Define actions with clear conditions
  • Plan relationships to other Object Types upfront

Pattern 4: Integration Testing

Description

Generate integration tests that verify VT components work correctly with the platform SDK, including API calls, state management, and user interactions.

When to Use

  • Testing custom components
  • Verifying API integration logic
  • Testing multi-step workflows
  • Ensuring error handling works correctly
  • Regression testing after refactoring

Example Prompt

Write integration tests for a CaseSubmission component that:
1. Fetches case data using useResources
2. Validates required documents are uploaded
3. Submits the case via SDK when user clicks "Submit"
4. Shows success/error messages
5. Updates case status in the store

Use Jest and React Testing Library. Mock the platform SDK.

Expected Output

import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
import { CaseSubmission } from '@/components/CaseSubmission';
import { useResources } from '@eai/platform-sdk/react';
import { sdk } from '@eai/platform-sdk';
import { StoreProvider } from '@/lib/store';

// Mock the platform SDK
jest.mock('@eai/platform-sdk', () => ({
sdk: {
resources: {
get: jest.fn(),
update: jest.fn(),
},
},
}));

jest.mock('@eai/platform-sdk/react', () => ({
useResources: jest.fn(),
}));

describe('CaseSubmission Integration', () => {
const mockCase = {
id: 'case-123',
caseNumber: 'CASE-000001',
status: 'draft',
requiredDocuments: ['Passport', 'Birth Certificate'],
submittedDocuments: ['doc-1', 'doc-2'],
completionPercentage: 100,
};

beforeEach(() => {
jest.clearAllMocks();
});

it('fetches case data on mount', async () => {
(useResources as jest.Mock).mockReturnValue({
resources: [mockCase],
isLoading: false,
error: null,
});

render(
<StoreProvider>
<CaseSubmission caseId="case-123" />
</StoreProvider>
);

await waitFor(() => {
expect(screen.getByText('CASE-000001')).toBeInTheDocument();
});
});

it('validates required documents before submission', async () => {
const incompleteCase = { ...mockCase, completionPercentage: 50 };

(useResources as jest.Mock).mockReturnValue({
resources: [incompleteCase],
isLoading: false,
error: null,
});

render(
<StoreProvider>
<CaseSubmission caseId="case-123" />
</StoreProvider>
);

const submitButton = screen.getByRole('button', { name: /submit case/i });
expect(submitButton).toBeDisabled();

expect(screen.getByText(/missing required documents/i)).toBeInTheDocument();
});

it('submits case successfully and updates status', async () => {
(useResources as jest.Mock).mockReturnValue({
resources: [mockCase],
isLoading: false,
error: null,
});

(sdk.resources.update as jest.Mock).mockResolvedValue({
...mockCase,
status: 'submitted',
});

render(
<StoreProvider>
<CaseSubmission caseId="case-123" />
</StoreProvider>
);

const submitButton = screen.getByRole('button', { name: /submit case/i });
expect(submitButton).toBeEnabled();

fireEvent.click(submitButton);

await waitFor(() => {
expect(sdk.resources.update).toHaveBeenCalledWith(
'immigration_case',
'case-123',
{ status: 'submitted' }
);
});

expect(screen.getByText(/case submitted successfully/i)).toBeInTheDocument();
});

it('handles submission errors gracefully', async () => {
(useResources as jest.Mock).mockReturnValue({
resources: [mockCase],
isLoading: false,
error: null,
});

(sdk.resources.update as jest.Mock).mockRejectedValue(
new Error('Network error')
);

render(
<StoreProvider>
<CaseSubmission caseId="case-123" />
</StoreProvider>
);

const submitButton = screen.getByRole('button', { name: /submit case/i });
fireEvent.click(submitButton);

await waitFor(() => {
expect(screen.getByText(/failed to submit case/i)).toBeInTheDocument();
expect(screen.getByText(/network error/i)).toBeInTheDocument();
});

// Verify case status was not changed
expect(screen.getByText(/draft/i)).toBeInTheDocument();
});

it('shows loading state during submission', async () => {
(useResources as jest.Mock).mockReturnValue({
resources: [mockCase],
isLoading: false,
error: null,
});

let resolveUpdate: (value: any) => void;
const updatePromise = new Promise((resolve) => {
resolveUpdate = resolve;
});

(sdk.resources.update as jest.Mock).mockReturnValue(updatePromise);

render(
<StoreProvider>
<CaseSubmission caseId="case-123" />
</StoreProvider>
);

const submitButton = screen.getByRole('button', { name: /submit case/i });
fireEvent.click(submitButton);

// Check loading state
expect(screen.getByText(/submitting/i)).toBeInTheDocument();
expect(submitButton).toBeDisabled();

// Resolve the update
act(() => {
resolveUpdate({ ...mockCase, status: 'submitted' });
});

await waitFor(() => {
expect(screen.queryByText(/submitting/i)).not.toBeInTheDocument();
});
});
});

Tips

  • Mock the platform SDK to avoid real API calls
  • Test loading, success, and error states
  • Verify store updates happen correctly
  • Test user interactions (clicks, form inputs)
  • Use waitFor for async operations

Pattern 5: Migration Assistant

Description

Use AI to help migrate from one tenant config structure to another, whether upgrading VT versions, refactoring config organization, or adapting to new platform features.

When to Use

  • Upgrading to a new VT version with breaking changes
  • Refactoring config structure (e.g., splitting large configs)
  • Migrating from legacy patterns to new conventions
  • Adapting to new platform SDK features
  • Bulk updates to field definitions or validation rules

Example Prompt

I need to migrate this tenant config from VT v1.0 to v2.0.

Changes in v2.0:
- `objectTypes` field renamed to `dataModels`
- Field `type: 'text'` is now `type: 'string'`
- Actions now require explicit `permissions` array instead of `roles`
- Component `type: 'resource-list'` is now `type: 'data-table'`
- Store slices must define `persist: boolean`

Here's my current config:
[paste config]

Migrate this config to v2.0 format, preserving all functionality.

Expected Output

// BEFORE (v1.0)
export const oldConfig: TenantConfig = {
id: 'legal-practice',
objectTypes: [
{
name: 'Case',
apiName: 'case',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'status', type: 'select', options: ['open', 'closed'] },
],
actions: [
{
name: 'close',
label: 'Close Case',
roles: ['attorney', 'admin'],
},
],
},
],
sections: [
{
id: 'cases',
components: [
{ type: 'resource-list', objectType: 'case' },
],
},
],
};

// AFTER (v2.0)
export const migratedConfig: TenantConfig = {
id: 'legal-practice',
version: '2.0',

// Renamed: objectTypes -> dataModels
dataModels: [
{
name: 'Case',
apiName: 'case',
fields: [
// Changed: type 'text' -> 'string'
{ name: 'title', type: 'string', required: true },
{ name: 'status', type: 'select', options: ['open', 'closed'] },
],
actions: [
{
name: 'close',
label: 'Close Case',
// Changed: roles -> permissions
permissions: [
{ role: 'attorney', allow: true },
{ role: 'admin', allow: true },
],
},
],
},
],

sections: [
{
id: 'cases',
components: [
// Changed: resource-list -> data-table
{ type: 'data-table', dataModel: 'case' },
],
},
],

// New: Store slices must declare persistence
store: {
slices: {
caseFilters: {
persist: true,
initialState: { status: 'open' },
},
},
},
};

// Migration notes:
// 1. All text fields changed to string type
// 2. Action roles converted to permissions array format
// 3. resource-list components updated to data-table
// 4. Store slices now explicitly declare persistence
// 5. Added version field to track config version

Tips

  • Provide clear "before and after" requirements
  • Ask AI to document all changes made
  • Request a migration checklist for manual review
  • Test thoroughly after migration
  • Consider using AI to generate migration scripts for data

Best Practices

Prompt Engineering

  1. Be Specific: Include exact types, hook names, and patterns you want to use
  2. Provide Context: Share relevant parts of your existing config or code
  3. Set Constraints: Specify what to avoid (e.g., "don't use any", "must be type-safe")
  4. Request Examples: Ask for multiple variations or edge cases
  5. Iterate: Refine the output with follow-up prompts

Code Review

Always review AI-generated code for:

  • TypeScript type safety
  • Platform SDK best practices
  • Security considerations (auth, validation)
  • Performance implications
  • Accessibility compliance

Version Control

  • Commit AI-generated code separately for easier review
  • Document which code was AI-generated in commit messages
  • Review diffs carefully before merging

Testing

AI-generated code should always include:

  • Type checking (no any types)
  • Error handling
  • Loading states
  • Tests or testing guidance

Advanced Patterns

Multi-Step Workflows

For complex features, use AI iteratively:

  1. Discovery: "Analyze the requirements and suggest an architecture"
  2. Design: "Create the data model and config structure"
  3. Implementation: "Generate the component code"
  4. Testing: "Write integration tests"
  5. Documentation: "Generate API docs and usage examples"

Code Generation Templates

Create reusable prompts for common tasks:

// Save this as a snippet or template
const componentPrompt = `
Create a VT component named {{componentName}} that:
- Uses {{hooks}} hooks
- Displays {{objectType}} data
- Includes {{features}}
- Follows VT store patterns
- Has loading/error states
- Is fully typed with TypeScript
`;

Validation Loops

Use AI to validate its own output:

Review the code you just generated and check for:
1. TypeScript errors or unsafe types
2. Missing error handling
3. Performance issues
4. Accessibility problems
5. VT best practice violations

Provide a checklist with pass/fail for each item.

Common Pitfalls

Avoid These Anti-Patterns

  1. Over-reliance: Don't skip code review because AI wrote it
  2. Copy-Paste: Understand the generated code before using it
  3. Ignoring Types: AI sometimes uses any - fix these
  4. Missing Context: Provide enough context for accurate generation
  5. No Testing: Always test AI-generated code thoroughly

Red Flags

Watch for these issues in AI-generated code:

  • Hard-coded values that should be configurable
  • Missing TypeScript types or any usage
  • Security vulnerabilities (exposed secrets, no validation)
  • Deprecated APIs or patterns
  • Missing accessibility attributes
  • Overly complex solutions to simple problems

Resources

Next Steps