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-managementtestrail-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
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., 1234Discover 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 priorityThe 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 += 250Newer 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.pyAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Hard-coded type_id, priority_id | IDs differ per project | Discover via get_case_types / get_priorities |
Steps in custom_steps (Text template) | Per-step results unavailable | Use custom_steps_separated (Steps template) |
Plain title with no refs | Coverage reports show 0% requirement coverage | Always set refs to requirement IDs |
| Creating cases in the root section | Hard to find later; organisation chaos | Always pick a section; create sections as needed |
No pagination on get_cases | Misses cases beyond first 250 | Loop with offset until empty |
| Storing API key in code | Token leak | Environment variable |
| Polling for case existence on every CI run | Rate-limited | Cache case-ID-by-title within the CI run |
Limitations
References
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.