Generate Cron Expression for Last Weekday of Every Month

Quick Win: The LW Shortcut Most People Don't Know About

Here's the fastest answer if you're using a scheduler that supports Quartz-style cron syntax: 0 0 12 ? * LW fires at noon on the last weekday of every month. That single LW token—combining "last" (L) with "weekday" (W)—does all the heavy lifting. If your system supports it, you can stop reading right here and paste it into your job scheduler.

But if you're working with standard Unix crontab, AWS EventBridge, or a platform that doesn't recognize LW, you'll need a different playbook. Below are six practical techniques to get this scheduling pattern working reliably—regardless of your stack.

1. Understand Why Standard Cron Can't Express "Last Weekday" Natively

Standard Vixie cron—the format used by Linux crontab—gives you five fields: minute, hour, day-of-month, month, day-of-week. The day-of-month field accepts ranges (1–31), lists (1,15), and step values (*/2), but it has no concept of "last weekday."

You can target the last day of the month on some systems using L in the day-of-month field (e.g., 0 0 12 L * ?), but that includes weekends. And the W modifier—meaning "nearest weekday"—only works when attached to a specific day number like 15W. There's no native way to say "the last Monday-through-Friday in the month."

This gap is why developers end up with workarounds, shell-script guards, or Quartz-based schedulers. Knowing which limitation you're hitting is step one.

Which Platforms Support LW?

  • Quartz Scheduler (Java) — Full support for LW
  • Spring Scheduling — Supports LW via CronExpression (Spring 5.3+)
  • AWS EventBridge — Does not support LW; requires a workaround
  • Standard Linux crontab — No support; needs a script-level check
  • GitHub Actions scheduled workflows — No support; use a conditional check in the workflow

2. The Quartz Expression: Break Down Every Field

If you're on Quartz or Spring, the expression 0 0 12 ? * LW is your golden ticket. Let's dissect it field by field so you can customize it confidently:

FieldValueMeaning
Seconds0At the top of the minute (zero seconds)
Minutes0Minute zero
Hours12Noon (24-hour format)
Day-of-month?No specific day—let the day-of-week field decide
Month*Every month, January through December
Day-of-weekLWLast weekday (Mon–Fri) of the month

Want it at 9 AM instead? Change hours to 9. Need it to run at 5:30 PM on the last business day? Use 0 30 17 ? * LW. The LW token stays put—it's the scheduling brain of the expression.

One Critical Gotcha: LW vs. 5L vs. FRIL

Don't confuse LW with 5L or FRIL. Those mean "the last Friday of the month"—a narrower target. LW means "the last weekday," which could be a Monday, Tuesday, Wednesday, Thursday, or Friday, depending on where the calendar lands.

For example, in August 2024, the last day of the month is Saturday the 31st. The last weekday is Friday the 30th. In September 2024, the last day is Monday the 30th—so the last weekday is the last day. LW handles both scenarios automatically.

3. The Unix Crontab Workaround: Run on Days 28–31, Filter in Shell

For standard Linux crontab, you need a two-part strategy: schedule the job to run on the last four possible days of each month, then use a shell condition to execute only if today is actually the last weekday.

Here's the crontab entry:

0 12 28-31 * * /usr/local/bin/last-weekday-check.sh

And here's what goes inside last-weekday-check.sh:

Thursday, leap year). Continue the loop through all 12 months and eyeball every date against a real calendar. If any entry lands on a Saturday or Sunday, your logic has a bug.

Common Validation Pitfalls to Watch For

When your test output looks wrong, check these usual suspects before tearing apart your script:

  • Timezone drift: The date command uses your server's local timezone. If your crontab runs in UTC but your business logic expects Eastern time, the "last weekday" shifts by up to five hours. Always set TZ explicitly inside your script: export TZ="America/New_York".
  • Quartz vs. POSIX confusion: Quartz uses ? in the day-of-month field when day-of-week is specified, and it treats LW as a single token. Standard crontab has no ? and no L modifier. Mixing the two syntaxes is the most common cause of silent failures.
  • Off-by-one in date arithmetic: GNU date -d "+1 day" is reliable, but on Alpine Linux (BusyBox date), the -d flag behaves differently. If you're running inside a minimal Docker container, install coreutils or rewrite the logic using pure shell arithmetic.
  • February edge cases: February is the shortest month and the one most likely to expose bugs. In a leap year, the 29th is a valid day; in a non-leap year, scheduling 0 12 29 * * simply skips February entirely. The 28–31 range strategy handles this gracefully, but only if your filter logic is solid.

7. Timezone Strategy: Don't Let UTC Silently Shift Your Schedule

If your scheduler runs in UTC (as most cloud platforms default to) but your business operates in a local timezone, the "last weekday" can shift in surprising ways. Consider a scenario where you're in US Eastern time and the last day of August is a Saturday. In UTC, midnight August 31 arrives five hours before it does in New York. If your job fires at 00:00 UTC, it's still Friday evening in Eastern time—and your "last weekday" calculation may already be looking at Saturday.

The fix is to normalize everything to a single timezone before doing any date math:

For shell scripts, add this at the top:

export TZ="America/New_York"

For Lambda functions, set the TZ environment variable in your function configuration. For GitHub Actions, prefix your step with TZ=America/New_York or use the timezones input in a setup action.

Once the timezone is pinned, every date call in your logic will operate in the same frame of reference, and your "last weekday" determination will match what a human in that timezone would expect.

8. Handling Holidays: The Next Layer of Complexity

So far we've defined "last weekday" as the last Monday–Friday of the month. But if the last business day of August is a Friday and that Friday happens to be Labor Day weekend, your job will fire on a holiday—even though no one is in the office to act on the results.

Cron expressions alone can't account for public holidays. You need a holiday calendar lookup. The most maintainable approach:

  • Maintain a JSON or CSV file of observed holidays for your operating region. Update it annually.
  • In your Lambda or shell script, after confirming today is the last weekday, check whether today's date appears in the holiday list.
  • If it's a holiday, skip execution and let the job run on the previous business day instead. This means you also need to schedule the job for days 27–31 to catch the fallback.

For shell, a simple grep check works:

if grep -q "$(date +%Y-%m-%d)" /usr/local/share/holidays.csv; then exit 0; fi

For Lambda, use a library like date-holidays (npm) or holidays (Python) to compute observed dates dynamically without maintaining a static file.

9. Monitoring and Observability: Know When It Fires

A cron job that runs once a month is easy to forget about. If it silently fails in March, you might not notice until April—by which point you've missed a full cycle. Build observability into every last-weekday job:

  • Emit a metric on every execution (success or skip). In AWS, use CloudWatch Embedded Metric Format. In GitHub Actions, use the actions/check-run API or a third-party monitoring action.
  • Set an alert if the job hasn't run in 35 days. This catches cases where the scheduler itself is broken or the filter logic is too aggressive.
  • Log the computed "last weekday" date alongside the actual run date. If they don't match, you have a bug.
  • Use a dead-letter queue for jobs that trigger but fail downstream. The last weekday of the month is often a high-stakes window (month-end reporting, billing runs, data snapshots)—failures need immediate visibility.

10. Quick Reference: Which Scheduler Supports What

Before you commit to an approach, confirm what your scheduler actually supports. Here's a quick compatibility matrix:

  • Quartz Scheduler: Full LW support. Use 0 0 12 ? * LW. No workaround needed.
  • Spring @Scheduled (via Quartz): Same as Quartz. The LW token works if you're using the Quartz-backed cron trigger.
  • Spring @Scheduled (native): Spring's native cron parser does not support L or LW. You'll need the shell-style workaround or a programmatic check inside the method body.
  • Linux crontab (Vixie cron): No L or LW. Use the 28–31 range plus shell filter.
  • AWS EventBridge: No L or LW. Use 28–31 plus Lambda guard.
  • GitHub Actions: POSIX cron only. Use 28–31 plus conditional step.
  • Kubernetes CronJob: Uses standard crontab syntax. No LW. Apply the same shell-filter strategy inside the container.
  • Airflow: Use a custom Timetable or a sensor that checks the date. Airflow's cron parser doesn't support LW natively.
  • Google Cloud Scheduler: Standard cron syntax. No LW. Use the 28–31 pattern with a filtering step in your target function.

The Bottom Line

Scheduling a job for the last weekday of every month is one of those deceptively simple requirements that exposes the limits of standard cron syntax. If you're on Quartz or Spring with Quartz integration, you're in luck—0 0 12 ? * LW does all the heavy lifting in a single token. But if you're working with POSIX crontab, AWS EventBridge, GitHub Actions, Kubernetes, or any cloud-native scheduler, you'll need the two-part strategy: schedule broadly (days 28–31) and filter precisely (check whether today is actually the last weekday).

The shell script approach is the most portable and works everywhere GNU date is available. The Lambda and GitHub Actions variants follow the same logic in different languages. Whatever platform you're on, the three rules are the same: pin your timezone, validate against a 12-month test, and add monitoring so a silent failure in March doesn't become a crisis in April.

And if your definition of "last weekday" needs to exclude public holidays, remember that no cron expression can do that for you. That's a job for your application logic—schedule the candidate days, then let your code make the final call. The cron expression gets you to the right neighborhood; your filter logic walks you to the exact door.

Frequently Asked Questions

How do I write a cron expression for the last weekday of every month?

Standard Linux cron does not have a direct "last weekday" token, making it difficult to write a single expression for this. You typically need to use a workaround involving a script that checks if tomorrow is the first day of the next month and if today is Monday through Friday. Alternatively, advanced cron schedulers like Quartz support the 'L' and 'W' characters combined (e.g., LW) to achieve this easily.

What is the Quartz cron expression for the last weekday of the month?

In Quartz CronTrigger, you can use the expression `0 0 12 LW * ?` to trigger a job at noon on the last weekday of every month. The 'LW' character sequence specifically tells the Quartz scheduler to fire on the last weekday (Monday to Friday) of the month. This is the most reliable way to handle last weekday schedules if your tool supports Quartz syntax.

Can standard Linux crontab run a job on the last business day of the month?

No, standard Vixie cron cannot natively identify the last business day or weekday of the month. To achieve this in Linux, you must run a cron job on the last three days of the month and use a shell script to verify if the current day is the final weekday. The script will exit without executing your main logic if it is not the actual last weekday.

How do I run a cron job on the last Friday of every month?

To run a task specifically on the last Friday of the month, standard cron requires a workaround because it doesn't support 'last Friday' directly. You can schedule a job for Fridays 25-31 and use a script to check if the current Friday is indeed the last one of the month. If you are using Quartz, the expression `0 0 12 ? * 6L` will trigger exactly on the last Friday.

What does the LW symbol mean in cron expressions?

The 'LW' symbol is a special character used in advanced cron implementations like Quartz to denote the last weekday of the month. 'L' stands for the last day of the month, and 'W' stands for the closest weekday. When combined as 'LW', it instructs the scheduler to run the job on the final working day of that specific month.

How to trigger a script on the last working day of the month using bash and cron?

You can schedule a cron job to run on the 28th through 31st of every month using an expression like `0 0 28-31 * *`. Within your bash script, calculate tomorrow's date and check if tomorrow's month differs from today's month. If it does, and today is a weekday, execute your logic; otherwise, exit the script.

Why doesn't my standard cron expression work for the last weekday of the month?

If your expression isn't working, you are likely using standard Linux cron, which lacks the native 'LW' or 'L' operators for weekdays. Many users mistakenly try to use Quartz syntax on a standard Linux crontab, which will result in a syntax error. You must either use a platform that supports Quartz cron or implement a bash script workaround.

Is there a difference between the last day of the month and the last weekday of the month in cron?

Yes, the last day of the month is simply the final calendar date (28th, 29th, 30th, or 31st), which can fall on a weekend. The last weekday specifically excludes Saturdays and Sundays, meaning it must fall on a Monday through Friday. If the last day of the month is a Saturday, the last weekday would actually be the 30th.

How do I schedule a task for the last weekday of the month in AWS EventBridge?

AWS EventBridge supports cron expressions with an advanced syntax that includes the 'L' token for the last day of the month. To target the last weekday, you can set the day-of-month field to 'L' and the day-of-week field to 'MON-FRI'. However, testing is recommended as cloud providers sometimes have specific parsing rules for combined weekday and last-day logic.