GitHub Actions Workflow Generator
Compose a workflow YAML from triggers, a stack preset and jobs — with GitHub's real constraints checked as you build.
No constraint warnings.
Save it as .github/workflows/workflow.yml on your default branch.
Share links carry the form's structure and commands; env and with values never ride — the restored form shows their keys with empty values.
Press ? for shortcuts
Do it offline:$ mkdir -p .github/workflows
How to use it
- Name the workflow and pick a stack preset — each job then opens with cited checkout and setup steps for that language.
- Enable triggers: push and pull_request take branch filters, schedule takes one 5-field UTC cron per line, workflow_dispatch adds the manual run button.
- Add jobs and give each an id and a runs-on label; the suggestions are the GitHub-hosted runners, and self-hosted labels stay legal free entry.
- Attach your own steps — an action reference with with: inputs, or a shell command — plus env rows where the job needs variables.
- Read the constraint strip, then copy or download the YAML and commit it under .github/workflows on your default branch.
Assemble a GitHub Actions workflow file from parts you pick: triggers, a language preset, jobs, steps and env rows. The builder validates what GitHub actually enforces — job-id grammar, the 5-field schedule syntax, unique ids — and prints least-privilege token permissions by default. Everything is composed on this device; the YAML lands in your clipboard, not on a server.
How it works
The form compiles to a single document tree in the key order the workflow syntax reference documents: name, on, permissions, concurrency, jobs. Validation runs first and collects every finding in one pass — a bad job id does not hide an invalid cron — and splits results into refusals, which block emission, and advisories, which ride alongside the finished YAML in the constraint strip.
Identifiers are checked against GitHub's published grammar rather than guessed: a job id must open with a letter or underscore and stay within letters, digits, hyphen and underscore, ids must be unique inside one file, and env keys follow the shell-variable shape because the runner exports them into the job's process environment. Unknown runs-on labels are deliberately a warning, not an error — self-hosted and larger-runner labels are real.
Schedule lines pass through the same constraint model the cron helper on this site ships: macros like @daily are refused with their five-field rewrite printed in the message, each field is range-checked positionally, day-of-week 7 is rejected because the documentation defines 0 through 6, and a cadence tighter than five minutes draws an advisory quoting the observed gap.
Emission goes through the yaml library with folding disabled, then every cron scalar is forced into single quotes — an expression starting with an asterisk would otherwise collide with YAML's alias indicator, and quoting matches how the documentation prints schedules. Values that could re-parse as numbers or booleans come out double-quoted automatically, so the file always reads back as the strings you typed.
Two safety rails are on by default. The permissions preset writes contents: read, the least-privilege GITHUB_TOKEN setting the security-hardening guide recommends, and unchecking it simply omits the key so your repository default applies. Separately, a credential-shaped value typed into an env or with row triggers a warning that names the key and suggests the secrets context — and share links exclude env and with values structurally, so a pasted token cannot ride out in a URL.
Examples
Node CI on push and pull_request to mainname: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Both triggers filter on main, which is the shape that avoids double runs: pushes to feature branches fire nothing, the pull request fires the PR event, and the merge fires push. The node preset supplies checkout, setup-node with lts/* and npm caching, then npm ci and npm test.
Python nightly at 02:30 UTC with a manual triggername: Nightly checks
on:
schedule:
- cron: '30 2 * * *'
workflow_dispatch: {}
permissions:
contents: read
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: 3.x
- name: Upgrade pip
run: python -m pip install --upgrade pip
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest
30 2 * * * means 02:30 UTC — GitHub evaluates every schedule in UTC and offers no timezone knob, so translate from local time before you commit. Minute 30 also sits off the congested top of the hour, and workflow_dispatch rides along so the nightly can be rehearsed by hand.
Manual release with concurrency and a custom stepname: Release
on:
workflow_dispatch: {}
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Build release artifact
run: make release
workflow_dispatch alone means the run starts only from the Actions tab or the API. The custom preset injects nothing beyond checkout, your own step follows it, and the concurrency block keys on workflow plus ref so a second dispatch cancels a release still in flight.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
schedule is UTC with a five-minute floor
Every cron here fires on GitHub's clock, not yours: expressions are read in UTC, the shortest documented interval is five minutes, and busy periods can start a tick late or drop it. This builder validates the syntax and flags sub-floor cadences; for local-time conversion, DST duality tables and next-run previews, use the dedicated GitHub Actions cron page here.
contents: read is a deliberate default
Without a permissions key, the GITHUB_TOKEN in each job gets whatever your repository or organization default grants, which on older repositories is broad write access. The hardening guide recommends declaring the minimum, so the generated file opens at contents: read — jobs that push commits, publish packages or write checks must widen it themselves, which is the point.
Major tags float; SHA pins do not
actions/checkout@v7 tracks whatever v7 release is newest, which keeps you patched but means the code behind the tag changes. GitHub's hardening guidance for third-party actions is to pin the full commit SHA instead. The suggestions in this builder are official actions/* references recorded on a stated date — swap any of them for a SHA when your threat model asks for it.
Runner labels retarget over time
ubuntu-latest and macos-latest are aliases the runner-images project re-points as new images stabilize, so a workflow that compiled against one toolchain can meet another after a migration window. Version-suffixed labels like ubuntu-24.04 and macos-15 hold still; the free-entry field accepts them and any self-hosted label without complaint.
Common errors
on.schedule takes plain 5-field cron only — `@daily` becomes `0 0 * * *`.- Vixie shorthands never reached the Actions grammar. The refusal prints the equivalent five fields ready to paste; only @reboot gets no rewrite because nothing reboots, and the message points at workflow_dispatch instead.
Job id `build job` is invalid — an id starts with a letter or _ and uses only letters, digits, - and _.- Job ids become YAML keys and expression references, so GitHub restricts their alphabet. Spaces are the usual surprise — build-job and build_job are both fine.
No trigger is enabled — turn on at least one of push, pull_request, schedule or workflow_dispatch.- A workflow with an empty on: block can never start, so the builder refuses to print one. workflow_dispatch is the smallest honest choice while you are still drafting.
This schedule can fire runs 4 minutes apart — GitHub's shortest schedule interval is 5 minutes.- The YAML still emits because the syntax is legal; the timetable is what GitHub will not honor. Widen the step to */5 or more so the committed cadence matches what the platform delivers.
The env value of `AWS_ACCESS_KEY_ID` looks like a credential — use ${{ secrets.AWS_ACCESS_KEY_ID }} and keep the real value in the repository's Actions secrets.- Workflow files are committed plaintext, so a pasted key would land in git history. The warning names the key, never repeats the value, and share links from this page structurally exclude env and with values either way.
FAQ
Where does the workflow file go?
Into the .github/workflows directory at the repository root, with any filename ending in .yml or .yaml — the path is the contract, the filename is yours. GitHub reads every file in that directory on the default branch, so the one-line setup is: run mkdir -p .github/workflows, save the download as workflow.yml inside it, commit and push.
Is it .yml or .yaml?
Both extensions work and GitHub's own documentation mixes them. The download here is named workflow.yml purely because the shorter form dominates in the starter-workflows repository — renaming it changes nothing about how the file is parsed.
How do I run on push and pull_request without duplicate runs?
Filter both triggers. With push restricted to main and pull_request targeting main, a feature branch push triggers nothing by itself, opening the PR runs the pull_request event, and merging runs push exactly once. Duplicates appear when push is left unfiltered, because the PR's branch pushes then fire alongside the PR event.
What is runs-on?
The label that picks the machine for a job. GitHub-hosted labels like ubuntu-latest, macos-15 and windows-latest map to images the runner-images project publishes; anything else is matched against your own self-hosted runners' labels. That is why this builder only warns on labels it does not recognize instead of refusing them.
How do secrets work in the generated YAML?
Store the value under the repository's Settings → Secrets and variables → Actions, then reference it as ${{ secrets.NAME }} inside env or with. The runner injects it at execution time and masks it in logs. Nothing you type into this page is a secret store — which is why credential-shaped values draw a warning here rather than a place to live.
Why is my scheduled workflow not firing?
The usual causes are timing and placement rather than syntax: the expression is read in UTC, only the default branch's workflow file is scheduled, public repositories pause schedules after roughly two months without activity, and load can delay a tick. The GitHub Actions cron helper on this site walks those diagnostics and previews the next five runs for any expression.
Keyboard shortcuts
- /
- Focus the workflow name (outside any field).
- ⌘/Ctrl + Enter
- Rebuild the YAML from the current form.
- ⌘/Ctrl + S
- Download the generated file as workflow.yml.
- ⌘/Ctrl + C
- With no text selected, copy the workflow YAML.
- ⌘/Ctrl + Shift + C
- Force the YAML copy while a selection is active.
- t
- Cycle the site theme (inactive while typing in a field).
- ? or ⌘/Ctrl + /
- Show the shortcut overlay.
- Esc
- Dismiss the topmost dialog.