Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
8.6 KiB
Inline Forms in Workflows
This guide demonstrates how to use inline forms in your workflows. Inline forms allow you to define form schemas directly within your workflow code, without needing to pre-register them separately.
Use Cases
Inline forms are ideal for:
- Ad-hoc forms that are only used in one workflow
- Dynamic forms with schema determined at runtime
- Prototyping and development where you want to iterate quickly
- Forms with a short lifecycle or single-use forms
Implementation Overview
The system supports inline forms with the following components:
- A database schema with an
is_temporaryflag on form definitions - A
createTaskWithInlineFormmethod that creates temporary form definitions on-the-fly - A
createInlineTaskAndWaitForResultcomposite action that creates a task and waits for its completion - A cleanup job that removes temporary forms periodically
When a task is created using an inline form, the system dynamically generates temporary, tenant-specific entries in the workflow_form_definitions, workflow_form_schemas, and workflow_task_definitions tables. The task instance in workflow_tasks then links to these temporary definitions. This allows the Task Inbox to discover and render the form using its standard mechanisms, as if it were a pre-registered form, while still allowing for ad-hoc form creation within workflows.
Example Usage
Basic Example: Creating a Task with Inline Form
// In your workflow definition
async function myWorkflow(context) {
// Create a task with an inline form definition
const createTaskResult = await context.actions.create_task_with_inline_form({
title: "Approve Service Request",
description: "Please review and approve this service request",
priority: "high",
dueDate: new Date(Date.now() + 86400000).toISOString(), // 1 day from now
assignTo: {
roles: ["approver"],
},
contextData: {
requestId: "REQ-12345",
serviceName: "Server Provisioning",
requestedBy: "john.doe@example.com"
},
form: {
jsonSchema: {
type: "object",
required: ["approved", "comments"],
properties: {
requestInfo: {
type: "string",
title: "Request Information",
default: "Service: ${contextData.serviceName}\nRequested by: ${contextData.requestedBy}",
readOnly: true
},
approved: {
type: "boolean",
title: "Approve Request",
default: false
},
comments: {
type: "string",
title: "Comments"
}
}
},
uiSchema: {
requestInfo: {
"ui:widget": "textarea",
"ui:options": {
rows: 3
}
},
comments: {
"ui:widget": "textarea",
"ui:options": {
rows: 5
}
}
}
},
formCategory: "approvals"
});
if (createTaskResult.success) {
console.log(`Task created with ID: ${createTaskResult.taskId}`);
// Continue with workflow logic...
// Note that the task is asynchronous - the workflow continues while the task is pending
}
}
Advanced Example: Creating a Task and Waiting for Result
// In your workflow definition
async function customerServiceWorkflow(context) {
try {
// Create a task with inline form and wait for its completion
const taskResult = await context.actions.createInlineTaskAndWaitForResult({
title: `Mapping Error for Product: ${context.state.productName}`,
description: 'Please resolve this mapping issue',
priority: 'high',
contextData: {
serviceId: context.state.serviceId,
errorDetails: context.state.errorMessage
},
form: {
jsonSchema: {
type: 'object',
properties: {
errorDetails: {
type: 'string',
title: 'Error Details',
readOnly: true,
default: '${contextData.errorMessage}'
},
resolution: {
type: 'string',
title: 'Resolution Notes'
},
resolved: {
type: 'boolean',
title: 'Mark as Resolved',
default: false
}
},
required: ['resolution', 'resolved']
},
uiSchema: {
errorDetails: {
'ui:widget': 'textarea',
'ui:readonly': true
},
resolution: {
'ui:widget': 'textarea'
}
}
},
waitForEventTimeoutMilliseconds: 3600000 // Optional: 1 hour timeout
});
if (taskResult.success) {
// Process the result data
const resolutionData = taskResult.resolutionData;
if (resolutionData.resolved) {
console.log(`Issue resolved: ${resolutionData.resolution}`);
await context.actions.update_ticket_status({
ticketId: context.state.ticketId,
status: 'resolved',
resolution: resolutionData.resolution
});
} else {
console.log('Issue marked as unresolved');
await context.actions.escalate_ticket({
ticketId: context.state.ticketId,
notes: resolutionData.resolution
});
}
} else {
// Handle error or timeout
console.error(`Task failed: ${taskResult.error}`);
await context.actions.log_error({
message: `Task failed: ${taskResult.error}`,
details: taskResult.details
});
}
} catch (error) {
console.error('Error in workflow execution:', error);
}
}
Dynamic Form Generation
One of the key benefits of inline forms is the ability to generate form schemas dynamically based on runtime data:
async function dynamicFormWorkflow(context) {
// Get data that will influence the form
const serviceData = await context.actions.get_service_data({
serviceId: context.state.serviceId
});
// Dynamically build form schema based on service fields
const formProperties = {
serviceId: {
type: 'string',
title: 'Service ID',
default: serviceData.id,
readOnly: true
},
serviceName: {
type: 'string',
title: 'Service Name',
default: serviceData.name,
readOnly: true
}
};
// Add fields based on service configuration
if (serviceData.hasScheduling) {
formProperties.scheduleDate = {
type: 'string',
format: 'date',
title: 'Schedule Date'
};
formProperties.scheduleTimeSlot = {
type: 'string',
title: 'Time Slot',
enum: serviceData.availableTimeSlots
};
}
if (serviceData.requiresApproval) {
formProperties.approvalNote = {
type: 'string',
title: 'Approval Notes'
};
}
// Create form with dynamically generated schema
const taskResult = await context.actions.createInlineTaskAndWaitForResult({
title: `Configure Service: ${serviceData.name}`,
assignTo: {
roles: ['service_manager']
},
form: {
jsonSchema: {
type: 'object',
required: serviceData.hasScheduling ? ['scheduleDate', 'scheduleTimeSlot'] : [],
properties: formProperties
}
}
});
// Process the result...
}
Form Cleanup
The system automatically cleans up temporary forms using a scheduled job that runs daily. You can also manually trigger cleanup if needed:
// Manually trigger cleanup in a workflow if needed
await context.actions.cleanup_temporary_forms({
tenant: context.tenant
});
Best Practices
-
Use inline forms for single-use cases: If a form will be reused across multiple workflows, consider registering it normally instead.
-
Keep form schemas modular: Focus on what the current task needs rather than creating large, complex forms.
-
Provide rich context data: Include all relevant information in the
contextDataproperty to make forms more useful. -
Set realistic timeouts: When using
createInlineTaskAndWaitForResult, set timeouts appropriate to your business process. -
Handle timeouts gracefully: Always check the
successflag and handle errors appropriately. -
Use proper form categories: Setting a meaningful
formCategoryhelps with organization and filtering in the UI.
Limitations
-
Inline forms are marked as temporary and will be cleaned up periodically.
-
They are only accessible within the workflow execution context and associated tasks.
-
They don't appear in the form registry for general use outside their specific tasks.
-
Changes to inline forms require workflow code updates; they can't be edited separately.