Testland
Browse all skills & agents

testrail-case-management

Author and manage test cases in TestRail via REST API v2 - create cases, organise into suites + sections, update steps + expected results, bulk import from CSV/JSON, set automation status, link to references (Jira / requirements). Covers the Steps / Text / Exploratory templates, custom-field discovery (`get_case_fields`), and pagination on `get_cases`. Use for pre-execution case authoring and repository management. Do NOT use for submitting test-run results (pass/fail, status updates): posting results via add_results_for_cases is a separate post-execution concern.

Install with skills.sh (any agent)

npx skills add testland/qa --skill testrail-case-management
View source

testrail-case-management

Overview

TestRail organises tests as cases inside sections inside suites inside projects. The API v2 covers full CRUD on each plus templates, custom fields, references, and types. Authentication is HTTP Basic with email + API key per the TestRail support docs (support.testrail.com/hc/en-us/articles/7077871398036-Cases - Cloudflare-protected, cite by stable URL).

For the canonical anatomy this skill operates on, see test-case-anatomy-reference.

Differentiation vs testrail-integration: that skill posts test-run results via add_results_for_cases. This one operates on the case repository - create, update, organise, traceability - a strictly upstream concern.

When to use

  • Creating cases from a spec / requirement / acceptance criterion.
  • Bulk-importing legacy cases from CSV / Excel.
  • Migrating between TestRail instances or to/from another TCM.
  • Programmatic case updates (mass-edit type, priority, tags).
  • Case-repository quality scans.

Authoring

Authentication

Per TestRail API docs (Cloudflare-protected, support.testrail.com):

export TR_BASE="https://your-tenant.testrail.io"
export TR_EMAIL="you@company.com"
export TR_KEY="<api-key-from-user-profile>"
import requests, base64, os, json

auth = base64.b64encode(
    f"{os.environ['TR_EMAIL']}:{os.environ['TR_KEY']}".encode()
).decode()
HEADERS = {
    "Authorization": f"Basic {auth}",
    "Content-Type": "application/json",
}
BASE = os.environ["TR_BASE"]

def api(path, method="GET", body=None):
    url = f"{BASE}/index.php?/api/v2/{path.lstrip('/')}"
    r = requests.request(method, url, headers=HEADERS,
                         data=json.dumps(body) if body else None)
    r.raise_for_status()
    return r.json()

Create a case

POST /index.php?/api/v2/add_case/:section_id:

def create_case(section_id, title, template_id=1, type_id=1, priority_id=2,
                preconditions=None, steps=None, refs=None):
    """
    template_id: 1=Steps, 2=Text, 3=Exploratory
    type_id: per project - discover via get_case_types
    priority_id: 1=Low, 2=Medium, 3=High, 4=Critical (default project enum)
    steps: list of {"content": "Action", "expected": "Outcome"}
    refs: comma-separated requirement IDs (e.g., "REQ-123,REQ-124")
    """
    body = {
        "title": title,
        "template_id": template_id,
        "type_id": type_id,
        "priority_id": priority_id,
    }
    if preconditions:
        body["custom_preconds"] = preconditions
    if steps and template_id == 1:
        body["custom_steps_separated"] = steps
    if refs:
        body["refs"] = refs
    return api(f"add_case/{section_id}", method="POST", body=body)

Steps template (template_id=1) example

steps = [
    {"content": "Navigate to /login", "expected": "Login form rendered"},
    {"content": "Enter alice@example.com + correct password",
     "expected": "Submit button enabled"},
    {"content": "Click Submit",
     "expected": "Redirected to /dashboard within 2 s"},
]
new_case = create_case(
    section_id=42,
    title="Login with valid credentials redirects to dashboard",
    template_id=1,
    steps=steps,
    preconditions="User `alice@example.com` exists; password set to 'pw123'.",
    refs="REQ-AUTH-001",
)
print(new_case["id"])  # e.g., 1234

Discover fields, types + priorities

Each tenant defines its own custom fields, case types, and priorities; discover their IDs at runtime rather than hard-coding them:

fields = api("get_case_fields")   # system_name, label, type_id per field
types = api("get_case_types")     # id + name per case type
prios = api("get_priorities")     # id + name per priority

The type_id enumeration, the field-discovery loop, and the section / suite hierarchy are in references/testrail-api-reference.md.

Update a case

POST /index.php?/api/v2/update_case/:case_id:

def update_case(case_id, **fields):
    return api(f"update_case/{case_id}", method="POST", body=fields)

# Bulk re-tag:
update_case(1234, refs="REQ-AUTH-001,REQ-AUTH-002")

Get + list cases

case = api(f"get_case/1234")
# List with pagination:
cases = []
offset = 0
while True:
    page = api(f"get_cases/{project_id}&suite_id={suite_id}"
               f"&limit=250&offset={offset}")
    cases.extend(page.get("cases", page) if isinstance(page, dict) else page)
    if isinstance(page, dict) and page.get("size", 0) < 250:
        break
    offset += 250

Newer TestRail versions return {"offset", "limit", "size", "_links", "cases": [...]}; older return a bare array. Handle both.

Running

Bulk import from CSV (with per-row duplicate check)

Build name-to-id maps once, then inside the loop check for an existing case by title before create so re-runs stay idempotent. get_cases accepts filter (substring match on title):

import csv

# Name -> id maps, discovered once (see Discover fields, types + priorities)
type_ids = {t["name"]: t["id"] for t in api("get_case_types")}
prio_ids = {p["name"]: p["id"] for p in api("get_priorities")}

created, skipped = [], []
for row in csv.DictReader(open("legacy-cases.csv")):
    title = row["title"]
    existing = api(f"get_cases/{project_id}&suite_id={suite_id}"
                   f"&filter={requests.utils.quote(title[:60])}")
    if existing.get("cases"):
        skipped.append((title, existing["cases"][0]["id"]))
        continue  # already present; skip and log
    steps = [
        {"content": s, "expected": e}
        for s, e in zip(row["steps"].split("|"), row["expected"].split("|"))
    ]
    case = create_case(
        section_id=int(row["section_id"]),
        title=title,
        template_id=1,
        type_id=type_ids[row["type"]],
        priority_id=prio_ids[row["priority"]],
        preconditions=row.get("preconditions"),
        steps=steps,
        refs=row.get("refs"),
    )
    created.append(case["id"])

Verify: assert len(created) + len(skipped) equals the row count before declaring the import done; a shortfall means a row raised - inspect it, fix the source row, and re-run (already-created cases are skipped by the title check).

Parsing results

add_case response is the full case object with id, created_on, updated_on, created_by etc. Permalink:

url = f"{BASE}/index.php?/cases/view/{case_id}"

CI integration

Sync cases from a tests/ directory layout. One pattern: each spec file has front-matter declaring the TestRail case ID; on PR merge, post updates back:

- name: Sync test cases to TestRail
  env:
    TR_BASE: ${{ vars.TR_BASE }}
    TR_EMAIL: ${{ vars.TR_EMAIL }}
    TR_KEY: ${{ secrets.TR_KEY }}
  run: python scripts/sync-testrail.py

Anti-patterns

Anti-patternWhy it failsFix
Hard-coded type_id, priority_idIDs differ per projectDiscover via get_case_types / get_priorities
Steps in custom_steps (Text template)Per-step results unavailableUse custom_steps_separated (Steps template)
Plain title with no refsCoverage reports show 0% requirement coverageAlways set refs to requirement IDs
Creating cases in the root sectionHard to find later; organisation chaosAlways pick a section; create sections as needed
No pagination on get_casesMisses cases beyond first 250Loop with offset until empty
Storing API key in codeToken leakEnvironment variable
Polling for case existence on every CI runRate-limitedCache case-ID-by-title within the CI run

Limitations

  • Cloudflare protection. TestRail support docs require browser validation; this skill cites by stable URL but Headless WebFetch fails. Authenticated API calls work fine (different surface).
  • Custom-field discipline varies. Tenants define different custom fields; scripts must discover field IDs at runtime.
  • refs is free text. TestRail doesn't validate that referenced IDs exist in Jira / Linear / etc. Pair with traceability matrix reconciliation.
  • Hierarchical sections are recursive but display is shallow. Deep section trees are hard to navigate in the UI; keep ≤3 levels.
  • Bulk operations are sequential. No native bulk endpoint; loop + throttle for large imports.

References

  • TestRail API v2 Cases reference - support.testrail.com/hc/en-us/articles/7077871398036-Cases (Cloudflare-protected; cite by stable URL).
  • TestRail API v2 Suites + Sections + Custom Fields docs - support.testrail.com/hc/en-us/categories/7076541806228.
  • Sibling references: test-case-anatomy-reference.
  • Sibling skills (other platforms): xray-case-management, zephyr-scale-case-management, allure-testops-case-management, qase-io-case-management.
  • Sibling-plugin neighbour: testrail-integration (in the qa-test-reporting plugin) - different scope (result sync; not case authoring).

TestRail API v2 reference

View source (opens in new window)

TestRail API v2 reference

Custom-field discovery, type / priority enums, and the section + suite hierarchy for the TestRail API v2. Per the TestRail API docs (support.testrail.com; Cloudflare-protected, cite by stable URL).

Discover custom fields

Each tenant defines its own custom fields. Discover IDs once:

fields = api("get_case_fields") for f in fields: print(f["system_name"], f["label"], f["type_id"]) # custom_preconds Preconditions 3 # custom_severity Severity 6 # custom_automation_type Automation Type 6

type_id values per TestRail docs: 1=String, 2=Integer, 3=Text, 4=URL, 5=Checkbox, 6=Dropdown, 7=User, 8=Date, 9=Milestone, 10=Steps, 11=Multi-select.

Discover types + priorities

types = api("get_case_types") # -> [{"id", "name", "is_default"}, ...] prios = api("get_priorities") # -> [{"id", "name", "priority", ...}, ...]

Build name-to-id maps from these; never hard-code the integers (they differ per project).

Sections + suites

api("add_suite/123", method="POST", body={"name": "Authentication"}) api("add_section/123", method="POST", body={"suite_id": 7, "name": "Login flows", "parent_id": None})

Hierarchy: project -> suite -> section (nests via parent_id) -> case. Keep section trees at 3 levels or fewer; the UI display is shallow.

Related skills

allure-testops-case-management

Author and manage Allure TestOps test cases via the REST API - create cases, manage projects + suites, attach scenarios with nested steps, link to Jira / GitHub issues, sync with allure-results from CI runs. Covers Bearer-token auth, /api/rs/testcase CRUD endpoints, nested-step `scenario` shape, and the unique Allure TestOps feature of linking automated results back to manual case definitions. Use for pre-execution case authoring in teams using Allure TestOps as the canonical TCM.

qase-io-case-management

Author and manage Qase.io test cases via the Public API v1 - create cases, organise into suites, attach structured steps, link to Jira/Linear/GitHub, manage shared steps, and bulk-import via JSON. Covers Token header auth, /case/{project_code} CRUD endpoints, the steps array with action / expected_result / data shape, and shared-step reuse. Use for pre-execution case authoring in teams using Qase as a modern lightweight TCM.

test-case-anatomy-reference

Pure-reference catalog of test-case anatomy - what fields a well-formed test case must have and what each field means. Enumerates the ISO/IEC/IEEE 29119-3:2021 test-case template fields (identifier, objective, preconditions, inputs, steps, expected results, postconditions, environment, traceability) and the ISTQB CTAL-TM specification-technique-driven additions (equivalence partition, boundary value, decision table, state transition). Maps the canonical anatomy to five tracker-specific schemas (TestRail, Xray, Zephyr Scale, Allure TestOps, Qase). Use as the authoritative source when authoring a case template, reviewing case quality, or migrating between tools.

test-case-review-rubric

Scores an already-written test case against six per-case quality axes (objective specificity, precondition executability, step granularity, step abstraction level, expected-result observability, traceability validity) and six set-level axes (partition coverage, boundary coverage, duplication, orphan and uncovered requirements, tier shape, identifier consistency). Derives a per-case PASS / WEAK / FAIL verdict and a set verdict from it without averaging, and marks every threshold as either standard-backed (ISTQB glossary, ISTQB CTFL v4.0, ISO/IEC/IEEE 29119-3:2021) or practitioner convention (step-count ceiling, tier bands, provenance threshold). Assumes the case field list and field cardinality are already defined by a test-case anatomy reference and judges content quality only. Use when reviewing a batch of hand-written test cases before promoting them to a release suite or handing them to an automation engineer.

traceability-matrix-builder

Build-an-X workflow that produces a requirements-to-tests traceability matrix from a TCM case repository + a requirements source (Jira / Linear / GitHub Issues). Walks the author through (1) extracting requirements with stable IDs, (2) extracting cases + their refs, (3) computing coverage (which requirements have at least one test, which tests verify which requirements, orphaned cases / orphaned requirements), (4) emitting a CSV / Markdown / HTML matrix, and (5) producing an executive summary (X% requirement coverage, Y orphans, Z over-tested). Use for test coverage audits, finding requirements-coverage gaps, sprint-end coverage reviews, compliance documentation, and traceability in regulated industries.

xray-case-management

Author and manage Xray test cases (Jira issues with Test issue type) via the GraphQL + REST APIs - create tests, attach steps, link preconditions, set testType (Manual / Cucumber / Generic), associate with requirements, bulk import via JSON. Covers OAuth client_id/client_secret auth, the GraphQL createTest mutation, the REST /api/v2/import/test/bulk endpoint, and the Cucumber-style scenario authoring path. Use for pre-execution case authoring in Jira-anchored teams using Xray. Distinct from Xray's test-execution / test-run features which post results.

zephyr-scale-case-management

Author and manage Zephyr Scale Cloud test cases via the REST API v2 - create tests, attach steps, link to Jira issues, organise into folders, manage test cycles. Covers Bearer-token auth, the /testcases endpoints, the testScript / steps shape, and folder hierarchy. Use for pre-execution case authoring in Jira-anchored teams using Zephyr Scale (formerly TM4J). Distinct from Zephyr's test-cycle / execution endpoints which post results.