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.
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:
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
activeand 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.
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
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-5says "weekdays, business hours" more plainly than a start time plus a nine-hour duration.
An hour range does not make a window on its own. 0 20-22 * * * names three separate moments — 20:00, 21:00 and 22:00 — so it opens three windows a day, not one stretch from 20:00 to 22:00. For a single 20:00–22:00 window, use 0 20 * * * with a two-hour duration, or * 20-21 * * * with no duration at all.
Keep the minute field to a single value. A window-opening rule fires at most once per matching hour, so listing several minutes — 0,30 — or stepping through them — */15 — does not open several windows in that hour. Only the earliest of those minutes takes effect: */15 9-16 * * 1-5 behaves as 0 9-16 * * 1-5. Several hours are fine (0 12,19 * * * opens two windows a day); it is only minutes within one hour that collapse. Sub-hourly patterns cannot be expressed today — use one window per hour, or list the slots as inclusions.
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:
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:
The recurrence rule produces its stream of windows.
Inclusions add their windows on top.
Windows from those two steps that overlap or touch are merged into one.
Exclusions are cut out of every merged window — splitting a window in two where a blackout lands inside it.
end_datetrims any window that would run past it.
Step 3 is a merge, not an intersection. Where a rule window and an inclusion overlap, the offer runs across both: a rule saying Monday 09:00–17:00 plus an inclusion saying Monday 06:00–10:00 leaves the offer live from 06:00 to 17:00 — the earlier start and the later end. Inclusions can only ever widen a schedule. To narrow one, use exclusions.
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.
timezone applies to the recurrence rule and to nothing else. Inclusion and exclusion ranges are absolute moments, so they are not shifted by it: with timezone set to Europe/Istanbul, an inclusion written as 10:00Z still starts at 10:00 UTC, which is 13:00 in Istanbul. Write those ranges with the offset you mean — 10:00+03:00 — or convert them to UTC yourself.
4.1. A happy hour, every evening
The campaign: a discount that runs from 17:00 to 19:00, every day.
cron
* 17-18 * * *
duration
leave unset
timezone
e.g. Europe/Istanbul — UTC 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.
cron
0 9 * * 1-5
duration
PT9H
timezone
e.g. Europe/Istanbul — UTC 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.
cron
0 12,19 * * *
duration
PT1H
timezone
e.g. Europe/Istanbul — UTC 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.
cron
0 22 * * *
duration
PT4H
timezone
e.g. Europe/Istanbul — UTC 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.
cron
0 18 * * 5
duration
P2DT6H
timezone
e.g. Europe/Istanbul — UTC 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.
cron
0 0 1 * *
duration
P1D
timezone
e.g. Europe/Istanbul — UTC 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.
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/Istanbul — UTC 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.
cron
your pattern
duration
as the pattern needs
start_date
the launch date
timezone
e.g. Europe/Istanbul — UTC 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.
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.
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.
cron
0 9 * * 1-5
duration
PT8H
inclusions
the Black Friday weekend
exclusions
New Year's Day
timezone
e.g. Europe/Istanbul — UTC 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.
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/Istanbul — UTC 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.
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.
cron
your pattern, written in local time
timezone
e.g. Europe/Istanbul — UTC 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.
cron
your pattern
duration
as the pattern needs
timezone
e.g. Europe/Istanbul — UTC 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:
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.
Always send the whole rule, not just the part you are changing. A schedule is stored as a whole, so anything left out of the change goes back to its default — the rule to empty, the duration and end date to nothing, inclusions and exclusions to empty lists, the timezone back to UTC. The start date is the one exception and keeps the value it already had.
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
passivewhen 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
passivestayspassivethrough 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.
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:
Check the offer's status. A
passiveoffer never sells, whatever its window says.Check the current window it reports. If it is not the window you meant, the rule is wrong — not the schedule.
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.
Check the bounds. A start date in the future or an end date in the past both leave a perfectly good rule producing nothing.
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:
How to Create and Manage Campaigns in Omnitron — managing the offers themselves in Omnitron
Last updated
Was this helpful?

