Direct answer
Audit the workflow as a state machine, not as an email template. Before every recovery send, reconcile the captured cart with current WooCommerce order state, verify consent and suppression, inspect the scheduled action, and apply a hard retry ceiling. A message is safe only when all four checks pass at send time.
This guide documents an audit method. It does not mean a StoreFixKit plugin is available. No feature or delivery date is promised.
Observed failure signals
These three reports shaped this audit. Each report was reviewed for direct relevance and preserved with a content hash. They identify failure modes worth testing; they do not prove how often the problem occurs across WooCommerce stores.
Abandoned carts remain after completed purchase
Completed purchases could leave a duplicate abandoned-cart record that had to be removed manually to prevent follow-up email.
Open the reviewed WordPress.org reportBUG: Endless email retry loop when recipient address is undeliverable
One invalid recipient triggered 4,678 failed recovery-email attempts over roughly 50 days with no retry ceiling.
Open the reviewed WordPress.org reportScheduled emails only appear after manually clicking “Reschedule emails”
Recovery emails appeared only after manually pressing Reschedule emails even though WordPress cron and scheduled actions were running.
Open the reviewed WordPress.org reportThe send-time decision path
Resolve cart, shopper, and candidate orders
Stop if a matching order is paid or completed
Check consent, suppression, and sequence state
Run one traceable, idempotent action
Classify failure and enforce a retry ceiling
Run the audit from the next send backward
Start with one real or staging recovery attempt and build a timeline. Do not begin by changing email copy, adding SMTP, or rescheduling every action. Those changes can hide the state error you need to see. Record the captured cart ID, shopper identity, cart update time, candidate order IDs, order status changes, scheduled action ID, every delivery attempt, and the final sequence status.
- Use a controlled test identity.Create a staging checkout with an email address you control. Capture the cart, then complete the order through the same checkout path. Do not test a live customer without permission.
- Freeze the timeline before repairing it.Export or screenshot the cart record, order notes, scheduled action, and mail log. A manual reschedule can overwrite the most useful clue.
- Resolve every plausible order.Search the normalized billing email, customer ID, checkout session or recovery token, and the relevant time window. Email alone is a useful key, but it is not always a complete identity model.
- Inspect the action that will actually send.Open WooCommerce > Status > Scheduled Actions and filter by the recovery hook. Record its status, arguments, scheduled time, group, claim, and log entries.
- Separate generation from delivery.A generated message can fail before mail handoff, after mail handoff, or at the recipient provider. Use the WooCommerce transactional email log and the SMTP provider log as separate evidence.
- Classify the final state.The sequence should be suppressed, pending, sent, temporarily failed, permanently failed, or manually cancelled. “Abandoned” is a cart observation, not a sufficient send state.
export AUDIT_EMAIL='your-controlled-test-address'
wp cron event list --fields=hook,next_run_gmt,next_run_relative,recurrence
wp option get cron --format=json
wp wc order list --search="$AUDIT_EMAIL" --fields=id,status,date_created,date_paid
Command availability depends on WP-CLI and WooCommerce CLI packages. Use a staging copy or a recent database backup before any write operation.
Reconcile the cart with current order truth
A common design mistake is to cancel recovery only when one order-status hook fires. Hooks are useful, but webhooks, payment callbacks, imports, asynchronous gateways, email corrections, and plugin conflicts can make a single event incomplete. The send worker should perform a final read before it creates the message.
| Identity signal | What it can establish | Where it can fail | Audit action |
|---|---|---|---|
| Normalized billing email | Likely shopper match | Typos, changed address, shared inboxes | Compare captured and final order values side by side. |
| Logged-in customer ID | Strong account link | Guest checkout or account switching | Query orders for the customer within the sequence window. |
| Checkout or recovery token | Links one captured session | Token rotation or missing storage | Verify that the token is immutable and indexed. |
| Order status and paid date | Current commercial outcome | Custom statuses or delayed gateway callbacks | Define which statuses suppress sends and why. |
| Cart fingerprint | Supporting product match | Customer changed quantities or products | Use only as corroboration, not the sole key. |
Use wc_get_orders() or WC_Order_Query for supported order lookup and pagination. Direct SQL against legacy order posts is a fragile shortcut because WooCommerce supports High-Performance Order Storage. A safe query limits the date range, returns only the fields or IDs needed, and handles multiple plausible orders instead of picking the first row.
cart = load_captured_cart(action.cart_id)
orders = find_candidate_orders(cart.identities, cart.captured_at)
if any(order.is_paid_or_suppressing() for order in orders):
cancel_remaining_actions(cart.sequence_id, reason="matching_order")
elif not cart.has_current_consent or cart.is_suppressed:
cancel_remaining_actions(cart.sequence_id, reason="consent_or_suppression")
elif attempt_count_reached_limit(cart.sequence_id):
mark_failed(cart.sequence_id, reason="retry_limit")
else:
send_once_with_idempotency_key(action.id)
The exact suppression statuses are a store decision. A paid order normally stops the sequence. A pending payment order may require a short hold rather than an immediate recovery message, depending on the gateway. Record the rule in configuration and expose the reason in the log so support does not have to infer it later.
Use a finite, failure-aware retry policy
Retrying every failure the same way turns a temporary queue into permanent noise. One reviewed support report described 4,678 failed attempts for one invalid address over roughly 50 days. That is one report, not a market-wide rate, but it demonstrates why a retry ceiling and failure classification belong in the safety design.
| Failure class | Example evidence | Recommended state | Pass condition |
|---|---|---|---|
| Permanent recipient failure | Invalid domain, mailbox does not exist, explicit hard bounce | Stop immediately and suppress this address for the sequence | No future action remains queued. |
| Temporary delivery failure | Provider timeout, rate limit, transient 4xx response | Retry with increasing delay and a small hard limit | Attempt count and next run are visible. |
| Application failure | Fatal error, missing template, invalid action arguments | Fail the action for operator review | Stack or error context is retained without exposing customer data. |
| State conflict | Matching order paid after the action was queued | Cancel, not retry | Log names the order and suppression rule. |
| Unknown response | No provider status or ambiguous handoff | One cautious retry, then manual review | The sequence cannot loop indefinitely. |
Use one idempotency key per logical send, usually the scheduled action or sequence-step ID. If a worker times out after the provider accepts the message, a retry should discover the prior accepted attempt instead of creating a duplicate. Store a provider message ID when available, but do not put raw email addresses or recovery URLs into broad debug logs.
Diagnose the scheduler as a queue, not a switch
“WP-Cron is enabled” is not a result. WP-Cron checks due work when WordPress receives traffic, so low-traffic sites can run late. Action Scheduler adds a traceable queue, but the specific action still needs valid arguments, a registered callback, capacity to claim the job, and a successful state transition.
- Find the exact hook.Filter Scheduled Actions by hook and group. A generic cron health plugin cannot prove that this recovery action exists.
- Inspect action arguments.Confirm the cart or sequence ID still resolves and the callback expects the same argument shape.
- Compare due time with actual run time.A late run points toward traffic-triggered cron or runner capacity; a failed run points toward the callback.
- Review failed and in-progress queues.A stuck claim or repeated fatal can block later work even while unrelated actions complete.
- Run one action manually only after capture.A successful manual run proves the callback can execute now; it does not explain why the original automatic claim failed.
- Verify the post-run transition.The action should move the sequence to sent, cancelled, or failed and schedule only the next legitimate step.
When the queue is healthy but mail is missing, open WooCommerce > Status > Logs and inspect the transactional email source. A “sent” application result means WooCommerce handed the message to the mail system; it does not prove inbox delivery. Use the mail provider's event log for accepted, deferred, bounced, or delivered status.
Pass conditions for a production-safe workflow
| Control | Test | Pass condition |
|---|---|---|
| Paid-order suppression | Capture a cart, complete the order, then let the original action become due. | No recovery message is generated; the log names the matching order and cancellation reason. |
| Identity mismatch | Change the final billing email while preserving the account or checkout link. | The documented secondary identity resolves the order, or the case enters manual review without sending. |
| Duplicate worker execution | Invoke the same logical action twice. | One provider send exists and both runs resolve to one idempotent result. |
| Permanent failure | Use a controlled address that produces a hard failure. | The sequence stops after the classified failure and no future retry remains. |
| Temporary failure | Simulate a provider timeout or retryable response. | Backoff and maximum attempts are visible; the final state cannot remain ambiguous. |
| Scheduler delay | Pause traffic-triggered cron, then restore the runner. | The delayed action runs once, rereads current order state, and records actual run time. |
Keep this test set after launch. Run it after WooCommerce updates, checkout replacement, payment-gateway changes, identity-field changes, and recovery-plugin upgrades. The objective is not a perfect dashboard. It is a defensible answer to four support questions: why was this shopper eligible, why did this action run, what did the provider return, and why will it or will it not run again?
FAQ
Why can a paid customer still receive an abandoned-cart email?
The captured cart may not have been reconciled with the final order, the billing identity may differ, or a previously queued action may not recheck order state when it runs. Compare the cart identity, order identity, status history, and scheduled action log.
How many times should a failed recovery email retry?
There is no universal number, but retries must be finite and failure-aware. A permanent address failure should stop immediately; a temporary provider failure can use a small backoff schedule and then move to a visible failed state.
Does a working WP-Cron prove recovery scheduling works?
No. It proves only that WordPress can dispatch due events. You still need to verify the specific hook, action arguments, next run, claim, callback result, and follow-up state transition.
Should a manual resend create another scheduled sequence?
No. A resend should reference the existing attempt or create a clearly separate operator action. It should not silently duplicate the automated sequence.
Is this guide announcing a StoreFixKit abandoned-cart plugin?
No. StoreFixKit is testing merchant demand and operating requirements before development is authorized. The linked research form records interest only.
Sources and review boundary
Technical recommendations rely on official WooCommerce, WordPress, or Action Scheduler documentation. Public support reports are used only as observed pain signals. StoreFixKit has not converted those reports into frequency, revenue, or adoption claims.
Official technical references
- WooCommerce: wc_get_orders() and order queriesUse supported order queries, pagination, and current order fields instead of brittle direct SQL.
- Action Scheduler: Scheduled Actions administrationInspect pending, failed, and in-progress actions and their logs.
- WordPress Plugin Handbook: CronUnderstand that WP-Cron is traffic-triggered and can run late.
- WooCommerce: Email troubleshootingSeparate message generation from mail handoff and final delivery.
- WooCommerce: Cart Abandonment EmailsConfirm that purchase state must suppress abandoned-cart follow-up.
Observed merchant reports
- Abandoned carts remain after completed purchaseOne public report; it does not establish frequency or market size.
- BUG: Endless email retry loop when recipient address is undeliverableOne public report; it does not establish frequency or market size.
- Scheduled emails only appear after manually clicking “Reschedule emails”One public report; it does not establish frequency or market size.
Merchant research
Does this failure happen in your store?
Share one concrete example, your current workaround, monthly order range, and acceptable annual price. The response is reviewed as market evidence; it does not create a purchase or reservation.