Guide · Build system

Wrap xcodebuild with structured output.

Translate your existing xcodebuild scripts to FlowDeck command by command. Run both in parallel. Compare the output. Switch over piecemeal without ripping out your CI. xcodebuild stays the underlying engine the whole time.

Before you start

This guide assumes:

  • FlowDeck is installed and your trial is active. If not, follow the getting-started guide first.
  • You have at least one existing xcodebuild script you'd like to migrate. A fastlane lane that shells out to xcodebuild works too.
  • The script currently runs (even if grudgingly).

By the end you'll have a side-by-side translation of your existing commands, a smoke-tested replacement, and a switch-over plan that keeps xcodebuild available as a fallback the whole time.

Step 01

Audit your current xcodebuild commands

List every place your project shells out to xcodebuild, xcrun simctl, xcrun devicectl, or log stream. Common locations:

  • A scripts/ or tools/ directory in your repo.
  • Your CI config (.github/workflows/, bitrise.yml, Jenkinsfile, etc.).
  • A Fastfile with sh "xcodebuild ..." calls.
  • Build phases inside Xcode that shell out.
  • Makefiles, justfiles, or npm scripts in mixed-repo projects.

For each call site, write down what it does in plain English. "Build for CI release on iPhone 16 simulator." "Run the smoke test suite." "Boot a simulator and wait for it." The plain-English version is your migration target.

Step 02

Translate command by command

Use this table as a starting point. The patterns are deliberately mechanical so the migration can happen one line at a time.

What you're doing xcodebuild / xcrun FlowDeck
Discover schemes xcodebuild -list -workspace App.xcworkspace flowdeck context --json
Build for a simulator xcodebuild build -workspace ... -scheme ... -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' flowdeck build -S "iPhone 16"
Run a single test -only-testing:MyAppTests/LoginTests/testValidLogin --only LoginTests/testValidLogin
Skip a slow test -skip-testing:MyAppTests/SlowTests --skip SlowTests
Boot a simulator UDID=$(xcrun simctl list ...); xcrun simctl boot "$UDID" flowdeck simulator boot "iPhone 16"
Stream logs from one app xcrun simctl spawn booted log stream --predicate 'subsystem contains "com.myapp"' flowdeck logs <app-id>
Clean derived data xcodebuild clean + rm -rf ~/Library/Developer/Xcode/DerivedData flowdeck clean --all
Parse output Pipe through a regex, hope subsystem strings didn't change Add --json, read typed events

This is not a one-to-one feature map. xcodebuild archive, xcodebuild -exportArchive, and the signing-and-distribution pipeline don't have FlowDeck equivalents, those operations are out of scope. Keep xcodebuild for them.

Step 03

Run both in parallel and compare

Before switching anything, run the FlowDeck command alongside the existing xcodebuild call. This catches surprises while you still have a rollback.

# Existing script (unchanged)
xcodebuild build -workspace App.xcworkspace -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16'

# New, side-by-side
flowdeck build -w App.xcworkspace -s App -S "iPhone 16" --json \
  > /tmp/flowdeck-build.ndjson

Compare the exit code, the time taken, and the failure mode if anything goes wrong. The FlowDeck command should be at least as fast (often faster, because scheme discovery is native instead of a xcodebuild -list hit) and the exit code should match.

If output differs (FlowDeck reports a warning your CI ignored, or your CI flags an error FlowDeck rolled up), it's almost always FlowDeck being more precise. Investigate before assuming it's wrong.

Step 04

Save a project config to eliminate flag repetition

One of the wins of migrating is that you stop passing the same -workspace / -scheme / -destination on every command. FlowDeck reads from a per-project config:

flowdeck config set \
  -w App.xcworkspace \
  -s App \
  -S "iPhone 16" \
  -C Debug

After that, flowdeck build, flowdeck run, flowdeck test, and the rest pick up the saved values. The config is stored in .flowdeck/ in your project root. CI commits it (so jobs use the same configuration); developers either commit it or .gitignore it depending on whether your team standardizes on one scheme.

You can still pass flags on the command line; they override the saved config for that single call. Useful for "build the same scheme but on a different simulator just this once."

Step 05

Switch over piecemeal

The lowest-risk migration is one job at a time. A typical order:

  1. Local developer scripts first. The build-and-run.sh someone wrote three years ago. Lowest blast radius, easy to roll back.
  2. PR validation builds next. The fastest, most-run CI job. Catches problems quickly because it runs on every push.
  3. Nightly regression runs after that. Once PR jobs are stable, move the nightly suite. Volume is lower; observation period is longer.
  4. Release builds last. Or not at all. If your release job uses xcodebuild archive and -exportArchive, leave it on xcodebuild. Releases are not the place to swap out tooling.

Keep the previous script in version control with a date in the filename (build.legacy.sh, workflow-2026-03.yml) until you've shipped through a full release cycle on the new tooling. The cost of keeping it is small. The cost of needing it and not having it is large.

Step 06

If something breaks, roll back fast

The migration is designed so any single job can revert to raw xcodebuild without disturbing the rest. Concrete recipe:

  1. Revert the one file. If a PR validation job fails after the swap, restore the build.legacy.sh (or workflow-2026-03.yml) you kept around. Jobs are independent; the local dev script and the nightly job keep using FlowDeck.
  2. Capture the diff. Before reverting, save the FlowDeck output: flowdeck build --json > /tmp/failed-build.ndjson. The structured events make it obvious where FlowDeck and xcodebuild diverged. Almost always a different scheme, configuration, or simulator selection, not a real build difference.
  3. File an issue with the NDJSON attached. FlowDeck issues with structured output attached are resolved faster than "the build failed."

Once the migrated job is back on FlowDeck and stable for a release cycle (PRs, nightly, release), the legacy script can be deleted. Not before.

Step 07

Convert text parsers to JSON consumers

If your CI has a script that parses xcodebuild output to detect failures or count warnings, replace it. The new version is shorter and won't break across Xcode versions:

# Before: a regex that breaks when xcodebuild changes its output
xcodebuild ... | grep "error:" | wc -l

# After: structured filtering with jq
flowdeck build --json \
  | jq 'select(.type == "error") | .file + ":" + (.line|tostring)'

The same pattern works for tests:

flowdeck test --json \
  | jq 'select(.type == "test_failed") | .name'

And for build summaries you can dump to a CI artifact:

flowdeck build --json > build-events.ndjson
# Attach build-events.ndjson as a CI artifact on failure

Stable schemas mean these parsers don't break the next time Apple changes a string in xcodebuild's output.

Patterns that come up during migration

Keep xcodebuild for archive and export.
FlowDeck doesn't wrap xcodebuild archive or -exportArchive. Release pipelines that produce a signed .ipa should stay on raw xcodebuild. There's no benefit to migrating them.
Use --verbose when debugging a migration.
FlowDeck summarizes output by default. If you're comparing against a xcodebuild baseline and the FlowDeck output looks suspiciously short, pass --verbose to see the underlying xcodebuild stream. Useful for catching cases where your old script depended on a warning the summary suppressed.
Compose with fastlane, don't replace it.
If you have a Fastfile that orchestrates lanes, the right move is usually to keep the Fastfile and swap the sh "xcodebuild ..." calls inside lanes for sh "flowdeck ...". Fastlane keeps its orchestration role; FlowDeck replaces the manual xcodebuild plumbing.
Pin the FlowDeck version in CI.
Same way you pin Xcode in CI. The install script accepts a --version argument, so you can pin to a known build for reproducibility: curl -sSL https://flowdeck.studio/install.sh | sh -s -- --version <tag>.

When things go wrong

FlowDeck "succeeds" but produces no output where xcodebuild did
FlowDeck summarizes by default. Add --verbose to see the raw xcodebuild stream you're used to. Useful as a sanity check during migration, less useful as a permanent setting.
The build is slower than expected on CI
Two common causes. First, the FlowDeck install step shouldn't repeat per job, install it on the runner image, or cache the install. Second, if you removed your DerivedData cache during migration, the first run rebuilds from scratch; subsequent runs benefit from the cache.
A test that passed in xcodebuild fails in FlowDeck (or vice versa)
Almost never the test. Usually a different scheme, configuration, or simulator. Diff your old xcodebuild invocation against your flowdeck config get output: workspace, scheme, configuration, simulator. Mismatches surface here.
Some xcodebuild flag isn't supported
FlowDeck deliberately doesn't expose every xcodebuild flag (that surface is huge and most flags are rarely used). For uncommon flags, pass them through with --xcodebuild-options "-FLAG VALUE" on commands that accept it. If you can't find a way to pass a flag, you can always shell out to raw xcodebuild for that one step.
The signing setup breaks under FlowDeck
It doesn't, but the symptom can show up here first because FlowDeck makes the error message clearer. Signing identity, provisioning profile, and entitlement issues come from Xcode's signing pipeline, not from FlowDeck. The fix is the same one you'd apply to a failing xcodebuild build: open Xcode, check the signing tab, make sure the profile is current.

Read next

Further reading

  1. FlowDeck vs xcodebuild, same build system, different surface.
  2. Basic CLI commands, the day-to-day surface once the migration is done.
  3. iOS log streaming, the deep dive on per-app logging.
  4. Full CLI reference, in the FlowDeck docs.