TL;DR
The decision in 20 seconds:
xcodebuildcompiles your code. It does not boot simulators, install on devices, stream app logs, discover tests without a build, or emit structured output. Those are different Apple CLIs,xcrun simctl,xcrun devicectl,log stream,xcresulttool,instruments, each with its own flag syntax and its own failure modes.- FlowDeck unifies the whole toolchain.
xcodebuildis still doing the build. FlowDeck does the orchestration, the destination resolution, the simulator lifecycle, the log streaming, and the JSON output, behind eight commands. - The build itself is unchanged. Same project files, same schemes, same signing, same derived data, same Xcode toolchain. FlowDeck is a layer above, not a fork below.
- If you have a single hand-tuned CI script that already works, you don't need FlowDeck. If you're running fifty
xcodebuild-flavored shell calls a day or wiring an AI agent into the loop, you do.
What is xcodebuild?
xcodebuild is Apple's command-line interface to the Xcode build system. It ships with Xcode itself, lives at /usr/bin/xcodebuild, and is the authoritative way to compile, archive, test, and export Apple-platform code outside the Xcode IDE. fastlane wraps it. tuist generates projects it consumes. Every CI runner for iOS and macOS, GitHub Actions, Bitrise, Xcode Cloud, Jenkins, invokes it under the hood.
The surface is broad:
xcodebuild -list # discover schemes / configurations
xcodebuild -showBuildSettings # print every effective build var
xcodebuild build # compile
xcodebuild test # build + run tests
xcodebuild archive # build + archive for distribution
xcodebuild -exportArchive # export an .ipa from an archive
xcodebuild -resolvePackageDependencies # resolve Swift packages
xcodebuild clean # delete derived data for a project
Combined with the destination flag (-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4'), it targets simulators, paired devices, Mac Catalyst, visionOS, and tvOS. It's the same engine Xcode uses; running it from the terminal produces byte-identical output.
It is, in other words, the actual build system. There is no Apple-supported way to compile an iOS app that bypasses xcodebuild. FlowDeck does not pretend otherwise, every flowdeck build shells out to it.
What xcodebuild does well
Four things xcodebuild gets right, and gets right by virtue of being Apple's own tool.
Authoritative compilation
If Xcode can build it, xcodebuild can build it. Every project format, every signing configuration, every entitlement, every framework. There's no compatibility lag and no third-party translation layer to drift out of sync. When Apple ships a new Xcode, xcodebuild updates with it the same day.
Reproducibility
A successful xcodebuild command line is portable. Paste it into a CI script, paste it into a colleague's terminal, paste it into a build farm, the same flags produce the same build (modulo Xcode version and signing identity). That reproducibility is why every CI vendor speaks fluent xcodebuild.
No license, no telemetry
It's part of Xcode. No separate install, no extra account, no opt-out toggles to remember. For teams with strict offline or compliance requirements, the Apple-toolchain-only path is the safest path.
Stable contract for archives and exports
The archive/export flow is finicky but stable. -exportArchive with an export options plist is the supported way to ship an .ipa to TestFlight or the App Store, and it works the same in Xcode 14 as in Xcode 16. Higher-level tools that change shape with every release can't make that promise.
The criticism in the rest of this article isn't of xcodebuild as a compiler. It's of xcodebuild as a daily workflow tool, a role it was never really designed for.
Where the seams show
A real iOS or macOS workflow isn't just compilation. It's build, run, see what happened, iterate. xcodebuild compiles. Everything else is somewhere else. Here's what an honest day looks like at the Apple-CLI layer.
Destination strings change every year
Targeting a simulator requires constructing a -destination string by hand:
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4'
Get the platform string wrong, get the OS wrong, get the simulator name wrong (case-sensitive), forget the quotes, and you get xcodebuild: error: Unable to find a destination matching the provided destination specifier. The full available list is buried in xcodebuild -showdestinations -scheme MyApp, which itself takes a few seconds. Every new Xcode reshuffles the OS strings and the destination flag formatting gets one more edge case.
Simulator lifecycle is a separate CLI
To boot, install, and launch, you switch tools: xcrun simctl. The same .app you just built is now opaque, you have to look up BUILT_PRODUCTS_DIR with another xcodebuild -showBuildSettings call, extract the bundle ID from Info.plist with PlistBuddy, find your simulator UDID with xcrun simctl list, boot it, install the .app, launch the bundle ID. Four tools, five commands, before the app is on screen.
Physical devices use a third CLI
For real iPhones, replace simctl with devicectl, Apple's newer device tool. Different identifier format, different command surface, different failure modes. The agent or script that handled simulators correctly does not handle devices correctly without learning a second toolchain.
Logs are a fourth tool
Once your app is running, its print() and OSLog output goes to the unified logging system. To read it, you reach for xcrun simctl spawn booted log stream with a predicate language nobody remembers:
xcrun simctl spawn booted log stream \
--predicate 'process == "MyApp"' \
--style compact
For a physical device, the path is idevicesyslog (third-party) or Console.app (GUI). For Mac apps, it's log stream with a different predicate. Each surface has its own quoting rules and its own filtering semantics, and none of them produce structured output an agent or CI parser can consume.
Output is text, not data
The output of xcodebuild build is text, and a lot of it. A typical build prints hundreds of lines of CompileSwift, Ld, CodeSign, and copy steps interleaved with whatever your warnings and errors are. To find the error you wrote, you grep. To extract it programmatically, you write a regex. To know whether the build succeeded, you check the exit code and hope the stderr/stdout interleaving didn't swallow anything. Other formatters (xcpretty, xcbeautify, xcsift) exist precisely because this output is unworkable as data, but they parse after the build, not during, and they only fix the build leg of the workflow.
Scheme discovery is slow
xcodebuild -list takes 2-5 seconds the first time, more on a cold cache. It runs the project loader to enumerate schemes, configurations, and targets, useful information, slow to retrieve. For a human at the keyboard, fine. For an agent that re-derives the world on every tool call, it's a multi-second tax on every loop.
Test discovery requires a build
To list available tests, xcodebuild test -list-tests requires a build first. To run one specific test, you construct -only-testing:TargetName/ClassName/methodName by hand. There's no "preview the test surface without compiling" mode. For a tight TDD or agent loop, that's a real-time penalty on top of the build itself.
Clean is half a job
xcodebuild clean removes per-project derived data, but most "actually clean" workflows additionally rm -rf ~/Library/Developer/Xcode/DerivedData, clear the Swift package cache, and sometimes wipe the simulator runtime. Each of those is a different command, and the order matters when packages have changed.
No project state
Every xcodebuild invocation is stateless. There's no "the scheme is MyApp and the simulator is iPhone 16" memory across calls, you re-specify everything, every time. Shell aliases help; project-aware tooling helps more.
None of this is a flaw in xcodebuild's job as a compiler. It's that compilation is a small piece of "develop an app from a terminal." Everything else, runtime, observability, lifecycle, structured output, is somewhere else.
How FlowDeck addresses each seam
FlowDeck doesn't replace xcodebuild. It calls it. The build is identical to what Xcode runs. What FlowDeck adds is the layer that xcodebuild alone forces you to write yourself, plus the simulator, device, logging, testing, and UI-automation tools as siblings under one CLI.
- Destinations
-S "iPhone 16"for simulators,-D "My Mac"or-D "Daniel's iPhone"for Mac and physical devices. FlowDeck resolves names against the live runtime list, constructs the destination string, and reports specific errors ("no simulator named 'iPhone 16'; closest matches: iPhone 16 Pro, iPhone 16 Plus") instead of generic "no destinations matched."- Simulator lifecycle
flowdeck runhandles boot, install, and launch in one step, reading the bundle ID from the build product.flowdeck simulator boot "iPhone 16 Pro"if you want it explicit. NoPlistBuddy, no UDID lookup, no four-shell sequence.- Physical devices
- Same surface,
-Dinstead of-S. Pairing, signing, install, and launch are folded behind one command. You don't learnxcrun devicectl; you don't need to. - Logs
flowdeck logs <app-id>. Per-app filtering by bundle ID and subsystem.OSLog,print(), and crash output, formatted and live. Pipe it torgto filter further. No predicate language.- Structured output
- Every command accepts
--jsonand emits versioned NDJSON, one event per line, schema documented and stable across minor releases. Build start, per-file compile, warning, error, build complete: each lands on stdout the moment it happens, so an agent or CI parser can react mid-build instead of waiting for the buffer. - Scheme discovery
flowdeck context --jsonparses the project file natively (noxcodebuild -listshell-out), returns in milliseconds, and includes available simulators and connected devices in the same payload.- Test discovery
flowdeck test discover --jsonuses AST parsing, it reads your Swift source for@Test,func test*(), andXCTestCasesubclasses without compiling.flowdeck test --only LoginTests/testValidLoginfor a single test; no-only-testing:Target/Class/methodconstruction.- UI automation
flowdeck ui simulator session start, thentap,type,swipe,screen,tree. The accessibility tree comes back as structured JSON. Screenshots are JPEGs. macOS apps get the same surface:flowdeck ui mac click,type,menu click "File > Save".- Clean
flowdeck cleanfor the current project,flowdeck clean --allfor global DerivedData plus FlowDeck's own caches. One command, norm -rfincantation to remember.- Project state
.flowdeck/config.jsonsaves your workspace, scheme, simulator, device, and configuration per project. Every subsequent command picks them up. The agent stops re-deriving the world on every invocation.
A day in the loop, side by side
The workflow: discover schemes, build for a simulator, run the app, stream its logs, run one test.
With Apple's CLIs alone, the script you'd have to write:
# 1. Discover schemes (slow: 2-5s)
xcodebuild -list -workspace MyApp.xcworkspace
# 2. Build
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
-configuration Debug build
# 3. Find the built .app and bundle id
BUILT=$(xcodebuild ... -showBuildSettings | awk -F' = ' '/BUILT_PRODUCTS_DIR/ {print $2}')
BUNDLE=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
"$BUILT/MyApp.app/Info.plist")
# 4. Boot the simulator
UDID=$(xcrun simctl list devices available -j | jq -r '...')
xcrun simctl boot "$UDID"
# 5. Install + launch
xcrun simctl install "$UDID" "$BUILT/MyApp.app"
xcrun simctl launch "$UDID" "$BUNDLE"
# 6. Stream logs (in another shell)
xcrun simctl spawn "$UDID" log stream \
--predicate 'process == "MyApp"' \
--style compact
# 7. Run one test
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
-only-testing:MyAppTests/LoginTests/testValidLogin
That's the well-known archaeology: seven commands, four tools, two shell sessions, and a regex over every output. Every variable you extract is one more chance for a tool call to fail in a way the next tool can't tell you about.
With FlowDeck, the same workflow:
# 1. Discover (instant, no build)
flowdeck context --json
# 2. Build, install, and launch
flowdeck run
# 3. Stream logs
flowdeck logs <app-id>
# 4. Run one test
flowdeck test --only LoginTests/testValidLogin
Same Xcode toolchain underneath. Same signing, same schemes, same project files. The difference is the orchestration disappears, and so does the per-step structured-output gap. flowdeck build --json emits one event per file compiled, one for each warning, one for each error, with file, line, column, and message fields you can hand to an agent or CI runner directly.
When xcodebuild alone is the right answer
FlowDeck is not the answer to everything. There are reasonable cases for staying on Apple's CLIs.
- A single, stable CI script that already works.
- If your one CI job is a hand-tuned
xcodebuild archive+-exportArchivesequence that ships builds to TestFlight and you don't iterate on it, there's no upside to changing tools. FlowDeck shines in the iterate-fifty-times-an-hour loop; a once-per-release CI script isn't that. - Learning the toolchain.
- If you're new to iOS or macOS development and want to understand what's actually happening underneath, running
xcodebuildby hand teaches you something FlowDeck deliberately abstracts away. Use the raw tools to build the mental model, then move up the stack when the orchestration becomes the bottleneck. - Strict offline or compliance environments.
- FlowDeck is a separately installed binary that performs license validation against a remote service. If your build environment forbids any tool outside the Apple-provided toolchain, that's a constraint FlowDeck can't satisfy.
xcodebuildships with Xcode and stays there. - Open-source-only stack.
- FlowDeck is a commercial product (14-day free trial; $59/year after). If license posture is the decision criterion, the comparison ends there.
The rule of thumb: stay on xcodebuild when the orchestration around it is already solved for your situation. The moment "I have to write the script that boots the simulator and parses the logs" becomes a recurring sentence, the orchestration has stopped being incidental and started being the workflow.
Annex
Quick reference
The same operations side by side.
| What you're doing | Apple's tools | FlowDeck |
|---|---|---|
| Target a simulator | xcrun simctl list, find UDID, construct platform=iOS Simulator,name=iPhone 16,OS=18.4 |
-S "iPhone 16" |
| Target a device | xcrun devicectl list, copy UDID, build destination string |
-D "iPhone" |
| Build for Mac | Construct platform=macOS destination |
-D "My Mac" |
| Build and run | xcodebuild + simctl install + simctl launch |
flowdeck run |
| Stream logs | simctl spawn booted log stream --predicate '...' |
flowdeck logs <id> |
| Run one test | -only-testing:MyAppTests/LoginTests/testValidLogin |
--only LoginTests/testValidLogin |
| Discover tests without a build | Not supported | flowdeck test discover (AST) |
| Find schemes | xcodebuild -list (2-5 s) |
flowdeck context (instant) |
| Clean everything | xcodebuild clean + rm -rf ~/Library/Developer/Xcode/DerivedData |
flowdeck clean --all |
| Parse output | Regex over stdout | --json (versioned NDJSON) |
| UI automation | Separate stack (XCUITest, AXe, Maestro) | flowdeck ui ... |
FAQ
Does FlowDeck replace xcodebuild?
No. FlowDeck calls xcodebuild under the hood for the actual compilation. It replaces the workflow around xcodebuild, simulators, devices, logs, tests, output parsing, not the build engine itself.
Do I still need Xcode installed?
Yes. Xcode (or at minimum the Xcode command-line tools) provides the compiler, the simulator runtimes, and the device support that any iOS or macOS build depends on. FlowDeck does not bring its own toolchain.
Will my .xcodeproj and signing config still work?
Yes. FlowDeck reads your project; it doesn't own it. Schemes, configurations, build settings, signing identities, provisioning profiles, and Swift package dependencies are unchanged.
Can I see the raw xcodebuild command FlowDeck runs?
Yes. Run any command with --verbose to see the underlying xcodebuild invocation and its full stdout. Useful for debugging when behavior differs between a FlowDeck call and a raw shell call.
How is FlowDeck's --json different from a formatter like xcbeautify or xcsift?
Formatters parse xcodebuild's text output after the fact. FlowDeck emits structured events as the build runs, with a versioned schema that's stable across minor releases. You don't pipe through anything; the events are on stdout the moment they happen. See FlowDeck vs xcsift for the longer comparison.
Will my existing CI scripts keep working alongside FlowDeck?
Yes. FlowDeck and raw xcodebuild coexist; nothing about installing FlowDeck affects the Xcode toolchain. Many teams migrate one job at a time, leaving the archive/export job on xcodebuild and moving the iterative build/run/test loop to FlowDeck.
Is FlowDeck open source?
FlowDeck is a commercial product. 14-day free trial, $59/year after for two machines. Stable releases, no breaking changes between minor versions, direct support.
Where to next
- Get started with FlowDeck, install, configure, build, run, and test in 10 minutes.
- FlowDeck vs XcodeBuildMCP, native CLI vs Node.js MCP server.
- FlowDeck vs xcsift, parsing build output vs running the whole loop.
- iOS simulator management, the deep dive on what makes
simulator boot "iPhone 16"reliable. - iOS log streaming, the deep dive on per-app logging.
- Start your FlowDeck free trial, install in one command, no credit card.