●IOS27 — iOS 27 and iPadOS 27 land on September 14. For apps assembled with no-code or AI tooling, the week a new OS ships is when the ground moves most●DUO — Apple's first foldable, the iPhone Duo, starts at $1,999. A new screen shape is also the first place a generated layout tends to break●SPLIT — Standard Rork produces React Native through Expo. Rork Max generates native Swift. Neither will adapt to a folding screen at the same pace, or fail in the same way●SUBMIT — Automated submission absorbs every change Apple makes to the process. An article praising the convenience owes its readers that caveat●RATING — Answering the age rating questionnaire became mandatory in September. Apps built without code are no exception●SILICON — The A20 Pro in the iPhone 18 Pro is reported to be the first high-volume smartphone processor built on TSMC's 2nm node●IOS27 — iOS 27 and iPadOS 27 land on September 14. For apps assembled with no-code or AI tooling, the week a new OS ships is when the ground moves most●DUO — Apple's first foldable, the iPhone Duo, starts at $1,999. A new screen shape is also the first place a generated layout tends to break●SPLIT — Standard Rork produces React Native through Expo. Rork Max generates native Swift. Neither will adapt to a folding screen at the same pace, or fail in the same way●SUBMIT — Automated submission absorbs every change Apple makes to the process. An article praising the convenience owes its readers that caveat●RATING — Answering the age rating questionnaire became mandatory in September. Apps built without code are no exception●SILICON — The A20 Pro in the iPhone 18 Pro is reported to be the first high-volume smartphone processor built on TSMC's 2nm node
Your First EAS Workflow in a Rork Repo, and the Alert That Goes Missing Exactly When You Need It
Putting two files into .eas/workflows in a repo exported from Rork, and why a notification wired with needs stays silent on exactly the nights it fails — with the output of a small local checker I actually ran.
Kick off a build late at night, wait for it to pass, then type the submit command. When you keep a handful of apps going on your own, that order settles into your fingers. It's not worth writing down, and yet getting it wrong costs you half an hour — and that little routine repeats once per app.
The other day, after shipping an update to one of my wallpaper apps, I found myself typing the same sequence again and thinking: this order is exactly the thing that shouldn't live in my fingers.
A repo exported from Rork is, at that point, an ordinary Expo project. If you have eas.json next to your source and the repo is on GitHub, one configuration file is enough to hand that order over.
The first one I wrote was wrong, though. It parses. eas accepts it. And it stays silent in the one situation where I actually want to hear from it.
Where the file goes, and two limits that bite early
Workflow files live in .eas/workflows/. The .eas directory sits at the same level as eas.json. Put it one level off and the command simply won't find anything.
The extension has to be .yml or .yaml, and a workflow file must be 16 KiB or smaller. You won't hit 16 KiB writing normally, but you will notice it the day you decide to consolidate every job into one file. Splitting by purpose ages better.
There's a second thing worth knowing before you type anything. The trigger defaults empty out whichever list you leave unwritten. If on.push has neither branches nor tags, you get branches: ['*'] and tags: []. But write only one of them and the other becomes []. Delete the branches line while adding tags and branch pushes stop firing entirely. That may be what you want — it wasn't what I meant the first time.
Getting a preview update out when a pull request opens
The first file publishes a preview update when a pull request opens. It replaces an eas update I used to type by hand, and since nothing reaches production, it's a safe place to start.
A quick read-through. paths narrows what counts as a change, and a leading ! excludes. Not burning a run on a README-only pull request is a small thing that adds up. branches defaults to ['*'], so that line is optional — I keep it for the version of me who reads this in six months.
type: update maps to eas update. You can set branch or channel, never both; here the branch name comes from the pull request number, so branch it is.
Then there's hooks. That's where I got stuck first.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll be able to put two YAML files in your own repo so a pull request publishes a preview update and a tag carries a build all the way to TestFlight
✦You'll be able to avoid building a pipeline that stays quiet on precisely the nights it breaks, before you ever ship it
✦You'll be able to catch the needs-versus-after mix-up, the jobs that can't take steps, and the jobs where env silently does nothing, all from your own machine before you push
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Only custom jobs and type: build jobs can carry steps. Adding steps to a type: update or type: submit job won't work. When you want something to run around the job, you use that job's own hooks — before_update, after_update, and so on.
There's a second trap of the same shape. Jobs that don't get a VM ignore env entirely.
Job type
What it does
env
build / update / submit / deploy
Checks out your code and runs it
Works
slack / github-comment
Only posts a message
Ignored
get-build / update-rollout / branch-delete
Reads or writes EAS-side state
Ignored
require-approval / doc
Waits on a person, or renders text
Ignored
Passing an environment variable to a notification job and having it quietly do nothing costs real time to diagnose. I managed to produce a run where the webhook URL was empty and no message went anywhere.
The environment default is also job-specific, which is worth remembering. A build job infers it from the build profile in eas.json, a submit job inherits it from the build being submitted, maestro jobs default to preview, and everything else defaults to production. Reading production secrets during what you thought was a preview publish happens precisely when you leave the key out. That's why environment: preview is spelled out above.
Carrying a tag through to TestFlight
The second file takes a tag, produces a production build, and moves it to TestFlight. The order I used to type by hand becomes a chain of needs.
# .eas/workflows/release.yml — leave this as-is and the failed nights stay quietname: Releaseon: push: tags: - v*jobs: build_ios: type: build params: platform: ios profile: production ship_testflight: needs: [build_ios] type: testflight params: build_id: ${{ needs.build_ios.outputs.build_id }} external_groups: [Early] submit_beta_review: true notify: needs: [ship_testflight] type: slack params: webhook_url: ${{ env.SLACK_HOOK_URL }} message: "The release path made it all the way through"
type: build requires platform, and profile falls back to production. It hands back a build_id, which the next job picks up.
type: testflight takes either build_id (a build to upload) or asc_build_id (one already uploaded) — one or the other, never both. Processing waits are capped by wait_processing_timeout_seconds, which defaults to 1800 seconds. A build that hasn't come back in half an hour gets cut off there.
Up to that point it's a faithful copy of what I used to type. Rereading it, nothing looked off.
The night it fails is the night nothing arrives
And yet that notify job never fires. More precisely, it fires when things go well and stays silent when they don't.
needs means "proceed once the listed jobs have succeeded." On the run where ship_testflight fails, notify never starts, because its prerequisite didn't succeed. The message I want isn't the one about the good night.
When you want the outcome regardless of success, you reach for after.
The status of a job you waited on with after reads from after.<job_id>.status, and the value is success, failure, or skipped. One thing to watch: wiring with after and then reading needs.<job_id>.status is an easy slip. The namespace has to match the key you used.
needs waits for success; after waits for an answer. It's one line once it's written down, but while I was writing the file my head was only doing "put these in order." Copy a manual sequence directly and needs is what you'll reach for — because the manual version carried an unstated assumption, that I'd be sitting there watching when it broke. That assumption is the one piece automation quietly drops.
Three things a nightly schedule stands on
Once the order is handed over, the next thought is to run it every night. on.schedule exists for that, and the documentation states three conditions. In order of how much they hurt:
Condition
What it means for you
Scheduled runs only happen on the default branch
A schedule sitting on a feature branch will never fire, no matter how long you wait
Cron is interpreted in GMT
0 0 * * * lands mid-morning in Japan. Your quiet window turns into working hours
Runs may be skipped, and may run more than once
The docs say so plainly. The job has to survive running twice
The third one surprised me. I had been designing on the assumption that a schedule arrives exactly once. What you want instead is a job that doesn't mind a second pass — one that reads what already exists rather than minting a new version number on the spot.
One more small thing: putting [eas skip] or [skip eas] in a commit message suppresses push and pull_request triggered runs. Handy while you're editing the workflow files themselves.
A check you can run before you push
Every trap above is syntactically fine. The YAML parses, the command accepts it, and you find out on the night the notification doesn't arrive.
So I wrote a small check to run locally first. It doesn't replace what EAS validates — it only catches the mistakes I keep making.
#!/usr/bin/env python3"""A small local check for EAS Workflows files, to run before pushing.Usage: python3 check_workflow.py .eas/workflows/*.yml"""import sys, pathlib, yamlSIZE_LIMIT = 16 * 1024STEPS_ALLOWED = {"build", None} # only build jobs and custom jobs may carry stepsNO_VM_JOBS = { # no VM means env does nothing "apple-device-registration-request", "branch-delete", "doc", "get-build", "github-comment", "require-approval", "slack", "update-rollout",}NOTIFY_TYPES = {"slack", "github-comment"}def check(path): p = pathlib.Path(path) problems, notes = [], [] size = p.stat().st_size notes.append(f"size={size}B / limit={SIZE_LIMIT}B") if size > SIZE_LIMIT: problems.append(f"over the 16 KiB limit ({size}B)") try: doc = yaml.safe_load(p.read_text(encoding="utf-8")) or {} except yaml.YAMLError as e: return [f"not valid YAML: {e}"], notes jobs = doc.get("jobs") or {} notes.append(f"jobs={len(jobs)} ({', '.join(jobs)})") for jid, job in jobs.items(): job = job or {} jtype = job.get("type") for key in ("needs", "after"): for dep in job.get(key) or []: if dep not in jobs: problems.append(f"{jid}.{key}: points at a job that does not exist, '{dep}'") if "steps" in job and jtype not in STEPS_ALLOWED: problems.append(f"{jid}: type={jtype} cannot carry steps (use hooks instead)") if "env" in job and jtype in NO_VM_JOBS: problems.append(f"{jid}: type={jtype} has no VM, so env is ignored") if jtype in NOTIFY_TYPES and job.get("needs") and not job.get("after"): problems.append( f"{jid}: notification job hangs off needs. " f"needs requires the upstream job to succeed, so this never fires on a failed run" ) return problems, notesif __name__ == "__main__": bad = 0 for arg in sys.argv[1:]: problems, notes = check(arg) print(f"== {arg}") for n in notes: print(f" - {n}") for pr in problems: print(f" NG {pr}") bad += 1 if not problems: print(" OK nothing to flag") sys.exit(1 if bad else 0)
Running it against the file with the mistake
Run it against the release.yml above, the one with notify, and it says:
== .eas/workflows/release.yml - size=517B / limit=16384B - jobs=3 (build_ios, ship_testflight, notify) NG notify: notification job hangs off needs. needs requires the upstream job to succeed, so this never fires on a failed run
Running it against four deliberate mistakes
I also fed it a pull request file with four deliberate mistakes: steps on a type: update job, env on a github-comment job, and one character wrong in an after reference.
== .eas/workflows/pr-preview.yml - size=717B / limit=16384B - jobs=3 (publish_preview, comment, tell_me_anyway) NG publish_preview: type=update cannot carry steps (use hooks instead) NG comment: type=github-comment has no VM, so env is ignored NG comment: notification job hangs off needs. needs requires the upstream job to succeed, so this never fires on a failed run NG tell_me_anyway.after: points at a job that does not exist, 'publsh_preview'
Running it against the corrected pair
All four came out. The corrected pair returns this:
== .eas/workflows/pr-preview-fixed.yml - size=599B / limit=16384B - jobs=2 (publish_preview, comment) OK nothing to flag== .eas/workflows/release-fixed.yml - size=561B / limit=16384B - jobs=3 (build_ios, ship_testflight, report) OK nothing to flag
The job-ID typo check turned out to be the part I appreciated most. EAS would reject it too, of course, but catching it here saves a push and a wait.
Whatever you put under on, you can always start a workflow by hand with eas workflow:run .eas/workflows/release.yml. Doing that once, before you cut a tag, is a calmer way to find out whether it works.
Before you add the second file
Handing an order over to a machine turned out not to be transcription. It was relocating an assumption — that I'd be sitting there watching when it broke — somewhere the machine could hold it. The after line is where that assumption went.
If you take one step from here, make it this: drop in .eas/workflows/pr-preview.yml alone, then open a pull request that only touches the README. If your paths exclusion works, nothing happens. If it doesn't, an update goes out. Either way you learn whether the file is being read the way you meant it. The production path can wait until after that.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.