Document Approval Workflow
This example illustrates how to implement a document approval workflow that involves multiple approvers, waits for their responses with a timeout, and processes the outcomes.
-
Define Workflow and Input
The workflow will take a document ID and a list of approver IDs as input.
import * as v from '@identity-flow/sdk/valibot';import { NonRetryableError, defineWorkflow } from '@identity-flow/sdk';const ApprovalInputSchema = v.object({documentId: v.string(),approverIds: v.array(v.string([v.minLength(1)])),// You might also include the document content or a reference to it});export default defineWorkflow('document-approval-example',{ schema: ApprovalInputSchema },async (flow) => {// Workflow steps will go here},); -
Submit Document & Request Approvals (Parallel)
The first step might be to formally log the document submission. Then, trigger approval requests to all listed approvers concurrently using
Promise.allwithflow.dialogfor each.// Inside the async (flow) => { ... }await flow.do('log document submission', async () => {// Log or update document status to 'Pending Approval'logDocumentEvent(flow.params.documentId, 'SUBMITTED_FOR_APPROVAL');return { submissionLogged: true };});flow.log('Requesting approvals for document:', flow.params.documentId);const approvalDialogPromises = flow.params.approverIds.map((approverId) =>flow.dialog(`request approval from ${approverId} for doc ${flow.params.documentId}`,{// Schema for the expected response from each approverschema: v.object({ approved: v.boolean(), comments: v.optional(v.string()) }),},({ token }) => ({params: {form: 'document-approval-form', // UI identifierdocumentId: flow.params.documentId,approverId,token, // Token for this specific approver's dialog},assignees: [approverId], // Assign this dialog to the specific approvermessage: `Approval required for document ${flow.params.documentId}`,// Individual dialog timeout (optional, overall timeout handled by Promise.race)}),),); -
Wait for All Responses with Overall Timeout
Use
Promise.raceto wait for either all individual dialogs (Promise.all(approvalDialogPromises)) to complete or an overall timeout (flow.sleep) to occur.// Inside the async (flow) => { ... }flow.log('Waiting for approvals with a 24-hour timeout...');const approvalOutcome = await Promise.race([Promise.all(approvalDialogPromises),flow.sleep('overall approval timeout', '24 hours').then(() => 'TIMEOUT'), // Distinguish timeout]); -
Process Approval Outcome
Check if the outcome was a timeout or the collected approval responses. Based on the responses, mark the document as approved or rejected.
// Inside the async (flow) => { ... }if (approvalOutcome === 'TIMEOUT') {flow.warn('Approval timed out for document:', flow.params.documentId);await flow.do('handle approval timeout', async () => {logDocumentEvent(flow.params.documentId, 'APPROVAL_TIMEOUT');// Notify admin or originator about the timeoutsendTimeoutNotification(flow.params.documentId);});return { documentId: flow.params.documentId, status: 'APPROVAL_TIMEOUT' };} else {// approvalOutcome is an array of responses if not TIMEOUTconst allApproved = approvalOutcome.every(response => response.approved);flow.log(`Approvals received for document ${flow.params.documentId}. Overall status: ${allApproved ? 'APPROVED' : 'REJECTED'}`);if (allApproved) {await flow.do('mark document approved', async () => {updateDocumentStatus(flow.params.documentId, 'APPROVED');// Notify originator of approvalsendApprovalNotification(flow.params.documentId, 'APPROVED', approvalOutcome);});return { documentId: flow.params.documentId, status: 'APPROVED', responses: approvalOutcome };} else {await flow.do('mark document rejected', async () => {updateDocumentStatus(flow.params.documentId, 'REJECTED');// Notify originator of rejectionsendApprovalNotification(flow.params.documentId, 'REJECTED', approvalOutcome);});return { documentId: flow.params.documentId, status: 'REJECTED', responses: approvalOutcome };}}
import * as v from '@identity-flow/sdk/valibot';import { NonRetryableError, defineWorkflow } from '@identity-flow/sdk';
// --- Mock external functions for demonstration ---async function logDocumentEvent(documentId: string, eventType: string) { console.log(`Logging event for document ${documentId}: ${eventType}`);}async function updateDocumentStatus(documentId: string, status: string) { console.log(`Updating document ${documentId} status to: ${status}`);}async function sendTimeoutNotification(documentId: string) { console.log(`Sending approval timeout notification for document ${documentId}`);}async function sendApprovalNotification(documentId: string, status: string, responses: any) { console.log( `Sending approval notification for document ${documentId}. Status: ${status}. Responses:`, responses, );}// --- End mock functions ---
const ApprovalInputSchema = v.object({ documentId: v.string(), approverIds: v.array(v.string([v.minLength(1)])),});
export default defineWorkflow( 'document-approval-example', { schema: ApprovalInputSchema }, async (flow) => { flow.log( 'Starting document approval workflow for:', flow.params.documentId, 'Approvers:', flow.params.approverIds, );
await flow.do('log document submission', async () => { logDocumentEvent(flow.params.documentId, 'SUBMITTED_FOR_APPROVAL'); return { submissionLogged: true }; });
flow.log('Requesting approvals for document:', flow.params.documentId); const approvalDialogPromises = flow.params.approverIds.map((approverId) => flow.dialog( `request approval from ${approverId} for doc ${flow.params.documentId}`, { schema: v.object({ approved: v.boolean(), comments: v.optional(v.string()) }) }, ({ token }) => ({ params: { form: 'document-approval-form', documentId: flow.params.documentId, approverId, token, }, assignees: [approverId], message: `Approval required for document ${flow.params.documentId}`, }), ), );
flow.log('Waiting for approvals with a 24-hour timeout...'); const approvalOutcome = await Promise.race([ Promise.all(approvalDialogPromises), flow.sleep('overall approval timeout', '5 seconds').then(() => 'TIMEOUT'), // Shortened for demo ]);
if (approvalOutcome === 'TIMEOUT') { flow.warn('Approval timed out for document:', flow.params.documentId); await flow.do('handle approval timeout', async () => { logDocumentEvent(flow.params.documentId, 'APPROVAL_TIMEOUT'); sendTimeoutNotification(flow.params.documentId); }); return { documentId: flow.params.documentId, status: 'APPROVAL_TIMEOUT' }; } else { const allApproved = approvalOutcome.every((response) => response.approved); flow.log( `Approvals received for document ${flow.params.documentId}. Overall status: ${allApproved ? 'APPROVED' : 'REJECTED'}`, );
if (allApproved) { await flow.do('mark document approved', async () => { updateDocumentStatus(flow.params.documentId, 'APPROVED'); sendApprovalNotification(flow.params.documentId, 'APPROVED', approvalOutcome); }); return { documentId: flow.params.documentId, status: 'APPROVED', responses: approvalOutcome, }; } else { await flow.do('mark document rejected', async () => { updateDocumentStatus(flow.params.documentId, 'REJECTED'); sendApprovalNotification(flow.params.documentId, 'REJECTED', approvalOutcome); }); return { documentId: flow.params.documentId, status: 'REJECTED', responses: approvalOutcome, }; } } },);