Durable workflows: how to stop automations disappearing after a button click
Learn how durable workflow queues prevent missed emails and failed automation handoffs in production apps, using practical Make.com comparisons.

Durable workflows: how to stop automations disappearing after a button click
If you come from Make, Zapier, or another no-code tool, a workflow usually feels simple:
- Something changes.
- The scenario runs.
- An email is sent, a record is updated, or someone is notified.
That model is useful, but it hides an important question: how does the workflow get from “something changed” to “the scenario is definitely going to run”?
For small automations, it is easy to assume that handoff always works. In a production app, that assumption can create a frustrating failure: the button says the change worked, the record updates, but the email or follow-up automation never happens.
This post explains a practical way to avoid that problem without turning a simple app into an operations project.
The failure that is easy to miss
Imagine an admin approves a job listing.
The expected outcome is straightforward:
- the job becomes Live;
- the client receives an approval email;
- the legacy CMS or Airtable record is updated;
- the action is visible in the workflow history.
A common implementation looks like this:
Save the job as Live
Return success to the Admin screen
Start the approval workflow in the background
This feels sensible. The screen stays quick, and a slow email provider does not make the admin wait.
The problem is the gap between the second and third lines. If the background handoff times out, loses its credentials, or never starts, the job is still Live. The person who clicked Approve sees success. But the workflow has not begun, so there may be no email, no mirror update, and no useful failure record.
In Make terms, it is like receiving a webhook, replying 200 OK, and then discovering that no bundle was ever safely placed in the scenario queue.
The practical fix: save a workflow to-do item
The simplest reliable pattern is called an outbox or durable workflow queue. The name is less important than the idea.
When the business change is saved, create one small database row that says:
“This workflow still needs to be run.”
For example:
| Field | Example |
|---|---|
| Workflow | job_board.status_changed |
| Record | This specific job |
| Change | Listing in review → Live |
| Status | queued |
| Attempts | 0 |
| Payload | The original status change and reason |
The important part is that the job status update and this queued workflow row are saved together. Either both are saved, or neither is.
That removes the dangerous gap.
Approve job
→ save job as Live
→ save “run approval workflow” queue row
→ show success in Admin
The admin action remains fast. We are not waiting for MailerSend, Airtable, Slack, or a large email campaign. We are only making sure the system has a durable instruction to finish the work.
The queue is the part no-code tools usually manage for you
A useful Make comparison looks like this:
| In an app | Rough Make equivalent |
|---|---|
| A workflow queue row | A queued bundle, or a Data Store row waiting to be processed |
| A worker | A scheduled scenario that picks up the next bundle |
| A workflow run | The scenario execution history |
| An email task | One iterator bundle for one recipient |
| A dead-letter item | An error-handler route that needs human attention |
No-code platforms often give you much of this machinery automatically. When you move workflow logic into an app, you need to deliberately keep the useful reliability parts.
You do not need a separate queue for every feature. One shared workflow_dispatch_jobs table can carry jobs for job approvals, freelancer decisions, support replies, onboarding completion, and other important actions.
The row simply says which workflow to run and which record it belongs to.
What happens after an item is queued
A small scheduled worker checks for queued workflow jobs every minute or few minutes.
For each one, it:
- Claims the job so another worker cannot run it at the same time.
- Runs the relevant workflow using the original payload.
- Records the result.
- Marks the job as succeeded, retryable, or needing attention.
A healthy lifecycle looks like this:
queued → running → succeeded
If MailerSend or another service has a temporary outage:
queued → running → queued again for a later retry
After a sensible number of failed attempts:
queued → running → dead_letter
“Dead letter” sounds dramatic, but it just means: the system has stopped guessing and needs an operator to review this one item.
That is much better than a missing email that nobody can see or retry.
Retries must not send duplicate emails
Retries are useful only if they are safe.
Consider a workflow that sends an approval email. A worker might successfully send the email, crash before recording success, then retry the job. Without protection, the client could receive the same email twice.
The answer is not to avoid retries. The answer is to make the email itself idempotent.
In plain language, give each email a stable identity such as:
job approval + job ID + recipient email
Before sending, check whether that exact email task has already been accepted by the provider. If it has, treat the retry as complete rather than sending a second copy.
For a large job-board notification, the pattern becomes two layers:
One workflow job
→ plans the notification
→ creates one email task per recipient
→ a bounded email worker sends those tasks in small batches
This matters because a workflow that needs to email 100 people should not try to finish all 100 sends inside one serverless request. It should create the safe to-do items, process a manageable batch, and continue from saved progress.
Keep the original change, not just the latest record
There is one subtle edge case worth understanding.
A job might move from In Review to Live, then quickly move from Live to Closed before the first queued workflow has run.
If the worker only looks at the job’s current state, it may not know what the original action meant. It could send an approval email after the job is closed or overwrite a newer mirror update.
That is why the queue stores the original transition:
{
"previousStatus": "Listing in review",
"nextStatus": "Live",
"reason": null
}
The worker can then make an explicit decision: run the original action, skip it because it is stale, or flag it for review. It should never silently guess.
Dry-run mode is part of the design, not an afterthought
A production-safe dry run is one of the best ways to build confidence.
A dry run should use the real queue and the real worker, but stop before live side effects. It should:
- create and claim a queue row;
- validate the record and workflow routing;
- show the planned email, Airtable, Slack, or other effects;
- write clear execution evidence;
- send no emails and make no external writes.
For safety, restrict dry runs to tagged QA records and require an explicit operator action.
This gives you a useful middle ground between “it passed locally” and “we sent an accidental customer email.”
What should go through a durable queue?
Use the queue for actions where a person expects a meaningful business outcome:
- approving or rejecting a job;
- approving, waitlisting, or rejecting a freelancer;
- a customer submitting an application;
- a support reply that should trigger a notification;
- completing onboarding.
Do not use it for everything.
Activity analytics, cache refreshes, image optimisation, and non-critical enrichment can often remain best-effort background work. The rule of thumb is simple:
If the action would be a real problem to lose, queue it. If it is housekeeping that can safely be missed or recalculated, keep it lightweight.
A manageable operating model
This does not need to become a management nightmare.
A practical setup has:
- one shared workflow queue;
- one worker;
- the existing per-email task queue for fan-out;
- one Admin view with Queued, Retrying, Needs attention, and Completed;
- a dry-run button for QA records.
That is enough to make workflow failures visible and recoverable without creating a table, worker, or dashboard for every part of the product.
The takeaway
The goal is not to make workflows slower or more complicated. It is to make a simple promise true:
When a button says an important action worked, the follow-up workflow will either run, retry safely, or show up clearly for review.
That is the reliability layer that makes no-code-style automation feel dependable once it becomes part of a production app.
Ready to learn more? Check out our Playbooks for step-by-step guidance.
View PlaybooksContinue Reading


