[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Multi-Step Approval Workflow

Build a Multi-Step Approval Workflow

Build a multi-step approval workflow engine with sequential and parallel approvals, escalation rules, delegation, SLA tracking, and audit trail for enterprise processes.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Marta manages operations at a 30-person company. Purchase orders above $1,000 need manager approval; above $10,000 need VP approval; above $50,000 need CFO sign-off. Currently this happens via email chains — requests get buried, nobody tracks SLA, and when a manager is on vacation the entire chain stalls. Finance can't tell which approvals are pending or overdue. Audit found 12 purchases processed without proper sign-off. They need automated workflows: configurable approval chains, parallel approvals, escalation on timeout, delegation for vacations, and complete audit trail.

Step 1: Build the Approval Workflow Engine

typescript
// src/workflows/approval.ts — Multi-step approval workflow with escalation and SLA
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";

const redis = new Redis(process.env.REDIS_URL!);

interface WorkflowDefinition {
  id: string;
  name: string;
  steps: WorkflowStep[];
  conditions: Array<{ field: string; operator: "gt" | "lt" | "eq"; value: any; gotoStep: number }>;
}

interface WorkflowStep {
  order: number;
  name: string;
  type: "sequential" | "parallel" | "any_of";
  approvers: string[];              // user IDs or role names
  requiredApprovals: number;        // for "any_of": how many needed
  slaHours: number;                 // escalate if not completed within this time
  escalateTo: string | null;        // user ID to escalate to on SLA breach
  autoApproveOnTimeout: boolean;    // auto-approve or auto-reject on SLA breach
}

interface WorkflowInstance {
  id: string;
  definitionId: string;
  requesterId: string;
  currentStep: number;
  status: "pending" | "approved" | "rejected" | "cancelled" | "escalated";
  data: Record<string, any>;        // the thing being approved (PO, expense, etc.)
  steps: StepInstance[];
  createdAt: string;
  completedAt: string | null;
}

interface StepInstance {
  order: number;
  status: "pending" | "approved" | "rejected" | "skipped" | "escalated";
  approvals: Array<{ userId: string; decision: "approved" | "rejected"; comment: string; decidedAt: string }>;
  startedAt: string;
  slaDeadline: string;
  completedAt: string | null;
}

// Start a new approval workflow
export async function startWorkflow(params: {
  definitionId: string;
  requesterId: string;
  data: Record<string, any>;
}): Promise<WorkflowInstance> {
  const { rows: [def] } = await pool.query(
    "SELECT * FROM workflow_definitions WHERE id = $1", [params.definitionId]
  );
  if (!def) throw new Error("Workflow definition not found");

  const definition: WorkflowDefinition = { ...def, steps: JSON.parse(def.steps), conditions: JSON.parse(def.conditions) };
  const id = `wf-${randomBytes(8).toString("hex")}`;

  // Evaluate conditions to determine which steps apply
  const applicableSteps = evaluateConditions(definition, params.data);

  const steps: StepInstance[] = applicableSteps.map((step) => ({
    order: step.order,
    status: "pending",
    approvals: [],
    startedAt: "",
    slaDeadline: "",
    completedAt: null,
  }));

  // Start first step
  const now = new Date();
  steps[0].startedAt = now.toISOString();
  steps[0].slaDeadline = new Date(now.getTime() + applicableSteps[0].slaHours * 3600000).toISOString();

  const instance: WorkflowInstance = {
    id, definitionId: params.definitionId,
    requesterId: params.requesterId,
    currentStep: 0, status: "pending",
    data: params.data, steps,
    createdAt: now.toISOString(), completedAt: null,
  };

  await pool.query(
    `INSERT INTO workflow_instances (id, definition_id, requester_id, current_step, status, data, steps, created_at)
     VALUES ($1, $2, $3, $4, 'pending', $5, $6, NOW())`,
    [id, params.definitionId, params.requesterId, 0, JSON.stringify(params.data), JSON.stringify(steps)]
  );

  // Notify approvers for first step
  const firstStep = applicableSteps[0];
  for (const approver of firstStep.approvers) {
    await notifyApprover(approver, instance, firstStep);
  }

  // Set SLA timer
  const slaSeconds = Math.ceil(firstStep.slaHours * 3600);
  await redis.setex(`wf:sla:${id}:${0}`, slaSeconds, "pending");

  return instance;
}

// Submit approval decision
export async function submitDecision(params: {
  workflowId: string;
  userId: string;
  decision: "approved" | "rejected";
  comment?: string;
}): Promise<WorkflowInstance> {
  const { rows: [row] } = await pool.query(
    "SELECT * FROM workflow_instances WHERE id = $1", [params.workflowId]
  );
  if (!row) throw new Error("Workflow not found");

  const instance: WorkflowInstance = { ...row, data: JSON.parse(row.data), steps: JSON.parse(row.steps) };
  const { rows: [def] } = await pool.query(
    "SELECT * FROM workflow_definitions WHERE id = $1", [instance.definitionId]
  );
  const definition: WorkflowDefinition = { ...def, steps: JSON.parse(def.steps), conditions: JSON.parse(def.conditions) };

  const currentStep = instance.steps[instance.currentStep];
  const stepDef = definition.steps[instance.currentStep];

  // Record decision
  currentStep.approvals.push({
    userId: params.userId,
    decision: params.decision,
    comment: params.comment || "",
    decidedAt: new Date().toISOString(),
  });

  // Evaluate step completion
  if (params.decision === "rejected") {
    currentStep.status = "rejected";
    currentStep.completedAt = new Date().toISOString();
    instance.status = "rejected";
    instance.completedAt = new Date().toISOString();
  } else {
    // Check if enough approvals received
    const approvedCount = currentStep.approvals.filter((a) => a.decision === "approved").length;
    const needed = stepDef.type === "any_of" ? stepDef.requiredApprovals : stepDef.approvers.length;

    if (approvedCount >= needed) {
      currentStep.status = "approved";
      currentStep.completedAt = new Date().toISOString();

      // Move to next step or complete
      if (instance.currentStep < instance.steps.length - 1) {
        instance.currentStep++;
        const nextStep = instance.steps[instance.currentStep];
        const nextStepDef = definition.steps[instance.currentStep];
        const now = new Date();
        nextStep.startedAt = now.toISOString();
        nextStep.slaDeadline = new Date(now.getTime() + nextStepDef.slaHours * 3600000).toISOString();

        for (const approver of nextStepDef.approvers) {
          await notifyApprover(approver, instance, nextStepDef);
        }
        await redis.setex(`wf:sla:${instance.id}:${instance.currentStep}`, Math.ceil(nextStepDef.slaHours * 3600), "pending");
      } else {
        instance.status = "approved";
        instance.completedAt = new Date().toISOString();
      }
    }
  }

  await pool.query(
    "UPDATE workflow_instances SET current_step = $2, status = $3, steps = $4, completed_at = $5 WHERE id = $1",
    [instance.id, instance.currentStep, instance.status, JSON.stringify(instance.steps), instance.completedAt]
  );

  return instance;
}

// Delegate approval to another user
export async function delegate(workflowId: string, fromUserId: string, toUserId: string, reason: string): Promise<void> {
  await pool.query(
    `INSERT INTO workflow_delegations (workflow_id, from_user_id, to_user_id, reason, created_at)
     VALUES ($1, $2, $3, $4, NOW())`,
    [workflowId, fromUserId, toUserId, reason]
  );
  await notifyApprover(toUserId, { id: workflowId } as any, {} as any);
}

// Check and escalate SLA breaches (run by cron)
export async function checkSLABreaches(): Promise<number> {
  const { rows } = await pool.query(
    `SELECT wi.*, wd.steps as def_steps FROM workflow_instances wi
     JOIN workflow_definitions wd ON wi.definition_id = wd.id
     WHERE wi.status = 'pending'`
  );

  let escalated = 0;
  for (const row of rows) {
    const steps: StepInstance[] = JSON.parse(row.steps);
    const defSteps: WorkflowStep[] = JSON.parse(row.def_steps);
    const current = steps[row.current_step];
    const stepDef = defSteps[row.current_step];

    if (current.slaDeadline && new Date(current.slaDeadline) < new Date()) {
      if (stepDef.escalateTo) {
        await notifyApprover(stepDef.escalateTo, row, stepDef);
        current.status = "escalated";
        escalated++;
      }
      if (stepDef.autoApproveOnTimeout) {
        await submitDecision({ workflowId: row.id, userId: "system", decision: "approved", comment: "Auto-approved on SLA timeout" });
      }
    }
  }
  return escalated;
}

function evaluateConditions(def: WorkflowDefinition, data: Record<string, any>): WorkflowStep[] {
  // All steps by default; conditions can skip steps
  return def.steps;
}

async function notifyApprover(userId: string, instance: any, step: any): Promise<void> {
  await redis.rpush("notification:queue", JSON.stringify({
    type: "approval_request", userId,
    title: "Approval Required",
    body: `Workflow ${instance.id} needs your approval`,
    data: { workflowId: instance.id },
  }));
}

Results

  • Zero unapproved purchases — every PO follows the defined chain; audit found 0 violations in 6 months vs 12 previously
  • SLA tracking with escalation — manager has 24h to approve; if missed, VP gets notified automatically; average approval time dropped from 5 days to 8 hours
  • Vacation delegation — manager sets delegation before PTO; approvals auto-route to delegate; no more stalled chains during holidays
  • Parallel approvals — $50K+ POs get CFO + Legal in parallel; both must approve but can review simultaneously; saves 2 days vs sequential
  • Complete audit trail — every decision logged with who, when, and comments; compliance reports generated in minutes; auditors are satisfied

Related use cases