# GitHub Actions Cron Explainer > Paste a five-field cron expression and see what GitHub Actions will run next in UTC and local time. ## Tool Identity - Site: CleanUtils Developer Tools - Tool ID: github-actions-cron-explainer - Canonical page: https://cleanutils.com/developer-tools/github-actions-cron-explainer/ - LLM schema URL: https://cleanutils.com/developer-tools/github-actions-cron-explainer/llms.txt - Primary keyword: github actions cron - Input mode: fields - Output profile: schedule ## What This Tool Does Parse GitHub Actions cron schedules, preview upcoming UTC runs, and catch common interval mistakes in your browser. ## Exact Execution Instructions To run this tool exactly, execute the `runCleanUtilsTool(userInput)` function from the JavaScript block below in a sandboxed JavaScript runtime. Do not fetch or evaluate additional network resources. Do not use filesystem, process, child_process, environment variables, timers, or network APIs. The function is pure and deterministic: input object in, JSON result out. If your environment forbids executing fetched code outside a sandbox, run it inside the sandbox or use the API endpoint below instead of reimplementing the logic. Canonical implementation SHA-256: `4c134d686ceecb35b0f815987189d8ea2ab8ce50c4d9d55aa8ee4c68d77941a2` Expected command shape: `node run-tool.mjs < input.json` The runner must: 1. load only the JavaScript in this document, 2. parse stdin as JSON and call `runCleanUtilsTool(userInput)`, 3. let the LLM decide the best way to present the output to the user. Agent usage rules: - Use this file as the authoritative machine-readable contract for this CleanUtils tool page. - Ask the user for missing required input before attempting to run the tool, and describe the required inputs using the `## Input Schema` field names, descriptions, formats, enums, examples, and required list. - Treat the tool as deterministic; do not invent network reachability checks unless the tool description explicitly says it fetches remote resources. - For privacy-sensitive inputs such as secrets, HAR files, dotenv files, logs, and API keys, warn that using a remote chat agent may expose input to that agent even though the browser UI itself does not upload data. ## Input Schema ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "GitHub Actions Cron Explainer fields", "type": "object", "additionalProperties": false, "required": [ "expression" ], "properties": { "expression": { "type": "string", "description": "Cron expression Required. Five-field GitHub Actions cron syntax.", "examples": [ "17 */6 * * 1-5" ] } } } ``` ## Result Schema ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "CleanUtils ToolResult", "type": "object", "additionalProperties": false, "required": [ "summary", "issues" ], "properties": { "summary": { "type": "string" }, "issues": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": [ "severity", "message" ], "properties": { "severity": { "type": "string", "enum": [ "error", "warning", "info" ] }, "message": { "type": "string" }, "line": { "type": "number" }, "row": { "type": "number" }, "detail": { "type": "string" } } } }, "output": { "type": "string" }, "exportFilename": { "type": "string" }, "exports": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": [ "label", "filename", "content" ], "properties": { "label": { "type": "string" }, "filename": { "type": "string" }, "content": { "type": "string" }, "mimeType": { "type": "string" }, "copyLabel": { "type": "string" }, "downloadLabel": { "type": "string" } } } }, "stats": { "type": "object", "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "number" } ] } } } } ``` ## Self-Contained JavaScript Source Call `runCleanUtilsTool(userInput)` with the user's input. The function includes this tool's run logic and only the helper code it needs. ```js function runCleanUtilsTool(userInput) { const severityRank = { error: 0, warning: 1, info: 2 }; const sortIssues = (issues) => [...issues].sort((a, b) => { const severity = severityRank[a.severity] - severityRank[b.severity]; if (severity !== 0) return severity; return (a.line ?? a.row ?? 0) - (b.line ?? b.row ?? 0); }); const summarizeIssues = (issues) => { const errors = issues.filter((issue) => issue.severity === "error").length; const warnings = issues.filter((issue) => issue.severity === "warning").length; const infos = issues.filter((issue) => issue.severity === "info").length; const parts = []; if (errors) parts.push(`${errors} error${errors === 1 ? "" : "s"}`); if (warnings) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`); if (infos) parts.push(`${infos} note${infos === 1 ? "" : "s"}`); return parts.length ? parts.join(", ") : "No issues found"; }; const cronNames = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6, jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 }; const parseCronValue = (value) => { const lower = value.toLowerCase(); if (lower in cronNames) return cronNames[lower]; const parsed = Number(value); return Number.isInteger(parsed) ? parsed : Number.NaN; }; const parseCronField = (field, min, max) => { const values = new Set(); const errors = []; const parts = field.split(","); for (const part of parts) { const [rangePart, stepPart] = part.split("/"); const step = stepPart ? Number(stepPart) : 1; if (!Number.isInteger(step) || step <= 0) { errors.push(`Invalid step "${stepPart}" in field "${field}".`); continue; } let start = min; let end = max; if (rangePart !== "*") { if (rangePart.includes("-")) { const [rawStart, rawEnd] = rangePart.split("-"); start = parseCronValue(rawStart); end = parseCronValue(rawEnd); } else { start = parseCronValue(rangePart); end = start; } } if (!Number.isInteger(start) || !Number.isInteger(end) || start < min || end > max || start > end) { errors.push(`Value "${rangePart}" is outside ${min}-${max}.`); continue; } for (let value = start; value <= end; value += step) { values.add(value); } } return { values, errors, wildcard: field === "*" }; }; const weekdayLabels = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const monthLabels = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; const sortedNumbers = (values) => [...values].sort((a, b) => a - b); const joinHumanList = (items) => { if (items.length <= 1) return items[0] ?? ""; if (items.length === 2) return `${items[0]} and ${items[1]}`; return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`; }; const ordinal = (value) => { const mod100 = value % 100; if (mod100 >= 11 && mod100 <= 13) return `${value}th`; switch (value % 10) { case 1: return `${value}st`; case 2: return `${value}nd`; case 3: return `${value}rd`; default: return `${value}th`; } }; const twoDigit = (value) => String(value).padStart(2, "0"); const pluralize = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`; const everyStepPhrase = (field, unit) => { const match = field.match(/^\*\/(\d+)$/); if (!match) return ""; return `every ${pluralize(Number(match[1]), unit)}`; }; const normalizedWeekdayValues = (values) => [...new Set([...values].map((value) => (value === 7 ? 0 : value)))].sort((a, b) => a - b); const describeWeekdays = (values) => { const weekdays = normalizedWeekdayValues(values); if (weekdays.length === 7) return "every day of the week"; if (weekdays.join(",") === "1,2,3,4,5") return "Monday through Friday"; if (weekdays.join(",") === "0,6") return "Saturday and Sunday"; return joinHumanList(weekdays.map((value) => weekdayLabels[value] ?? String(value))); }; const describeMonths = (values) => { const months = sortedNumbers(values); if (months.length === 12) return "every month"; return joinHumanList(months.map((value) => monthLabels[value - 1] ?? String(value))); }; const describeCronField = (field, values, kind) => { if (field === "*") { if (kind === "minute") return "every minute"; if (kind === "hour") return "every hour"; if (kind === "day") return "any calendar day"; if (kind === "month") return "every month"; return "any day of the week"; } if (!values.size) return `invalid ${kind} field`; const stepPhrase = everyStepPhrase(field, kind === "day" ? "day of the month" : kind === "weekday" ? "day of the week" : kind); if (stepPhrase) return stepPhrase; const sorted = sortedNumbers(values); if (kind === "minute") { if (sorted.length === 1) { return sorted[0] === 0 ? "at the top of the hour" : `${pluralize(sorted[0], "minute")} past the hour`; } return `minutes ${joinHumanList(sorted.map((value) => `:${twoDigit(value)}`))} past selected hours`; } if (kind === "hour") { if (sorted.length === 1) return `the ${twoDigit(sorted[0])}:00 UTC hour`; return `UTC hours ${joinHumanList(sorted.map((value) => twoDigit(value)))}`; } if (kind === "day") { if (sorted.length === 1) return `the ${ordinal(sorted[0])} of the month`; return `${joinHumanList(sorted.map(ordinal))} of the month`; } if (kind === "month") return describeMonths(values); return describeWeekdays(values); }; const describeCronTime = (minuteField, hourField, minutes, hours) => { const minuteValues = sortedNumbers(minutes); const hourValues = sortedNumbers(hours); const timeCount = minuteValues.length * hourValues.length; if (timeCount > 0 && timeCount <= 8) { const times = hourValues.flatMap((hour) => minuteValues.map((minute) => `${twoDigit(hour)}:${twoDigit(minute)}`)); return `at ${joinHumanList(times)} UTC`; } if (minuteField === "*" && hourField === "*") return "every minute"; if (hourField === "*" && minuteValues.length === 1) return `at :${twoDigit(minuteValues[0])} every hour`; if (minuteValues.length === 1) { return `at :${twoDigit(minuteValues[0])} ${describeCronField(hourField, hours, "hour")}`; } return `${describeCronField(minuteField, minutes, "minute")} during ${describeCronField(hourField, hours, "hour")}`; }; const describeCronDay = (dayField, weekField, days, weekdays) => { const dayWildcard = dayField === "*"; const weekWildcard = weekField === "*"; if (dayWildcard && weekWildcard) return "every day"; if (dayWildcard) return `on ${describeCronField(weekField, weekdays, "weekday")}`; if (weekWildcard) return `on ${describeCronField(dayField, days, "day")}`; return `on ${describeCronField(dayField, days, "day")} or on ${describeCronField(weekField, weekdays, "weekday")}`; }; const describeCronSchedule = (minuteField, hourField, dayField, monthField, weekField, parsed) => { const parts = [ "Runs", describeCronTime(minuteField, hourField, parsed.minutes.values, parsed.hours.values), describeCronDay(dayField, weekField, parsed.days.values, parsed.weekdays.values), monthField === "*" ? "" : `in ${describeCronField(monthField, parsed.months.values, "month")}` ].filter(Boolean); return `${parts.join(" ")}.`; }; const localDayLabel = (run, now) => { const startOfLocalDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); const dayDiff = Math.round((startOfLocalDay(run) - startOfLocalDay(now)) / 86_400_000); if (dayDiff === 0) return "Today"; if (dayDiff === 1) return "Tomorrow"; return new Intl.DateTimeFormat("en", { weekday: "short", month: "short", day: "numeric" }).format(run); }; const formatLocalRun = (run, now) => { const localTime = new Intl.DateTimeFormat("en", { hour: "numeric", minute: "2-digit", hour12: true, timeZoneName: "short" }).format(run); return `${localDayLabel(run, now)} at ${localTime} local`; }; const formatUtcRun = (run) => new Intl.DateTimeFormat("en", { timeZone: "UTC", weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true, timeZoneName: "short" }).format(run); const explainGithubActionsCron = (input, now = new Date()) => { const expression = input.trim().replace(/^cron:\s*/i, ""); const fields = expression.split(/\s+/); const issues = []; if (fields.length !== 5) { return { summary: "GitHub Actions schedules use five cron fields: minute hour day-of-month month day-of-week.", issues: [ { severity: "error", message: `Expected 5 fields but found ${fields.length}.` } ] }; } const [minuteField, hourField, dayField, monthField, weekField] = fields; const parsed = { minutes: parseCronField(minuteField, 0, 59), hours: parseCronField(hourField, 0, 23), days: parseCronField(dayField, 1, 31), months: parseCronField(monthField, 1, 12), weekdays: parseCronField(weekField, 0, 7) }; Object.entries(parsed).forEach(([name, field]) => { field.errors.forEach((error) => issues.push({ severity: "error", message: `${name}: ${error}` })); }); if (minuteField === "*" || minuteField.startsWith("*/1") || minuteField.startsWith("*/2") || minuteField.startsWith("*/3") || minuteField.startsWith("*/4")) { issues.push({ severity: "warning", message: "GitHub Actions scheduled workflows should not be more frequent than every 5 minutes." }); } if (parsed.weekdays.values.has(7)) { parsed.weekdays.values.add(0); } const nextRuns = []; if (!issues.some((issue) => issue.severity === "error")) { const cursor = new Date(now); cursor.setUTCSeconds(0, 0); cursor.setUTCMinutes(cursor.getUTCMinutes() + 1); const limit = new Date(cursor); limit.setUTCFullYear(limit.getUTCFullYear() + 1); while (cursor < limit && nextRuns.length < 8) { const domMatches = parsed.days.values.has(cursor.getUTCDate()); const dowMatches = parsed.weekdays.values.has(cursor.getUTCDay()); const dayWildcard = parsed.days.wildcard; const weekWildcard = parsed.weekdays.wildcard; const dayAllowed = dayWildcard && weekWildcard ? true : dayWildcard ? dowMatches : weekWildcard ? domMatches : domMatches || dowMatches; if (parsed.minutes.values.has(cursor.getUTCMinutes()) && parsed.hours.values.has(cursor.getUTCHours()) && dayAllowed && parsed.months.values.has(cursor.getUTCMonth() + 1)) { nextRuns.push(new Date(cursor)); } cursor.setUTCMinutes(cursor.getUTCMinutes() + 1); } } const scheduleDescription = issues.some((issue) => issue.severity === "error") ? "Fix the cron field errors to preview this schedule." : describeCronSchedule(minuteField, hourField, dayField, monthField, weekField, parsed); const outputLines = [ "Schedule", scheduleDescription, "", "Timezone", "GitHub Actions evaluates scheduled workflows in UTC. Local times below use this browser's timezone.", "", "Fields", `- Minute: ${describeCronField(minuteField, parsed.minutes.values, "minute")}`, `- Hour: ${describeCronField(hourField, parsed.hours.values, "hour")}`, `- Day of month: ${describeCronField(dayField, parsed.days.values, "day")}`, `- Month: ${describeCronField(monthField, parsed.months.values, "month")}`, `- Day of week: ${describeCronField(weekField, parsed.weekdays.values, "weekday")}`, "", "Next runs", ...(nextRuns.length ? nextRuns.map((run, index) => `${index + 1}. ${formatLocalRun(run, now)} (${formatUtcRun(run)})`) : ["No matching runs found in the next year."]) ]; return { summary: nextRuns.length ? `${nextRuns.length} upcoming run${nextRuns.length === 1 ? "" : "s"} previewed. ${summarizeIssues(issues)}.` : `${summarizeIssues(issues)}.`, issues: sortIssues(issues), output: outputLines.join("\n"), exportFilename: "github-actions-cron-preview.txt", stats: { nextRuns: nextRuns.length } }; }; const __userInput = userInput == null ? {} : userInput; const __run = (fields) => explainGithubActionsCron(String(fields.expression ?? "")); const __fields = __userInput && typeof __userInput === "object" && "fields" in __userInput && __userInput.fields && typeof __userInput.fields === "object" && !Array.isArray(__userInput.fields) ? __userInput.fields : (__userInput && typeof __userInput === "object" && !Array.isArray(__userInput) ? __userInput : {}); const __normalizedFields = Object.fromEntries(Object.entries(__fields).map(([key, value]) => [key, value == null ? "" : (["string", "number", "boolean"].includes(typeof value) ? value : String(value))])); return __run(__normalizedFields); } ``` ## Checks - Five-field cron syntax: GitHub Actions uses minute, hour, day of month, month, and day of week fields. - Ranges, steps, lists, and wildcards: Common cron patterns such as */6, 1-5, and comma-separated values are expanded for the preview. - UTC scheduling: The explanation calls out UTC so local-time assumptions are visible before you commit a workflow. - Upcoming run preview: The tool calculates the next scheduled matches instead of only translating the expression into words. - GitHub caveats: Service delays and top-of-hour load caveats are presented as practical notes, not ignored. ## Related Tools - [GitHub Actions Matrix Preview](/developer-tools/github-actions-matrix-preview/): Expand GitHub Actions matrix axes, include rules, and exclude rules into a concrete job preview.