# What You’ll Build#
A solid n8n approval workflow does more than wait for someone to click approve. In production you need timeouts, reminders, escalation, and an audit trail that survives retries, duplicate clicks, and channel switching.
In this guide you’ll implement:
- Slack or Microsoft Teams approvals, plus email fallback
- A single canonical approval record with status and timestamps
- Human-in-the-loop waiting with timeouts, reminders, and escalation paths
- An audit trail you can export for compliance or post-mortems
- Duplicate-prevention using idempotency and optimistic locking
If you already have workflows running, this guide pairs well with our n8n operational patterns in n8n error handling, retries, and alerting and our library approach in n8n workflow templates guide. If you want help implementing this across your stack, see our automation services.
# Prerequisites and Architecture#
What you need#
| Requirement | Recommended | Notes |
|---|---|---|
| n8n | 1.30+ | Supports modern nodes and stable execution behavior |
| Slack or Teams app | Configured | Slack interactive messages or Teams adaptive cards |
| Email provider | SMTP or API | For fallback and escalation |
| Database | Postgres recommended | For audit trail and dedupe. SQLite works for small setups |
| A public webhook URL | Yes | Needed for Slack or Teams callbacks |
High-level design#
Instead of pausing one long-running execution for days, treat approvals as a state machine persisted to a database:
- 1Create an approval record with
PENDINGstatus and anapprovalId - 2Notify approvers in one or more channels
- 3Wait for a callback event that carries
approvalIdand a decision - 4Validate and store the first final decision
- 5Continue the business process based on
APPROVEDorREJECTED
This design avoids “zombie executions”, makes retries safe, and creates a consistent audit trail.
Data model for approvals and audit logs#
Use two tables: one for the current state, and one for append-only audit events.
| Table | Purpose | Key fields |
|---|---|---|
approval_requests | Canonical state for an approval | approval_id, status, requested_by, requested_at, expires_at, decided_by, decided_at, decision_reason, version |
approval_events | Append-only audit trail | event_id, approval_id, event_type, actor, channel, payload, created_at |
A version integer helps with optimistic locking if you expect high concurrency.
ℹ️ Note: For compliance-heavy environments, keep
approval_events.payloadas JSON with the raw Slack or Teams callback, plus any request context. It makes investigations faster and reduces guesswork.
# Step 1: Create a Canonical Approval Record#
Start from any trigger: HTTP Webhook, schedule, a CRM update, or a “new invoice” event. Immediately create an approvalId and store the pending request.
Generate a stable approvalId#
If your approval is tied to a business object like an invoice, generate a deterministic idempotency key:
approvalId = invoiceId + ":" + stepName + ":" + version- Or use a UUID for uniqueness and store an additional
dedupeKey
The goal is: replays and retries should not create multiple approvals for the same action.
Example SQL schema (Postgres)#
CREATE TABLE IF NOT EXISTS approval_requests (
approval_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
requested_by TEXT,
requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
decided_by TEXT,
decided_at TIMESTAMPTZ,
decision_reason TEXT,
version INT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS approval_events (
event_id BIGSERIAL PRIMARY KEY,
approval_id TEXT NOT NULL,
event_type TEXT NOT NULL,
actor TEXT,
channel TEXT,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_approval_events_approval_id
ON approval_events (approval_id);Insert the request and first audit event in n8n#
In n8n, you can use a Postgres node, or an HTTP node to your internal API. The pattern is the same:
- 1Check if an approval already exists for the same dedupe key
- 2If not, insert a new
PENDINGrequest - 3Insert an
REQUESTEDevent
If you do this directly in SQL, keep it atomic. One transaction is ideal.
💡 Tip: If your approvals trigger expensive downstream work, do not start it until the approval is final. Store the input payload in your database or object storage and continue only after approval.
# Step 2: Send Approval Requests to Slack or Teams#
Approvers need clear context and a safe way to decide. Your message should include:
- What is being approved and why it matters
- The impact of approving
- A link to the underlying record in your system
- A short TTL, plus what happens on timeout
- A unique
approvalIdembedded in the action buttons or callback URL
Slack message pattern#
Slack supports interactive components. In practice, you can implement approvals by sending a message with two buttons and directing button clicks to a webhook.
| Element | Value |
|---|---|
| Channel | #ops-approvals or a private group |
| Buttons | Approve, Reject |
| Callback includes | approvalId, action, actorSlackId |
| Security | Verify Slack signature on your webhook |
Teams adaptive card pattern#
Teams uses adaptive cards with action submit. The callback similarly must contain approvalId and an action.
Email fallback#
Email should be your universal fallback, especially for executives who do not live in Slack or Teams.
A practical approach is to include two signed links:
- Approve link: goes to your webhook endpoint with
approvalIdand a short-lived token - Reject link: same but
action=reject
Keep links time-bound to reduce risk.
# Step 3: Implement the Waiting Pattern Without Fragile Long Pauses#
There are two common ways to “wait for approval” in n8n:
- 1Wait node with a resume webhook
- 2Decoupled callback where the original workflow ends and a separate workflow continues
For most teams, decoupling is more reliable at scale because you avoid long-running executions and you can independently retry the callback path.
Recommended: decoupled callback with a continuation workflow#
You build two workflows:
- Workflow A: creates approval request, sends notifications, schedules reminders, and ends
- Workflow B: receives callbacks, validates, writes the decision, and triggers the next step
That next step can be:
- Calling a third workflow via Execute Workflow
- Publishing to a queue
- Updating a record that triggers another automation
Why this matters#
Long waits increase operational complexity. If your n8n instance restarts, upgrades, or hits execution limits, your approval might vanish unless it is stored externally.
# Step 4: Add Timeouts, Reminders, and Escalation Paths#
Approvals fail in real life for boring reasons: people are in meetings, notifications get buried, and channels are muted. You need a predictable cadence.
Define an SLA for approvals#
Pick one based on the business impact:
| Approval type | Reminder cadence | Escalation | Final timeout |
|---|---|---|---|
| Operational change | 10 min, 30 min | Team lead at 45 min | 60 to 120 min |
| Finance payment | 2 hours, 8 hours | CFO delegate at 12 hours | 24 to 48 hours |
| Compliance exception | Daily | Compliance lead at day 2 | 3 to 7 days |
Implementation pattern in n8n#
Use a scheduler-driven reminder workflow:
- 1A cron runs every 5 minutes
- 2It queries
approval_requestswherestatus = PENDINGandexpires_atis not passed - 3It checks the elapsed time and sends reminders if thresholds are crossed
- 4It escalates to a wider group or manager when escalation thresholds are crossed
- 5It marks requests as
TIMED_OUToncenow()is pastexpires_at
Store every reminder and escalation as events.
Example reminder query#
SELECT approval_id, requested_at, expires_at
FROM approval_requests
WHERE status = 'PENDING'
AND expires_at > now();From there, compute elapsed time in an n8n Function node, or do it in SQL with now() - requested_at.
⚠️ Warning: Do not send reminders purely based on “last reminder time” stored only in n8n execution data. Persist reminder timestamps in your database, or you will resend reminders after restarts or redeploys.
Escalation strategies that work#
- Escalate to a different channel, not just more pings in the same channel
- Escalate with a summary plus a single action link
- Escalate only once per level to reduce noise
A practical path:
- 1Reminder to original approver
- 2Escalation to
#on-callor the team lead - 3Final escalation to email plus a “timeout will auto-reject” notice
If auto-approve is acceptable, document it explicitly in the message and the audit trail.
# Step 5: Build the Decision Endpoint and Store Final Decisions Safely#
All channels should converge to one decision handler. The handler must be:
- Authenticated and tamper-resistant
- Idempotent
- Concurrency-safe
- Audit-friendly
Decision rules#
| Rule | Behavior |
|---|---|
| First final decision wins | APPROVED or REJECTED locks the request |
| Later decisions are ignored | Respond with current status |
| Timeouts create final status | TIMED_OUT is final |
| Optional “revise and resubmit” | Creates a new approvalId and links to previous |
Implement idempotency and duplicate prevention#
Duplicate approvals happen because:
- Slack retries callbacks on non-200 responses
- Users click buttons multiple times
- Two approvers act at the same time
- Your workflow retries after transient errors
Your protection is a single atomic update:
- Update the request only if
status = PENDING - If the update affected zero rows, it was already decided
Example Postgres update:
UPDATE approval_requests
SET status = $1,
decided_by = $2,
decided_at = now(),
decision_reason = $3,
version = version + 1
WHERE approval_id = $4
AND status = 'PENDING';Then check the row count:
- 1 row updated means decision accepted
- 0 rows updated means ignore and return existing status
Record audit events for every interaction#
At minimum store:
| Event type | When it happens | Why it matters |
|---|---|---|
REQUESTED | Approval created | Proves the process started |
NOTIFIED | Slack or Teams or email sent | Proves it reached approvers |
REMINDER_SENT | Reminder fired | Shows SLA enforcement |
ESCALATED | Escalation fired | Shows governance |
DECISION_RECEIVED | Callback received | Shows who responded and via which channel |
DECISION_ACCEPTED | First final decision stored | Final authority record |
DECISION_IGNORED | Duplicate decision | Shows duplicate handling |
TIMED_OUT | Approval expired | Explains downstream behavior |
# Step 6: Implement Slack and Email Callbacks in n8n#
Slack interactive callback workflow#
Workflow B starts with a Webhook trigger endpoint. It should:
- 1Verify request signature (best via an API gateway or your backend)
- 2Parse
approvalIdandaction - 3Write
DECISION_RECEIVEDtoapproval_events - 4Run the conditional update to store the final decision
- 5Reply back to Slack with the outcome
Keep the response fast. Slack expects a quick 200.
If signature verification is too heavy inside n8n, place a thin verification layer in front of it and forward verified payloads.
Email approval link workflow#
For email links, do not rely on obscurity. Use a signed token.
Token strategy:
- Generate a short-lived token per action: approve token and reject token
- Store a token hash in your database with expiry
- On click, validate token, then apply decision
Even if a link is forwarded, expiry reduces risk.
Minimal token validation example in Node style#
Use this logic in your service or inside an n8n Code node if necessary.
const crypto = require('crypto');
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}Store the hash, not the raw token.
💡 Tip: When approvals are sensitive, require re-authentication. Email links can land the user on a simple approval page protected by SSO, which then calls your decision webhook.
# Step 7: Continue the Business Process After Approval#
Once a request reaches a final state, trigger the downstream action:
- If approved: run the change, send the document, deploy, pay the invoice
- If rejected: notify requester and stop
- If timed out: apply your policy, usually reject and notify
A robust continuation mechanism is to emit a message:
| Method | When to use | Pros | Cons |
|---|---|---|---|
| Execute Workflow | All-in-n8n setups | Simple | Tighter coupling |
| Webhook to internal API | When business logic is in your app | Strong validation | Requires backend |
| Queue publish | High volume | Resilient | More infrastructure |
For operational resilience, combine this with the patterns in n8n error handling, retries, and alerting. Your approval workflow should fail loudly when it cannot log events or store decisions.
# Step 8: Reporting and Audit Trail Exports#
Audit trails are only useful if you can query them quickly during an incident or an audit.
Practical queries you should support#
| Question | Query idea |
|---|---|
| Who approved a specific action | Lookup by approval_id and join events |
| How long approvals take | decided_at - requested_at for APPROVED and REJECTED |
| Which approvals time out most | Count by workflow type and TIMED_OUT |
| Reminder effectiveness | Compare decisions after reminder events |
If you store a workflow_name or approval_type field on approval_requests, reporting becomes much easier.
Example: approval cycle time query#
SELECT
status,
percentile_cont(0.5) WITHIN GROUP (ORDER BY decided_at - requested_at) AS p50,
percentile_cont(0.9) WITHIN GROUP (ORDER BY decided_at - requested_at) AS p90
FROM approval_requests
WHERE decided_at IS NOT NULL
GROUP BY status;This gives you median and p90 approval times, which are actionable SLA metrics.
🎯 Key Takeaway: Treat approvals as a measurable process. Once you track p50 and p90 cycle time, you can tune reminder and escalation thresholds instead of guessing.
# Common Pitfalls and How to Avoid Them#
- 1
Relying on a single Slack message as the system of record
Slack is a channel, not a database. Always store state and decisions outside Slack. - 2
No idempotency, leading to duplicate approvals
Make the final decision update conditional onstatus = PENDING. - 3
Timeout logic living only in one execution
Use a cron-based reminder and timeout enforcer that reads from the database. - 4
No “decision ignored” logging
If you do not log duplicates, you will waste time debugging “why did it approve twice” reports. - 5
No escalation path, only reminders
Escalation should change the recipient, not just increase message frequency.
# Practical Blueprint: A Production Approval Flow You Can Copy#
Here is a concrete blueprint that maps to n8n workflows.
| Workflow | Trigger | Responsibilities |
|---|---|---|
| A: Create approval | Business event | Create request, store payload reference, notify, write REQUESTED and NOTIFIED |
| B: Receive decision | Webhook | Validate, write DECISION_RECEIVED, store decision atomically, notify requester |
| C: Reminders and timeouts | Cron | Send reminders, escalate, mark TIMED_OUT, write events |
| D: Continue process | Execute Workflow or webhook | Run approved action, write ACTION_EXECUTED event |
If you build templates internally, standardize these four workflows. It reduces maintenance and makes approvals consistent across teams. Our n8n workflow templates guide shows how to keep these reusable without turning them into an unmaintainable mess.
# Key Takeaways#
- Store approval state in a database and treat Slack, Teams, and email as delivery channels, not the source of truth.
- Prevent duplicate approvals with an atomic update that only succeeds when status is
PENDING, and log ignored duplicates explicitly. - Implement reminders, escalation, and timeouts with a cron-driven enforcer workflow that queries pending approvals from the database.
- Keep an append-only
approval_eventstable for a real audit trail, including notifications, reminders, decisions, and timeouts. - Measure approval performance using p50 and p90 cycle times and tune reminder thresholds based on data, not intuition.
# Conclusion#
A production-ready n8n approval workflow is a state machine with durable storage, multiple response channels, and strict decision rules. When you add timeouts, reminders, escalations, and audit logs, you get approvals that are reliable under retries and transparent under scrutiny.
If you want Samioda to implement approval workflows end-to-end, including Slack or Teams apps, email fallback, database audit trails, and operational alerting, contact us via Samioda Automation.
FAQ
More in Business Automation
All →Lead-to-Cash Automation with n8n: From Form Submit to Invoice (End-to-End Workflow)
A practical lead to cash automation blueprint in n8n: capture leads, enrich data, route to sales, create deals, generate contracts, and trigger invoicing.
How to Self-Host n8n with Docker in 2026: Security, Backups, and Environment Setup
A practical step-by-step guide to self host n8n with Docker Compose, including persistence, secrets management, SSL, network isolation, and backup and restore procedures.
n8n Error Handling in Production: Retries, Dead-Letter Flows, and Alerting
A practical guide to n8n error handling in production — including retry strategies, idempotency, partial failure patterns, dead-letter flows, and Slack or email alerting you can reuse.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
How to Self-Host n8n with Docker in 2026: Security, Backups, and Environment Setup
A practical step-by-step guide to self host n8n with Docker Compose, including persistence, secrets management, SSL, network isolation, and backup and restore procedures.
n8n Error Handling in Production: Retries, Dead-Letter Flows, and Alerting
A practical guide to n8n error handling in production — including retry strategies, idempotency, partial failure patterns, dead-letter flows, and Slack or email alerting you can reuse.
n8n Webhook Tutorial: Automate Anything with Webhooks (2026 Step-by-Step)
A practical n8n webhook tutorial that shows how to capture webhook events, transform data, handle errors, and ship reliable automations with real examples.