Automating App Store Connect with Fastlane (and Letting Agents Drive)

I ship a handful of small Mac and iOS apps. Lexicon, idea.log, Evergreen. None of them are big businesses, which means none of them justify me spending an afternoon clicking around App Store Connect every time I want to fix a typo in the description or bump a price.

App Store Connect is fine as a website. It’s just slow in the way that any web UI is slow when you have to do the same thing across three apps and two platforms each. Update the subtitle, find the right localization, paste the keywords, drag in the screenshots, remember whether you already set the price in the EU storefront. Multiply by however many apps you have and you start avoiding the work entirely, which is how you end up with a year-old description that still mentions a feature you removed.

So I moved all of it into fastlane. And once it was in fastlane, it turned out to be the kind of thing I could hand to an agent.

The shape of the setup

Every app has a fastlane/ directory that looks roughly the same. The interesting parts are the lanes and the metadata.

The metadata is just text files on disk. fastlane/metadata/en-US/description.txt, keywords.txt, subtitle.txt, promotional_text.txt, release_notes.txt, and so on. This is the part I wish I’d done years ago. Your store listing lives in your repo, in version control, where you can diff it and grep it and edit it in your actual editor instead of a textarea that eats your formatting. The 100-character keyword limit stops being a guessing game because the file is right there.

Pushing that up is one lane:

lane :metadata do
  deliver(
    api_key: asc_api_key,
    app_identifier: ASC_BUNDLE_ID,
    app_version: APP_VERSION,
    platform: "osx",
    metadata_path: File.expand_path("metadata", __dir__),
    screenshots_path: assemble_screens,
    skip_binary_upload: true,
    overwrite_screenshots: true,
    submit_for_review: false,
    force: true
  )
end

Two things matter here. skip_binary_upload: true means this lane never touches the build. And submit_for_review: false means it never submits anything. All it does is populate the editable draft version of the listing. I still go hit the actual submit button myself when I’m ready. The automation handles the tedious 95%, and the one irreversible action stays a human decision.

Auth without passwords

The thing that makes this pleasant is App Store Connect API keys. No Apple ID, no password, no app-specific password, no two-factor prompt interrupting a script halfway through.

You generate a key once under Users and Access > Integrations, download the .p8 file, and point fastlane at it through environment variables:

export ASC_KEY_ID=XXXXXXXXXX
export ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ASC_KEY_PATH=/some/path/outside/the/repo/AuthKey_XXXXXXXXXX.p8

The key file lives outside the repo, obviously. But the nice property is that there’s no interactive login anywhere in the loop. A script can run start to finish without me, which is the whole prerequisite for letting something else run it.

The lanes I actually use

Beyond uploading metadata, the lanes I lean on most are the read-only ones. Every app has an audit lane that just tells me the current state of something:

  • subscriptions_audit lists every subscription group and product with its current status
  • price_audit shows the current price schedule
  • verify (my favorite) walks the whole listing and reports whether it’s actually ready to submit: version state, localized copy present, screenshots attached, build attached, subscriptions in a submittable state, on both platforms

These do nothing but look and report. You can run them a hundred times and the worst thing that happens is you learn something.

The write lanes follow one rule: dry-run by default. The subscription and pricing lanes hit the App Store Connect API directly (I wrote small Ruby modules that generate a JWT from the .p8 key and make raw HTTP calls, because the official plugins didn’t cover everything I wanted). But none of them write anything unless you ask:

fastlane subscriptions                       # dry run, shows what it would do
APPLY=1 fastlane subscriptions               # actually create/verify them
APPLY=1 APPLY_PRICING=1 fastlane subscriptions   # and set the prices too

The dry run prints lines like [dry-run] would create com.heltonlabs.app.pro.monthly. You read it, decide it looks right, then run it again with the flag. The modules are idempotent on top of that, so if a subscription group already exists it gets reused instead of duplicated. Running it twice doesn’t make a mess.

Letting an agent drive

Here’s where it gets fun. Once everything is a lane that’s either read-only or dry-run-by-default, the safety properties stop being about me being careful and start being structural. And structural safety is exactly what you need before you hand the keys to an agent.

So now a lot of my App Store Connect work happens through Claude Code. I’ll be working on Evergreen and just say “check whether the listing is ready to submit,” and it runs fastlane verify and reads back the report. “What are the current prices across the apps?” runs the audit lanes. “Update the keywords to include ‘offline’” edits the text file and runs the metadata lane. The agent is doing exactly what I’d do, except I didn’t have to remember the lane names or the environment variables.

The reason I trust it isn’t that I trust the model to be careful. It’s that the dangerous version of each operation doesn’t exist as a default. The agent literally cannot submit an app for review, because no lane I’ve written does that. It can’t accidentally write a price, because writing requires a flag it would have to deliberately add. The worst case for a misunderstood instruction is a dry-run that prints what it would do, and I read that before anything happens. The blast radius is bounded by the design, not by vigilance.

That’s the general pattern I keep coming back to with agents and external systems. Don’t give the agent a powerful, sharp tool and hope it’s careful. Build the tool so the safe path is the default and the irreversible action requires an explicit, visible step. Then handing it to an agent is no more dangerous than running it yourself, and a lot less tedious.

What I’d still like

The honest gap right now is that I run these lanes by hand or through an agent ad hoc, rather than having tidy slash commands or Makefile targets wrapping them. A /asc-verify style command per repo would make the agent flow even smoother, and is probably my next afternoon project. The submission step staying manual is on purpose and I don’t plan to change it. Some buttons should require a human to mean it.

If you ship even one app and you’re still managing the listing by hand, move the metadata into text files and write the upload lane first. That alone is worth it. The agent stuff is gravy on top.