PSA/docs/workflow/inline-form-example.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

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:

  1. A database schema with an is_temporary flag on form definitions
  2. A createTaskWithInlineForm method that creates temporary form definitions on-the-fly
  3. A createInlineTaskAndWaitForResult composite action that creates a task and waits for its completion
  4. 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

  1. Use inline forms for single-use cases: If a form will be reused across multiple workflows, consider registering it normally instead.

  2. Keep form schemas modular: Focus on what the current task needs rather than creating large, complex forms.

  3. Provide rich context data: Include all relevant information in the contextData property to make forms more useful.

  4. Set realistic timeouts: When using createInlineTaskAndWaitForResult, set timeouts appropriate to your business process.

  5. Handle timeouts gracefully: Always check the success flag and handle errors appropriately.

  6. Use proper form categories: Setting a meaningful formCategory helps with organization and filtering in the UI.

Limitations

  1. Inline forms are marked as temporary and will be cleaned up periodically.

  2. They are only accessible within the workflow execution context and associated tasks.

  3. They don't appear in the form registry for general use outside their specific tasks.

  4. Changes to inline forms require workflow code updates; they can't be edited separately.