Implementation of Document Approval and Signing Workflow
An approval workflow is a multi-step process where a document sequentially or in parallel goes through approvers before final signing. It's not just an "Approve" button—it's a state machine with roles, timeouts, escalations, and complete audit logs.
Workflow Types
Sequential approval—each next approver sees the document only after previous approval. Used when order matters (manager → director → CEO).
Parallel approval—all approvers receive the document simultaneously. Document approved when all (or N of M) approve.
Mixed—combination: group of approvers in parallel, then final director signature.
Data Model
-- Workflow templates
CREATE TABLE workflow_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200),
description TEXT,
steps JSONB NOT NULL, -- Array of steps with configuration
created_by UUID REFERENCES users(id)
);
-- Workflow instance for specific document
CREATE TABLE workflow_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
template_id UUID REFERENCES workflow_templates(id),
document_id UUID REFERENCES documents(id),
initiator_id UUID REFERENCES users(id),
current_step INT DEFAULT 1,
status VARCHAR(50) DEFAULT 'in_progress', -- in_progress, approved, rejected, cancelled
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
-- Approval tasks
CREATE TABLE workflow_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
instance_id UUID REFERENCES workflow_instances(id),
step_number INT NOT NULL,
assignee_id UUID REFERENCES users(id),
assignee_role VARCHAR(100), -- Alternative to assignee_id for dynamic roles
task_type VARCHAR(50), -- 'approve', 'sign', 'review'
status VARCHAR(50) DEFAULT 'pending', -- pending, approved, rejected, delegated
comment TEXT,
due_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
completed_by_id UUID REFERENCES users(id) -- If delegated
);
State Machine Engine
class WorkflowEngine {
async processDecision(
taskId: string,
decision: 'approve' | 'reject' | 'request_changes',
comment: string,
userId: string
) {
const task = await db.workflowTasks.findOne(taskId, { include: 'instance.template' });
if (task.assigneeId !== userId) throw new Error('Not authorized');
await db.workflowTasks.update(taskId, {
status: decision,
comment,
completedAt: new Date(),
});
await auditLog.record({
action: `task.${decision}`,
userId,
taskId,
instanceId: task.instanceId,
});
switch (decision) {
case 'approve':
await this.onTaskApproved(task);
break;
case 'reject':
await this.onTaskRejected(task);
break;
case 'request_changes':
await this.returnToInitiator(task, comment);
break;
}
}
private async onTaskApproved(task: WorkflowTask) {
const instance = task.instance;
const template = JSON.parse(instance.template.steps);
const currentStep = template[task.stepNumber - 1];
// Parallel step: check if all approved
if (currentStep.type === 'parallel') {
const stepTasks = await db.workflowTasks.findAll({
instanceId: instance.id,
stepNumber: task.stepNumber,
});
const allApproved = stepTasks.every(t => t.status === 'approve');
const anyRejected = stepTasks.some(t => t.status === 'reject');
if (anyRejected) return this.onTaskRejected(task);
if (!allApproved) return; // Wait for others
}
// Move to next step
const nextStep = template[task.stepNumber]; // Next element
if (!nextStep) {
// All steps completed—workflow done
await this.completeWorkflow(instance.id);
} else {
await this.activateStep(instance.id, nextStep, task.stepNumber + 1);
}
}
private async activateStep(instanceId: string, step: WorkflowStep, stepNumber: number) {
await db.workflowInstances.update(instanceId, { currentStep: stepNumber });
const assignees = await this.resolveAssignees(step);
const dueAt = step.deadlineHours ? addHours(new Date(), step.deadlineHours) : null;
for (const assignee of assignees) {
const task = await db.workflowTasks.create({
instanceId,
stepNumber,
assigneeId: assignee.id,
taskType: step.taskType,
dueAt,
});
await notifyAssignee(assignee, task);
}
}
}
Delegation
An approver can delegate a task to another person:
async function delegateTask(taskId, delegateToId, reason, requesterId) {
const task = await db.workflowTasks.findByPk(taskId);
if (task.assigneeId !== requesterId) throw new Error('Not authorized');
// Close current task
await db.workflowTasks.update(taskId, {
status: 'delegated',
comment: `Delegated: ${reason}`,
completedAt: new Date(),
});
// Create new for delegate
await db.workflowTasks.create({
...task.toJSON(),
id: undefined,
assigneeId: delegateToId,
status: 'pending',
completedAt: null,
metadata: { delegatedFrom: task.assigneeId, reason },
});
await notifyDelegate(delegateToId, taskId);
}
Timeouts and Escalation
// Cron job: check overdue tasks every hour
async function processOverdueTasks() {
const overdueTasks = await db.workflowTasks.findAll({
status: 'pending',
dueAt: { lt: new Date() },
escalationSentAt: null,
});
for (const task of overdueTasks) {
const step = getStepConfig(task);
if (step.escalationUserId) {
// Notify manager
await notifyEscalation(step.escalationUserId, task);
await db.workflowTasks.update(task.id, { escalationSentAt: new Date() });
}
if (step.autoApproveOnTimeout) {
await workflowEngine.processDecision(task.id, 'approve', 'Auto-approved on timeout', 'system');
}
}
}
Progress Visualization
Timeline workflow for initiator: which step completed, who approved, who hasn't responded yet, how long we're waiting. React component with vertical timeline, status icons (✓, ✗, ⏳) and tooltips with comments.
Notifications
| Event | Who | Urgency |
|---|---|---|
| Task assigned | Approver | Immediate |
| 4h to deadline | Approver | Push + Email |
| Task overdue | Approver + escalation | |
| Document approved | Initiator | In-app + Email |
| Document rejected | Initiator | Immediate, all channels |
Timeline
Basic workflow engine with sequential approval, tasks, notifications—7–10 days. Parallel approval, delegation, escalation, timeouts—another 5–7 days. Visual workflow template builder—7–10 days.







