Skip to content

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.

  1. 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
    },
    );
  2. 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.all with flow.dialog for 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 approver
    schema: v.object({ approved: v.boolean(), comments: v.optional(v.string()) }),
    },
    ({ token }) => ({
    params: {
    form: 'document-approval-form', // UI identifier
    documentId: flow.params.documentId,
    approverId,
    token, // Token for this specific approver's dialog
    },
    assignees: [approverId], // Assign this dialog to the specific approver
    message: `Approval required for document ${flow.params.documentId}`,
    // Individual dialog timeout (optional, overall timeout handled by Promise.race)
    }),
    ),
    );
  3. Wait for All Responses with Overall Timeout

    Use Promise.race to 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
    ]);
  4. 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 timeout
    sendTimeoutNotification(flow.params.documentId);
    });
    return { documentId: flow.params.documentId, status: 'APPROVAL_TIMEOUT' };
    } else {
    // approvalOutcome is an array of responses if not TIMEOUT
    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');
    // Notify originator of approval
    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');
    // Notify originator of rejection
    sendApprovalNotification(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,
};
}
}
},
);