For the complete documentation index, see llms.txt. This page is also available as Markdown.

How to Schedule Basket Offer Windows

A basket offer normally runs over one fixed date range: it turns on at its start date, off at its end date, and that is the whole of its life. A campaign that is supposed to come back — every evening, every weekend, on the first of each month — has to be either recreated each time or left running and switched on and off by hand.

Scheduling replaces that single range with a rule and lets Commerce work out the dates for you, over and over, for as long as the rule holds. Once the rule is right nobody needs to touch the offer again.

This guide is about deciding what to schedule and how the rule should be shaped. For the exact request and response bodies, field types and validation responses, see Admin → Basket Offers in the Commerce OpenAPI reference.

Scheduling needs a background routine to be running on your deployment. If it is not, any attempt to save a schedule is rejected and nothing is stored — see 7. Troubleshooting. Ask your platform team to enable it before you start.

1. Overview

A schedule is attached to a basket offer — one schedule per offer, either on creation or on an offer that already exists. From it, Commerce continuously works out windows: the stretches of time during which the offer should be live. It keeps two of them at hand, the window the offer is in now and the one coming up next, and hands the offer over from one to the next on its own.

A schedule is built from three things, and you can use any one of them or all three together:

Field
Description

cron

The repeating pattern — "every weekday at 09:00", "every Saturday".

inclusions

Individual date ranges that are always part of the schedule, whether or not the pattern covers them.

exclusions

Blackout ranges that are always cut out of it.

Two more fields bound and anchor the whole thing: timezone decides what local time the pattern is read in — UTC unless you set it — and start_date / end_date decide the period over which the schedule produces anything at all.

Key characteristics of scheduling:

  • The offer's own dates are managed for you. The current window is written onto the offer's start and end date, so everything that already reads those dates — the storefront, the Campaigns list, reports — keeps working unchanged. In exchange those two dates become read-only: while a schedule is attached, any attempt to set them by hand is rejected.

  • The handover is automatic. A routine checks every minute whether a window has closed and moves the offer onto the next one when it has. Nobody has to be watching.

  • The status stays yours. Scheduling never changes an offer's status. A scheduled offer sells only while its status is active and the current moment falls inside its window.

  • Nothing is applied retroactively. A schedule describes when the offer is live from now on. Changing it never rewrites what happened in windows that have already closed.

2. Do You Need a Schedule?

Scheduling earns its keep when a campaign has a shape in time. When it does not, plain start and end dates are simpler and easier for everyone to read.

What the campaign does
How to set it up

Runs once, continuously, between two dates

Plain start and end dates — no schedule

Comes back on a pattern (daily, weekly, monthly)

A schedule with a recurrence rule

Runs on a handful of dates somebody picked

A schedule with inclusions only

Runs continuously but must go quiet on certain days

A schedule with an always-on rule and exclusions

Comes back on a pattern, plus a few extra dates, minus a few others

A schedule using all three sources

An always-on rule with nothing cut out of it is just an offer with a start date and no end date. If there is no pattern and no blackout to express, do not reach for a schedule.

3. Shaping the Recurrence Rule

The rule is a standard 5-field cron expression. Each field says when the rule applies:

Each field takes a single value (5), a list (1,4), a range (9-17), a step (*/15), or * for "every". The L, W and #n shorthands that some cron dialects support are not accepted — if you need "the last day of the month" or "the second Tuesday", list those dates as inclusions instead.

3.1. From moments to windows

A cron expression on its own describes moments, not stretches of time. There are two ways to turn those moments into windows, and you do not choose between them with a flag — it follows from the minute field.

The minute field is...

What the rule means

duration

A single minute — 0, 30

Each match opens a window, and duration says how long it stays open

Required

Exactly * — as in * 9-17 * * 1-5

The rule describes every minute the offer is live; the window length follows from the rule itself

Must be left out

Both describe simple daily patterns equally well, so pick whichever reads more plainly. Beyond those:

  • Windows that span days need the first style. No minute-matching rule can express "Friday 23:00 until Monday 00:00" — you need a start moment plus a long duration.

  • Windows that are really a run of hours read better in the second. * 9-16 * * 1-5 says "weekdays, business hours" more plainly than a start time plus a nine-hour duration.

3.2. Expressing a duration

Durations are written in the ISO 8601 format — a P, then a number of days, then a T followed by hours, minutes and seconds:

You want
Write

15 minutes

PT15M

90 minutes

PT1H30M

2 hours

PT2H

9 hours

PT9H

1 day

P1D

1 day and 6 hours

P1DT6H

2 days and 6 hours (Friday 18:00 → Monday 00:00)

P2DT6H

1 week

P7D

Only days, hours, minutes and seconds are accepted. There is no designator for weeks, months or years, so express those in days — a week is P7D, not P1W.

3.3. How the three sources come together

When a schedule uses more than one source, Commerce combines them in a fixed order every time it works out a window:

  1. The recurrence rule produces its stream of windows.

  2. Inclusions add their windows on top.

  3. Windows from those two steps that overlap or touch are merged into one.

  4. Exclusions are cut out of every merged window — splitting a window in two where a blackout lands inside it.

  5. end_date trims any window that would run past it.

Two related rules to keep in mind while writing the ranges: inclusions must not overlap each other — merge them into one range before saving — while exclusions are free to overlap.

4. Common Scenarios

Each scenario below lists exactly what to set. Anything not listed should be left unset — remember that a schedule is stored as a whole, so a field you leave out is a field at its default, not one kept from before.

timezone is the field worth setting deliberately. Left unset it is UTC, so a rule written for local hours runs at those hours in UTC — in Türkiye that puts the campaign three hours off. Set it whenever the recurrence rule names a time of day; it is listed below wherever that applies.

4.1. A happy hour, every evening

The campaign: a discount that runs from 17:00 to 19:00, every day.

Field
Set it to

cron

* 17-18 * * *

duration

leave unset

timezone

e.g. Europe/IstanbulUTC when left unset

A run of hours every day, so the minute-matching style reads best. The hour range is the part worth checking: the window runs to the end of the last matching hour, so 17-18 gives you 17:00 to 19:00 — not 17-19.

Watch out for: writing 0 17-19 * * * instead. That names three moments and opens three windows a day.

4.2. A weekday campaign, business hours only

The campaign: live Monday to Friday between 09:00 and 18:00, nothing at the weekend.

Field
Set it to

cron

0 9 * * 1-5

duration

PT9H

timezone

e.g. Europe/IstanbulUTC when left unset

* 9-17 * * 1-5 with no duration gives the same window and reads more plainly if the campaign is really about opening hours. Prefer the start-plus-duration form when the start moment is what matters.

Watch out for: the two forms are not interchangeable once a duration is stored. If you switch the rule to the minute-matching form, drop the duration in the same change — a minute-matching rule with a duration attached is rejected.

4.3. Two slots a day — lunch and dinner

The campaign: one hour at midday and one hour in the evening, every day.

Field
Set it to

cron

0 12,19 * * *

duration

PT1H

timezone

e.g. Europe/IstanbulUTC when left unset

Listing several hours works exactly as you would expect: this opens 12:00–13:00 and 19:00–20:00 every day. Both slots have to be the same length, because one schedule carries one duration.

Watch out for: wanting slots of different lengths, or more than one slot inside the same hour. Neither fits one rule — see 4.12, and the note on the minute field in 3.1.

4.4. An overnight window that crosses midnight

The campaign: live from 22:00 until 02:00 the next morning.

Field
Set it to

cron

0 22 * * *

duration

PT4H

timezone

e.g. Europe/IstanbulUTC when left unset

The date rolls over on its own: each firing at 22:00 stays live until 02:00 the following day, as one window rather than two.

* 22-1 * * * with no duration gives the same window. A descending hour range is read as the set 22, 23, 0, 1, and the window runs straight through midnight to the end of the last matching hour — so this is one of the few places where either form works for a window that changes date.

Watch out for: the end of that hour range. * 22-2 * * * is 22:00 to 03:00, not 02:00 — the last hour you list is fully live. If counting that through midnight feels error-prone, use the start-plus-duration form, where the end is explicit.

4.5. A weekend-long campaign

The campaign: live from Friday evening straight through to Monday morning.

Field
Set it to

cron

0 18 * * 5

duration

P2DT6H

timezone

e.g. Europe/IstanbulUTC when left unset

This window spans days, so it has to be a start moment plus a duration. Count the hours rather than the days when you write it — Friday 18:00 to Monday 00:00 is 54 hours, which is two days and six hours, not three days.

Watch out for: trying to express it as * * * * 5,6,0. That is live from Friday 00:00, not Friday evening.

4.6. The first of every month

The campaign: a payday promotion, all day on the 1st.

Field
Set it to

cron

0 0 1 * *

duration

P1D

timezone

e.g. Europe/IstanbulUTC when left unset

The day-of-month field carries the pattern and the duration covers the day. Leave the day-of-week field as * — setting both day fields narrows the schedule to dates that satisfy each of them.

4.7. A seasonal campaign

The campaign: weekday mornings, but only for this summer.

Field
Set it to

cron

0 10 * * 1-5

duration

PT4H

start_date

first day of the season

end_date

last day of the season

timezone

e.g. Europe/IstanbulUTC when left unset

The pattern goes in the rule and the season goes in the bounds. The schedule produces nothing outside them, and a window that would straddle the end date is trimmed to it.

Watch out for: putting the season in the rule's month field instead. 0 10 * 6-8 * does describe summer, but it comes back every year — right for an annual campaign, wrong for a one-off season.

4.8. A campaign that starts later

The campaign: a pattern that should not begin until a launch date some months away.

Field
Set it to

cron

your pattern

duration

as the pattern needs

start_date

the launch date

timezone

e.g. Europe/IstanbulUTC when left unset

Nothing is produced before the start date, and the first window reported back is the first one on or after it — so you can set the whole thing up now and check it reads correctly, months ahead of launch.

Watch out for: leaving the offer's status as active and assuming that makes it live. It is the window that decides, and until the launch date there is no window covering now.

4.9. A permanent campaign that goes quiet on holidays

The campaign: always on, except on public holidays and during a maintenance slot.

Field
Set it to

cron

* * * * *

duration

leave unset

exclusions

one range per holiday, plus the maintenance slot

This is the one case where an always-on rule earns its place — without exclusions it is just an offer with no end date.

Watch out for: nothing, really — a blackout in the middle of a live stretch splits it rather than cancelling it, which is exactly what you want here. A half-day holiday leaves the offer live either side of it.

4.10. A campaign on a handful of dates

The campaign: the Black Friday weekend, Christmas, and New Year's Eve. No pattern at all.

Field
Set it to

cron

leave unset

duration

leave unset

inclusions

one range per event

The offer is live across those ranges and inactive at every other time.

Watch out for: overlapping ranges. A Christmas range and a New Year range that both cover 31 December are rejected — merge them into one range that covers both.

4.11. A recurring campaign with a few extra dates

The campaign: the usual weekday pattern, plus the whole Black Friday weekend, minus New Year's Day.

Field
Set it to

cron

0 9 * * 1-5

duration

PT8H

inclusions

the Black Friday weekend

exclusions

New Year's Day

timezone

e.g. Europe/IstanbulUTC when left unset

All three sources in one schedule. There is no need to work the extra dates into the pattern; they are added on top of it and merged.

4.12. Different hours at the weekend

The campaign: 09:00–17:00 on weekdays but 10:00–16:00 at the weekend.

Field
Set it to

cron

0 9 * * 1-5

duration

PT8H

inclusions

one 10:00–16:00 range per weekend day you want to cover

timezone

e.g. Europe/IstanbulUTC when left unset

One schedule carries one rule and one duration, so two different daily shapes cannot both be patterns. Put the shape that repeats in the rule and list the other one as inclusions.

Watch out for: how far ahead this covers. Inclusions are fixed dates, so they run out — either list enough weekends to cover the campaign, or run two separate offers, each with its own pattern.

4.13. A pattern cron cannot express

The campaign: every other Tuesday, or the last day of every month.

Field
Set it to

cron

leave unset

duration

leave unset

inclusions

one range per occurrence

Cron has no notion of "every other" or "the last one", and the L, W and #n shorthands other dialects use for this are not accepted. Work out the dates and list them.

Watch out for: 0 9 * * */2 looking like "every other week". A step on the day-of-week field steps through the days themselves, not the weeks — it means Sunday, Tuesday, Thursday and Saturday.

4.14. A campaign in a specific local time

The campaign: live at 09:00 local time in the market it serves, whatever the server is set to.

Field
Set it to

cron

your pattern, written in local time

timezone

e.g. Europe/IstanbulUTC when left unset

The rule is read in that local time, so the campaign keeps its wall-clock hours across daylight-saving changes instead of drifting by an hour twice a year.

Watch out for: the dates that come back are always in UTC, whatever zone you set, so a 09:00 Istanbul window reads as 06:00Z. Zone abbreviations like EST and fixed offsets like +03:00 are not accepted — the point of a zone name is that it knows about daylight saving, and an offset does not.

4.15. Making an existing campaign recurring

The campaign: an offer that has been running on fixed dates should now come back on a pattern.

Field
Set it to

cron

your pattern

duration

as the pattern needs

timezone

e.g. Europe/IstanbulUTC when left unset

start_datetime / end_datetime

do not send them — the schedule takes them over

Attach a schedule to the offer as it stands. Its dates stop being yours to set from that moment and are overwritten with the first window the rule produces, so check that window is where you expect before saving — especially if the offer is live right now.

Watch out for: an offer whose status is used or revoked cannot be scheduled. If the campaign is finished, schedule a fresh offer instead of reviving that one.

5. What the Schedule Reports Back

Reading an offer back — after saving it, or when retrieving or listing offers — returns its schedule together with what it has worked out. Four dates describe the windows and one describes the handover:

What it reports
How to read it

The current window

The window the offer is in — or, in a gap between windows, the one it is waiting for. These are the dates mirrored onto the offer itself.

The next window

The window after that one. Empty when the schedule has nothing further to offer.

The next update

When the offer will next be re-evaluated and handed over — normally the end of the current window. Empty once there is nothing further to move to.

This is the quickest way to check that a rule says what you meant. If a rule is meant to make the offer live tomorrow morning and the current window starts next month, the rule is wrong — and you can see that before anyone shops against it. See Admin → Basket Offers in the Commerce OpenAPI reference for the field names and an example response.

A few things worth knowing when reading this back:

  • Windows are worked out up to a year ahead of the point the schedule is read from — which is the start date when that is still in the future, so a schedule set up well in advance still reports its first window correctly.

  • An empty set of windows means the schedule has nothing left. It happens when the end date has passed, when every inclusion is in the past, or when the rule matches nothing in the coming year. The offer keeps the dates of its last window and quietly stops being live once they pass.

  • Durations read back differently from how you wrote them. They are returned as days, hours, minutes and seconds rather than in the ISO format, so a nine-hour duration comes back as 09:00:00.

6. Living with a Schedule

6.1. Changing a schedule

Saving a schedule works out the current and next windows again straight away, and the offer's dates follow immediately.

In practice this means adding a single blackout is not a one-line change: resend the rule, the duration and the timezone alongside the new exclusion, or you will silently drop them. Read the offer first, change what you need in what comes back, and send that.

6.2. Removing a schedule

Clear the schedule and the offer goes back to manual control. You can hand it fresh start and end dates in the same change — and that is the only way to give those dates a value on an offer that currently has a schedule. Sending a date on its own, without clearing the schedule, is rejected.

6.3. Pausing a scheduled offer

Set the offer's status to passive. It stops selling immediately, whatever its window says, while the schedule carries on advancing in the background — so setting the status back to active picks up on whichever window is current by then. This is the right way to pause a scheduled campaign; editing the rule to route around today is not.

Two consequences worth knowing:

  • An offer with a schedule is not switched to passive when one of its windows closes. Waiting for the next window is normal, so the routine that retires expired offers leaves scheduled ones alone.

  • Because the status is never touched by the schedule, an offer left passive stays passive through every window until somebody activates it.

7. Troubleshooting

Nothing is stored when a change is rejected — an existing schedule is left exactly as it was.

Message
What it means

Scheduling is not available on this deployment. Contact the platform team to enable it.

The background routine that drives schedules is not running here, so a schedule would never fire.

Provide at least one of cron or inclusions.

The schedule has no rule and no explicit dates, so there is nothing to work out a window from. Usually the result of sending a partial change — see section 6.1.

duration is required when cron fires at specific minutes.

The rule opens windows at named minutes, so it needs to know how long each one lasts.

Do not set duration when cron uses minute-matching mode (minute=*).

The rule already describes which minutes are live, so a duration would contradict it. Drop the duration, or name a minute in the rule.

cron must have 5 space-separated fields.

Count the fields: minute, hour, day-of-month, month, day-of-week.

Duration has wrong format.

The duration could not be read. Check it against the table in section 3.2 — P1W is a common cause, since weeks are not accepted.

Ranges must not overlap.

Two inclusions cover some of the same time. Merge them into a single range.

start must be before end

One of the ranges ends at or before it starts.

end_date must be after start_date.

The bounds are the wrong way round, so the schedule could never produce a window.

Unknown timezone.

Not a recognised IANA zone name. Use a full name such as Europe/Istanbul.

Managed by scheduler; clear scheduler to set start_datetime/end_datetime manually.

The offer's dates belong to its schedule. Clear the schedule in the same change to take them back — see section 6.2.

Cannot attach a scheduler to a USED or REVOKED offer.

The offer has reached the end of its life and cannot be scheduled.

If a schedule saves cleanly but the offer is not live when you expect, work through these in order:

  1. Check the offer's status. A passive offer never sells, whatever its window says.

  2. Check the current window it reports. If it is not the window you meant, the rule is wrong — not the schedule.

  3. Check the timezone. A rule written for local time but left on UTC is the most common cause of an offer that goes live a few hours out.

  4. Check the bounds. A start date in the future or an end date in the past both leave a perfectly good rule producing nothing.

  5. Check the exclusions. A blackout that is wider than you thought can swallow a whole window.

8. Reference

In the Commerce OpenAPI reference:

  • Admin → Basket Offers — every schedule field, the request and response bodies, the rule-shape examples, and the validation responses

Elsewhere in the documentation:

Last updated

Was this helpful?