Stop Dreading the Rejection Email: Triaging App Store Review Feedback with an Antigravity Agent
A working setup for feeding App Store rejection notices into Antigravity, classifying them against a guideline playbook, and gating resubmission with an Info.plist lint — with real turnaround numbers.
Some mornings there is a message from App Review sitting in the inbox, and the subject line alone is enough to reshuffle the whole day. As an indie developer running several apps in parallel, I get a rejection a few times a year — always when I have stopped expecting one.
For a long time my response was improvised. Which guideline number is this? How did I fix it last time? What did I write back? The records were scattered across the Resolution Center, old emails, and commit logs, so every rejection meant starting the investigation from zero. Half a day, gone.
These days I hand the first pass to an Antigravity agent. The judgment calls stay with me; what the agent owns is the prep work — classification, playbook lookup, and checklist generation. This article shares that setup, including the actual playbook and scripts I use at Dolice Labs.
Why rejection handling is slow in the first place
The delay is rarely technical. In my case it came down to three things.
First, emotional friction. Rejection notices are written in a flat, procedural tone, but the person reading them braces anyway. Just opening the message takes willpower.
Second, scattered history. What you did last time lives in three different places, and reassembling it is the real first task.
Third, the absence of classification. Apple's feedback maps to numbered sections of the App Store Review Guidelines, but unless you have translated each number into your own action list, every rejection sends you back to rereading the guideline text.
The agent's job is the third item: fixing the classification and the procedure. Once that is in place, the first item softens too. An email you know will be sorted for you is a much easier email to open.
Step 1: Capture the notice and structure it
One constraint up front: Resolution Center messages are not available through the App Store Connect API. You can track review status via the API, but the actual text of the feedback exists only on the web page and in email. Manual copy or attachment is the entry point — there is no way around it.
Antigravity added PDF attachment support in its late-June update, so I save the review email as a PDF and attach it to the conversation directly. Screenshots work as well, but multi-page feedback survives better as a PDF.
The agent's first task is converting the attachment into this JSON shape:
The field that matters most is binary_or_metadata. Whether the fix requires a new binary or only metadata changes — description text, screenshots, purpose strings — completely changes the path back to review. Metadata-only means no rebuild. Pinning this decision at intake saves one unnecessary build per incident.
✦
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
✦A YAML playbook that pins down first-response steps per frequently-cited guideline, ready to drop into your repo
✦A Node lint script that machine-checks Info.plist purpose strings before you resubmit
✦The operational flow that cut my triage-to-resubmission time from half a day to roughly 90 minutes
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.
The structured feedback is then matched against a playbook checked into the repository. Here is an excerpt of the one I actually run:
# .antigravity/review-playbook.yamlplaybook: "2.1": label: "App Completeness / request for information" first_actions: - "Check for attached crash logs (highest priority if present)" - "Write out reproduction steps on a clean install of the latest OS" - "Verify demo account and review notes are present" reply_tone: "Facts only: reproduction steps and what changed" "4.3": label: "Design - Spam / similar apps" first_actions: - "List concrete differences from the cited app (features, audience)" - "Identify UI that could be mistaken as template-derived" human_only: true # the argument itself is written by a human "5.1.1": label: "Privacy / permission purpose strings" first_actions: - "Run the lint script against every UsageDescription" - "Confirm the permission prompt timing matches feature context" reply_tone: "Quote the corrected strings verbatim"
Sections marked human_only: true are ones where the agent never drafts the reply. For findings like 4.3, where the substance is a dialogue with the reviewer, the agent does the research and I write the words. Declaring the boundary in YAML keeps the agent's behavior from drifting day to day.
Step 3: Turn machine-checkable findings into a resubmission gate
The most expensive rejection is the repeat rejection — being sent back for the same thing twice. Purpose-string problems are machine-checkable, so I wrote a lint that runs before every resubmission:
// scripts/lint-usage-descriptions.mjs// Usage: node scripts/lint-usage-descriptions.mjs path/to/Info.plistimport { readFileSync } from "node:fs";const GENERIC_PATTERNS = [ /for app functionality/i, /required by the app/i, /to provide app features/i,];const MIN_LENGTH = 20; // explaining "why and for what" naturally exceeds thisconst plist = readFileSync(process.argv[2], "utf8");const keys = [...plist.matchAll( /<key>(NS\w+UsageDescription)<\/key>\s*<string>([\s\S]*?)<\/string>/g)];let failed = false;for (const [, key, value] of keys) { const text = value.trim(); if (text.length < MIN_LENGTH) { console.error(`✗ ${key}: only ${text.length} chars. State the purpose concretely.`); failed = true; } if (GENERIC_PATTERNS.some((p) => p.test(text))) { console.error(`✗ ${key}: generic wording. Spell out what and why.`); failed = true; }}if (keys.length === 0) console.warn("⚠ No UsageDescription keys found");process.exit(failed ? 1 : 0);
This lint lives not in CI but as one line item in the agent's resubmission checklist. When it fails, the agent proposes corrected strings and I approve or rewrite them.
SDK requirements sit in the same gate. Since late April this year, builds must be produced with the iOS 26 SDK to be accepted, so an Xcode version mismatch gets bounced at upload time. That failure happens on a different path than a rejection, but it belongs on the checklist for the same reason: it prevents the "I thought I resubmitted, but nothing moved" morning.
Step 4: One table for the frequent citations
As the landing zone for classification, I keep a single table of frequently-cited guidelines and their first responses. The agent references it, and I scan it visually before every resubmission.
Guideline
Typical finding
First response
Rebuild?
2.1
Crashes / request for information
Reproduce, then fix review notes
Depends
2.3.10
References to other platforms
Edit description and screenshots
No
4.3
Similar apps / spam signal
Diff analysis, human-written reply
No (dialogue first)
5.1.1
Weak permission purpose strings
Run lint, fix strings
Yes
Writing the table taught me something: a surprising share of rejections need no rebuild at all. I used to open Xcode reflexively. A metadata-only resubmission is a matter of tens of minutes, not hours.
Measured results: half a day down to 90 minutes
Under this setup I have handled four rejections over the past six months. From attaching the notice to having the classification, the matching playbook section, and a generated checklist takes about five minutes. Including the fixes and reviewing the reply draft, I am averaging roughly 90 minutes back to resubmission — work that previously consumed half a day, or spilled into the next one when I could not face it.
The other change is the repeat-rejection rate. Before the purpose-string lint existed, I was once sent back on 5.1.1 twice in a row. Since the gate went in, zero repeat citations on the same section. Taking machine-checkable findings out of the hands of human attention pays off less in the numbers than in the peace of mind.
What I deliberately do not automate
Two boundaries, stated explicitly.
The final wording of any argument or explanation to the reviewer is always mine. That is what human_only in the playbook encodes. Interpreting the guidelines is the developer's own responsibility, and delegating it degrades the quality of the dialogue.
Spotting phishing emails disguised as rejections is also a human job. Before anything reaches the agent, I verify the sender against the actual Resolution Center entry.
If you want to start somewhere, take one past rejection email and transcribe it into the JSON shape above. From the second incident onward, that record becomes the seed of your own playbook. I hope this saves someone else the same kind of morning.
Share
Thank You for Reading
Antigravity 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.