53 KiB
Workflow Routes Frontend Documentation
This document describes customer journeys for managing workflows through the frontend, focusing on how users interact with workflows and how the backend routes support these experiences. All UI components are dynamically generated from backend metadata—no hardcoding required.
Table of Contents
- Overview
- Customer Journey 1: Discovering and Browsing Workflows
- Customer Journey 2: Viewing Workflow Details
- Customer Journey 3: Monitoring Workflow Execution
- Customer Journey 4: Editing Workflow Properties
- Customer Journey 5: Managing Workflow Messages
- Customer Journey 6: Cleaning Up Workflows
- Backend Metadata System
- Implementation Patterns
Overview
The workflow routes (/api/workflows) enable users to manage and monitor workflows that are already running or completed. These routes focus on management and monitoring rather than creation (workflows are created through the chat playground routes).
Key Principles:
- User-Centric: Documentation organized around what users want to accomplish
- Backend-Driven: All forms, tables, and UI components generated from backend metadata
- No Hardcoding: Field definitions, labels, validation rules, and options come from the backend
- Real-Time Updates: Support for polling and incremental data loading
- Permission-Aware: Backend enforces permissions; frontend handles gracefully
Customer Journey 1: Discovering and Browsing Workflows
User Goal
"I want to see all my workflows and find the one I'm looking for."
User Story
As a user, I want to browse my workflows, search for specific workflows, filter by any field, sort them by different criteria, and quickly identify which ones are running, completed, or have errors.
User Story Flow
sequenceDiagram
participant User
participant Frontend
participant Backend
User->>Frontend: Navigate to /workflows
Frontend->>Backend: GET /api/attributes/ChatWorkflow
Backend-->>Frontend: Attribute definitions (fields, labels, types)
Frontend->>Backend: GET /api/workflows/?pagination=...
Backend-->>Frontend: Paginated workflows list
Frontend->>Frontend: Generate table columns from attributes
Frontend->>Frontend: Generate filter controls from attributes
Frontend->>Frontend: Render workflows table + search + filters
Frontend-->>User: Display workflow list with search/filter UI
alt User performs general search
User->>Frontend: Type in search box (e.g., "invoice")
Frontend->>Frontend: Update search query
Frontend->>Backend: GET /api/workflows/?pagination=...filters:{"search":"invoice"}
Backend-->>Frontend: Filtered workflows list
Frontend-->>User: Display matching workflows
end
alt User applies field filter
User->>Frontend: Select filter field (e.g., "Status")
Frontend->>Frontend: Show filter options from attribute metadata
User->>Frontend: Select filter value (e.g., "running")
Frontend->>Frontend: Update filter parameters
Frontend->>Backend: GET /api/workflows/?pagination=...filters:{"status":"running"}
Backend-->>Frontend: Filtered workflows list
Frontend-->>User: Display filtered workflows
end
alt User combines search + filter + sort
User->>Frontend: Apply search + filter + sort
Frontend->>Frontend: Combine all parameters
Frontend->>Backend: GET /api/workflows/?pagination=...filters:{"search":"invoice","status":"running"}...sort:desc
Backend-->>Frontend: Filtered, sorted workflows list
Frontend-->>User: Display results
end
User->>Frontend: Click column header (e.g., "Last Activity")
Frontend->>Frontend: Update sort parameters
Frontend->>Backend: GET /api/workflows/?pagination=...sort:desc
Backend-->>Frontend: Sorted workflows list
Frontend-->>User: Display sorted workflows
User->>Frontend: Click workflow row
Frontend->>Frontend: Navigate to /workflows/workflowId
Frontend-->>User: Show workflow detail page
Frontend Requirements
Page: Workflow List (/workflows)
What the user sees:
- A table or card grid showing all workflows
- Each workflow shows: name, status, last activity, progress indicators
- General search box (searches across all fields)
- Field-specific filter controls (one filter per visible field)
- Sorting controls (by name, status, last activity, etc.)
- Pagination controls (if many workflows)
- Active filter indicators (showing which filters are applied)
What the frontend needs to do:
-
Fetch Workflow List
GET /api/workflows/?pagination={"page":1,"pageSize":20,"sort":[],"filters":null}- If user wants all workflows: omit
paginationparameter - If user wants paginated view: include
paginationJSON - Include
filtersin pagination object for search and field filtering - Filters structure:
{"search":"query","fieldName":"value",...}
- If user wants all workflows: omit
-
Fetch Field Definitions
GET /api/attributes/ChatWorkflow- Use this to generate table columns dynamically
- Determine which fields are visible, sortable, etc.
-
Render Dynamic Table
- Generate columns from attribute definitions (filter
visible: true) - Use
labelfor column headers (localized) - Format values based on
type:status→ Show as badge with color codinglastActivity→ Format as relative time ("2 hours ago")name→ Display as link to detail page
- Support sorting: when user clicks column header, update
sortin pagination and refetch
- Generate columns from attribute definitions (filter
-
Handle Pagination
- Display pagination controls using
paginationmetadata from response - Show "Page X of Y" and "Showing 1-20 of 45 workflows"
- Handle page changes: update
pagein pagination params and refetch
- Display pagination controls using
-
Handle General Search
- Display search input box (searches across all text fields)
- When user types: update
filters.searchin pagination params - Debounce search input (wait 300-500ms after user stops typing)
- Reset to page 1 when search changes
- Refetch workflows with search filter applied
-
Handle Field Filtering
- Generate filter controls dynamically from attribute definitions
- For each visible field, show appropriate filter UI:
textfields → Text input filterselectfields → Dropdown with options from metadataintegerfields → Number range filter (min/max)timestampfields → Date range filtercheckboxfields → Boolean toggle filter
- When user applies filter: update
filters[fieldName]in pagination params - Support multiple filters simultaneously
- Show active filter indicators (chips/badges)
- Allow removing individual filters
- Reset to page 1 when filters change
- Refetch workflows with filters applied
-
Handle Combined Search + Filter + Sort
- All parameters work together: search + filters + sort + pagination
- When any parameter changes, combine all and refetch
- Preserve all active filters when sorting or changing pages
-
Handle Row Clicks
- Navigate to workflow detail page:
/workflows/{workflowId}
- Navigate to workflow detail page:
Backend Routes Used
| Route | Purpose | When Used |
|---|---|---|
GET /api/workflows/ |
Get all workflows | Initial page load, pagination changes, sort changes, filter changes, search changes |
GET /api/attributes/ChatWorkflow |
Get field definitions | Page load (once) - used to generate columns, filters, and search UI |
Example User Flow
1. User navigates to /workflows
2. Frontend fetches: GET /api/attributes/ChatWorkflow
3. Frontend fetches: GET /api/workflows/?pagination={"page":1,"pageSize":20,"sort":[],"filters":null}
4. Frontend generates table columns from attributes
5. Frontend generates filter controls from attributes (one per visible field)
6. Frontend renders workflows table with search box and filter controls
7. User types "invoice" in search box
8. Frontend debounces (waits 400ms), then updates filters: {"search":"invoice"}
9. Frontend refetches: GET /api/workflows/?pagination={"page":1,"pageSize":20,"sort":[],"filters":{"search":"invoice"}}
10. Frontend displays matching workflows
11. User selects "Status" filter dropdown, chooses "running"
12. Frontend updates filters: {"search":"invoice","status":"running"}
13. Frontend refetches with combined filters
14. Frontend displays filtered results
15. User clicks "Last Activity" column header to sort
16. Frontend updates sort while preserving filters: {"search":"invoice","status":"running"} + sort
17. Frontend refetches with filters + sort
18. User clicks on a workflow row
19. Frontend navigates to /workflows/{workflowId}
Dynamic UI Generation
// Fetch attribute definitions
const attributes = await fetch('/api/attributes/ChatWorkflow').then(r => r.json());
// Filter visible, sortable columns
const tableColumns = attributes.attributes
.filter(attr => attr.visible)
.sort((a, b) => a.order - b.order)
.map(attr => ({
key: attr.name,
label: attr.label, // Localized label
type: attr.type,
sortable: true, // Can be made configurable
render: (value) => formatValue(value, attr.type, attr.options)
}));
// Generate filter controls from attributes
const filterControls = attributes.attributes
.filter(attr => attr.visible)
.map(attr => ({
field: attr.name,
label: attr.label,
type: attr.type,
options: attr.options, // For select fields
component: getFilterComponent(attr.type) // Returns appropriate filter UI component
}));
// Generate table from columns
<DynamicTable
columns={tableColumns}
data={workflows}
onSort={handleSort}
onRowClick={(workflow) => navigate(`/workflows/${workflow.id}`)}
/>
// Render search and filters
<SearchBox
value={searchQuery}
onChange={(query) => handleSearch(query)}
placeholder="Search workflows..."
/>
<FilterPanel>
{filterControls.map(filter => (
<FilterControl
key={filter.field}
filter={filter}
value={activeFilters[filter.field]}
onChange={(value) => handleFilterChange(filter.field, value)}
/>
))}
</FilterPanel>
Customer Journey 2: Viewing Workflow Details
User Goal
"I want to see everything about a specific workflow—its current state, what it's doing, and what it has accomplished."
User Story
As a user, I want to open a workflow and see its complete information, including its configuration, current status, messages, and logs, all in one place.
User Story Flow
sequenceDiagram
participant User
participant Frontend
participant Backend
User->>Frontend: Click workflow from list
Frontend->>Backend: GET /api/workflows/workflowId
Backend-->>Frontend: Workflow object
Frontend->>Backend: GET /api/attributes/ChatWorkflow
Backend-->>Frontend: Field definitions
Frontend->>Backend: GET /api/workflows/workflowId/messages
Backend-->>Frontend: Messages list
Frontend->>Backend: GET /api/workflows/workflowId/logs
Backend-->>Frontend: Logs list
Frontend->>Frontend: Generate UI from metadata
Frontend->>Frontend: Render workflow info section
Frontend->>Frontend: Render messages section
Frontend->>Frontend: Render logs section
Frontend-->>User: Display complete workflow view
Frontend Requirements
Page: Workflow Detail (/workflows/{workflowId})
What the user sees:
- Workflow header: name, status badge, key metrics
- Workflow information section: all workflow properties
- Messages section: conversation/execution history
- Logs section: execution logs with filtering
- Action buttons: Edit, Delete (if user has permission)
What the frontend needs to do:
-
Fetch Workflow Data
GET /api/workflows/{workflowId}- Get complete workflow object
- Includes nested data (messages, logs, stats) if available
-
Fetch Field Definitions
GET /api/attributes/ChatWorkflow- Use to render workflow information section
- Determine which fields are editable vs read-only
-
Fetch Messages (if not included in workflow object)
GET /api/workflows/{workflowId}/messages- Get all messages for the workflow
- Support pagination if many messages
-
Fetch Logs (if not included in workflow object)
GET /api/workflows/{workflowId}/logs- Get all logs for the workflow
- Support filtering by log type
-
Render Workflow Information
- Display read-only fields as formatted text
- Show editable fields with edit indicators (if user has permission)
- Format special fields:
status→ Status badge with colorlastActivity,startedAt→ Formatted timestampsworkflowMode→ Display mode label (not enum value)- Progress fields → Progress bars
-
Render Messages Section
- Display messages in chronological order
- Show message role (user/assistant/system)
- Display attached documents/files
- Support threading if
parentMessageIdexists
-
Render Logs Section
- Display logs in reverse chronological order (newest first)
- Color-code by type (info/warning/error)
- Show progress indicators if
progressfield exists - Filter by log type (if implemented)
Backend Routes Used
| Route | Purpose | When Used |
|---|---|---|
GET /api/workflows/{workflowId} |
Get workflow details | Page load |
GET /api/workflows/{workflowId}/messages |
Get workflow messages | Page load, refresh |
GET /api/workflows/{workflowId}/logs |
Get workflow logs | Page load, refresh |
GET /api/attributes/ChatWorkflow |
Get field definitions | Page load (once) |
User Navigation Flow
stateDiagram-v2
[*] --> WorkflowListPage
WorkflowListPage --> WorkflowDetailPage: Click workflow
WorkflowDetailPage --> WorkflowListPage: Click back
WorkflowDetailPage --> WorkflowEditPage: Click Edit
WorkflowDetailPage --> WorkflowDetailPage: View messages
WorkflowDetailPage --> WorkflowDetailPage: View logs
WorkflowDetailPage --> WorkflowDetailPage: Delete workflow
WorkflowEditPage --> WorkflowDetailPage: Save or Cancel
note right of WorkflowDetailPage
Component Structure:
- WorkflowHeader (name, status, metrics)
- WorkflowInfoSection (all fields)
- MessageList (chronological)
- LogViewer (reverse chronological)
- ActionButtons (edit, delete)
end note
note right of WorkflowDetailPage: User can scroll through<br/>messages and logs<br/>without leaving page
Example User Flow
1. User clicks workflow from list → navigates to /workflows/{workflowId}
2. Frontend fetches: GET /api/workflows/{workflowId}
3. Frontend fetches: GET /api/attributes/ChatWorkflow
4. Frontend fetches: GET /api/workflows/{workflowId}/messages
5. Frontend fetches: GET /api/workflows/{workflowId}/logs
6. Frontend renders:
- Workflow header with status badge
- Information section (read-only fields from attributes)
- Messages list (chronological)
- Logs viewer (reverse chronological)
7. User sees complete workflow state
Dynamic UI Generation
// Fetch workflow and attributes
const [workflow, attributes] = await Promise.all([
fetch(`/api/workflows/${workflowId}`).then(r => r.json()),
fetch('/api/attributes/ChatWorkflow').then(r => r.json())
]);
// Separate editable vs read-only fields
const readOnlyFields = attributes.attributes.filter(attr =>
attr.visible && !attr.editable
);
const editableFields = attributes.attributes.filter(attr =>
attr.visible && attr.editable
);
// Render information section
<WorkflowInfoSection>
{readOnlyFields.map(attr => (
<InfoField
key={attr.name}
label={attr.label}
value={formatValue(workflow[attr.name], attr.type, attr.options)}
/>
))}
</WorkflowInfoSection>
// Show edit button if user has editable fields and permission
{editableFields.length > 0 && (
<Button onClick={() => navigate(`/workflows/${workflowId}/edit`)}>
Edit Workflow
</Button>
)}
Customer Journey 3: Monitoring Workflow Execution
User Goal
"I want to watch my workflow as it runs and see updates in real-time."
User Story
As a user, I want to monitor a running workflow and see new messages and logs appear automatically without refreshing the page.
User Story Flow
sequenceDiagram
participant User
participant Frontend
participant Backend
User->>Frontend: Open workflow detail page
Frontend->>Backend: GET /api/workflows/id/status
Backend-->>Frontend: Current workflow status
Frontend->>Backend: GET /api/workflows/id/messages
Backend-->>Frontend: Initial messages
Frontend->>Backend: GET /api/workflows/id/logs
Backend-->>Frontend: Initial logs
Frontend-->>User: Display initial state
loop Every 5 seconds (if running)
Frontend->>Backend: GET /api/workflows/id/status
Backend-->>Frontend: Updated status
Frontend->>Frontend: Update status badge
Frontend-->>User: Show status update
end
loop Every 3 seconds (if running)
Frontend->>Backend: GET /api/workflows/id/messages?messageId=lastId
Backend-->>Frontend: New messages (if any)
alt New messages exist
Frontend->>Frontend: Append to messages list
Frontend-->>User: Show new messages
end
end
loop Every 3 seconds (if running)
Frontend->>Backend: GET /api/workflows/id/logs?logId=lastId
Backend-->>Frontend: New logs (if any)
alt New logs exist
Frontend->>Frontend: Prepend to logs list
Frontend-->>User: Show new logs
end
end
Backend->>Backend: Workflow completes
Frontend->>Backend: GET /api/workflows/id/status
Backend-->>Frontend: Status: "completed"
Frontend->>Frontend: Stop all polling
Frontend-->>User: Show completion message
Frontend Requirements
Component: Real-Time Workflow Monitor
What the user sees:
- Status indicator that updates automatically
- New messages appearing in the messages list
- New logs appearing in the logs viewer
- Progress indicators updating
- Activity indicators showing workflow is active
What the frontend needs to do:
-
Initial Load
- Fetch workflow status:
GET /api/workflows/{workflowId}/status - Fetch messages:
GET /api/workflows/{workflowId}/messages - Fetch logs:
GET /api/workflows/{workflowId}/logs
- Fetch workflow status:
-
Poll for Status Updates
GET /api/workflows/{workflowId}/status- Poll every 2-5 seconds if workflow status is "running"
- Stop polling if workflow status is "completed", "stopped", or "error"
- Update status badge and workflow information
-
Poll for New Messages (Incremental Loading)
GET /api/workflows/{workflowId}/messages?messageId={lastMessageId}- Store ID of last displayed message
- Poll every 2-3 seconds for messages after last message ID
- Append new messages to list (don't reload all)
- Only poll if workflow is "running"
-
Poll for New Logs (Incremental Loading)
GET /api/workflows/{workflowId}/logs?logId={lastLogId}- Store ID of last displayed log
- Poll every 2-3 seconds for logs after last log ID
- Prepend new logs to list (newest first)
- Only poll if workflow is "running"
-
Handle Workflow Completion
- Stop all polling when status changes to "completed" or "stopped"
- Show completion message
- Update UI to indicate workflow is finished
Backend Routes Used
| Route | Purpose | When Used |
|---|---|---|
GET /api/workflows/{workflowId}/status |
Get current status | Polling (every 2-5s) |
GET /api/workflows/{workflowId}/messages?messageId={id} |
Get new messages | Polling (every 2-3s) |
GET /api/workflows/{workflowId}/logs?logId={id} |
Get new logs | Polling (every 2-3s) |
User Interaction Flow
stateDiagram-v2
[*] --> ViewingWorkflow
ViewingWorkflow --> MonitoringActive: Workflow is running
MonitoringActive --> UpdatingStatus: Poll status (5s)
MonitoringActive --> UpdatingMessages: Poll messages (3s)
MonitoringActive --> UpdatingLogs: Poll logs (3s)
UpdatingStatus --> MonitoringActive: Continue if running
UpdatingStatus --> WorkflowCompleted: Status = completed
UpdatingMessages --> MonitoringActive: Continue if running
UpdatingMessages --> WorkflowCompleted: Status = completed
UpdatingLogs --> MonitoringActive: Continue if running
UpdatingLogs --> WorkflowCompleted: Status = completed
WorkflowCompleted --> ViewingWorkflow: Stop all polling
note right of MonitoringActive
User sees real-time updates:
- Status badge changes
- New messages appear
- New logs appear
- Progress bars update
end note
note right of WorkflowCompleted
All polling stops
Completion message shown
UI updates to final state
end note
Example User Flow
1. User opens workflow detail page for running workflow
2. Frontend loads initial data (workflow, messages, logs)
3. Frontend starts polling:
- Status: every 5 seconds
- Messages: every 3 seconds (with lastMessageId)
- Logs: every 3 seconds (with lastLogId)
4. User watches as:
- Status updates (if changed)
- New messages appear at bottom of list
- New logs appear at top of list
- Progress bars update
5. Workflow completes → status changes to "completed"
6. Frontend stops all polling
7. Frontend shows "Workflow completed" message
Implementation Pattern
function useWorkflowMonitoring(workflowId: string) {
const [workflow, setWorkflow] = useState(null);
const [messages, setMessages] = useState([]);
const [logs, setLogs] = useState([]);
const [lastMessageId, setLastMessageId] = useState(null);
const [lastLogId, setLastLogId] = useState(null);
// Status polling
useEffect(() => {
if (!workflow || workflow.status === 'completed' || workflow.status === 'stopped') {
return; // Stop polling if completed
}
const interval = setInterval(async () => {
const updated = await fetch(`/api/workflows/${workflowId}/status`).then(r => r.json());
setWorkflow(updated);
}, 5000);
return () => clearInterval(interval);
}, [workflowId, workflow?.status]);
// Message polling (incremental)
useEffect(() => {
if (!workflow || workflow.status !== 'running') return;
const interval = setInterval(async () => {
const url = lastMessageId
? `/api/workflows/${workflowId}/messages?messageId=${lastMessageId}`
: `/api/workflows/${workflowId}/messages`;
const response = await fetch(url).then(r => r.json());
if (response.items.length > 0) {
setMessages(prev => [...prev, ...response.items]);
setLastMessageId(response.items[response.items.length - 1].id);
}
}, 3000);
return () => clearInterval(interval);
}, [workflowId, workflow?.status, lastMessageId]);
// Log polling (incremental)
useEffect(() => {
if (!workflow || workflow.status !== 'running') return;
const interval = setInterval(async () => {
const url = lastLogId
? `/api/workflows/${workflowId}/logs?logId=${lastLogId}`
: `/api/workflows/${workflowId}/logs`;
const response = await fetch(url).then(r => r.json());
if (response.items.length > 0) {
setLogs(prev => [...response.items, ...prev]); // Prepend (newest first)
setLastLogId(response.items[0].id);
}
}, 3000);
return () => clearInterval(interval);
}, [workflowId, workflow?.status, lastLogId]);
return { workflow, messages, logs };
}
Customer Journey 4: Editing Workflow Properties
User Goal
"I want to change workflow settings like its name, status, or mode."
User Story
As a user, I want to edit a workflow's properties through a form that only shows fields I'm allowed to edit, with validation and clear error messages.
User Story Flow
sequenceDiagram
participant User
participant Frontend
participant Backend
User->>Frontend: Click "Edit" button
Frontend->>Frontend: Navigate to /workflows/id/edit
Frontend->>Backend: GET /api/workflows/workflowId
Backend-->>Frontend: Current workflow data
Frontend->>Backend: GET /api/attributes/ChatWorkflow
Backend-->>Frontend: Field definitions (editable fields)
Frontend->>Frontend: Filter editable fields
Frontend->>Frontend: Generate form from attributes
Frontend->>Frontend: Pre-populate form with workflow data
Frontend-->>User: Display edit form
User->>Frontend: Modify form fields
User->>Frontend: Click "Save"
Frontend->>Frontend: Validate form (required fields, types)
alt Validation fails
Frontend-->>User: Show validation errors
else Validation passes
Frontend->>Backend: PUT /api/workflows/workflowId + form data
alt Permission denied (403)
Backend-->>Frontend: 403 Forbidden
Frontend-->>User: Show permission error
else Validation error (400)
Backend-->>Frontend: 400 Bad Request + error details
Frontend-->>User: Show backend validation errors
else Success (200)
Backend-->>Frontend: Updated workflow
Frontend->>Frontend: Navigate to detail page
Frontend-->>User: Show updated workflow
end
end
Frontend Requirements
Page: Workflow Edit (/workflows/{workflowId}/edit)
What the user sees:
- Form with editable workflow fields
- Only fields the user can edit (based on backend metadata)
- Required field indicators
- Validation errors (if any)
- Save and Cancel buttons
What the frontend needs to do:
-
Fetch Workflow Data
GET /api/workflows/{workflowId}- Get current workflow values
- Pre-populate form
-
Fetch Field Definitions
GET /api/attributes/ChatWorkflow- Filter to only editable fields (
editable: true) - Use to generate form fields dynamically
- Get validation rules (required, type, options)
- Filter to only editable fields (
-
Generate Dynamic Form
- Create form fields from attribute definitions
- Use appropriate input types based on
type:text→ Text inputselect→ Dropdown withoptionsinteger→ Number inputcheckbox→ Checkbox
- Show required indicators for
required: truefields - Use
labelfor field labels (localized) - Use
placeholderfor placeholder text
-
Client-Side Validation
- Validate required fields before submit
- Validate types (e.g., integer fields must be numbers)
- Validate select fields (value must be in options)
-
Submit Changes
PUT /api/workflows/{workflowId}- Send only changed fields (or all form data)
- Handle 403 (permission denied) gracefully
- Handle 400 (validation errors) and display errors
- On success: navigate back to detail page or show success message
Backend Routes Used
| Route | Purpose | When Used |
|---|---|---|
GET /api/workflows/{workflowId} |
Get workflow to edit | Page load |
GET /api/attributes/ChatWorkflow |
Get editable field definitions | Page load |
PUT /api/workflows/{workflowId} |
Update workflow | Form submit |
User Navigation Flow
stateDiagram-v2
[*] --> WorkflowDetailPage
WorkflowDetailPage --> WorkflowEditPage: Click Edit button
WorkflowEditPage --> EditingForm: Form loaded
EditingForm --> EditingForm: Modify fields
EditingForm --> ValidatingForm: Click Save
ValidatingForm --> EditingForm: Validation errors
ValidatingForm --> SubmittingForm: All valid
SubmittingForm --> WorkflowDetailPage: Success
SubmittingForm --> EditingForm: Backend errors
WorkflowEditPage --> WorkflowDetailPage: Click Cancel
note right of EditingForm
Components:
- DynamicForm (generated from metadata)
- FormFields (text, select, number, etc.)
- ValidationErrors (inline)
end note
note right of ValidatingForm
Client-side validation:
- Required fields
- Type checking
- Format validation
end note
note right of SubmittingForm
Backend validation:
- Permission check
- Server-side rules
- Data integrity
end note
Example User Flow
1. User clicks "Edit" button on workflow detail page
2. Frontend navigates to /workflows/{workflowId}/edit
3. Frontend fetches workflow and attributes
4. Frontend generates form from editable attributes
5. Frontend pre-populates form with workflow values
6. User changes workflow name and status
7. User clicks "Save"
8. Frontend validates form (required fields, types)
9. Frontend submits: PUT /api/workflows/{workflowId} with form data
10. Backend validates and updates workflow
11. Frontend navigates back to detail page
12. User sees updated workflow
Dynamic Form Generation
// Fetch workflow and attributes
const [workflow, attributes] = await Promise.all([
fetch(`/api/workflows/${workflowId}`).then(r => r.json()),
fetch('/api/attributes/ChatWorkflow').then(r => r.json())
]);
// Filter editable fields
const editableFields = attributes.attributes.filter(attr =>
attr.visible && attr.editable
);
// Generate form fields
<DynamicForm
fields={editableFields}
initialValues={workflow}
onSubmit={async (formData) => {
try {
const response = await fetch(`/api/workflows/${workflowId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.status === 403) {
showError('You do not have permission to edit this workflow');
return;
}
if (!response.ok) {
const error = await response.json();
showError(error.detail);
return;
}
navigate(`/workflows/${workflowId}`);
} catch (error) {
showError('Failed to update workflow');
}
}}
/>
// DynamicForm component renders fields based on type
function DynamicForm({ fields, initialValues, onSubmit }) {
const [formData, setFormData] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
// Validate required fields
const newErrors = {};
fields.forEach(field => {
if (field.required && !formData[field.name]) {
newErrors[field.name] = `${field.label} is required`;
}
});
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
{fields.map(field => (
<FormField
key={field.name}
field={field}
value={formData[field.name]}
onChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
error={errors[field.name]}
/>
))}
<button type="submit">Save</button>
<button type="button" onClick={() => navigate(-1)}>Cancel</button>
</form>
);
}
Customer Journey 5: Managing Workflow Messages
User Goal
"I want to view, organize, and clean up messages in a workflow."
User Story
As a user, I want to see all messages in a workflow, view message details including attached files, and delete messages if needed.
User Story Flow
sequenceDiagram
participant User
participant Frontend
participant Backend
User->>Frontend: Open workflow detail page
Frontend->>Backend: GET /api/workflows/workflowId/messages
Backend-->>Frontend: Messages list
Frontend->>Frontend: Sort by publishedAt
Frontend->>Frontend: Render messages chronologically
Frontend-->>User: Display messages with files
User->>Frontend: Click delete button on message
Frontend->>Frontend: Show confirmation dialog
User->>Frontend: Confirm deletion
Frontend->>Backend: DELETE /api/workflows/workflowId/messages/messageId
Backend-->>Frontend: Success response
Frontend->>Frontend: Remove message from UI
Frontend-->>User: Updated message list
User->>Frontend: Click delete button on file
Frontend->>Frontend: Show confirmation dialog
User->>Frontend: Confirm deletion
Frontend->>Backend: DELETE /api/workflows/workflowId/messages/messageId/files/fileId
Backend-->>Frontend: Success response
Frontend->>Frontend: Remove file from message
Frontend-->>User: Updated message display
Frontend Requirements
Component: Message Management
What the user sees:
- List of all messages in chronological order
- Each message shows: role, content, timestamp, attached files
- Message actions: View details, Delete (if permitted)
- Pagination if many messages
What the frontend needs to do:
-
Fetch Messages
GET /api/workflows/{workflowId}/messages- Get all messages (or paginated if many)
- Support pagination for large message lists
-
Display Messages
- Show messages in chronological order (
publishedAt) - Display message role (user/assistant/system) with styling
- Show message content or summary
- Display attached documents/files if
documentsarray has items - Format timestamps as readable dates
- Show messages in chronological order (
-
Handle Message Deletion
DELETE /api/workflows/{workflowId}/messages/{messageId}- Show delete button/action for each message
- Show confirmation dialog before deletion
- After deletion: refresh message list or remove from UI
-
Handle File Deletion (within messages)
DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId}- Show delete button for each file attachment
- Show confirmation before deletion
- After deletion: update message display (remove file from list)
Backend Routes Used
| Route | Purpose | When Used |
|---|---|---|
GET /api/workflows/{workflowId}/messages |
Get all messages | Initial load, refresh |
DELETE /api/workflows/{workflowId}/messages/{messageId} |
Delete message | User action |
DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId} |
Delete file | User action |
User Interaction Flow
stateDiagram-v2
[*] --> ViewingMessages
ViewingMessages --> ViewingMessage: Click message
ViewingMessage --> ViewingFile: Click file
ViewingMessage --> DeletingMessage: Click delete
ViewingFile --> ViewingMessage: Close file view
DeletingMessage --> ConfirmDeleteMessage: Show dialog
ConfirmDeleteMessage --> ViewingMessages: Cancel
ConfirmDeleteMessage --> DeletingMessageAPI: Confirm
DeletingMessageAPI --> ViewingMessages: Success
ViewingMessage --> DeletingFile: Click delete file
DeletingFile --> ConfirmDeleteFile: Show dialog
ConfirmDeleteFile --> ViewingMessage: Cancel
ConfirmDeleteFile --> DeletingFileAPI: Confirm
DeletingFileAPI --> ViewingMessage: File removed
note right of ViewingMessages
Component: MessageList
- Chronological order
- Scrollable list
- Pagination support
end note
note right of ViewingMessage
Component: MessageCard
- Full message content
- Attached files
- Action buttons
end note
note right of DeletingMessageAPI
Updates UI immediately
Optimistic update pattern
end note
Example User Flow
1. User opens workflow detail page
2. Frontend fetches: GET /api/workflows/{workflowId}/messages
3. Frontend displays messages in chronological list
4. User sees a message with attached files
5. User clicks on a file to view it
6. User decides to delete a message
7. User clicks delete button → confirmation dialog appears
8. User confirms deletion
9. Frontend calls: DELETE /api/workflows/{workflowId}/messages/{messageId}
10. Frontend removes message from UI (or refreshes list)
11. User sees updated message list
Implementation Pattern
function MessageList({ workflowId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
fetch(`/api/workflows/${workflowId}/messages`)
.then(r => r.json())
.then(data => setMessages(data.items || data));
}, [workflowId]);
const handleDeleteMessage = async (messageId) => {
if (!confirm('Are you sure you want to delete this message?')) {
return;
}
try {
await fetch(`/api/workflows/${workflowId}/messages/${messageId}`, {
method: 'DELETE'
});
// Remove from UI
setMessages(prev => prev.filter(msg => msg.id !== messageId));
} catch (error) {
showError('Failed to delete message');
}
};
const handleDeleteFile = async (messageId, fileId) => {
if (!confirm('Are you sure you want to delete this file?')) {
return;
}
try {
await fetch(`/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`, {
method: 'DELETE'
});
// Update message in UI (remove file from documents array)
setMessages(prev => prev.map(msg =>
msg.id === messageId
? { ...msg, documents: msg.documents.filter(doc => doc.fileId !== fileId) }
: msg
));
} catch (error) {
showError('Failed to delete file');
}
};
return (
<div>
{messages.map(message => (
<MessageCard key={message.id} message={message}>
<MessageContent message={message} />
{message.documents?.map(doc => (
<FileAttachment
key={doc.fileId}
file={doc}
onDelete={() => handleDeleteFile(message.id, doc.fileId)}
/>
))}
<Button onClick={() => handleDeleteMessage(message.id)}>
Delete Message
</Button>
</MessageCard>
))}
</div>
);
}
Customer Journey 6: Cleaning Up Workflows
User Goal
"I want to delete workflows I no longer need."
User Story
As a user, I want to delete workflows that are completed or no longer useful, with a clear confirmation to prevent accidental deletion.
User Story Flow
sequenceDiagram
participant User
participant Frontend
participant Backend
User->>Frontend: Click "Delete" button
Frontend->>Backend: GET /api/workflows/workflowId
Backend-->>Frontend: Workflow data (for name)
Frontend->>Frontend: Show confirmation dialog<br/>"Delete 'Workflow Name'?"
User->>Frontend: Cancel deletion
Frontend-->>User: Dialog closed, no action
User->>Frontend: Click "Delete" button again
Frontend->>Backend: GET /api/workflows/workflowId
Backend-->>Frontend: Workflow data
Frontend->>Frontend: Show confirmation dialog
User->>Frontend: Confirm deletion
Frontend->>Backend: DELETE /api/workflows/workflowId
alt Permission denied (403)
Backend-->>Frontend: 403 Forbidden
Frontend-->>User: Show permission error
else Not found (404)
Backend-->>Frontend: 404 Not Found
Frontend-->>User: Show not found error
else Success (200)
Backend-->>Frontend: Success response
Frontend->>Frontend: Show success message
Frontend->>Frontend: Navigate to /workflows
Frontend-->>User: Display workflow list (without deleted workflow)
end
Frontend Requirements
Action: Delete Workflow
What the user sees:
- Delete button/action on workflow detail page or list
- Confirmation dialog before deletion
- Success message after deletion
- Redirect to workflow list after deletion
What the frontend needs to do:
-
Show Delete Action
- Display delete button (only if user has permission—backend will enforce)
- Can be on detail page or in list row actions
-
Handle Delete Confirmation
- Show confirmation dialog when user clicks delete
- Display workflow name in confirmation
- Warn about permanent deletion
-
Execute Deletion
DELETE /api/workflows/{workflowId}- Call delete endpoint
- Handle 403 (permission denied) gracefully
- Handle 404 (not found) gracefully
-
Handle Success
- Show success message
- If on detail page: redirect to workflow list
- If on list page: remove workflow from list (or refresh)
Backend Routes Used
| Route | Purpose | When Used |
|---|---|---|
DELETE /api/workflows/{workflowId} |
Delete workflow | User confirmation |
User Navigation Flow
stateDiagram-v2
[*] --> WorkflowDetailPage
WorkflowDetailPage --> ViewingDeleteButton: Hover delete button
ViewingDeleteButton --> ClickingDelete: Click delete
ClickingDelete --> FetchingWorkflowName: Fetch workflow name
FetchingWorkflowName --> ShowingConfirmation: Show dialog
ShowingConfirmation --> WorkflowDetailPage: Cancel
ShowingConfirmation --> ConfirmingDeletion: Confirm
ConfirmingDeletion --> DeletingWorkflow: API call
DeletingWorkflow --> WorkflowListPage: Success
DeletingWorkflow --> ShowingError: Error (403/404)
ShowingError --> WorkflowDetailPage: Dismiss error
note right of ShowingConfirmation
ConfirmationDialog Component:
- Workflow name
- Warning message
- Cancel/Confirm buttons
end note
note right of DeletingWorkflow
DELETE /api/workflows/{id}
- Backend deletes workflow
- Deletes all associated data
- Returns success response
end note
note right of WorkflowListPage
List automatically refreshes
Deleted workflow removed
Success message shown
end note
Example User Flow
1. User is on workflow detail page
2. User clicks "Delete" button
3. Frontend shows confirmation dialog: "Are you sure you want to delete 'Workflow Name'? This action cannot be undone."
4. User confirms deletion
5. Frontend calls: DELETE /api/workflows/{workflowId}
6. Backend deletes workflow and all associated data
7. Frontend receives success response
8. Frontend shows success message: "Workflow deleted successfully"
9. Frontend redirects to /workflows (list page)
10. User sees updated workflow list (without deleted workflow)
Implementation Pattern
function WorkflowDetailPage({ workflowId }) {
const navigate = useNavigate();
const handleDelete = async () => {
// Fetch workflow name for confirmation
const workflow = await fetch(`/api/workflows/${workflowId}`).then(r => r.json());
if (!confirm(`Are you sure you want to delete "${workflow.name || 'this workflow'}"? This action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/api/workflows/${workflowId}`, {
method: 'DELETE'
});
if (response.status === 403) {
showError('You do not have permission to delete this workflow');
return;
}
if (response.status === 404) {
showError('Workflow not found');
return;
}
if (!response.ok) {
const error = await response.json();
showError(error.detail || 'Failed to delete workflow');
return;
}
showSuccess('Workflow deleted successfully');
navigate('/workflows');
} catch (error) {
showError('Failed to delete workflow');
}
};
return (
<div>
{/* Workflow content */}
<Button onClick={handleDelete} variant="danger">
Delete Workflow
</Button>
</div>
);
}
Backend Metadata System
How Metadata Works
The backend serves all UI metadata through the /api/attributes/{entityType} endpoint. This enables completely dynamic frontend generation with no hardcoding.
Attribute Definition Structure
interface AttributeDefinition {
name: string; // Field name (e.g., "status", "name")
type: string; // Frontend type: "text", "select", "integer", "timestamp", "checkbox"
label: string; // Display label (localized to user's language)
description: string; // Field description
required: boolean; // Whether field is required
readonly: boolean; // Whether field is read-only
editable: boolean; // Whether field can be edited (inverse of readonly)
visible: boolean; // Whether field should be displayed
order: number; // Display order (lower = earlier)
placeholder: string; // Placeholder text for inputs
options?: Option[]; // Options for select fields (with localized labels)
default?: any; // Default value
}
interface Option {
value: string; // Option value (e.g., "running")
label: { // Localized labels
[language: string]: string; // e.g., {"en": "Running", "fr": "En cours"}
};
}
Fetching Metadata
// Fetch attribute definitions for ChatWorkflow
const response = await fetch('/api/attributes/ChatWorkflow');
const data = await response.json();
const attributes = data.attributes; // AttributeDefinition[]
Using Metadata for Forms
// Generate form fields from attributes
const formFields = attributes
.filter(attr => attr.visible && attr.editable) // Only editable, visible fields
.sort((a, b) => a.order - b.order) // Sort by order
.map(attr => ({
name: attr.name,
label: attr.label,
type: attr.type,
required: attr.required,
options: attr.options,
placeholder: attr.placeholder,
default: attr.default
}));
Using Metadata for Tables
// Generate table columns from attributes
const tableColumns = attributes
.filter(attr => attr.visible) // Only visible fields
.sort((a, b) => a.order - b.order) // Sort by order
.map(attr => ({
key: attr.name,
label: attr.label,
type: attr.type,
sortable: true, // Can be made configurable
render: (value) => formatValue(value, attr.type, attr.options)
}));
Field Type Handling
Different field types require different UI components:
| Type | UI Component | Example Values |
|---|---|---|
text |
Text input | "My Workflow" |
textarea |
Textarea | Long descriptions |
integer |
Number input | 5, 10, 100 |
select |
Dropdown | Options from options array |
checkbox |
Checkbox | true/false |
timestamp |
Date/time picker | Unix timestamp |
Localization
All labels and option labels support multiple languages. The backend serves labels in the user's language (determined by authentication context). The frontend should:
- Use
labeldirectly (already localized) - For select options, use
option.label[userLanguage]or fallback tooption.value
Implementation Patterns
Pattern 1: Dynamic Table Component
function DynamicTable({ entityType, data, pagination, onSort, onPageChange, onRowClick }) {
const [attributes, setAttributes] = useState([]);
useEffect(() => {
fetch(`/api/attributes/${entityType}`)
.then(r => r.json())
.then(data => setAttributes(data.attributes.filter(attr => attr.visible)));
}, [entityType]);
const columns = attributes
.sort((a, b) => a.order - b.order)
.map(attr => ({
key: attr.name,
label: attr.label,
type: attr.type,
render: (value) => formatValue(value, attr.type, attr.options)
}));
return (
<Table>
<thead>
<tr>
{columns.map(col => (
<th key={col.key} onClick={() => handleSort(col.key)}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id} onClick={() => onRowClick?.(row)}>
{columns.map(col => (
<td key={col.key}>{col.render(row[col.key])}</td>
))}
</tr>
))}
</tbody>
</Table>
);
}
Pattern 2: Dynamic Form Component
function DynamicForm({ entityType, initialValues, onSubmit, onCancel }) {
const [attributes, setAttributes] = useState([]);
const [formData, setFormData] = useState(initialValues || {});
const [errors, setErrors] = useState({});
useEffect(() => {
fetch(`/api/attributes/${entityType}`)
.then(r => r.json())
.then(data => {
const editable = data.attributes.filter(attr => attr.visible && attr.editable);
setAttributes(editable);
});
}, [entityType]);
const handleSubmit = (e) => {
e.preventDefault();
// Validate
const newErrors = {};
attributes.forEach(attr => {
if (attr.required && !formData[attr.name]) {
newErrors[attr.name] = `${attr.label} is required`;
}
});
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
{attributes
.sort((a, b) => a.order - b.order)
.map(attr => (
<FormField
key={attr.name}
field={attr}
value={formData[attr.name] ?? attr.default}
onChange={(value) => setFormData(prev => ({ ...prev, [attr.name]: value }))}
error={errors[attr.name]}
/>
))}
<button type="submit">Save</button>
{onCancel && <button type="button" onClick={onCancel}>Cancel</button>}
</form>
);
}
Pattern 3: Polling Hook
function usePolling(url, interval, condition = true) {
const [data, setData] = useState(null);
useEffect(() => {
if (!condition) return;
const fetchData = async () => {
try {
const response = await fetch(url);
if (response.ok) {
const result = await response.json();
setData(result);
}
} catch (error) {
console.error('Polling error:', error);
}
};
fetchData(); // Initial fetch
const intervalId = setInterval(fetchData, interval);
return () => clearInterval(intervalId);
}, [url, interval, condition]);
return data;
}
// Usage
const workflow = usePolling(
`/api/workflows/${workflowId}/status`,
5000,
workflow?.status === 'running'
);
Pattern 4: Incremental Data Loading
function useIncrementalData(baseUrl, lastIdKey) {
const [items, setItems] = useState([]);
const [lastId, setLastId] = useState(null);
const loadInitial = async () => {
const response = await fetch(baseUrl).then(r => r.json());
const newItems = response.items || response;
setItems(newItems);
if (newItems.length > 0) {
setLastId(newItems[newItems.length - 1][lastIdKey]);
}
};
const loadNew = async () => {
if (!lastId) return;
const url = `${baseUrl}?${lastIdKey}=${lastId}`;
const response = await fetch(url).then(r => r.json());
const newItems = response.items || response;
if (newItems.length > 0) {
setItems(prev => [...prev, ...newItems]);
setLastId(newItems[newItems.length - 1][lastIdKey]);
}
};
useEffect(() => {
loadInitial();
}, [baseUrl]);
return { items, loadNew };
}
// Usage for messages
const { items: messages, loadNew: loadNewMessages } = useIncrementalData(
`/api/workflows/${workflowId}/messages`,
'messageId'
);
// Poll for new messages
useEffect(() => {
if (workflow?.status !== 'running') return;
const interval = setInterval(loadNewMessages, 3000);
return () => clearInterval(interval);
}, [workflow?.status, loadNewMessages]);
Summary
This documentation focuses on customer journeys—what users want to accomplish with workflows. Each journey describes:
- User Goal: What the user is trying to achieve
- User Story: The scenario from the user's perspective
- Frontend Requirements: What pages/components are needed
- Backend Routes: Which API endpoints support the journey
- Example Flows: Step-by-step user interactions
- Implementation Patterns: Code examples for common patterns
The key principle throughout: Everything is dynamic and driven by backend metadata. The frontend should never hardcode field definitions, validation rules, labels, or UI structure. All of this comes from the backend's attribute definition system.
By following these customer journeys and using the backend metadata system, you can build a frontend that:
- Adapts automatically to backend changes
- Supports multiple languages
- Enforces permissions correctly
- Provides real-time updates
- Requires minimal maintenance