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

# Result Statuses and Gating

> When a test result is finished, and how to decide whether it passed — for CI pipelines and scripts that poll the API

export const CopyPageButton = () => {
  if (typeof window !== 'undefined') {
    setTimeout(function () {
      if (document.getElementById('ck-tools')) return;
      var anchor = document.getElementById('content-area') || document.querySelector('.mdx-content');
      if (!anchor) return;
      if (!document.getElementById('ck-style')) {
        var s = document.createElement('style');
        s.id = 'ck-style';
        s.textContent = '#ck-tools{position:absolute;top:6px;right:0;z-index:100;font-family:inherit;}' + '.ck-row{display:inline-flex;align-items:stretch;border:1px solid rgba(0,0,0,0.15);border-radius:8px;overflow:hidden;background:#fff;}' + ':root.dark .ck-row{background:rgba(255,255,255,0.06);border-color:rgba(255,255,255,0.12);}' + '.ck-btn{padding:5px 12px;border:none;background:none;cursor:pointer;font-size:13px;font-weight:500;font-family:inherit;color:#374151;}' + ':root.dark .ck-btn{color:#d1d5db;}' + '.ck-btn:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-btn:hover{background:rgba(255,255,255,0.06);}' + '.ck-chevron{padding:5px 8px;border:none;background:none;cursor:pointer;font-size:14px;font-family:inherit;color:#374151;}' + ':root.dark .ck-chevron{color:#d1d5db;}' + '.ck-chevron:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-chevron:hover{background:rgba(255,255,255,0.06);}' + '.ck-divider{width:1px;background:rgba(0,0,0,0.12);flex-shrink:0;}' + ':root.dark .ck-divider{background:rgba(255,255,255,0.12);}' + '.ck-dd{position:absolute;top:calc(100% + 4px);right:0;min-width:180px;background:#fff;border:1px solid rgba(0,0,0,0.12);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);padding:4px;display:none;z-index:200;}' + ':root.dark .ck-dd{background:#1f2937;border-color:rgba(255,255,255,0.1);box-shadow:0 4px 16px rgba(0,0,0,0.35);}' + '.ck-item{display:block;width:100%;padding:7px 12px;border:none;background:none;border-radius:6px;cursor:pointer;font-size:13px;font-family:inherit;text-align:left;color:#374151;}' + ':root.dark .ck-item{color:#d1d5db;}' + '.ck-item:hover{background:rgba(0,0,0,0.05);}' + ':root.dark .ck-item:hover{background:rgba(255,255,255,0.07);}';
        document.head.appendChild(s);
      }
      var wrap = document.createElement('div');
      wrap.id = 'ck-tools';
      var row = document.createElement('div');
      row.className = 'ck-row';
      var mainBtn = document.createElement('button');
      mainBtn.className = 'ck-btn';
      mainBtn.textContent = 'Copy page';
      var divider = document.createElement('span');
      divider.className = 'ck-divider';
      var chevron = document.createElement('button');
      chevron.className = 'ck-chevron';
      chevron.textContent = '▾';
      var dd = document.createElement('div');
      dd.className = 'ck-dd';
      function closeDD() {
        dd.style.display = 'none';
      }
      function openDD() {
        dd.style.display = 'block';
      }
      chevron.onclick = function (e) {
        e.stopPropagation();
        if (dd.style.display === 'block') {
          closeDD();
        } else {
          openDD();
        }
      };
      document.addEventListener('click', function (e) {
        if (!e.target.closest('#ck-tools')) {
          closeDD();
        }
      });
      document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') {
          closeDD();
        }
      });
      function makeItem(label, fn) {
        var b = document.createElement('button');
        b.className = 'ck-item';
        b.textContent = label;
        b.onclick = function () {
          fn();
          closeDD();
        };
        return b;
      }
      function getMarkdown() {
        var walk = function (node) {
          if (!node) return '';
          if (node.nodeType === 3) return node.textContent || '';
          if (node.nodeType !== 1) return '';
          var tag = node.tagName.toLowerCase();
          var skip = ['script', 'style', 'svg', 'noscript', 'button', 'iframe'];
          if (skip.indexOf(tag) !== -1) return '';
          if (node.id === 'ck-tools') return '';
          var ch = Array.from(node.childNodes).map(walk).join('');
          if (tag === 'h1') return '\n# ' + ch.trim() + '\n\n';
          if (tag === 'h2') return '\n## ' + ch.trim() + '\n\n';
          if (tag === 'h3') return '\n### ' + ch.trim() + '\n\n';
          if (tag === 'p') return '\n' + ch.trim() + '\n\n';
          if (tag === 'pre') return '\n```\n' + node.textContent.trim() + '\n```\n\n';
          if (tag === 'li') return '- ' + ch.trim() + '\n';
          if (tag === 'code') return '`' + ch.trim() + '`';
          return ch;
        };
        var content = document.querySelector('.mdx-content') || document.getElementById('content-area') || document.body;
        return walk(content).replace(/\n\n\n+/g, '\n\n').trim();
      }
      function copyMd() {
        var md = getMarkdown();
        navigator.clipboard.writeText(md).then(function () {
          mainBtn.textContent = 'Copied!';
          setTimeout(function () {
            mainBtn.textContent = 'Copy page';
          }, 2000);
        });
      }
      function viewMd() {
        var md = getMarkdown();
        var safe = md.split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;');
        var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:monospace;max-width:860px;margin:40px auto;padding:0 24px;line-height:1.7;white-space:pre-wrap;word-wrap:break-word}</style></head><body>' + safe + '</body></html>';
        window.open(URL.createObjectURL(new Blob([html], {
          type: 'text/html'
        })), '_blank');
      }
      function openClaude() {
        var prompt = 'Can you read this Cekura docs page ' + window.location.href + ' so I can ask you questions?';
        window.open('https://claude.ai/new?q=' + encodeURIComponent(prompt), '_blank');
      }
      mainBtn.onclick = copyMd;
      dd.appendChild(makeItem('Copy page', copyMd));
      dd.appendChild(makeItem('View as Markdown', viewMd));
      dd.appendChild(makeItem('Open in Claude', openClaude));
      row.appendChild(mainBtn);
      row.appendChild(divider);
      row.appendChild(chevron);
      wrap.appendChild(row);
      wrap.appendChild(dd);
      anchor.style.position = 'relative';
      anchor.insertBefore(wrap, anchor.firstChild);
    }, 50);
  }
  return null;
};

<CopyPageButton />

Every way of starting evaluators — the dashboard, the run endpoints, [Tests-as-Code](/documentation/guides/testing-agents/tests-as-code), the CLI, the SDK, the CI plugins — creates one **result** that groups the **runs** it started. Starting a run returns as soon as the runs are queued: nothing has been dialled yet. A pipeline that stops there passes while the agent is broken, so poll the result until it is finished, then decide pass or fail from its counts.

This page is the reference for both steps. The [GitHub Actions](/documentation/guides/github-actions-ci-cd), [GitLab](/documentation/guides/gitlab-ci-cd) and [Bitbucket](/documentation/guides/bitbucket-pipelines-ci-cd) integrations and `cekura run start --wait` already follow it; use it when you call the API yourself.

## The result ID

Every run endpoint responds with the result object. Its `id` is the result ID you poll — there is no separate `result_id` or `run_id` key.

```json theme={null}
{
  "id": 9001,
  "status": "running",
  "runs": [{ "id": 501, "status": "pending" }, { "id": 502, "status": "pending" }],
  "...": "..."
}
```

## Result statuses

A result stays in a non-terminal status until **every one of its runs has ended**. It then settles at exactly one terminal status and does not change again.

| Status                                                                      | Terminal | Meaning                                                                                                                                                         |
| --------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`, `running`, `in_queue`, `scaling_up`, `in_progress`, `evaluating` | No       | Runs are waiting for capacity, dialling, in conversation, or being scored. Keep polling.                                                                        |
| `completed`                                                                 | **Yes**  | Every run has ended and **at least one run completed**. This says nothing about whether runs passed — some may have failed their checks, errored, or timed out. |
| `failed`                                                                    | **Yes**  | Every run has ended, none completed, and not all of them timed out — typically connection or infrastructure failures, not failed metrics.                       |
| `timeout`                                                                   | **Yes**  | Every run timed out.                                                                                                                                            |
| `cancelled`                                                                 | **Yes**  | The result was cancelled.                                                                                                                                       |

Poll until the status is one of `completed`, `failed`, `timeout` or `cancelled`. A loop that stops only on `completed` or `failed` keeps polling a timed-out result until its own deadline.

## Deciding pass or fail

`status` alone is not a verdict. Read these fields from the finished result:

| Field                | What it counts                                       |
| -------------------- | ---------------------------------------------------- |
| `total_runs_count`   | Every run in the result                              |
| `success_runs_count` | Runs that completed **and** passed their evaluation  |
| `failed_runs_count`  | Runs that completed but failed their evaluation      |
| `success_rate`       | Percentage of runs that passed, on a **0–100** scale |

A run that errored, never connected, timed out or was cancelled is counted in `total_runs_count` but in **neither** `success_runs_count` nor `failed_runs_count`. So `failed_runs_count == 0` does not mean every run passed.

**A result passes when:**

```text theme={null}
status == "completed"  and  total_runs_count > 0  and  success_runs_count == total_runs_count
```

To allow some failures, compare `success_rate` (0–100) against your threshold instead of requiring every run — but still fail on any status other than `completed`.

Poll every 15–30 seconds. Individual runs are listed under `runs` in the result, keyed by run ID; each has `status`, `success` and, when it could not connect, `error_message`.

<Note>
  The AI summary of a result's failures (`ai_summary`) is written shortly *after* the result reaches a terminal status. If you post it somewhere, wait while `ai_summary_status` is `pending`; it becomes `completed`, or `skipped_no_failures` when every run passed.
</Note>

## Examples

<Tabs>
  <Tab title="curl + jq">
    ```bash theme={null}
    API=https://api.cekura.ai
    AUTH="X-CEKURA-API-KEY: $CEKURA_API_KEY"

    RESULT_ID=$(curl -sf -X POST "$API/test_framework/v1/scenarios/run_scenarios/" \
      -H "$AUTH" -H "Content-Type: application/json" \
      -d '{"agent_id": 123, "scenarios": [1, 2, 3]}' | jq -r .id)

    while :; do
      RESULT=$(curl -sf "$API/test_framework/v1/results/$RESULT_ID/" -H "$AUTH")
      case "$(jq -r .status <<<"$RESULT")" in
        completed|failed|timeout|cancelled) break ;;
      esac
      sleep 30
    done

    jq -r '"\(.success_runs_count)/\(.total_runs_count) runs passed (status: \(.status))"' <<<"$RESULT"
    jq -e '.status == "completed" and .total_runs_count > 0
           and .success_runs_count == .total_runs_count' <<<"$RESULT" >/dev/null
    ```

    The last command exits non-zero unless every run passed, which fails the job.
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    import sys
    import time

    from cekura import Cekura

    TERMINAL = {"completed", "failed", "timeout", "cancelled"}

    client = Cekura()  # reads CEKURA_API_KEY
    result_id = client.scenarios.run(agent_id=123, scenarios=[1, 2, 3])["id"]

    while True:
        result = client.results.get(result_id)
        if result["status"] in TERMINAL:
            break
        time.sleep(30)

    total, passed = result["total_runs_count"], result["success_runs_count"]
    print(f"{passed}/{total} runs passed (status: {result['status']})")
    if result["status"] != "completed" or total == 0 or passed != total:
        sys.exit(1)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    cekura run start --agent-id 123 --scenario-ids 1,2,3 \
      --wait --fail-on-threshold 1.0
    ```

    `--wait` polls to a terminal status and `--fail-on-threshold` takes a pass rate from `0.0` to `1.0`; `1.0` requires every run to pass. The command exits `1` when the threshold is not met and `2` when it cannot start the run or the wait times out (`--timeout`, default 3600 seconds).
  </Tab>
</Tabs>

The same polling applies to a [Tests-as-Code](/documentation/guides/testing-agents/tests-as-code) run: `POST /test_framework/v1/scenarios/run_scenarios_json/` returns the same result object.
