As AI-powered automation takes center stage in modern business processes, ensuring responsible oversight is more critical than ever. Human-in-the-loop (HITL) approval mechanisms allow organizations to combine the speed of automation with the judgment and compliance of human reviewers. In this tutorial, we’ll walk you through a practical, reproducible approach to embedding HITL approval into your automated workflows using state-of-the-art tools and best practices for 2026.
For a broader context on this topic and how it fits into the evolution of workflow automation, see our Ultimate Guide to Automating Approval Workflows with AI in 2026. Here, we’ll focus on the nuts and bolts of implementing human-in-the-loop AI workflows, providing hands-on steps, code, and troubleshooting tips.
Prerequisites
- Technical Skills: Familiarity with Python (3.10+), REST APIs, and basic workflow automation concepts.
- Tools & Services:
- Python 3.10 or higher
- FastAPI (0.110+)
- Celery (5.3+)
- Redis (7.0+)
- PostgreSQL (14+)
- ngrok (for local webhook testing)
- Slack (or Microsoft Teams) for human approval interface
- Optional: OpenAI API or other LLM provider for AI decisioning
- Accounts: Access to a Slack workspace and API credentials for your chosen LLM provider.
- Environment: Unix-like OS (Linux/macOS recommended), with
pipandvirtualenvinstalled.
1. Define Your Approval Workflow and HITL Requirements
-
Map the Workflow:
- Identify the process you want to automate (e.g., expense approvals, procurement, onboarding).
- Determine which decision points require human oversight (e.g., amounts over $5,000, ambiguous LLM outputs, regulatory triggers).
-
Specify HITL Triggers:
- When should the workflow pause for human review?
- What data should be presented to the reviewer?
- What actions can the reviewer take (approve, reject, request more info)?
-
Document Requirements:
- Write a simple specification, e.g.:
Expense approval workflow: - LLM auto-approves expenses < $1,000 - HITL required for $1,000–$10,000 or if LLM confidence < 95% - Human reviewer receives request in Slack, can Approve/Reject/Comment - All decisions logged to PostgreSQL
For inspiration, review How to Automate Workflow Approval Loops with Custom AI Agents (Step-by-Step, 2026), which covers agent-based approaches to workflow automation.
2. Set Up Your Development Environment
-
Create a Python Virtual Environment:
python3 -m venv hitl-env source hitl-env/bin/activate
-
Install Required Packages:
pip install fastapi uvicorn celery[redis] psycopg2-binary slack_sdk openai pydantic sqlalchemy python-dotenv
-
Start Redis and PostgreSQL:
- Install Redis and PostgreSQL via your OS package manager if not already present.
redis-server brew services start postgresql
-
Configure Environment Variables:
- Create a
.envfile in your project root with:
OPENAI_API_KEY=sk-... SLACK_BOT_TOKEN=xoxb-... SLACK_SIGNING_SECRET=... DATABASE_URL=postgresql://user:password@localhost:5432/hitl_demo
- Create a
3. Build the Core Workflow Automation with FastAPI and Celery
-
Set Up the FastAPI App:
- Create
main.py:
from fastapi import FastAPI, Request from pydantic import BaseModel from celery import Celery import os app = FastAPI() celery_app = Celery("tasks", broker="redis://localhost:6379/0") class ExpenseRequest(BaseModel): employee: str amount: float description: str @app.post("/submit-expense/") async def submit_expense(expense: ExpenseRequest): task = celery_app.send_task("tasks.process_expense", args=[expense.dict()]) return {"task_id": task.id, "status": "submitted"} - Create
-
Define the Celery Task (AI + HITL Logic):
- Create
tasks.py:
from celery import Celery import openai import os from slack_sdk import WebClient celery_app = Celery("tasks", broker="redis://localhost:6379/0") openai.api_key = os.getenv("OPENAI_API_KEY") slack_client = WebClient(token=os.getenv("SLACK_BOT_TOKEN")) def ai_decision(expense): prompt = (f"Should the following expense be approved? " f"Amount: ${expense['amount']}, Description: {expense['description']}") response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], temperature=0.2 ) answer = response["choices"][0]["message"]["content"] confidence = 0.98 # Simulated; parse from answer if your LLM supports confidence return answer, confidence @celery_app.task(name="tasks.process_expense") def process_expense(expense): answer, confidence = ai_decision(expense) if expense["amount"] < 1000 and confidence > 0.95 and "approve" in answer.lower(): # Auto-approve log_decision(expense, "auto-approved", answer, confidence) notify_user(expense, "auto-approved") else: # HITL required send_slack_approval(expense, answer, confidence) def send_slack_approval(expense, ai_answer, confidence): slack_client.chat_postMessage( channel="#approvals", text=(f"Expense approval needed:\n" f"Employee: {expense['employee']}\n" f"Amount: ${expense['amount']}\n" f"Description: {expense['description']}\n" f"AI Suggestion: {ai_answer} (confidence: {confidence*100:.1f}%)\n" f"Please reply with Approve/Reject/Comment.") ) def log_decision(expense, status, ai_answer, confidence): # Write to PostgreSQL (implement as needed) pass def notify_user(expense, status): # Notify the submitter via Slack/email pass - Create
-
Start the API and Celery Worker:
uvicorn main:app --reload celery -A tasks worker --loglevel=info
-
Test the API Endpoint:
curl -X POST http://localhost:8000/submit-expense/ \ -H "Content-Type: application/json" \ -d '{"employee": "Jane Doe", "amount": 2500, "description": "Conference travel"}'You should see a message posted to your Slack
#approvalschannel for human review if the amount or confidence triggers HITL.
4. Implement the Human Approval Response Handler
-
Create a Slack Event Webhook:
- In your Slack app settings, add a Request URL (e.g.,
https://your-ngrok-url/slack/events). - Subscribe to
message.channelsevent.
- In your Slack app settings, add a Request URL (e.g.,
-
Expose Your Local Server with ngrok:
ngrok http 8000
- Copy the HTTPS URL and set it as your Slack request URL.
-
Add the Slack Event Handler in FastAPI:
- Update
main.py:
from fastapi import APIRouter, Request, BackgroundTasks from slack_sdk.signature import SignatureVerifier router = APIRouter() signature_verifier = SignatureVerifier(os.getenv("SLACK_SIGNING_SECRET")) @router.post("/slack/events") async def slack_events(request: Request, background_tasks: BackgroundTasks): body = await request.body() if not signature_verifier.is_valid_request(body, request.headers): return {"ok": False} event = await request.json() # Slack URL verification challenge if event.get("type") == "url_verification": return {"challenge": event["challenge"]} # Handle message in #approvals if event.get("event", {}).get("type") == "message": text = event["event"]["text"].lower() user = event["event"]["user"] if "approve" in text: background_tasks.add_task(handle_decision, event, "approved") elif "reject" in text: background_tasks.add_task(handle_decision, event, "rejected") return {"ok": True} def handle_decision(event, decision): # Parse expense from context, update DB, notify user, etc. pass app.include_router(router) - Update
-
Test Human Approval:
- When a HITL expense appears in
#approvals, reply with "Approve" or "Reject". - Check that the workflow records the decision and continues processing (e.g., notifies the submitter, updates DB).
- When a HITL expense appears in
5. Persist Decisions and Audit Trails in PostgreSQL
-
Define the Database Model:
- Create
models.py:
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker import os import datetime DATABASE_URL = os.getenv("DATABASE_URL") engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(bind=engine) Base = declarative_base() class ExpenseDecision(Base): __tablename__ = "expense_decisions" id = Column(Integer, primary_key=True, index=True) employee = Column(String) amount = Column(Float) description = Column(String) status = Column(String) decision_by = Column(String) ai_answer = Column(String) confidence = Column(Float) timestamp = Column(DateTime, default=datetime.datetime.utcnow) def init_db(): Base.metadata.create_all(bind=engine) - Create
-
Log Decisions in Your Workflow:
- Update your
log_decisionfunction intasks.py:
from models import SessionLocal, ExpenseDecision def log_decision(expense, status, ai_answer, confidence, decision_by="AI"): db = SessionLocal() record = ExpenseDecision( employee=expense["employee"], amount=expense["amount"], description=expense["description"], status=status, decision_by=decision_by, ai_answer=ai_answer, confidence=confidence ) db.add(record) db.commit() db.close() - Update your
-
Initialize the Database:
python >>> from models import init_db >>> init_db()
-
Verify Audit Trail:
- Query the
expense_decisionstable to confirm all approvals (AI and human) are logged.
- Query the
6. (Optional) Enhance with LLM Prompt Engineering and Advanced HITL Triggers
-
Refine LLM Prompts:
- Use advanced prompt engineering to improve AI decision quality and confidence estimation.
- See Prompt Engineering for Approval Workflows: Templates & Real-World Examples for inspiration.
-
Add Custom HITL Triggers:
- Trigger HITL on ambiguous language, missing documentation, or policy exceptions using LLM outputs.
- Route approvals to specific reviewers based on business rules.
-
Integrate with Other Platforms:
- Swap Slack for Microsoft Teams or custom web dashboards as needed.
For a deep dive into prompt design and generative AI in approval workflows, check out Generative AI Prompt Engineering for Approval Workflow Automation.
Common Issues & Troubleshooting
-
Slack Events Not Received:
- Check your ngrok tunnel is active and the URL matches your Slack app configuration.
- Verify that your FastAPI endpoint is running and accessible.
- Confirm that your Slack bot has permission to read messages in the
#approvalschannel.
-
Celery Tasks Not Executing:
- Ensure Redis is running and accessible at the configured URL.
- Check Celery worker logs for errors.
- Verify that your tasks are properly registered in
tasks.py.
-
Database Errors:
- Confirm your
DATABASE_URLis correct and PostgreSQL is running. - Run
init_db()to create tables if they’re missing.
- Confirm your
-
LLM API Issues:
- Check your OpenAI (or other provider) API key and quota.
- Handle API downtime or rate limits gracefully in your Celery tasks.
-
Security & Compliance:
- Never expose secrets in code or logs. Use environment variables and secure vaults.
- Review Security & Compliance Risks in Automated Approval Workflows: How to Mitigate in 2026 for best practices.
Next Steps
- Productionize: Harden your endpoints, add authentication, and deploy using Docker/Kubernetes.
- Expand Integrations: Connect to enterprise workflow tools (e.g., ServiceNow, SAP, Jira) and HR/finance systems.
- Monitor & Audit: Build dashboards for real-time visibility into AI and human approval decisions.
- Optimize: Use analytics to identify bottlenecks and further automate low-risk decisions.
- Stay Informed: Track regulatory trends by reading New US FTC Guidance on Automated AI Approval Workflows—What Every Enterprise Must Know.
- Further Reading: For industry-specific value cases, see The Business Value of Human-in-the-Loop AI Workflows for Regulated Industries.
Summary
Adding human-in-the-loop approval to automated workflows is no longer optional—it's essential for trust, compliance, and operational resilience in AI-driven enterprises. By following the blueprint above, you can build robust, auditable, and flexible HITL workflows that balance automation with human judgment.
For a comprehensive overview of AI-powered approval automation, revisit our Ultimate Guide to Automating Approval Workflows with AI in 2026.