Operations & Admin
process workflow
How to Automate Multi-Step Approval Workflows
Build approval chains that route through the right people automatically.
Jay Banlasan
The AI Systems Guy
Manual approval chains stall because someone forgets to forward an email. I built a system to automate multi-step approval workflows that routes requests through the right people in the right order, sends reminders, escalates when stalled, and logs every decision.
No more "who has this request?" conversations.
What You Need Before Starting
- Python 3.8+
- SQLite for tracking requests
- Email or Slack for notifications
- Approval chain definitions per request type
Step 1: Define Approval Chains
APPROVAL_CHAINS = {
"expense_under_1000": {
"steps": [
{"role": "direct_manager", "action": "approve_or_reject"},
],
"timeout_hours": 48,
"escalation": "department_head"
},
"expense_over_1000": {
"steps": [
{"role": "direct_manager", "action": "approve_or_reject"},
{"role": "finance_director", "action": "approve_or_reject"},
],
"timeout_hours": 72,
"escalation": "cfo"
},
"new_vendor": {
"steps": [
{"role": "department_head", "action": "approve_or_reject"},
{"role": "legal", "action": "review_terms"},
{"role": "finance_director", "action": "approve_or_reject"},
],
"timeout_hours": 120,
"escalation": "coo"
}
}
Step 2: Create the Request Tracker
import sqlite3
from datetime import datetime
def init_approval_db(db_path="approvals.db"):
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS approval_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_type TEXT,
requester TEXT,
details TEXT,
current_step INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
created_at TEXT,
completed_at TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS approval_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER,
step_number INTEGER,
approver_email TEXT,
action TEXT,
comments TEXT,
acted_at TEXT
)
""")
conn.commit()
return conn
Step 3: Submit and Route Requests
def submit_request(conn, request_type, requester, details):
conn.execute(
"INSERT INTO approval_requests (request_type, requester, details, created_at) VALUES (?,?,?,?)",
(request_type, requester, details, datetime.now().isoformat())
)
conn.commit()
request_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
chain = APPROVAL_CHAINS.get(request_type)
if chain and chain["steps"]:
first_approver = chain["steps"][0]
notify_approver(first_approver["role"], request_id, details)
return request_id
def notify_approver(role, request_id, details):
print(f"Notifying {role} about request #{request_id}: {details[:100]}")
Step 4: Process Approvals and Advance
def process_approval(conn, request_id, approver_email, action, comments=""):
request = conn.execute(
"SELECT request_type, current_step, status FROM approval_requests WHERE id=?",
(request_id,)
).fetchone()
if request[2] != "pending":
return {"error": "Request is no longer pending"}
conn.execute(
"INSERT INTO approval_actions (request_id, step_number, approver_email, action, comments, acted_at) VALUES (?,?,?,?,?,?)",
(request_id, request[1], approver_email, action, comments, datetime.now().isoformat())
)
if action == "reject":
conn.execute(
"UPDATE approval_requests SET status='rejected', completed_at=? WHERE id=?",
(datetime.now().isoformat(), request_id)
)
elif action == "approve":
chain = APPROVAL_CHAINS[request[0]]
next_step = request[1] + 1
if next_step >= len(chain["steps"]):
conn.execute(
"UPDATE approval_requests SET status='approved', completed_at=? WHERE id=?",
(datetime.now().isoformat(), request_id)
)
else:
conn.execute(
"UPDATE approval_requests SET current_step=? WHERE id=?",
(next_step, request_id)
)
notify_approver(chain["steps"][next_step]["role"], request_id, "")
conn.commit()
return {"status": "processed", "action": action}
Step 5: Handle Timeouts and Escalation
from datetime import timedelta
def check_stalled_requests(conn):
rows = conn.execute(
"SELECT id, request_type, current_step, created_at FROM approval_requests WHERE status='pending'"
).fetchall()
for req_id, req_type, step, created in rows:
chain = APPROVAL_CHAINS[req_type]
created_dt = datetime.fromisoformat(created)
hours_waiting = (datetime.now() - created_dt).total_seconds() / 3600
if hours_waiting > chain["timeout_hours"]:
print(f"ESCALATING: Request #{req_id} stalled for {int(hours_waiting)}h")
notify_approver(chain["escalation"], req_id, "ESCALATED - timeout exceeded")
What to Build Next
Add a dashboard showing all pending approvals per person. Most approval bottlenecks come from one or two people who do not check their queue. Visibility fixes that faster than reminders.
Related Reading
- How to Audit Your Operations for AI Opportunities - finding workflow bottlenecks
- The Feedback Loop That Powers Everything - approval feedback loops
- Building Your First Automation: A Complete Guide - automation step by step
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment