Comparison

FlowDeck vs xcodebuild

xcodebuild is the build engine for every iOS, macOS, watchOS, tvOS, and visionOS project on the planet. FlowDeck calls it under the hood. The difference is everything around it, destinations, simulators, devices, logs, tests, UI automation, and a structured output format your agents and CI can actually use.

TL;DR

The decision in 20 seconds:

  • xcodebuild compiles 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. xcodebuild is 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 run handles 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. No PlistBuddy, no UDID lookup, no four-shell sequence.
Physical devices
Same surface, -D instead of -S. Pairing, signing, install, and launch are folded behind one command. You don't learn xcrun 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 to rg to filter further. No predicate language.
Structured output
Every command accepts --json and 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 --json parses the project file natively (no xcodebuild -list shell-out), returns in milliseconds, and includes available simulators and connected devices in the same payload.
Test discovery
flowdeck test discover --json uses AST parsing, it reads your Swift source for @Test, func test*(), and XCTestCase subclasses without compiling. flowdeck test --only LoginTests/testValidLogin for a single test; no -only-testing:Target/Class/method construction.
UI automation
flowdeck ui simulator session start, then tap, 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 clean for the current project, flowdeck clean --all for global DerivedData plus FlowDeck's own caches. One command, no rm -rf incantation to remember.
Project state
.flowdeck/config.json saves 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 + -exportArchive sequence 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 xcodebuild by 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. xcodebuild ships 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