Autonomy Docs
Integrations

CircleCI

Start Autonomy runs from CircleCI for a web preview URL or a mobile build, and fail the pipeline unless every run passes.

Use CircleCI to start Autonomy runs after a deployment or mobile build. This page has the shared helper, complete configurations and the result gate. The job passes only when every run is Passed.

Workflow placement

Run Autonomy after the preview responds or the mobile build exists. Each configuration below is a complete alternative .circleci/config.yml; copy the one for your platform. In an existing workflow, keep your deployment job and make the Autonomy job depend on it with requires. Pass the final deployed URL, not a CI container's localhost.

Required secrets

In CircleCI, open Project Settings → Environment Variables and add the values below. CircleCI injects them into the job shell; don't write the API key into YAML. A restricted context can hold the same values instead: attach it with context on the job's entry under workflows, not under jobs.

  • AUTONOMY_API_KEY: an organization API key starting with aut_. Owners and admins create keys in Organization → API keys.
  • AUTONOMY_TEST_PLAN_ID: open the plan in the project's Test plans and copy the last part of the page URL, /p/<project>/plans/<id>. The plan needs at least one test case for the platform. To run one test case instead, set AUTONOMY_TEST_CASE_ID (from /p/<project>/tests/<id>) and leave the plan ID unset. Set exactly one of the two.
  • Web: PREVIEW_URL, the reachable HTTPS deployment URL.
  • Mobile: APP_IDENTIFIER, the iOS bundle ID or Android package name of the build.
  • Optional: AUTONOMY_ENVIRONMENT_SLUG runs against a saved environment of the same project, with its targets and environment values, instead of this job's URL or build. The helper then skips the upload. AUTONOMY_WAIT_SECONDS changes the total wait time.

Without an environment, a run has no environment values: a step that uses {{env.NAME}} ends as Couldn't start. Set AUTONOMY_ENVIRONMENT_SLUG, or use test cases that don't need environment values.

Use a separate plan for each platform. The configurations below expect AUTONOMY_TEST_PLAN_ID and APP_IDENTIFIER to match the recipe. To combine platforms, supply the values through separate contexts or platform-specific variables. See Test plans for grouping test cases.

Web preview URL

Create .ci/ in your repository, save this script as .ci/autonomy.sh, and commit it with .circleci/config.yml. The jobs call it with Bash, so it doesn't need to be executable. It needs Bash, curl and jq, which the Linux images below include.

.ci/autonomy.sh
#!/usr/bin/env bash
# Usage: bash .ci/autonomy.sh web|ios|android
# Starts Autonomy runs for this CI job and exits 0 only if every run passed.
set -euo pipefail
set +x
: "${AUTONOMY_API_KEY:?Set an aut_ organization API key}"
: "${AUTONOMY_REQUEST_ID:?Set an ID that is unique for this CI job attempt}"
platform="${1:?Usage: bash .ci/autonomy.sh web|ios|android}"
api="${AUTONOMY_API_URL:-https://api.autonomyqa.com}"
api="${api%/}"

post() {
  curl --fail-with-body --silent --show-error \
    --connect-timeout 15 --max-time 90 \
    -X POST "$api$1" \
    -H "Authorization: Bearer $AUTONOMY_API_KEY" \
    -H 'Content-Type: application/json' --data-binary "$2"
}

# 1. What to run: a test plan or one test case.
if [ -n "${AUTONOMY_TEST_PLAN_ID:-}" ] && [ -z "${AUTONOMY_TEST_CASE_ID:-}" ]; then
  what=$(jq -n --arg id "$AUTONOMY_TEST_PLAN_ID" '{testPlanId:$id}')
elif [ -n "${AUTONOMY_TEST_CASE_ID:-}" ] && [ -z "${AUTONOMY_TEST_PLAN_ID:-}" ]; then
  what=$(jq -n --arg id "$AUTONOMY_TEST_CASE_ID" '{testCaseId:$id}')
else
  echo 'Set exactly one of AUTONOMY_TEST_PLAN_ID or AUTONOMY_TEST_CASE_ID.' >&2
  exit 2
fi

# 2. Where to run: a saved environment, or this job's preview URL or build.
if [ -n "${AUTONOMY_ENVIRONMENT_SLUG:-}" ]; then
  where=$(jq -n --arg slug "$AUTONOMY_ENVIRONMENT_SLUG" '{environmentSlug:$slug}')
else
  case "$platform" in
    web)
      : "${PREVIEW_URL:?Set PREVIEW_URL, or AUTONOMY_ENVIRONMENT_SLUG}"
      where=$(jq -n --arg url "$PREVIEW_URL" '{targets:{web:{baseUrl:$url}}}')
      ;;
    ios|android)
      : "${ARTIFACT_PATH:?Set the path to the zipped .app, .apk or .aab}"
      : "${APP_IDENTIFIER:?Set the iOS bundle ID or Android package name}"
      file=$(basename "$ARTIFACT_PATH")
      bytes=$(wc -c < "$ARTIFACT_PATH" | tr -d '[:space:]')
      if [ "$platform" = ios ]; then
        type=application/zip
      else
        type=application/vnd.android.package-archive
      fi
      upload=$(post /api/artifacts/upload-url "$(jq -n \
        --arg platform "$platform" --arg fileName "$file" \
        --arg contentType "$type" --argjson declaredBytes "$bytes" \
        '{platform:$platform,fileName:$fileName,contentType:$contentType,declaredBytes:$declaredBytes}')")
      storage=$(jq -er '.storageId' <<< "$upload")
      intent=$(jq -er '.intentId' <<< "$upload")
      curl --fail-with-body --silent --show-error \
        --connect-timeout 15 --max-time 600 \
        -X PUT "$(jq -er '.uploadUrl' <<< "$upload")" \
        -H "Content-Type: $type" --data-binary "@$ARTIFACT_PATH"
      scan=$(post /api/artifacts/scan "$(jq -n \
        --arg storageId "$storage" --arg intentId "$intent" \
        '{storageId:$storageId,intentId:$intentId}')")
      if [ "$(jq -r '.status' <<< "$scan")" != clean ]; then
        echo 'The build did not pass the safety check, so no run was started:' >&2
        jq -r '.findings[]?.message' <<< "$scan" >&2
        exit 1
      fi
      where=$(jq -n --arg p "$platform" --arg storageId "$storage" \
        --arg fileName "$file" --arg id "$APP_IDENTIFIER" \
        '{targets:{($p):({storageId:$storageId,sourceMode:"upload",fileName:$fileName}
          + (if $p == "ios" then {bundleId:$id} else {packageName:$id} end))}}')
      ;;
    *) echo 'Usage: bash .ci/autonomy.sh web|ios|android' >&2; exit 2 ;;
  esac
fi

# 3. Start the runs. A plan starts one run per test case.
#    The request ID's prefix (github, gitlab, ...) names the CI provider.
body=$(jq -n --argjson what "$what" --argjson where "$where" \
  --arg platform "$platform" \
  --arg branch "${AUTONOMY_BRANCH:-}" --arg pr "${AUTONOMY_PR:-}" \
  --arg commitSha "${AUTONOMY_COMMIT_SHA:-}" \
  --arg provider "${AUTONOMY_REQUEST_ID%%:*}" \
  --arg key "$AUTONOMY_REQUEST_ID:$platform" \
  '$what + $where + ({
     platforms: [$platform],
     branch: $branch,
     pr: $pr,
     deployment: ({provider: $provider, branch: $branch, commitSha: $commitSha}
       | with_entries(select(.value != ""))),
     idempotencyKey: $key
   } | with_entries(select(.value != "")))')
run_ids=$(post /api/v1/run.trigger "$body" | jq -er '.runIds[]')
printf 'Started Autonomy runs:\n%s\n' "$run_ids"

# 4. Follow each run until it ends. run.get waits up to 25 s for a change;
#    sending the last cursor back makes each call wait for something new.
deadline=$((SECONDS + ${AUTONOMY_WAIT_SECONDS:-1200}))
all_passed=true
for run_id in $run_ids; do
  cursor=null
  while :; do
    left=$((deadline - SECONDS))
    if [ "$left" -le 0 ]; then
      echo "Stopped waiting for run $run_id after AUTONOMY_WAIT_SECONDS. It keeps going in Autonomy." >&2
      exit 1
    fi
    wait_ms=$(( left < 25 ? left * 1000 : 25000 ))
    run=$(post /api/v1/run.get "$(jq -n --arg runId "$run_id" \
      --argjson cursor "$cursor" --argjson waitMs "$wait_ms" \
      '{runId:$runId,cursor:$cursor,waitMs:$waitMs}')")
    cursor=$(jq '.cursor' <<< "$run")
    status=$(jq -r '.status' <<< "$run")
    [ "$status" = queued ] || [ "$status" = running ] || break
  done

  name="$(jq -r '.testCaseName' <<< "$run") ($run_id)"
  case "$status" in
    passed)
      echo "Passed: $name" ;;
    failed)
      echo "Failed: $name. Open the run in Autonomy to see which step failed." >&2
      all_passed=false ;;
    couldnt_start)
      echo "Couldn't start: $name. The environment couldn't be tested, so this says nothing about your app. Open the run for the reason, fix it and run again." >&2
      all_passed=false ;;
    cancelled)
      echo "Cancelled: $name." >&2
      all_passed=false ;;
    *)
      echo "Unexpected status $status: $name." >&2
      all_passed=false ;;
  esac
done

if [ "$all_passed" != true ]; then
  echo 'Not every Autonomy run passed.' >&2
  exit 1
fi
echo 'Every Autonomy run passed.'

Save this configuration as .circleci/config.yml. Set PREVIEW_URL to an already deployed preview for a first run, then push the files to staging. The readiness check must pass before Autonomy starts.

For per-commit previews, export PREVIEW_URL and call the helper in the same run step. A plain export doesn't carry over to another step. To use it in a later run step of the same job, add this line after the URL is known in the deployment step; CircleCI loads BASH_ENV in later steps:

printf 'export PREVIEW_URL=%q\n' "$PREVIEW_URL" >> "$BASH_ENV"

For separate jobs, write the URL as a single line to /tmp/autonomy-preview/preview-url.txt. Use persist_to_workspace with root: /tmp/autonomy-preview and paths: [preview-url.txt] in the deployment job. In the Autonomy job, use attach_workspace with at: /tmp/autonomy-preview, then run export PREVIEW_URL="$(cat /tmp/autonomy-preview/preview-url.txt)" before the readiness check. BASH_ENV doesn't carry values between jobs.

.circleci/config.yml
version: 2.1

jobs:
  autonomy-web:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - run:
          name: Check preview and run Autonomy
          no_output_timeout: 30m
          command: |
            set -euo pipefail
            set +x
            : "${PREVIEW_URL:?Set the deployed preview URL}"
            curl --fail --silent --show-error --location \
              --connect-timeout 15 --max-time 30 \
              --retry 10 --retry-delay 3 --retry-all-errors \
              --output /dev/null "$PREVIEW_URL"
            export AUTONOMY_BRANCH="${CIRCLE_BRANCH:-local}"
            export AUTONOMY_COMMIT_SHA="${CIRCLE_SHA1:-$(git rev-parse HEAD)}"
            export AUTONOMY_REQUEST_ID="circleci:${CIRCLE_WORKFLOW_ID:-local}:${CIRCLE_BUILD_NUM:-$(date +%s)-$$}"
            bash .ci/autonomy.sh web

workflows:
  preview-qa:
    jobs:
      - autonomy-web:
          filters:
            branches:
              only: staging

For a local check on macOS or Linux, start Docker and download the official legacy CLI v0.1.47860 release. Choose the .tar.gz for your OS (darwin for macOS, linux for Linux) and CPU (arm64 for Apple silicon or ARM, amd64 for Intel or AMD). Extract it and put the circleci-v0 executable on your PATH. The current CLI v1 removed local execution, so the commands below use the legacy binary.

Export the three values in your local shell, then run these commands from the committed repository. CircleCI doesn't import project secrets or contexts locally, so each -e is needed. Version 2.1 configuration must be processed before it runs.

circleci-v0 config validate .circleci/config.yml
circleci-v0 config process .circleci/config.yml > /tmp/autonomy-circleci.yml
circleci-v0 local execute -c /tmp/autonomy-circleci.yml \
  -e "AUTONOMY_API_KEY=$AUTONOMY_API_KEY" \
  -e "AUTONOMY_TEST_PLAN_ID=$AUTONOMY_TEST_PLAN_ID" \
  -e "PREVIEW_URL=$PREVIEW_URL" \
  -e "AUTONOMY_WAIT_SECONDS=${AUTONOMY_WAIT_SECONDS:-1200}" \
  autonomy-web

Local execution runs this Docker job only. It doesn't prove workflow scheduling, context restrictions, macOS execution or the iOS workspace handoff.

Mobile artifact handoff

For iOS and Android the helper asks for an upload URL with the file size and type, uploads with PUT, and sends both storageId and intentId to the safety check. It starts the runs only after a clean result. Keep .ci/autonomy.sh from above and replace .circleci/config.yml with the configuration you need.

iOS

iOS testing is coming soon. Keep this for when it's available. Until then, Autonomy doesn't start runs on iOS.

Build the simulator .app on macOS, zip it so permissions and symlinks survive, then pass the zip to a Linux job with persist_to_workspace and attach_workspace. The upload and the wait use Linux credits.

This example assumes a native Xcode project at ios/MyApp.xcodeproj, a shared MyApp scheme and a MyApp.app product. Replace those names with your own. For an Xcode workspace, replace -project with -workspace and its path; keep your dependency installation steps before xcodebuild. Don't upload a device .ipa.

CircleCI currently documents 27.0.0 on m4pro.medium. macOS is available on the Free plan within its credit allowance; check your balance before running builds. circleci-v0 local execute can't run this macOS job.

.circleci/config.yml
version: 2.1

jobs:
  build-ios:
    macos:
      xcode: "27.0.0"
    resource_class: m4pro.medium
    steps:
      - checkout
      - run:
          name: Build and zip the simulator app
          command: |
            set -euo pipefail
            xcodebuild \
              -project ios/MyApp.xcodeproj \
              -scheme MyApp \
              -configuration Release \
              -sdk iphonesimulator \
              -destination 'generic/platform=iOS Simulator' \
              -derivedDataPath ios/build \
              CODE_SIGNING_ALLOWED=NO build
            mkdir -p /tmp/autonomy-ios
            (cd ios/build/Build/Products/Release-iphonesimulator && \
              zip -qry /tmp/autonomy-ios/MyApp.app.zip MyApp.app)
      - persist_to_workspace:
          root: /tmp/autonomy-ios
          paths:
            - MyApp.app.zip

  autonomy-ios:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - attach_workspace:
          at: /tmp/autonomy-ios
      - run:
          name: Upload and run Autonomy
          no_output_timeout: 30m
          command: |
            set -euo pipefail
            set +x
            export ARTIFACT_PATH=/tmp/autonomy-ios/MyApp.app.zip
            export AUTONOMY_BRANCH="$CIRCLE_BRANCH"
            export AUTONOMY_COMMIT_SHA="$CIRCLE_SHA1"
            export AUTONOMY_REQUEST_ID="circleci:$CIRCLE_WORKFLOW_ID:$CIRCLE_BUILD_NUM"
            bash .ci/autonomy.sh ios

workflows:
  ios-qa:
    jobs:
      - build-ios:
          filters:
            branches:
              only: staging
      - autonomy-ios:
          requires:
            - build-ios
          filters:
            branches:
              only: staging

Android

The Linux Android image includes the SDK, Java, curl and jq. This example assumes a native Gradle project under android/, its committed executable gradlew, and the app module's debug APK. Change the Gradle task and APK path for your module or product flavor. Keep your dependency preparation if the app also needs JavaScript or other build tools. An .aab works too.

.circleci/config.yml
version: 2.1

jobs:
  autonomy-android:
    docker:
      - image: cimg/android:2026.08
    resource_class: medium
    steps:
      - checkout
      - run:
          name: Build the debug APK
          command: |
            set -euo pipefail
            cd android
            ./gradlew --no-daemon :app:assembleDebug
      - run:
          name: Upload and run Autonomy
          no_output_timeout: 30m
          command: |
            set -euo pipefail
            set +x
            export ARTIFACT_PATH=android/app/build/outputs/apk/debug/app-debug.apk
            export AUTONOMY_BRANCH="$CIRCLE_BRANCH"
            export AUTONOMY_COMMIT_SHA="$CIRCLE_SHA1"
            export AUTONOMY_REQUEST_ID="circleci:$CIRCLE_WORKFLOW_ID:$CIRCLE_BUILD_NUM"
            bash .ci/autonomy.sh android

workflows:
  android-qa:
    jobs:
      - autonomy-android:
          filters:
            branches:
              only: staging

Gate the pipeline on the result

The helper starts the runs with POST /api/v1/run.trigger and prints their IDs. A test plan starts one run per test case. It then follows each run with POST /api/v1/run.get, sending back the last cursor with waitMs: 25000, so each call waits until the run changes. A response with timedOut: true only means nothing changed yet. See Runs and evidence.

Run resultThe jobWhat the log says
PassedPasses, if every other run passed too. A flaky pass counts as passed.Passed
FailedFails.Open the run to see which step failed and why, including a Runner problem.
Couldn't startFails.The environment couldn't be tested, for example the build didn't install or a value was missing. This says nothing about your app; fix the cause and run again.
CancelledFails.Cancelled

API errors, a build that fails the safety check and running out of wait time also fail the job. AUTONOMY_WAIT_SECONDS is the total wait time for all runs, 1,200 seconds by default. Keep the CI job's time limit longer than that plus build and upload time. Stopping the CI job doesn't cancel runs that already started.

AUTONOMY_REQUEST_ID, with the platform added, becomes the idempotencyKey of run.trigger: if the same job attempt sends the request again, it gets the same runs back instead of new ones. Give every job attempt its own ID. The part before the first colon is sent as the deployment provider, and the branch and commit go with the runs. The full request and response shapes are in Operations.

Raise no_output_timeout together with AUTONOMY_WAIT_SECONDS: CircleCI's default silence limit can otherwise stop a healthy wait. Put release jobs after the Autonomy job with requires, so a run that didn't pass blocks promotion.

Branch and event strategy

These examples run on staging. Widen filters only to trusted branches. Use a small smoke plan for reviewed preview changes and larger plans for release or scheduled pipelines. Set up push, pull request or scheduled triggers in the CircleCI project for your connected VCS; branch filters alone don't create them.

Keep Pass secrets to builds from forked pull requests off where that setting exists. Review untrusted code before running it in a job with the organization API key, and limit any shared context to the intended projects and trusted people. The request ID combines the workflow ID and job number, so a new job attempt starts new runs, while a repeated request within one attempt gets the same runs back.

Vendor references

Checked against CircleCI's public documentation on 20 September 2026:

Didn’t find it?

On this page