---
title: "as_has_scheduled_action: Prevent Duplicate Actions"
description: "as_has_scheduled_action() checks whether a matching action is already pending — signature, the hook, args and group parameters, return value, and dedup gotchas."
url: "https://wpwebhooks.org/blog/action-scheduler-prevent-duplicate-actions/"
date: "2026-06-22"
---

# as_has_scheduled_action: Prevent Duplicate Actions

**TL;DR:** Action Scheduler gives you two built-in ways to stop duplicate jobs — the `$unique` flag on the scheduling functions, and a guard with `as_has_scheduled_action()`.

-   `$unique = true` skips the insert if a matching action is already pending or in-progress, returning 0 instead of an action ID.
-   `as_has_scheduled_action( $hook, $args, $group )` (since 3.3.0) returns true when a matching action already exists — match is exact on hook, args, and group.
-   Neither is fully race-proof. `$unique` collapses the check and the insert into one call, so prefer it for events that can fire twice.

/ Two approaches

## How do you **prevent duplicate scheduled actions** in Action Scheduler?

There are two supported approaches, and both compare actions on the same triple: hook name, arguments array, and group. The first is the `$unique` parameter on the scheduling functions — set it to true and Action Scheduler refuses to create a second action that matches one already queued. The second is `as_has_scheduled_action()`, which lets you check whether a matching action exists and branch on the result before you schedule.

Use `$unique` when a single user event can fire the same enqueue twice. Use `as_has_scheduled_action()` when you set up a recurring action once and want to avoid stacking it on every plugin upgrade. The full signatures live in the [Action Scheduler API reference](https://actionscheduler.org/api/).

/ The flag

## What does the **$unique parameter** do?

`$unique` is a boolean argument on `as_enqueue_async_action()`, `as_schedule_single_action()`, and `as_schedule_recurring_action()`. When it is true, Action Scheduler will not create the action if one with the same hook, args, and group is already pending or in-progress — the call returns 0 rather than a new action ID. Because the uniqueness check happens inside the same call that performs the insert, it is the tightest guard the API offers.

Note the argument order: `$unique` sits after `$group` and before `$priority`. If you only ever passed a hook and args before, you have to fill the `$group` slot (even with an empty string) to reach it.

PHP — signatures and a unique enqueue

```
// Signatures (Action Scheduler core)
as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false, $priority = 10 );
as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 );

// At most one sync per order can ever be pending:
as_schedule_single_action(
    time() + 300,
    'sync_order',
    array( $order_id ),
    'orders',
    true   // $unique
);
```

/ The check

## How does **as\_has\_scheduled\_action()** check for duplicates?

It returns true if at least one action with the given hook — and optionally the given args and group — is currently pending or in-progress. It was added in Action Scheduler 3.3.0 and is the recommended way to guard recurring schedulers you register during activation or upgrade routines, so a plugin update never stacks a second copy of the same five-minute job.

Matching is exact. The args you pass must equal the args you scheduled with, element for element. Omit the args entirely to match any action on that hook. The Action Scheduler [usage guide](https://actionscheduler.org/usage/) shows the canonical guard pattern.

PHP — guard a recurring action on activation/upgrade

```
// Run on init / activation — never stack the recurring action.
if ( ! as_has_scheduled_action( 'my_cron_5min' ) ) {
    as_schedule_recurring_action(
        time(),
        5 * MINUTE_IN_SECONDS,
        'my_cron_5min'
    );
}
```

FIG 01 — Deduplication decision path

/ Which one

## **$unique vs as\_has\_scheduled\_action** — which should you use?

Use `$unique` for one-off enqueues fired from request handlers — order placed, form submitted, webhook received — where the same event can realistically fire twice in quick succession. Use `as_has_scheduled_action()` for recurring actions you register once in activation or upgrade code.

The reason is the race window. `as_has_scheduled_action()` is a check-then-act: another process can insert a matching action in the gap between your check returning false and your schedule call running. `$unique` has no gap — the check and the insert are one operation, so two concurrent requests cannot both win.

| Scenario | Raw dedup in your code | Webhook Actions |
| --- | --- | --- |
| Same event fires twice | Two actions unless you set $unique or guard manually | Idempotent dispatch — one delivery per event |
| Recurring setup re-runs on upgrade | Guard every time with as\_has\_scheduled\_action | Trigger bound once in the admin UI |
| Race between check and insert | Possible with check-then-schedule patterns | A single queued row per event |

/ Edge cases

## Why do **duplicates still slip through**?

Four reasons cover almost every case. First, args mismatch: a single differing argument — an integer where you later pass a string, an extra element, a different order — makes the triple look unique, so normalize args before scheduling and checking. Second, group mismatch: checking without the group you scheduled with. Third, a genuine race when two concurrent requests both pass `as_has_scheduled_action()` before either inserts — the case `$unique` exists to close.

Fourth, and the one that surprises people: completed actions do not count. `as_has_scheduled_action()` only sees pending and in-progress actions, so a single action that already ran will not block you from scheduling it again. That is usually the behaviour you want, but it means it is not a historical "has this ever run" check.

> Action Scheduler's idea of "the same action" is an exact match on hook, args, and group. Change one byte of the args array and it is a brand-new action. — Action Scheduler API

→

The [Webhook Actions plugin](https://wpwebhooks.org/wordpress-webhook-plugin/) binds a trigger to a webhook once in the admin UI and dispatches one delivery per event — so you get idempotent outbound webhooks without writing `$unique` guards by hand. See the [Action Scheduler PHP API reference](https://wpwebhooks.org/blog/action-scheduler-api-functions/) for the rest of the scheduling surface.

/Footnotes

¹ Function signatures: [actionscheduler.org/api](https://actionscheduler.org/api/).

² Recurring-action guard pattern and the 3.3.0 note for `as_has_scheduled_action()`: [actionscheduler.org/usage](https://actionscheduler.org/usage/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"as_has_scheduled_action: Prevent Duplicate Actions","description":"as_has_scheduled_action() checks whether a matching action is already pending — signature, the hook, args and group parameters, return value, and dedup gotchas.","datePublished":"2026-06-22","dateModified":"2026-06-22","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/action-scheduler-prevent-duplicate-actions/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["action scheduler unique parameter","as has scheduled action","action scheduler prevent duplicate actions","as enqueue async action unique","as schedule single action unique","action scheduler duplicate jobs"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"as_has_scheduled_action: Prevent Duplicate Actions","item":"https://wpwebhooks.org/blog/action-scheduler-prevent-duplicate-actions/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"How do I stop Action Scheduler from scheduling duplicate actions?","acceptedAnswer":{"@type":"Answer","text":"Use one of two built-in tools. Pass $unique = true to as_enqueue_async_action(), as_schedule_single_action(), or as_schedule_recurring_action() so a matching action is not created twice, or guard the schedule call with if ( ! as_has_scheduled_action(...) ). Both match on hook, args, and group."}},{"@type":"Question","name":"What is the $unique parameter in as_schedule_single_action?","acceptedAnswer":{"@type":"Answer","text":"It is a boolean that, when true, tells Action Scheduler not to create the action if one with the same hook, args, and group is already pending or in-progress. The call returns 0 instead of an action ID. Because the check and insert happen in one call, it is race-safe."}},{"@type":"Question","name":"What does as_has_scheduled_action() return?","acceptedAnswer":{"@type":"Answer","text":"It returns true if at least one action matching the given hook (and optional args and group) is currently pending or in-progress, and false otherwise. It was added in Action Scheduler 3.3.0 and is the recommended guard for recurring actions registered during activation or upgrade."}},{"@type":"Question","name":"Does as_has_scheduled_action check completed actions?","acceptedAnswer":{"@type":"Answer","text":"No. It only considers pending and in-progress actions. A single action that already ran to completion will not block you from scheduling it again, so it is not a historical \"has this ever run\" check — it answers \"is one queued right now\"."}},{"@type":"Question","name":"Why is as_has_scheduled_action still creating duplicates?","acceptedAnswer":{"@type":"Answer","text":"Usually an args or group mismatch — matching is exact, so a different type, an extra element, or a different order makes the action look unique. It can also be a genuine race: two concurrent requests both pass the check before either inserts. Use the $unique flag to close that race."}}]}
```
