Running a Solo AI Studio with Antigravity × Google AI — Automating Every Stage of App Development as One Developer
How to combine Antigravity 2.0 and Antigravity CLI with Stitch and Veo 3 into a pipeline that takes an app from design through implementation, testing, asset generation, and App Store submission, driven by one developer. Covers where to delegate to agents and the operational pitfalls.
Building apps that reach users around the world, continuously, as a solo developer — that used to be virtually impossible without a team. Not anymore.
My perspective shifted during one particular week. Monday: I sketched a concept for a wallpaper app. Tuesday: Antigravity generated the UI. Wednesday: tests passed. Thursday: store materials were ready. Friday: the review notification arrived. Throughout this process, what I actually did was make directional decisions and verify quality. The agents handled the execution.
This guide walks through how to build a "solo AI studio" — using Antigravity as the conductor for Google's AI tools (Stitch, Veo 3, Gemini CLI) — to automate the entire app development lifecycle at an implementation level.
Why Build a Solo AI Studio Now
Time is the defining constraint for indie developers. Programming, design, marketing, QA, localization, legal copy — doing all of this alone meant an average of 3–6 months from concept to release. That timeline has fundamentally changed.
Here's what the pipeline does to those numbers:
Time to first release: 8 weeks → 2–3 weeks
Update cycle: 3–4 weeks → under 1 week
Store asset refresh: 2–3 days → under half a day
4-language localization: 5–7 days → 1–2 hours
The point isn't just volume. When AI handles the repetitive work, your judgment and creativity can focus on what matters most: the actual user value you're trying to create.
In 2026, the gap between developers who have this kind of system and those who don't shows up not just in release frequency, but in their ability to respond to users and iterate meaningfully.
The Pipeline Architecture: 4 Phases × 4 Tools
The system runs across four phases using four tools.
Tool roles:
Antigravity: The conductor. Coordinates all phases via agents defined in AGENTS.md
Stitch (Google): UI prototyping and screenshot generation
Gemini CLI: Localization copy, store descriptions, and press materials
Veo 3 (Google): Promotional video and App Preview generation
Phase 4: Store submission & ASO optimization (App Store Connect API + Gemini CLI)
Antigravity orchestrates all phases through a team of agents defined in AGENTS.md. At each phase boundary, you review and approve before the next phase begins. This split — AI executes, human decides — is what makes the system reliable in practice.
✦
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
✦The full architecture of a one-person pipeline from design to store submission, built on Antigravity 2.0 and Antigravity CLI
✦Concrete setups for multi-agent division of labor via AGENTS.md, overnight background execution, and CI/CD integration
✦How to draw the line between work you delegate to agents and decisions you keep human, plus the pitfalls hit in real operation
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.
Toolchain Updates Through August 2026: From Gemini CLI to Antigravity CLI
I first assembled this pipeline in May 2026. Since then, one part of the plumbing has changed in a meaningful way. Gemini CLI and the personal Gemini Code Assist IDE extension stopped accepting requests on June 18, and the command-line experience was folded into Antigravity CLI.
Everywhere this article calls gemini, I now run that step through an Antigravity CLI agent instead. The new CLI was rewritten in Go, and the responsiveness is noticeably better. The benefit is largest in the steps where waiting time accumulates — overnight localization runs, for example.
Alongside that, Antigravity 2.0 (announced at I/O 2026) now ships as five surfaces — the desktop app, the CLI, an SDK, a Managed Agents API, and an enterprise deployment path — that all share the same agent harness. The agent behavior you designed on the desktop carries over to the CLI and the API unchanged. The AGENTS.md design in this article still holds on the new setup. If anything, with true parallel execution and background scheduling now being platform-standard, the overnight runs I used to express in .antigravity/tasks.json are simpler to compose.
A note on pricing: the public preview is free, and the AI Ultra plan ($100/month) raises the usage ceiling to five times that of AI Pro. The more continuous overnight execution you add, the more this ceiling starts to matter.
If I had to name one lesson from the migration, it is this: do not swap your whole automation over at once. I replaced each CLI call with its Antigravity CLI equivalent one at a time, checking that the output held the same quality before moving on. Early in a large update, it is safest to migrate the step you can least afford to break last.
What Changed in August
The driver's seat kept moving through the following weeks. Hub 2.10.0 (August 24) added an embedded terminal and Git operations inside the Review pane, so reading an agent's diff, staging it, and committing it now closes inside the app. In my setup, that specific loop was the one that kept pulling me out to an external terminal.
On the CLI side, 1.1.21 (August 26) is the substantial one. It bundles ripgrep for agent code search, so search behavior no longer depends on what happens to be installed in a fresh container. It also adds an always-proceed mode that auto-approves MCP tool calls and page reads — and in non-interactive overnight runs, waiting on approval is the single biggest reason a job stalls. How far to drop approvals is still a line you draw yourself rather than one convenience should draw for you. Relatedly, allow-always suggestions now scope to specific script names like npm run and cargo run instead of whole binaries, which matters because an overly broad permission is much harder to narrow after the fact than to withhold up front.
And the piece this article uses later on arrived in that same 1.1.21 release: an unrounded cost field in the status line data model.
Phase 1: Idea Validation and UI Design
Scoring Ideas with a Market Research Agent
Start by defining a market research agent that quantifies your app idea rather than relying on gut feeling.
# AGENTS.md — Phase 1: Idea Validator## market-researcherYou evaluate app ideas with quantitative market research. Search for data on App Store / Google Play category rankings, top competitor review counts, and keyword competition.Evaluate each idea across:1. Market size (ranking stability in the category, competitor trajectory)2. Technical feasibility (can one developer ship in 4 weeks?)3. Monetization outlook (AdMob CPM benchmarks, subscription pricing norms, one-time pricing)4. Differentiation (what does this offer that top-10 apps don't?)5. ASO difficulty (keyword competition for the 3 primary terms)Output: Markdown scoresheet + recommended ASO keywords + alternative concepts if score < 6
Feeding this agent a concept takes seconds, and the report comes back in minutes:
antigravity agent run market-researcher \ --input "A wallpaper app delivering one high-resolution nature photo per day. \ Offline save. Focused on Japanese seasons."
Sample output:
## Idea Evaluation — Score: 8.2/10
Strengths:
- Wallpaper category consistently holds top-100 in Photo apps globally
- Japanese seasons niche provides clear differentiation from generic competitors
- AdMob monetization timing (wallpaper swap) is highly natural; estimated CPM: $3–8
- SwiftUI + Cloudflare R2 architecture is feasible in ~3 weeks solo
Risks:
- Ongoing photo sourcing cost (consider Unsplash API or building a library)
- Seasonal content requires year-round planning
Recommended ASO keywords: wallpaper, 4K, Japan, seasons, nature, scenic, lock screen
Generating UI Prototypes with Stitch MCP
Once the concept passes, connect Antigravity to Stitch via MCP to generate the initial UI.
User: @stitch Create the main screen and settings screen for a wallpaper app.
Design requirements:
- Supports dark mode and light mode
- Main screen: full-bleed wallpaper, translucent bottom toolbar
(favorite, save, share icons)
- Settings screen: notification preferences, image quality, widget settings
- Typography: SF Pro Display (iOS native)
- Accent color: #2C7BE5
Stitch generates the design directly in your editor. Refinements happen the same way — plain language instructions.
Being able to refine in plain language cuts both ways: the granularity of your instruction becomes the quality of the output. How much you have to spell out before an agent returns what you meant is covered in Harness Engineering for Antigravity.
Phase 2: Core Implementation and Test Automation
Defining Specialized Agents in AGENTS.md
Rather than one generalist agent trying to do everything, split implementation into focused specialists.
# AGENTS.md — Phase 2: Implementation Team## ui-builderRole: Generate Swift / Kotlin UI components from Stitch outputConstraints:- SwiftUI / Jetpack Compose only (no UIKit / XML layouts)- Every interactive element and meaningful image must have an accessibilityLabel- Accept Stitch JSON as input, output working code- Handle Dynamic Type at all size classes — no layout breakage at largest accessibility sizesOutput: src/Views/## data-layer-engineerRole: Implement data persistence and CloudKit syncConstraints:- Use SwiftData (not Core Data)- CloudKit sync via CKContainer using private database only- Offline behavior: serve from local cache, sync in background after reconnection- Users without iCloud should get fully functional local-only experienceOutput: src/Models/ and src/DataLayer/## test-writerRole: Auto-generate XCTest / JUnit tests for implemented componentsConstraints:- Must cover happy path, error cases, and edge cases for each component- Target 80%+ line coverage- Use DI container for mocks; no real network calls in unit tests- Write async/await-compatible XCTest using structured concurrency## localizerRole: Produce localized strings for all app textLanguages: Japanese (source), English, Simplified Chinese, Korean, SpanishConstraints:- Natural expressions, not literal translations- No phrases that would trigger App Store review concerns- Output: Localizable.strings format per language
Overnight Implementation via Background Agent
While you're doing other things, Background Agent handles the heavy lifting.
Overnight tasks live in .antigravity/tasks.json:
{ "tasks": [ { "id": "overnight-data-layer", "description": "Implement SwiftData + CloudKit sync for the wallpaper app. Create WallpaperItem model and WallpaperRepository. Offline cache must fall back gracefully; sync resumes in background after reconnection.", "agent": "data-layer-engineer", "schedule": "22:00 JST", "outputDir": "./WallpaperApp/Models/", "validation": "swift build && swift test WallpaperAppTests/DataLayerTests" }, { "id": "overnight-ui", "description": "Convert Stitch-generated design (./stitch-output/main-screen.json) into SwiftUI code. Full accessibility support required.", "agent": "ui-builder", "schedule": "22:30 JST", "outputDir": "./WallpaperApp/Views/", "validation": "swift build" }, { "id": "overnight-l10n", "description": "Propagate newly added Localizable.strings keys into Japanese, Simplified Chinese, Korean, and Spanish.", "agent": "localizer", "schedule": "23:00 JST", "outputDir": "./WallpaperApp/Resources/", "validation": "python3 scripts/validate_strings.py WallpaperApp/Resources/Base.lproj/Localizable.strings WallpaperApp/Resources/*.lproj/Localizable.strings" } ]}
You wake up to implementation complete, tests passing. A few critical things still require human review:
Anything touching security (auth, billing, API key handling)
How user data is stored and transmitted — cross-reference your privacy policy
Performance characteristics — profile with Instruments before shipping
What AI-Generated Tests Actually Look Like
// WallpaperFetcherTests.swift — auto-generated by test-writer agentimport XCTest@testable import WallpaperApp@MainActorfinal class WallpaperFetcherTests: XCTestCase { // Happy path: correct URL generated for a valid date func testFetchURL_ValidDate_ReturnsCorrectURL() async throws { let fetcher = WallpaperFetcher( baseURL: URL(string: "https://cdn.example.com")! ) let date = Calendar.current.date( from: DateComponents(year: 2026, month: 5, day: 6) )! let url = try await fetcher.fetchURL(for: date) XCTAssertEqual(url.host, "cdn.example.com") XCTAssertTrue(url.path.hasSuffix("2026-05-06.jpg")) } // Edge case: falls back to cache when offline func testFetch_WhenOffline_ReturnsCachedURL() async throws { let mockMonitor = MockNetworkMonitor(isConnected: false) let cache = WallpaperCache() let cachedURL = URL(string: "https://cdn.example.com/cached.jpg")! await cache.set(key: "latest", value: cachedURL) let fetcher = WallpaperFetcher(networkMonitor: mockMonitor, cache: cache) let result = try await fetcher.fetchURL(for: Date()) XCTAssertEqual(result, cachedURL) } // Error case: future dates throw WallpaperFetchError func testFetch_FutureDate_ThrowsError() async { let fetcher = WallpaperFetcher( baseURL: URL(string: "https://cdn.example.com")! ) let futureDate = Date().addingTimeInterval(86400 * 30) do { _ = try await fetcher.fetchURL(for: futureDate) XCTFail("Expected error not thrown") } catch let error as WallpaperFetchError { XCTAssertEqual(error, .futureDateNotAllowed) } catch { XCTFail("Unexpected error type: \(error)") } }}
Phase 3: Generating Marketing Assets
This phase delivers the largest time savings in the pipeline. Store screenshots and promotional videos used to require 2–3 days with a designer. Now they take a morning.
Screenshot Automation (Stitch + Fastlane)
# fastlane/Fastfilelane :generate_screenshots do # Pull screenshot frame templates from Stitch sh("python3 scripts/fetch_stitch_frames.py") # Capture from simulator at all required device sizes capture_screenshots( workspace: "WallpaperApp.xcworkspace", scheme: "WallpaperAppUITests", output_directory: "fastlane/screenshots", devices: [ "iPhone 16 Pro Max", "iPhone SE (3rd generation)", "iPad Pro (12.9-inch) (6th generation)" ], languages: ["ja-JP", "en-US", "zh-Hans", "ko-KR"], override_status_bar: true, concurrent_simulators: true ) # Apply branded overlay from Stitch template sh("python3 scripts/apply_brand_overlay.py \ --input fastlane/screenshots \ --template stitch-output/screenshot-frame.json \ --output fastlane/framed_screenshots") frame_screenshots(path: "fastlane/framed_screenshots")end
Generating Promotional Videos with Veo 3
# scripts/generate_promo_video.py"""Generate three variants of a 30-second App Preview using Veo 3.Variants enable A/B testing in App Store Connect."""import timefrom pathlib import Pathfrom google import genaiclient = genai.Client(api_key="YOUR_GEMINI_API_KEY")PROMO_VARIANTS = { "morning": """ 30-second app promo video with a morning light theme: 1. Phone lock screen appears, showing a stunning mountain wallpaper (5s) 2. Slow swipe to a cherry blossom scene (8s) 3. User taps 'Favorite' to save the wallpaper (5s) 4. Home screen shows the wallpaper blending with a living room setting (8s) 5. App icon appears with the tagline 'Japan. Every day.' (4s) Style: cinematic, warm tones, no audio (silent) """, "zen": """ 30-second app promo with a minimal zen theme: Wallpapers slowly appear center-frame — Japanese garden, temple, snow scene. Quiet, unhurried transitions. Subtitle: 'Quiet Japan in your hand, every day.' Style: near-monochrome, extremely restrained """, "vivid": """ 30-second high-energy promo: Japanese seasons cycle quickly — cherry blossoms, summer green, fireworks, autumn leaves, snow. Each scene 2 seconds. App UI appears at the end. Tagline: 'Every season. Every day.' Style: vibrant colors, upbeat pacing """}def generate_all_variants(): operations = [] for name, prompt in PROMO_VARIANTS.items(): print(f"Generating: {name}") op = client.models.generate_videos( model="veo-3-0-generate-preview", prompt=prompt, config={ "numberOfVideos": 1, "durationSeconds": 30, "aspectRatio": "9:16", "resolution": "1080p", } ) operations.append((name, op)) time.sleep(2) # Wait for all to complete for name, op in operations: while not op.done: time.sleep(15) op = client.operations.get(op) uri = op.response.generated_videos[0].video.uri out = Path(f"fastlane/promo_videos/{name}.mp4") out.parent.mkdir(exist_ok=True) import urllib.request urllib.request.urlretrieve(uri, out) print(f"✅ {name}: {out}")if __name__ == "__main__": generate_all_variants()
#!/bin/bash# scripts/generate_metadata.sh — managed by AntigravityAPP_DESCRIPTION=$(cat docs/app-description-en.md)for lang in "ja-JP" "zh-Hans" "ko-KR" "es-ES"; do echo "Generating for: $lang" antigravity agent run localizer --input " Translate and localize the following English app description for the $lang market. Requirements: - Comply with App Store review guidelines - Use natural, native-sounding expressions — not literal translation - Include primary keywords in the first paragraph - Stay within 4,000 characters App name: Japan Seasons Wallpapers Description: ${APP_DESCRIPTION} " > "fastlane/metadata/${lang}/description.txt" sleep 2done
Antigravity's SwiftUI output is often functionally correct, but VoiceOver labels frequently get skipped.
// ❌ Common AI output — no accessibility contextImage(wallpaper.imageName) .resizable() .aspectRatio(contentMode: .fill) .ignoresSafeArea()// ✅ Properly labeledImage(wallpaper.imageName) .resizable() .aspectRatio(contentMode: .fill) .ignoresSafeArea() .accessibilityLabel("\(wallpaper.title) — \(wallpaper.locationName)") .accessibilityAddTraits(.isImage)
Fix: Explicitly state in AGENTS.md that every interactive element and semantically meaningful image requires an accessibilityLabel.
Pitfall 2: Using Gemini CLI output for store copy without review
Gemini CLI produces high-quality copy, but Apple's review team flags certain patterns — superlatives ("best in the world"), competitor comparisons, and claims that contradict your privacy policy. Always run generated copy through a human review step before submission.
Pitfall 3: Veo 3 video licensing
Veo 3-generated videos are subject to Google's terms of service. Verify commercial use rights against the current terms before submitting to the App Store. As of May 2026, Veo 3 API permits commercial use, but check for updates.
Pitfall 4: Long-running Background Agent tasks that stall
Background Agents can stop mid-task on long jobs. Design tasks around 30-minute completion windows:
# AGENTS.md — Task size principleBreak all long tasks into subtasks that complete within 30 minutes.❌ "Implement the entire app data layer" (could take hours)✅ "Implement WallpaperFetcher class only — network + error handling" (20–30 min)✅ "Implement WallpaperCache class only — read/write/expiry" (20–30 min)Commit after each subtask. Reference previous commit in the next task's description.
Can You Trust the Overnight localizer Output? I Measured It
Read back through that .antigravity/tasks.json and you'll notice the implementation tasks each carry a validation — swift build, swift test. The localizer carried nothing for a long time.
The reason was simple: I assumed translation quality can't be checked mechanically. But translation quality and string-resource structure turn out to be two different things. Localizable.strings is never validated by an Xcode build. String(format:) resolves its arguments at runtime. So a translation where %1$@ and %2$@ swapped places builds fine, tests fine, looks fine in the one language you can read — and ships.
I found this the hard way, staring at a Spanish screenshot where a number sat in a place that felt wrong. Manually reviewing four languages of overnight output every morning is not realistic. So I wrote a gate that ignores the prose entirely and looks only at the skeleton.
A gate that checks structure, not meaning
#!/usr/bin/env python3"""Detect format-specifier drift in Localizable.strings.Treats the base locale (usually en or ja) as truth and compares each translation'sspecifier set and positional arguments. Standard library only.Drop it at scripts/validate_strings.py and call it from CI."""from __future__ import annotationsimport re, sys, osfrom collections import Counter# "key" = "value"; (escaped \" allowed)ENTRY = re.compile(r'^\s*"((?:[^"\\]|\\.)*)"\s*=\s*"((?:[^"\\]|\\.)*)"\s*;', re.M)# matches %@ %d %1$@ %.2f %% and friendsSPEC = re.compile( r'%(?:(\d+)\$)?([-+ #0]*)(\d+|\*)?(?:\.(\d+|\*))?' r'(hh|h|ll|l|q|L|z|j|t)?([@dDiuUxXoOfeEgGcCsSpaAF%])')def parse(path: str) -> dict[str, str]: with open(path, encoding='utf-8-sig') as f: src = f.read() src = re.sub(r'/\*.*?\*/', '', src, flags=re.S) return {m.group(1): m.group(2) for m in ENTRY.finditer(src)}def specs(value: str) -> list[tuple]: out = [] for m in SPEC.finditer(value): if m.group(6) == '%': # %% is a literal, not an argument continue out.append((m.group(1), m.group(6))) # (position or None, conversion) return outdef check(base: str, tr: str) -> list[str]: b, t = specs(base), specs(tr) errs = [] bpos = any(p for p, _ in b) tpos = any(p for p, _ in t) if len(b) != len(t): errs.append(f'count {len(b)}->{len(t)}') if bpos != tpos and (b or t): errs.append('positional-mix') # only one side uses %1$@ form if bpos and tpos: bm = {int(p): c for p, c in b if p} tm = {int(p): c for p, c in t if p} if set(bm) != set(tm): errs.append(f'arg-index {sorted(bm)}->{sorted(tm)}') else: for i in sorted(bm): if bm[i] != tm[i]: errs.append(f'arg{i} type %{bm[i]}->%{tm[i]}') else: # without positions, order itself carries the argument mapping bt = [c for _, c in b] tt = [c for _, c in t] if bt != tt: if Counter(bt) == Counter(tt): errs.append(f'order {"".join(bt)}->{"".join(tt)}') else: errs.append(f'type {"".join(bt) or "-"}->{"".join(tt) or "-"}') return errsdef run(base_path: str, targets: list[str]) -> int: base = parse(base_path) bad = 0 for tp in targets: tr = parse(tp) loc = os.path.basename(os.path.dirname(tp)) or tp for key, bval in base.items(): if key not in tr: print(f'{loc}: [{key}] MISSING'); bad += 1; continue for e in check(bval, tr[key]): print(f'{loc}: [{key}] {e}'); bad += 1 for key in tr.keys() - base.keys(): print(f'{loc}: [{key}] ORPHAN'); bad += 1 return badif __name__ == '__main__': if len(sys.argv) < 3: sys.exit('usage: validate_strings.py <base.strings> <target.strings>...') n = run(sys.argv[1], sys.argv[2:]) print(f'--- {n} issue(s)') sys.exit(1 if n else 0)
It compares format specifiers and nothing else. The deliberate trade-off: don't grade the translation, just protect the skeleton the translation must not break.
What it catches, and what it can't
A gate you haven't tried to break is a gate you don't know. I built a ten-key base file shaped like the one the wallpaper app actually ships, then injected one instance of each drift class an overnight run can plausibly produce, and counted detections. These are numbers from a run on my machine — a mutation matrix, not production telemetry.
Drift class
Injected
Caught
Rate
Dropped argument
9
9
100%
Duplicated argument
9
9
100%
Position stripped (%1$@→%@)
3
3
100%
Position added (%@→%1$@)
5
5
100%
Conversion mismatch (%d→%@)
4
4
100%
%% collapsed to %
1
1
100%
Full-width characters (%@ / %d)
5
5
100%
Positional swap, differing types
1
1
100%
Positional swap, identical types
2
0
0%
Precision lost (%.1f→%f)
1
0
0%
Missing key
10
10
100%
Orphan key
10
10
100%
Total
60
57
95.0%
Healthy translation edits (false-positive check)
10
0
0%
Zero false positives on healthy edits matters more than the headline number. A noisy gate gets a --skip flag bolted onto it within a week.
The useful part was the column that didn't reach 100%. When two positional arguments share the same conversion type — %1$@ and %2$@ — a swap is undetectable in principle. The set matches, the types match, the count matches. Two of the ten keys met that condition.
Keys with 2+ same-type positional args: download_progress, quota_left (2/10)
"Downloading %1$@ of %2$@" rendered as "%2$@ of %1$@ downloading" passes silently. The user sees "12 of 3 downloading" and nothing crashes. The absence of a crash is exactly what makes it expensive.
Three things I changed after measuring
The numbers changed the process more than the code.
First, I stopped writing base strings with two same-type positional arguments. A string like "%1$@ / %2$@" gets rewritten so the types differ (make one %1$d), or split into two keys. The most reliable fix for an undetectable shape is to never author that shape.
Second, I keep a list of keys that still meet the risky condition and make sure they appear in the automated screenshots. Phase 3 already generates screenshots in five languages, so folding those screens in means a morning glance at a contact sheet is enough to spot a number in the wrong slot. Machines handle what machines can; eyes handle only the residue.
Third, the localizer task in tasks.json finally got its validation — that's the overnight-l10n entry above. If the check fails, the overnight task itself is recorded as failed. "Failed" is far easier to work with at 8am than "succeeded, but the contents are broken."
Catching drift like this by eye every morning does not scale. Writing the acceptance criteria into the prompt up front is the reliable path, and how to phrase it is in Defining 'Done' with Antigravity Agents.
If this pipeline taught me one thing, it's that an automated step needs an automated check shipped beside it. The faster generation gets, the wider the gap grows between what is produced and what a person can actually review. 95% is not a perfect score — but being able to name the remaining 5% was worth more to me than the number itself.
Where I Draw the Line on What Agents Handle
The question I get most about this pipeline is not how to configure the tools — it is how much I actually delegate to the agents. Over years of solo development, I have redrawn that line more than once.
My rule of thumb is simple. Any task whose correct answer is fixed by a spec, a guideline, or a past decision goes to an agent without hesitation. Test scaffolding, formatting Localizable.strings, batch-producing screenshots, drafting release notes — correctness here is measured against an external standard, so all I have to check is whether anything is missing.
Conversely, tasks whose answer lives only in my head stay with me. Which idea to ship next, what to price it at, which single sentence in the description should lead. These are judgments about user value, so I treat an agent's suggestions as raw material and choose the final words myself.
There is one more boundary I always keep on the human side: security and billing. Authentication flows, how API keys are handled, verification of payment logic — even when working code appears, I read it line by line. A single oversight there is not the kind of thing you can recover later through more automation.
When you run several projects in parallel on background schedules, the agents quietly pile up a large volume of output. That is exactly why the human decision of whether to accept what has been produced now weighs more on me than it used to. The further automation goes, the more what remains at the end is the quality of judgment.
Continuous Improvement: Running on Data
Automated Weekly Analytics Report
# scripts/weekly_report.py"""Runs every Monday on the Antigravity scheduler."""import jsonimport subprocessimport sysdef fetch_metrics(bundle_id: str) -> dict: """Pull last week's numbers from App Store Connect via fastlane.""" result = subprocess.run( ["fastlane", "run", "app_store_connect", "action:get_app_info", f"app_identifier:{bundle_id}"], capture_output=True, text=True, ) if result.returncode != 0: raise RuntimeError(f"fastlane failed: {result.stderr.strip()[:200]}") return json.loads(result.stdout)def generate_report(m: dict) -> str: prompt = f""" Analyze the following app performance data and identify the 3 highest-priority improvements for the next 2-week sprint. For each improvement, include: - What to change - Why it matters (user impact estimate) - How to implement it (concrete steps) Weekly metrics: - Downloads: {m['downloads']} - DAU: {m['dau']} - Crash rate: {m['crash_rate']}% - Average rating: {m['avg_rating']} ({m['review_count']} reviews) - Top user complaints: {m['top_complaints']} """ result = subprocess.run( ["antigravity", "agent", "run", "release-analyst", "--input", prompt], capture_output=True, text=True, ) if result.returncode != 0: raise RuntimeError(f"agent failed: {result.stderr.strip()[:200]}") return result.stdoutif __name__ == "__main__": try: print(generate_report(fetch_metrics("com.example.wallpaper"))) except (RuntimeError, KeyError, json.JSONDecodeError) as e: # Never swallow a failure into an empty report print(f"weekly_report failed: {e}", file=sys.stderr) sys.exit(1)
The first version of this script referenced downloads and dau only inside the prompt's f-string, and never defined them anywhere. I put it on the scheduler without running it once. Every Monday morning it died quietly with a NameError, and I only noticed weeks later when I realized no report had ever arrived. Reading generated code and nodding along is not the same as running it.
What This Pipeline Actually Costs, Broken Down by Phase
The more overnight execution you add, the more the end of the month gives you a single number and nothing else. A total tells you nothing about which phase is expensive. I spent the first half of this article on effort and left the cost side alone for far too long.
The unrounded cost field from CLI 1.1.21 is what closes that gap. The value that used to be rounded for display can now flow straight into your own aggregation.
There are only two moving parts. Your status line script appends one record per turn to a JSONL ledger, and something reads it back later. The phase label comes from the branch name or an environment variable.
I Measured the Rounding Effect and Got the Opposite of What I Expected
My assumption was that an unrounded value mattered because it made the total more accurate. So I built two ledgers of 1,160 turns each, identical in the number of runs per phase, differing only in the price of a single turn.
Ledger
Cost per turn
Unrounded total
Rounded per row
Drift
Mostly expensive turns
$0.0002–$0.085
$35.5019
$35.4800
-0.06%
Mostly cheap turns
$0.0002–$0.0085
$3.5502
$2.8600
-19.44%
When per-turn cost sits comfortably above the display precision, rounding up and rounding down cancel out and the total moves by 0.06%. Once per-turn cost drops below two decimal places, what gets truncated never comes back, and the same 1,160 rows lose roughly a fifth of the total.
So the unrounded cost field is not about total accuracy. It is about one thing: a pipeline made of many short, cheap turns looks cheaper than it is when you read a rounded display. That is exactly the shape of an overnight run.
Implementation dominates on both count and spend. Before I looked, I would have bet on asset generation — Veo 3 video and screenshot batches feel like the heavy operations. The weight turned out to be not the unit price of a single call but the number of small round trips inside the implementation phase.
If something has to come down, it is not asset resolution. It is the round-trip count in implement: writing acceptance criteria up front, and sending work back in larger units rather than one nudge at a time. Measuring the cost moved the place I would intervene from one end of the pipeline to the other.
Where These Numbers Come From
To be straight about it: the ledgers above are samples I generated for verification, not my actual invoices. What I verified is the aggregation logic and the rounding behavior. The generator uses a fixed seed, so the same script produces the same numbers on your machine. Runtime was 0.04 seconds for 1,160 rows (Node v22.23.2, identical across three runs).
If you want your own figures, adjust only how the phase label gets attached and feed it real JSONL. The reading half does not need to change.
Real Results: Before and After
Here's what this pipeline produced on an actual wallpaper app:
Metric
Before
After
Concept to release
~8 weeks
2–3 weeks
Screenshot + video production
3–4 days
Under half a day
4-language localization
5–7 days
1–2 hours
Update cycle
3–4 weeks
Under 1 week
Total effort (hours)
250–300
60–80
The 75% reduction in effort went directly into the next app and into responding to users. The velocity compounds: each shipped app becomes a source of user feedback that informs the next concept, which moves through the pipeline faster because the tools are already configured.
There's no need to implement all four phases at once. Start with whatever's costing you the most time.
Weeks 1–2: Environment setup
Configure Antigravity + Stitch MCP. Automate UI prototyping for your current project.
Weeks 3–4: Implementation automation
Define your first two agents in AGENTS.md. Run one overnight Background Agent task. Review the output in the morning.
Weeks 5–6: Test automation
Add the test-writer agent. Connect to GitHub Actions so tests run on every push.
Weeks 7–8: Marketing automation
Automate screenshot generation via Fastlane + Stitch. Use Gemini CLI for one set of store descriptions.
Week 9+: Video generation
Integrate Veo 3. Generate three variants and A/B test them in App Store Connect.
The tools powering this pipeline didn't exist in the same form a year ago. The combination will keep evolving. What stays constant is the underlying design — Antigravity as conductor, specialized agents as performers, you as director.
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.