> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.nunu.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Start a Run

> Start a new test run using a multiplayer test, a test plan, or ephemeral instructions.

<Info>
  **Permission Required**: `project:operate-runs`
</Info>

Start a new test run using a multiplayer test, a test plan, or ephemeral instructions.

## Request Body

<Note>
  Provide exactly one of `multiplayer_test_id`, `test_plan_id`, or `instructions`:

  * **`multiplayer_test_id`**: run an existing test
  * **`test_plan_id`**: run an existing test plan
  * **`instructions`**: run an ephemeral (one-off) test without creating a test case first
</Note>

<ParamField body="multiplayer_test_id" type="string">
  UUID of the test to run. Mutually exclusive with `test_plan_id` and `instructions`.
</ParamField>

<ParamField body="test_plan_id" type="string">
  UUID of the test plan to run. Mutually exclusive with `multiplayer_test_id`
  and `instructions`. This endpoint starts test plans with a single deployment
  config; to supply per-slot configs for a cross-platform plan, use
  [Start Test Plan Run](/api-reference/test-plans/start-test-plan-run) instead.
</ParamField>

<ParamField body="instructions" type="string">
  Test instructions to execute as an ephemeral (one-off) test. Mutually exclusive with `multiplayer_test_id` and `test_plan_id`.
</ParamField>

<ParamField body="deployment_config_id" type="string">
  UUID of the deployment configuration, used by every player that has no
  `player_deployment_configs` entry. Required, except for multiplayer tests
  where `player_deployment_configs` covers every player.
</ParamField>

<ParamField body="name" type="string">
  Display name for the run. Only applies when using `instructions`. Defaults to `"[api] ephemeral test"`.
</ParamField>

<ParamField body="build_id" type="string">
  UUID of a build from [Build Storage](/api-reference/builds/list-builds) to use for this run. Requires `deployment_config_id`. See [Using Build Storage Builds](#using-build-storage-builds) below.
</ParamField>

<ParamField body="player_deployment_configs" type="object">
  Per-player deployment configs for cross-platform multiplayer tests, keyed by
  player number (`"1"`, `"2"`, and so on). Only applies with `multiplayer_test_id`,
  and keys must be within the test's player count. The map does not need to
  cover every player: any player without an entry falls back to the default
  `deployment_config_id`. Omitting `deployment_config_id` is only allowed when
  every player has an entry; a partial map without a default returns a `400`
  error listing the missing players. Private device configs are allowed, but
  every player on a private device needs its own device; if the configs cannot
  give each player a distinct device, the request returns a `400` error.

  <Expandable title="entry properties">
    <ParamField body="player_deployment_configs.{player}.deployment_config_id" type="string" required>
      Deployment config to use for this player.
    </ParamField>

    <ParamField body="player_deployment_configs.{player}.build_id" type="string">
      Optional Build Storage build for this player (build-storage configs only).
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="agent_config_id" type="string">
  UUID of an agent configuration to use. Applies to all request variants.
</ParamField>

<ParamField body="additional_tags" type="string[]">
  Tags to add to the run. When using `instructions` (ephemeral), `"api"` is automatically prepended to the provided tags.
</ParamField>

<ParamField body="retries" type="integer" default="0">
  Number of times to retry the test if it fails. Only applies when using `multiplayer_test_id`. Must be between 0 and 10.
</ParamField>

<ParamField body="instances" type="integer" default="1">
  Number of parallel instances to run. Only applies when using `multiplayer_test_id`. Must be between 1 and 10.
</ParamField>

<ParamField body="render_templates" type="boolean" default="false">
  Enable template rendering
</ParamField>

<ParamField body="template_data" type="object">
  Key-value pairs for template variables
</ParamField>

<ParamField body="options" type="object">
  Per-run override knobs.

  <Note>
    The test time budget is **not** overridable here. It lives on the test version itself (`min_seconds` / `max_seconds` on the test body). Edit the test, or create a new version, when you want to change the budget. See [Create a Test](/api-reference/tests/create-test) for the per-framework defaults and rules.
  </Note>

  <Expandable title="options properties">
    <ParamField body="options.performance_monitoring" type="boolean">
      Enable performance data collection
    </ParamField>

    <ParamField body="options.mobile_auto_grant_permissions" type="boolean">
      Auto-grant app permissions on mobile
    </ParamField>

    <ParamField body="options.mobile_geolocation" type="string">
      IP geolocation proxy location (e.g., `"United States"`, `"London"`, `"San Francisco, California (US)"`). Routes device traffic through a proxy in the specified region.
    </ParamField>

    <ParamField body="options.mobile_gpslocation" type="string | object">
      GPS coordinates to report to the app. Use `"auto"` to automatically use coordinates matching the selected `mobile_geolocation`, or provide explicit coordinates: `{ "lat": "37.7749", "long": "-122.4194" }`. Note: `"auto"` requires `mobile_geolocation` to be set.
    </ParamField>
  </Expandable>
</ParamField>

## Response

Returns `201 Created` on success. The body contains the started-run data
directly (no `{ ok, status, message, data }` envelope).

<ResponseField name="job_ids" type="string[]" required>
  Array of job IDs created. For a single test, this contains one job. For a
  test plan, this contains one job per test in the plan. Use with
  [Get Job Details](/api-reference/jobs/get-job) to poll for completion.
</ResponseField>

<ResponseField name="build_id" type="string">
  Build ID used for the run. Returned for multiplayer test and test plan runs.
</ResponseField>

<ResponseField name="test_id" type="string">
  UUID of the created ephemeral test. Only returned for ephemeral (`instructions`) runs.
</ResponseField>

<ResponseField name="message" type="string">
  Optional status message returned for multiplayer test and test plan runs.
</ResponseField>

On failure, returns `{ "error": "<message>" }` with an appropriate HTTP status
(`400`, `401`, `403`, `404`, `500`).

<RequestExample>
  ```bash cURL (Test) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
      "deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
      "additional_tags": ["ci", "pr-123"]
    }'
  ```

  ```bash cURL (Ephemeral) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
      "instructions": "Navigate to the main menu and verify all buttons are clickable",
      "name": "PR #123 Smoke Test",
      "additional_tags": ["pr-123", "smoke"]
    }'
  ```

  ```bash cURL (Cross-platform) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
      "deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
      "player_deployment_configs": {
        "2": { "deployment_config_id": "9a1b2c3d-4e5f-6789-a0b1-c2d3e4f5a6b7" }
      }
    }'
  ```

  ```typescript JavaScript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  const response = await fetch("https://nunu.ai/api/v1/project/your-project-id/runs", {
    method: "POST",
    headers: {
      "X-Api-Key": process.env.NUNU_API_TOKEN,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      multiplayer_test_id: "789e0123-e89b-12d3-a456-426614174000",
      deployment_config_id: "456e7890-e89b-12d3-a456-426614174000",
      build_id: "123e4567-e89b-12d3-a456-426614174000", // optional: use specific build
      additional_tags: ["ci", "pr-123"],
    }),
  });
  const data = await response.json();
  ```

  ```python Python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  import requests
  import os

  response = requests.post(
      "https://nunu.ai/api/v1/project/your-project-id/runs",
      headers={
          "X-Api-Key": os.environ["NUNU_API_TOKEN"],
          "Content-Type": "application/json"
      },
      json={
          "multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
          "deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
          "additional_tags": ["ci", "pr-123"]
      }
  )
  data = response.json()
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Success (Test / Test Plan) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  {
    "message": "successfully queued test!",
    "build_id": "f14fa698-ee3e-4a3d-935d-bdbeb874ad25",
    "job_ids": [
      "cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
    ]
  }
  ```

  ```json 201 Success (Ephemeral) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  {
    "test_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "job_ids": [
      "cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
    ]
  }
  ```

  ```json 404 Not Found theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  {
    "error": "multiplayer test not found"
  }
  ```
</ResponseExample>

***

## Using Build Storage Builds

You can run tests against a specific build from [Build Storage](/api-reference/builds/list-builds) by providing the `build_id` parameter.

<Warning>
  The `build_id` parameter only works with deployment configs that use Build Storage as their build source. If the deployment config uses a different build source (e.g., direct URL), the request will return a `400` error.
</Warning>

### Example: Upload and Test

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
# 1. Upload a new build (see Build Storage docs)
BUILD_ID="123e4567-e89b-12d3-a456-426614174000"

# 2. Start a test using that build
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
  -H "X-Api-Key: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
    "deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
    "build_id": "123e4567-e89b-12d3-a456-426614174000",
    "additional_tags": ["ci", "build-test"]
  }'
```

***

## Complete CI/CD Example

Here's a complete example that starts a test and polls for completion:

<CodeGroup>
  ```typescript JavaScript/TypeScript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  const API_TOKEN = process.env.NUNU_API_TOKEN;
  const PROJECT_ID = process.env.NUNU_PROJECT_ID;
  const BASE_URL = `https://nunu.ai/api/v1/project/${PROJECT_ID}`;

  async function startAndWaitForTest(
    testId: string,
    deploymentConfigId: string
  ): Promise<any> {
    // Start the test
    const startResponse = await fetch(`${BASE_URL}/runs`, {
      method: "POST",
      headers: {
        "X-Api-Key": API_TOKEN,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        multiplayer_test_id: testId,
        deployment_config_id: deploymentConfigId,
      }),
    });

    const startResult = await startResponse.json();

    if (!startResponse.ok) {
      throw new Error(`Failed to start test: ${startResult.error}`);
    }

    const jobId = startResult.job_ids[0];
    console.log(`Test started with job: ${jobId}`);

    // Poll job for completion
    while (true) {
      const jobResponse = await fetch(`${BASE_URL}/jobs/${jobId}`, {
        headers: { "X-Api-Key": API_TOKEN },
      });

      const job = await jobResponse.json();

      if (job.status === "COMPLETED") {
        const result = job.runs[0]?.result ?? "unknown";
        console.log(`Test completed with result: ${result}`);
        return job;
      }

      console.log(`Test still running... (status: ${job.status})`);
      await new Promise((resolve) => setTimeout(resolve, 30000));
    }
  }
  ```

  ```python Python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  import requests
  import os
  import time

  API_TOKEN = os.environ["NUNU_API_TOKEN"]
  PROJECT_ID = os.environ["NUNU_PROJECT_ID"]
  BASE_URL = f"https://nunu.ai/api/v1/project/{PROJECT_ID}"

  def start_and_wait_for_test(test_id: str, deployment_config_id: str) -> dict:
      headers = {"X-Api-Key": API_TOKEN}

      # Start the test
      start_response = requests.post(
          f"{BASE_URL}/runs",
          headers={**headers, "Content-Type": "application/json"},
          json={
              "multiplayer_test_id": test_id,
              "deployment_config_id": deployment_config_id
          }
      )

      start_result = start_response.json()

      if not start_response.ok:
          raise Exception(f"Failed to start test: {start_result.get('error')}")

      job_id = start_result["job_ids"][0]
      print(f"Test started with job: {job_id}")

      # Poll job for completion
      while True:
          job_response = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=headers)
          job = job_response.json()

          if job["status"] == "COMPLETED":
              result = job["runs"][0]["result"] if job.get("runs") else "unknown"
              print(f"Test completed with result: {result}")
              return job

          print(f"Test still running... (status: {job['status']})")
          time.sleep(30)
  ```

  ```bash Shell Script theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  #!/bin/bash
  set -e

  API_TOKEN="${NUNU_API_TOKEN}"
  TEST_ID="${NUNU_TEST_ID}"
  DEPLOYMENT_ID="${NUNU_DEPLOYMENT_ID}"
  PROJECT_ID="${NUNU_PROJECT_ID}"
  BASE_URL="https://nunu.ai/api/v1/project/${PROJECT_ID}"

  # Start the test
  START_RESPONSE=$(curl -s -X POST "${BASE_URL}/runs" \
    -H "X-Api-Key: ${API_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
      \"multiplayer_test_id\": \"${TEST_ID}\",
      \"deployment_config_id\": \"${DEPLOYMENT_ID}\"
    }")

  JOB_ID=$(echo "$START_RESPONSE" | jq -r '.job_ids[0]')

  if [ "$JOB_ID" == "null" ]; then
    echo "Failed to start test: $START_RESPONSE"
    exit 1
  fi

  echo "Test started with job: $JOB_ID"

  # Poll job for completion
  while true; do
    JOB=$(curl -s "${BASE_URL}/jobs/${JOB_ID}" \
      -H "X-Api-Key: ${API_TOKEN}")

    STATUS=$(echo "$JOB" | jq -r '.status')

    if [ "$STATUS" == "COMPLETED" ]; then
      RESULT=$(echo "$JOB" | jq -r '.runs[0].result')
      echo "Test completed with result: $RESULT"
      [ "$RESULT" != "SUCCESS" ] && exit 1
      exit 0
    fi

    echo "Test running... (status: $STATUS)"
    sleep 30
  done
  ```
</CodeGroup>


## OpenAPI

````yaml POST /project/{projectId}/runs
openapi: 3.1.0
info:
  title: nunu.ai API
  description: >-
    The nunu.ai Public API allows you to programmatically interact with your
    projects, manage test runs, and upload builds.
  version: 1.0.0
servers:
  - url: https://nunu.ai/api/v1
security:
  - apiKeyAuth: []
paths:
  /project/{projectId}/runs:
    parameters:
      - name: projectId
        in: path
        required: true
        schema:
          type: string
        description: >-
          The project ID. You can copy it from the project settings page or the
          project URL in the dashboard.
    post:
      tags:
        - Runs
      summary: Start a Run
      description: >-
        Start a new test run using a multiplayer test, a test plan, or ephemeral
        instructions.
      operationId: startRun
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartRunRequest'
      responses:
        '201':
          description: Run started successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartRunResponse'
        '400':
          description: Bad Request - Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Test or deployment config not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    StartRunRequest:
      type: object
      anyOf:
        - required:
            - deployment_config_id
        - required:
            - multiplayer_test_id
            - player_deployment_configs
      dependentRequired:
        build_id:
          - deployment_config_id
      properties:
        multiplayer_test_id:
          type: string
          description: >-
            UUID of the test to run (provide this OR test_plan_id OR
            instructions)
        test_plan_id:
          type: string
          description: >-
            UUID of the test plan to run (provide this OR multiplayer_test_id OR
            instructions). This endpoint starts test plans with a single
            deployment config; to supply per-slot configs for a cross-platform
            plan, use POST /test-plans/{testPlanId}/runs instead.
        instructions:
          type: string
          description: >-
            Test instructions to execute as an ephemeral (one-off) test.
            Mutually exclusive with multiplayer_test_id and test_plan_id.
        deployment_config_id:
          type: string
          description: >-
            UUID of the deployment configuration, used by every player that has
            no `player_deployment_configs` entry. Required, except for
            multiplayer tests where `player_deployment_configs` covers every
            player.
        build_id:
          type: string
          format: uuid
          description: >-
            UUID of a build from Build Storage to use for this run. Only works
            with build-storage deployment configs, and requires
            `deployment_config_id`.
        player_deployment_configs:
          type: object
          description: >-
            Per-player deployment configs for cross-platform multiplayer tests,
            keyed by player number ("1", "2", and so on). Only applies with
            `multiplayer_test_id`. The map does not need to cover every player:
            any player without an entry falls back to the default
            `deployment_config_id`. Omitting `deployment_config_id` is only
            allowed when every player has an entry; a partial map without a
            default returns a 400 error. Private device configs are allowed, but
            every player on a private device needs its own device; if the
            configs cannot give each player a distinct device, the request
            returns a 400 error.
          additionalProperties:
            type: object
            required:
              - deployment_config_id
            properties:
              deployment_config_id:
                type: string
                format: uuid
                description: Deployment config to use for this player
              build_id:
                type: string
                format: uuid
                description: >-
                  Optional Build Storage build for this player (build-storage
                  configs only)
        name:
          type: string
          description: >-
            Display name for the run. Only applies when using instructions.
            Defaults to '[api] ephemeral test'.
        agent_config_id:
          type: string
          format: uuid
          description: UUID of an agent configuration to use
        additional_tags:
          type: array
          items:
            type: string
          description: >-
            Tags to add to the run. When using instructions (ephemeral), the tag
            'api' is automatically prepended.
        retries:
          type: integer
          default: 0
          minimum: 0
          maximum: 10
          description: >-
            Number of times to retry the test if it fails. Only applies when
            using multiplayer_test_id.
        instances:
          type: integer
          default: 1
          minimum: 1
          maximum: 10
          description: >-
            Number of parallel instances to run. Only applies when using
            multiplayer_test_id.
        render_templates:
          type: boolean
          default: false
          description: Enable template rendering
        template_data:
          type: object
          description: Key-value pairs for template variables
        options:
          $ref: '#/components/schemas/RunOptions'
    StartRunResponse:
      type: object
      description: >-
        Response body for POST /runs and POST /test-plans/{testPlanId}/runs.
        Returned with HTTP 201 on success. The body contains the started-run
        data directly (no { ok, status, message, data } envelope).
      properties:
        job_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            Array of job IDs created. Single test returns 1 job; test plan
            returns one job per test.
        build_id:
          type: string
          format: uuid
          description: >-
            Build ID used for the run. Returned for multiplayer test and test
            plan runs.
        test_id:
          type: string
          format: uuid
          description: >-
            UUID of the created ephemeral test. Only returned for ephemeral
            (instructions) runs.
        test_plan_execution_id:
          type: string
          format: uuid
          description: Test plan execution ID. Returned for test plan runs.
        slot_build_ids:
          type: object
          additionalProperties:
            type: string
            format: uuid
          description: >-
            For cross-platform test plan runs: the build pinned for each extra
            slot, keyed by slot index. Only present when slot configs resolved
            to builds.
        message:
          type: string
          description: >-
            Optional status message returned for multiplayer test and test plan
            runs.
    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: object
    RunOptions:
      type: object
      description: >-
        Per-run override knobs. The time budget is intentionally not overridable
        here; it lives on the test version itself (see `min_seconds` /
        `max_seconds` on the test body), so callers should edit the test (or
        create a new version) when they want to change the budget.
      properties:
        performance_monitoring:
          type: boolean
          description: Enable performance data collection
        mobile_auto_grant_permissions:
          type: boolean
          description: Auto-grant app permissions on mobile
        mobile_geolocation:
          type: string
          description: IP geolocation proxy location
        mobile_gpslocation:
          type: string|object
          description: >-
            GPS location proxy location, either 'auto' or an object with 'lat'
            and 'long' string properties. Note: 'auto' requires
            `mobile_geolocation` to be set
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: API key for authentication

````