1. Insight
Insight
The problem this article addresses and why it matters.
Cron is write-once, debug-frequently
Cron is one of those formats every developer can write but few can read aloud. 0 9 * * 1-5 is "weekdays at 9 AM" once you know the field order; */15 9-17 * * 1-5 is "every 15 minutes between 9 and 5 PM on weekdays". The expressions get committed, the team moves on, and three months later someone needs to debug why a job ran at the wrong time — and now they're re-deriving the field order from man page memory.
The harder problem is conflict detection across crontabs. A single team's crontab is manageable. Across a microservice deployment with dozens of services each scheduling its own jobs, two services hitting the same time window can cascade: both run at 0 2 * * *, both contend for the same database connection pool, both timeout, both alert on-call. The conflict was inevitable; nothing in the crontab tooling surfaces it.
Why humanise + conflict-check in one tool
The tool in this article does two related jobs. Humanise mode takes a single cron expression and produces a plain-English description plus the next N run times. That's the everyday "what does this expression mean?" workflow. Conflict mode takes an array of expressions — typically every cron expression in the team's deployment — and identifies overlaps and adjacencies.
The conflict detector returns severity ratings: a collision is two expressions firing at the exact same time, an adjacent finding is two expressions firing within 5 minutes of each other (close enough to contend for shared resources). The output tells you which pairs of expressions overlap and when.
What this article delivers
Humanise mode walked against three expressions of varying complexity, conflict mode walked against a representative team crontab, and the next-run-time calculation against timezone-aware schedules. We cover the edge cases around DST transitions, the ? and L extensions that some cron parsers support, and the cases where the tool's output should be sanity-checked against the actual scheduler's behaviour.
2. Intent
Intent
What you will be able to do after reading.
By the end of this article you will be able to:
- Translate any cron expression into plain English with parsed field breakdowns
- Compute the next N run times for a schedule in a chosen timezone, with DST awareness
- Run conflict-detection across an array of cron expressions to find collisions and resource-contention windows
- Read the conflict report's severity field — collision (exact same time) vs adjacent (within 5 minutes)
- Recognise the cron extensions (
?,L,W,#) that some parsers support and others reject
The Examples section walks through three expressions in humanise mode and a representative team crontab in conflict mode.
3. Examples
Examples
Annotated code and worked scenarios.
Before / after: humanising three expressions
cronHumanizer({
expression: '0 9 * * 1-5',
count: 3,
timezone: 'America/New_York',
});
// human: 'Every weekday at 9:00 AM (America/New_York)'
// valid: true
// nextRuns: [
// '2026-05-19T13:00:00.000Z', // Monday 9:00 ET
// '2026-05-20T13:00:00.000Z',
// '2026-05-21T13:00:00.000Z',
// ]
// parts: { minute: '0', hour: '9', dayOfMonth: '*', month: '*', dayOfWeek: '1-5' }Plain English plus the next three runs in UTC. The parts breakdown is useful when debugging — you can see exactly which fields the parser interpreted.
A more complex expression:
cronHumanizer({
expression: '*/15 9-17 * * 1-5',
});
// human: 'Every 15 minutes between 9:00 AM and 5:00 PM on weekdays'And an edge case with explicit ranges:
cronHumanizer({
expression: '0 0 1,15 * *',
});
// human: 'At midnight on the 1st and 15th of every month'Before / after: conflict detection across a team crontab
A representative microservice crontab — five jobs across the day:
cronHumanizer({
expression: '0 2 * * *', // self
conflictCheck: [
'0 2 * * *', // job A — same time
'5 2 * * *', // job B — 5 min later
'0 3 * * *', // job C — 1 hour later
'0 2 * * 0', // job D — same time but only Sunday
],
});
// conflicts: [
// {
// expressionA: '0 2 * * *', expressionB: '0 2 * * *',
// overlapWindow: 'Both run at 02:00 UTC every day',
// severity: 'collision',
// },
// {
// expressionA: '0 2 * * *', expressionB: '5 2 * * *',
// overlapWindow: 'Run within 5 minutes of each other at 02:00 UTC every day',
// severity: 'adjacent',
// },
// {
// expressionA: '0 2 * * *', expressionB: '0 2 * * 0',
// overlapWindow: 'Both run at 02:00 UTC every Sunday',
// severity: 'collision',
// },
// ]Three conflict findings. The collision with job A is the immediate problem (two services both think they own the 2 AM window). The adjacent finding for job B is the resource-contention warning — if either job runs longer than 5 minutes, they'll overlap and contend for whatever shared resource they touch.
Before / after: timezone-aware next-run computation
DST transitions break naive cron scheduling. A cron expression 0 2 * * * set in America/New_York produces two valid runs on the fall-back day (2 AM happens twice) and zero valid runs on the spring-forward day (2 AM is skipped). The tool's count parameter respects this:
cronHumanizer({
expression: '0 2 * * *',
count: 10,
timezone: 'America/New_York',
});
// nextRuns: [
// '2026-11-01T05:00:00.000Z', // 2 AM ET — first occurrence
// '2026-11-01T06:00:00.000Z', // 2 AM ET again after fall-back
// '2026-11-02T07:00:00.000Z',
// ...
// ]For DST-sensitive jobs (financial reconciliation, log rotation), use UTC explicitly in the expression's interpretation by setting timezone: 'UTC'. The tool's next-run output then reflects clock time independent of DST behaviour.
Before / after: conflict-detection wired into CI
A pre-deploy check that runs against every cron expression in the service-mesh:
# In CI:
EXPRESSIONS=$(yq '.services[].schedule' deployment.yaml | jq -R . | jq -s .)
RESULT=$(curl -X POST https://obfus.link/api/v1/cron_humanizer \
-d "$(jq -n --argjson exprs "$EXPRESSIONS" \
'{expression: $exprs[0], conflictCheck: $exprs[1:]}')")
COLLISIONS=$(echo "$RESULT" | jq -r '.conflicts | map(select(.severity == "collision")) | length')
[ "$COLLISIONS" -eq 0 ] || { echo "Crontab collisions detected: $COLLISIONS"; exit 1; }Now a new service whose schedule conflicts with an existing one fails the deploy. The conflict is caught at the deployment-config layer, not after the first 2 AM overlap actually triggers a database pool exhaustion.
When humans use this
The dominant use is reading: paste a cron expression you didn't write, get a plain-English description. The conflict-detection mode is the higher-leverage pre-launch check — run before adding a new scheduled job to verify it doesn't collide with an existing one. The next-run computation helps debug "did this job actually run?" investigations.
When agents use this
Two production patterns:
- Cron PR review. An agent reviewing PRs to any service's cron schedule runs the conflict detector against the existing schedules in the codebase. Any collision or close-adjacent finding opens a PR comment with the specific overlapping expressions.
- Infra-as-code validation. A pipeline that generates Kubernetes CronJobs from a YAML manifest runs every emitted schedule through the humaniser to verify the cron parser interprets it the same way the manifest author intended. Mismatches (e.g.
30 2 * * 1-5mistakenly meaning "30 minutes past 2 AM" not "2:30 AM") surface as warnings.
Edge cases
Cron dialects
Standard cron has 5 fields (minute, hour, day-of-month, month, day-of-week). Some systems extend this: Quartz uses 6 or 7 fields (adding seconds and year), Kubernetes CronJobs use 5, AWS EventBridge uses 6 with ? placeholders. The tool defaults to 5-field standard cron; pass dialect: 'quartz' for Quartz extensions.
L, W, # extensions
Some parsers support L (last day), W (nearest weekday), # (Nth weekday of month). The tool surfaces these in humanise mode with a note that the syntax is non-portable — verify your scheduler supports it before using.
Day-of-week numbering
Standard cron uses 0-6 with 0 = Sunday. Some systems use 1-7 with 1 = Sunday or 1 = Monday. The tool defaults to standard; if your scheduler differs, the humanised description will be off by one day. Verify against your scheduler's docs.
Zero-padding behaviour
05 is the same as 5 in standard cron; some strict parsers reject the zero-padded form. The tool accepts both. The humanised output uses unpadded numbers.
4. Documentation
Documentation
Reference signatures, edge cases, and lookup tables.
Input parameters
Field | Type | Required | Default | Description |
|---|---|---|---|---|
|
| ✓ | — | A cron expression to humanise / inspect |
|
| ✗ |
| Number of next-run timestamps to compute. 0 returns description only |
|
| ✗ |
| IANA timezone for next-run computation |
|
| ✗ | — | Array of other cron expressions to check for overlaps |
|
| ✗ |
| 5-field standard cron vs Quartz 6/7-field |
Output shape
{
human: string; // 'Every weekday at 9:00 AM'
valid: boolean;
nextRuns?: string[]; // ISO timestamps when count > 0
parts: {
minute: string;
hour: string;
dayOfMonth: string;
month: string;
dayOfWeek: string;
second?: string; // Quartz only
year?: string; // Quartz only
};
conflicts?: Array<{ // when conflictCheck provided
expressionA: string;
expressionB: string;
overlapWindow: string;
severity: 'collision' | 'adjacent';
}>;
}Severity definitions
Severity | Trigger |
|---|---|
| Both expressions fire at the exact same time at least once per common window |
| Both expressions fire within 5 minutes of each other |
Error codes
Code | When it fires | Recovery |
|---|---|---|
|
| Provide a cron expression |
| Expression doesn't parse as the chosen dialect | Verify field count + syntax matches |
| Field value out of range (minute > 59, hour > 23) | Fix the field |
| Expression uses an extension the dialect doesn't support ( | Switch dialect or rewrite without the extension |
|
| Split into multiple calls |
When NOT to use this tool
For complex scheduling needs (rate-limited jobs, dependency-graph triggers, conditional runs), cron is the wrong primitive. Use a workflow engine (Airflow, Temporal, Prefect) where dependencies and conditions are first-class. The humaniser handles cron specifically.
For scheduling with sub-minute precision, standard cron tops out at 1-minute resolution. Quartz dialect supports seconds; the humaniser handles those, but most schedulers don't. Pick a sub-minute scheduler if you need it.
Performance notes
Typical execution: under 3ms for humanise + parts. Next-run computation: 1ms per future run requested, capped at 100 runs. Conflict detection: O(n²) in the conflictCheck array — 100 expressions runs in about 50ms. The tool is deterministic — same input always produces the same output — so REST responses are Edge-Cache eligible (cache key includes the timezone and current time bucket to keep next-run estimates fresh).