appshots-automation-pipeline
App Screenshot Automation Pipeline
This skill teaches an AI agent how to automate the full App Store screenshot capture workflow:
Prepare App (Semantics) → Boot Simulators → Capture (Maestro/AXe) → Design Hand-off
Tool Selection
Choose the right capture tool based on your situation:
Need auto-waits, retry logic, or cross-platform capture?
├── YES → Use Maestro (see appshots-maestro-capture skill)
│ ├── Declarative YAML flows
│ ├── Auto-waits, no sleep timers
│ ├── Built-in takeScreenshot
│ ├── Cross-platform (iOS + Android)
│ └── Works with: Flutter, SwiftUI, UIKit, Compose, React Native
│
└── NO → Need raw accessibility tree JSON or iOS-only advanced control?
├── YES → Use AXe + bash scripts (see below)
└── NO → Use Maestro
Recommended for most apps: Always start with Maestro. Fall back to AXe only for edge cases.
When to Use Each Tool
| Scenario | Tool |
|---|---|
| Any mobile app — cross-platform capture | Maestro |
| Need auto-waits and retry logic | Maestro |
| Need raw accessibility tree JSON | AXe (axe describe-ui) |
| iOS-only native app (Swift/ObjC) | Either (Maestro or AXe) |
| CI environment without Maestro support | AXe |
| Hardware button simulation | AXe |
Prerequisites
# Option A: Maestro (Recommended — use official installer, NOT brew)
curl -fsSL "https://get.maestro.mobile.dev" | bash
export PATH="$PATH:$HOME/.maestro/bin"
# Option B: AXe (Fallback)
brew install cameroncooke/axe/axe
# Always needed
# xcrun simctl — Built-in with Xcode (no install needed)
Workflow Overview
Phase 0: Prepare the App
Before capture, ensure your app has stable accessibility identifiers. See the appshots-accessibility-ids skill for:
- Adding accessibility identifiers to tappable elements (per-framework API reference)
- Naming conventions (
screen_elementpattern) - Verification with
maestro studiooraxe describe-ui
Phase 0.5: Demo Data & Pre-Capture Setup
Screenshots need the app to look populated and polished — empty states, blank lists, and "getting started" screens ruin App Store screenshots.
Before writing any capture flows, ask the user:
- Does the app need demo/seed data? — Pre-populated lists, sample content, user profiles, etc.
- Does the app have a demo mode or test account? — Some apps have built-in mechanisms.
- Does any content need to be locale-specific? — Sample names, dates, currencies.
- Are there any onboarding/tutorial screens to skip? — First-launch modals, permission dialogs.
- Should the app show a specific state? — Logged-in, premium unlocked, specific tab selected.
Common Setup Strategies
| Strategy | When to use | How |
|---|---|---|
| Seed database | App has local DB (SQLite, Hive, Realm) | Pre-populate with sample records via script or test fixture |
| SharedPreferences / UserDefaults | Skip onboarding, set flags | Write keys before launch: xcrun simctl spawn <UDID> defaults write <bundle_id> has_seen_onboarding -bool true |
| Test account | App requires login | Use a dedicated screenshot account with curated content |
| Demo mode flag | App supports it | Launch with flag: xcrun simctl launch <UDID> <bundle_id> --demo-mode |
| Mock API | App fetches from server | Use a local mock server or cached responses |
| Clear and rebuild | Ensure clean state | xcrun simctl uninstall <UDID> <bundle_id> then reinstall |
Example: SharedPreferences Setup (Flutter)
# Skip onboarding and set demo state before launching the app
UDID="booted"
BUNDLE_ID="com.example.myapp"
# Write to the app's shared preferences via simctl
PREFS_DIR=$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)/Library/Preferences
defaults write "$PREFS_DIR/$BUNDLE_ID" has_completed_onboarding -bool true
defaults write "$PREFS_DIR/$BUNDLE_ID" demo_data_loaded -bool true
Key principle: The agent should always ask about demo data needs BEFORE writing capture flows. Capturing an empty app is wasted effort.
Phase 0.75: Detect Locale Handling Strategy
Apps handle locales in two fundamentally different ways. You MUST check the source code to determine which strategy the app uses — using the wrong one means screenshots won't change language.
Strategy A: System-Level Locale (Default)
The app follows the device/simulator language setting.
How to detect per framework:
| Framework | System locale indicators |
|---|---|
| Flutter | Standard AppLocalizations with MaterialApp.localizationsDelegates, no custom locale provider |
| SwiftUI/UIKit | Uses NSLocalizedString / String(localized:), .lproj bundles, no in-app picker |
| Android | Uses strings.xml in values-ja/, values-ko/, etc., no custom locale logic |
| React Native | Uses i18next or react-intl tied to I18nManager / system locale |
How to switch locale (iOS Simulator):
xcrun simctl spawn "$UDID" defaults write "Apple Global Domain" AppleLanguages -array "ja"
xcrun simctl spawn "$UDID" defaults write "Apple Global Domain" AppleLocale "ja_JP"
xcrun simctl shutdown "$UDID" && xcrun simctl boot "$UDID"
How to switch locale (Android Emulator):
adb -s emulator-5554 shell "su 0 setprop persist.sys.locale ja-JP; setprop ctl.restart zygote"
Strategy B: App-Level Locale (Custom)
The app manages its own language setting internally. Changing the simulator/emulator locale has NO EFFECT.
How to detect per framework:
| Framework | App-level locale indicators |
|---|---|
| Flutter | SharedPreferences key for locale, custom LocaleProvider / LanguageCubit, easy_localization package, MaterialApp.locale set from state |
| SwiftUI/UIKit | UserDefaults key for language, in-app language picker, custom LanguageManager class |
| Android | SharedPreferences key for locale, custom LocaleHelper, in-app language dropdown, AppCompatDelegate.setApplicationLocales() |
| React Native | AsyncStorage key for language, i18next.changeLanguage() called from settings screen |
How to switch locale:
# iOS: Write the preference directly before launch
PREFS_DIR=$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)/Library/Preferences
defaults write "$PREFS_DIR/$BUNDLE_ID" selected_locale -string "ja"
# Android: Write SharedPreferences directly
adb shell "run-as $PACKAGE_NAME cat /data/data/$PACKAGE_NAME/shared_prefs/*.xml"
# Then modify and push back, or use Maestro to tap through the UI
# Or use Maestro to tap the in-app language picker
- tapOn:
id: "tab_settings"
- tapOn:
id: "language_picker"
- tapOn:
text: "日本語"
Quick Detection Commands
# Flutter
grep -r "SharedPreferences.*locale\|SharedPreferences.*language\|easy_localization\|LocaleProvider\|LanguageCubit" lib/
# iOS Native (Swift)
grep -r "UserDefaults.*language\|LanguageManager\|setLanguage" --include="*.swift" .
# Android (Kotlin/Java)
grep -r "SharedPreferences.*locale\|LocaleHelper\|setApplicationLocales" --include="*.kt" --include="*.java" .
# React Native
grep -r "AsyncStorage.*language\|i18next.changeLanguage\|setLanguage" --include="*.js" --include="*.ts" --include="*.tsx" src/
If unsure, ask the user: "Does your app follow the system locale, or does it have its own language picker in settings?"
Phase 1: Capture Raw Screenshots
Option A: Maestro (Recommended)
See the appshots-maestro-capture skill for:
- Writing YAML flows with
takeScreenshot - Multi-locale parallel capture
scrollUntilVisible,extendedWaitUntil, and other smart commands
Option B: AXe + asc CLI (Fallback)
Create .asc/screenshots.json to define the steps AXe should take in the app.
{
"version": 1,
"app": {
"bundle_id": "com.example.app",
"udid": "booted",
"output_dir": "./screenshots/raw"
},
"steps": [
{ "action": "launch" },
{ "action": "wait", "duration_ms": 2000 },
{ "action": "screenshot", "name": "01_home" },
{ "action": "tap", "id": "settings_tab" },
{ "action": "wait", "duration_ms": 1000 },
{ "action": "screenshot", "name": "02_settings" }
]
}
Note: Use axe describe-ui to find the correct id and label values for the steps.
Parallel Multi-Locale Capture (AXe)
Instead of sequentially rebooting one simulator, map each target locale to a dedicated simulator UDID created via xcrun simctl create. This allows parallel capture.
#!/bin/bash
# parallel-capture.sh — Fast multi-locale capture
declare -A LOCALE_UDID=(
["en-US"]="UDID_EN_US"
["ja-JP"]="UDID_JA_JP"
["ko-KR"]="UDID_KO_KR"
)
# Configuration
IS_NATIVE_IOS=false # Set to true if app uses system locale
BUILD_APP=false # Set to true to build the app before capturing
BUNDLE_ID="com.example.app"
if [ "$BUILD_APP" = true ]; then
echo "Building app for simulator..."
# ⬇ Replace with your framework's build command
flutter build ios --simulator
fi
# 1. Boot all simulators and set their locales
for LOCALE in "${!LOCALE_UDID[@]}"; do
(
UDID="${LOCALE_UDID[$LOCALE]}"
LANG="${LOCALE%%-*}" # e.g., 'ja'
APPLE_LOCALE="${LOCALE/-/_}" # e.g., 'ja_JP'
xcrun simctl boot "$UDID" || true
if [ "$IS_NATIVE_IOS" = true ]; then
# Native iOS: change system locale
xcrun simctl spawn "$UDID" defaults write NSGlobalDomain AppleLanguages -array "$LANG"
xcrun simctl spawn "$UDID" defaults write NSGlobalDomain AppleLocale -string "$APPLE_LOCALE"
# Needs a reboot to apply locale changes cleanly
xcrun simctl shutdown "$UDID"
xcrun simctl boot "$UDID"
else
# Flutter / Shared Prefs: write directly to app preferences
PREFS_DIR=$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)/Library/Preferences
defaults write "$PREFS_DIR/$BUNDLE_ID" selected_locale -string "$LANG"
fi
# Install the built app (adjust path for your framework)
xcrun simctl install "$UDID" build/ios/iphonesimulator/Runner.app
) &
done
wait
# 2. Run captures in parallel using the asc plan
for LOCALE in "${!LOCALE_UDID[@]}"; do
(
UDID="${LOCALE_UDID[$LOCALE]}"
asc screenshots run \
--plan ".asc/screenshots.json" \
--udid "$UDID" \
--output-dir "./screenshots/raw/$LOCALE" \
--output json
echo "Captured $LOCALE on $UDID"
) &
done
wait
echo "All raw captures complete."
Phase 2: Design Hand-off
Once the raw screenshots are captured, immediately transition to the appshots-design-workflow skill to load these images into the App Screenshots desktop app for framing, backgrounds, text overlays, and PDF/PNG export.
Agent Guidelines
Thinking Process for AI Agents
When given a scenario like "capture screenshots of my fitness app", follow this process:
- Ask about demo data — Does the app need seed data, a test account, or flags to skip onboarding?
- Prepare the app — Add accessibility identifiers to key elements (see
appshots-accessibility-idsskill) - Set up demo state — Seed data, write SharedPreferences flags, set up test account
- Choose the capture tool — Maestro for most apps, AXe for edge cases
- Discover the app — Find the bundle ID, build the app if needed
- Boot a simulator — Pick the right device type for the target display
- Write capture flows — Maestro YAML or asc JSON plan
- Run single-locale first — Verify captures are correct
- Scale to multi-locale — Parallel capture across all target locales
- Design — Switch to
appshotsCLI skills to set up frames, text, backgrounds - Translate — Switch to
appshots-translationskill for AI text translation - Export — Export final screenshots for all locales
Pre-Capture Questions Checklist
Always ask the user these questions before starting capture:
- Which screens do you want to capture? (list all screenshots)
- Does the app need demo/seed data to look populated?
- Is there a test account or demo mode?
- Any onboarding or permission dialogs to skip?
- Which locales do you need? (e.g., en, ja, ko, de)
- Any specific app state needed? (premium, logged in, specific tab)
Device Selection Strategy (App Store Connect)
When choosing which simulators to boot and generate screenshots for, follow this device tiering:
- Required Default (6.9"):
iPhone 16 Pro MaxoriPhone 17 Pro Max. This covers the primary 6.9" App Store Connect requirement and should ALWAYS be captured. - Optional Sizes:
- 6.5" (Optional):
iPhone 15 Pro MaxoriPhone 11 Pro Max. - 5.5" (Optional):
iPhone 8 Plus(only if legacy compatibility is explicitly requested).
- 6.5" (Optional):
Key Tips
- Prepare identifiers first — The biggest time sink is missing accessibility identifiers
- Use Maestro for capture — Auto-waits eliminate fragile
sleeptimers - Use
--idover coordinates — Identifiers are stable across locales and screen sizes - Test single locale first — Catch issues before scaling to all locales
- Status Bar Overrides are Optional — Setting the simulator time to 9:41 and overriding network/battery states (
xcrun simctl status_bar) is optional. By default, simulators usually look clean enough, andappshotsdesigns often use backgrounds that can mask standard status bars.