Tool Specifications
Tool specifications are the heart of Unspec'd. They define what your interface should do using a declarative JSON-like structure, eliminating the need to write imperative UI code.
Basic Structure
Every tool specification follows this structure:
const myTool: ToolSpec = {
id: 'unique-tool-id', // Required: Unique identifier
title: 'Display Name', // Required: Human-readable title
content: { /* ... */ }, // Required: UI content definition
functions: { /* ... */ }, // Required: Backend logic
inputs?: { /* ... */ } // Optional: Configuration parameters
};
Required Properties
id: string
Unique identifier for the tool. Used for routing, API calls, and internal references.
Rules:
- Must be unique across all tools
- Use kebab-case (lowercase with hyphens)
- No spaces or special characters
- Should be descriptive but concise
// ✅ Good IDs
id: 'user-management'
id: 'order-dashboard'
id: 'inventory-report'
// ❌ Bad IDs
id: 'UserManagement' // PascalCase
id: 'user management' // Spaces
id: 'tool1' // Not descriptive
title: string
Human-readable display name shown in the UI navigation and headers.
title: 'User Management'
title: 'Live Order Dashboard'
title: 'Inventory Report Generator'
content: ContentSpec
Defines the type of interface to render and its configuration. This is where you specify whether you want a table, form, button, etc.
Available Content Types:
displayRecord
- Show single recordseditableTable
- CRUD data tableseditForm
- Single record editingactionButton
- Custom actionsstreamingTable
- Real-time data
functions: Record<string, Function>
Backend logic functions that power your interface. Functions are called automatically by the UI based on user interactions.
functions: {
loadData: async () => { /* fetch data */ },
saveRecord: async (data) => { /* save data */ },
validateInput: async (input) => { /* validate */ }
}
Optional Properties
inputs?: Record<string, any>
Configuration parameters that can be passed to your tool. Useful for making tools reusable with different settings.
inputs: {
apiEndpoint: 'https://api.example.com',
pageSize: 25,
enableFiltering: true
}
Access inputs in your functions:
functions: {
loadData: async (params) => {
const { apiEndpoint, pageSize } = params.inputs;
// Use configuration values
}
}
Content Types Reference
DisplayRecord
Shows a single record with formatted fields. Perfect for dashboards, detail views, and status displays.
content: {
type: 'displayRecord',
dataLoader: {
functionName: 'loadRecord'
},
displayConfig: {
fields: [
{
field: 'name',
label: 'Full Name'
},
{
field: 'email',
label: 'Email Address'
},
{
field: 'createdAt',
label: 'Created',
formatter: 'datetime'
},
{
field: 'status',
label: 'Status',
formatter: 'badge'
}
]
}
}
Field Formatters:
text
(default) - Plain textdatetime
- Formatted date/timecurrency
- Money formattingbadge
- Colored status badgesemail
- Clickable email linksurl
- Clickable web links
EditableTable
Full CRUD data table with inline editing, sorting, and pagination.
content: {
type: 'editableTable',
columns: {
name: {
type: 'text',
label: 'Full Name',
required: true
},
email: {
type: 'email',
label: 'Email Address',
required: true
},
role: {
type: 'select',
label: 'Role',
options: [
{ value: 'admin', label: 'Administrator' },
{ value: 'user', label: 'User' },
{ value: 'viewer', label: 'Viewer' }
]
},
active: {
type: 'boolean',
label: 'Active'
}
}
}
Column Types:
text
- Text inputemail
- Email input with validationnumber
- Numeric inputboolean
- Checkboxselect
- Dropdown with optionsdate
- Date pickertextarea
- Multi-line text
Required Functions:
loadData()
- Fetch table dataupdateRow(data)
- Update existing rowdeleteRow(id)
- Delete rowcreateRow(data)
- Create new row (optional)
EditForm
Single record editing form with validation and custom layouts.
content: {
type: 'editForm',
formConfig: {
fields: [
{
name: 'firstName',
type: 'text',
label: 'First Name',
required: true
},
{
name: 'lastName',
type: 'text',
label: 'Last Name',
required: true
},
{
name: 'bio',
type: 'textarea',
label: 'Biography',
rows: 4
}
]
},
onSubmit: {
functionName: 'saveUser'
}
}
Form Field Properties:
name
- Field identifiertype
- Input type (same as table columns)label
- Display labelrequired
- Validation requirementplaceholder
- Input placeholderdefaultValue
- Initial valuevalidation
- Custom validation rules
ActionButton
Custom action triggers for workflows, reports, or any business logic.
content: {
type: 'actionButton',
buttonConfig: {
label: 'Generate Report',
variant: 'primary',
icon: 'download'
},
action: {
functionName: 'generateReport'
}
}
Button Variants:
primary
- Blue, prominentsecondary
- Gray, subtlesuccess
- Green, positive actionswarning
- Yellow, cautiondanger
- Red, destructive actions
StreamingTable
Real-time data table with live updates and connection status.
content: {
type: 'streamingTable',
columns: {
timestamp: { type: 'text', label: 'Time' },
event: { type: 'text', label: 'Event' },
status: { type: 'text', label: 'Status' }
},
streamingConfig: {
functionName: 'streamEvents',
maxRows: 100,
autoScroll: true
}
}
Streaming Function Signature:
functions: {
streamEvents: async ({ onData, onError, onConnect, onDisconnect }) => {
// Set up streaming connection
const connection = setupStream();
connection.on('data', (event) => {
onData(event); // Send data to UI
});
connection.on('error', (error) => {
onError(error); // Handle errors
});
onConnect(); // Signal connection established
// Return cleanup function
return () => {
connection.close();
onDisconnect();
};
}
}
Function System
Functions are the backend logic that powers your tools. They're called automatically by the UI based on user interactions.
Function Parameters
All functions receive a parameters object:
functions: {
myFunction: async (params) => {
const {
inputs, // Tool inputs configuration
data, // User-provided data (forms, etc.)
id, // Record ID (for updates/deletes)
// ... other context
} = params;
}
}
Common Function Patterns
Data Loading:
loadData: async () => {
const response = await fetch('/api/users');
return response.json();
}
Data Saving:
saveUser: async ({ data }) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Failed to save user');
}
return { success: true };
}
Validation:
validateEmail: async ({ data }) => {
if (!data.email?.includes('@')) {
throw new Error('Invalid email address');
}
return { valid: true };
}
Error Handling
Functions can throw errors to display user-friendly messages:
functions: {
deleteUser: async ({ id }) => {
try {
await deleteUserFromDB(id);
return { success: true };
} catch (error) {
// This message will be shown to the user
throw new Error('Cannot delete user: they have active orders');
}
}
}
Complete Example
Here's a comprehensive tool specification showcasing all features:
const userManagementTool: ToolSpec = {
id: 'user-management',
title: 'User Management System',
inputs: {
apiBaseUrl: 'https://api.company.com',
department: 'engineering',
maxUsers: 1000
},
content: {
type: 'editableTable',
columns: {
firstName: {
type: 'text',
label: 'First Name',
required: true
},
lastName: {
type: 'text',
label: 'Last Name',
required: true
},
email: {
type: 'email',
label: 'Email Address',
required: true
},
role: {
type: 'select',
label: 'Role',
options: [
{ value: 'admin', label: 'Administrator' },
{ value: 'manager', label: 'Manager' },
{ value: 'developer', label: 'Developer' },
{ value: 'designer', label: 'Designer' }
]
},
startDate: {
type: 'date',
label: 'Start Date'
},
active: {
type: 'boolean',
label: 'Active'
}
}
},
functions: {
loadData: async ({ inputs }) => {
const response = await fetch(`${inputs.apiBaseUrl}/users?dept=${inputs.department}`);
const users = await response.json();
return users.slice(0, inputs.maxUsers);
},
createRow: async ({ data, inputs }) => {
// Validation
if (!data.email?.includes('@')) {
throw new Error('Please provide a valid email address');
}
const response = await fetch(`${inputs.apiBaseUrl}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...data,
department: inputs.department,
createdAt: new Date().toISOString()
})
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
},
updateRow: async ({ data, inputs }) => {
const response = await fetch(`${inputs.apiBaseUrl}/users/${data.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Failed to update user');
}
return response.json();
},
deleteRow: async ({ id, inputs }) => {
const response = await fetch(`${inputs.apiBaseUrl}/users/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete user');
}
return { success: true };
}
}
};
Best Practices
1. Descriptive IDs and Titles
// ✅ Good
id: 'customer-order-history'
title: 'Customer Order History'
// ❌ Bad
id: 'tool3'
title: 'Tool'
2. Consistent Function Naming
// ✅ Good - follows conventions
functions: {
loadData: async () => { /* ... */ },
createRow: async ({ data }) => { /* ... */ },
updateRow: async ({ data }) => { /* ... */ },
deleteRow: async ({ id }) => { /* ... */ }
}
3. Proper Error Messages
// ✅ Good - user-friendly
throw new Error('Email address is already in use');
// ❌ Bad - technical
throw new Error('UNIQUE constraint failed: users.email');
4. Input Validation
functions: {
saveUser: async ({ data }) => {
// Validate required fields
if (!data.name?.trim()) {
throw new Error('Name is required');
}
if (!data.email?.includes('@')) {
throw new Error('Please provide a valid email address');
}
// Proceed with save
return await saveToDatabase(data);
}
}
5. Use Inputs for Configuration
// ✅ Good - configurable
inputs: {
pageSize: 25,
allowDelete: true,
apiEndpoint: '/api/v1/users'
}
// ❌ Bad - hardcoded
functions: {
loadData: async () => {
return fetch('/api/v1/users?limit=25'); // Hardcoded values
}
}
Next Steps
- 📖 Components Guide - Deep dive into each content type
- 🎯 Focus Mode vs Normal Mode - Understanding display modes
- 💡 Examples - Real-world tool specifications
- 🔧 CLI Usage - Using tools with the command line
Master tool specifications and you'll master Unspec'd! These declarative definitions are the key to rapid, maintainable internal tool development. 🚀