# TMH file format specification

> TMH file format specification for Test Manager project backup and migration, covering manifest structure, object types, and encoding requirements.

A `.tmh` file is the project backup/migration format used by UiPath Test Manager. This document describes its structure, content, and encoding requirements in detail.

## Archive structure

```
<package-name>.tmh               ← ZIP archive
├── manifest.json                ← Package metadata and object counts
└── objects/
    ├── requirements/
    │   └── requirements-{n}.json
    ├── testcases/
    │   └── testcases-{n}.json
    ├── teststeps/
    │   └── teststeps-{n}.json
    ├── testsets/
    │   └── testsets-{n}.json
    ├── testsettestcaseassignments/
    │   └── testsettestcaseassignments-{n}.json
    ├── requirementtestcaseassignments/
    │   └── requirementtestcaseassignments-{n}.json
    ├── objectlabels/
    │   ├── objectlabels-testcase-{n}.json
    │   ├── objectlabels-testset-{n}.json
    │   ├── objectlabels-requirement-{n}.json
    │   ├── objectlabels-testexecution-{n}.json
    │   └── objectlabels-testcaselog-{n}.json
    ├── customfieldvalues/
    │   ├── customfieldvalues-testcase-{n}.json
    │   └── customfieldvalues-requirement-{n}.json
    ├── customfieldlabels/
    │   └── customfieldlabels-{n}.json
    ├── customfielddefinitions/
    │   └── customfielddefinitions-{n}.json
    ├── attachments/
    │   ├── attachments-testexecution-{n}.json
    │   ├── attachments-requirement-{n}.json
    │   ├── attachments-testcaselog-{n}.json
    │   ├── attachments-teststeplog-{n}.json
    │   └── attachments-project-{n}.json
    ├── testexecutions/
    │   ├── testexecutions-Manual-Finished-{n}.json
    │   ├── testexecutions-Automated-Finished-{n}.json
    │   └── testexecutions-Mixed-Finished-{n}.json
    ├── testcaselogs/
    │   └── testcaselogs-{n}.json
    ├── teststeplogs/
    │   └── teststeplogs-{n}.json
    ├── testcaselog-assertions/
    │   └── testcaselogassertions-{n}.json
    ├── testcaselogresultoverrides/
    │   └── testcaselogresultoverrides-{n}.json
    ├── assertion-attachments/
    │   └── assertion-attachments-{n}.json
    ├── defects/
    │   └── defects-{n}.json
    ├── parameters/
    │   └── parameters-{n}.json
    ├── testsetpackages/
    │   └── testsetpackages-{n}.json
    ├── testsettestcaseparameters/
    │   └── testsettestcaseparameters-{n}.json
    ├── testsetschedules/
    │   └── testsetschedules-testset-{n}.json
    ├── objectfilters/
    │   └── objectfilters-testset-{n}.json
    ├── projectauthorization/
    │   └── projectauthorization-{n}.json
    ├── projectsettings/
    │   └── projectsettings.json         ← singular, no number suffix
    └── prompts/
        └── prompts-{n}.json
```

:::note
Only object types that were actually exported are present. Missing folders/files are silently skipped during import.
:::

## File naming conventions

### Most object types — numbered chunks

Files are named `<type>-{n}.json` where `{n}` starts at **0** for real exports produced by the system. For example:

```
requirementtestcaseassignments-0.json
testcases-0.json
teststeps-0.json
```

The importer scans all files in each folder, so both `0`-based and `1`-based numbering work when hand-crafting files.

Large exports may be split across multiple files (`testcases-0.json`, `testcases-1.json`, etc.) — the importer reads all files in the folder.

:::tip
For optimal performance, limit each file to a maximum of 500 objects.
:::

### Type-scoped file names

Several object types scope their files by the object type they belong to. The importer reads **all** files in the folder regardless of the qualifier, but real exports always use these names:

| Object type | File name pattern |
|-------------|-------------------|
| Object labels | `objectlabels-testcase-{n}.json`, `objectlabels-testset-{n}.json`, `objectlabels-requirement-{n}.json`, … |
| Custom field values | `customfieldvalues-testcase-{n}.json`, `customfieldvalues-requirement-{n}.json` |
| Attachments | `attachments-testexecution-{n}.json`, `attachments-requirement-{n}.json`, … |
| Test executions | `testexecutions-Manual-Finished-{n}.json`, `testexecutions-Automated-Finished-{n}.json`, … |
| Object filters | `objectfilters-testset-{n}.json` |
| Test set schedules | `testsetschedules-testset-{n}.json` |

### Project settings — no number suffix

```
projectsettings/projectsettings.json
```

This file is always singular (no chunk number).

## Encoding requirements

All JSON files **must** be encoded as **UTF-8 without BOM**.

:::warning
PowerShell 5.1's `Set-Content -Encoding utf8` writes a UTF-8 BOM (`EF BB BF`). This causes `JSON.parse` in the browser to throw `SyntaxError: Unexpected token` at position 0. Always use `[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding($false)))` when creating `.tmh` files from PowerShell.
:::

## Schema versioning

The `manifest.json` carries a `schemaVersion` field (e.g., `"1.0.16"`). The importer uses this version together with `[IntroducedIn]` attributes on each DTO property to determine which fields to read. Fields introduced in a version newer than the package's schema version are ignored during deserialization.

Current latest version: **`1.0.16`**

Version history summary:

| Version  | Notable additions |
|----------|-------------------|
| `1.0.0`  | Initial: requirements, test cases, test steps, test sets, assignments, labels, attachments, defects, custom field values |
| `1.0.1`  | `preCondition` on test cases |
| `1.0.2`  | Test executions, test case log result overrides, assertions |
| `1.0.4`  | Subset export (`isSubsetExport`, `exportSubsetDetails`) |
| `1.0.5`  | Custom field labels |
| `1.0.7`  | User-defined prompts |
| `1.0.9`  | `postCondition` on test cases |
| `1.0.10` | Parameters |
| `1.0.11` | Test set packages, `folderKey`/`folderName` on test sets, `packageEntryPointUniqueId` etc. on test cases |
| `1.0.13` | `id` field on attachments |
| `1.0.14` | `studioWebFileId`/`studioWebProjectId` on test cases; `id` on test-set/test-case assignments |
| `1.0.15` | `assigneeEmail` on test-set/test-case assignments |
| `1.0.16` | Project configuration (settings, governance, signatories), custom field definitions, project authorizations |

## manifest.json

Located at the **root** of the archive (not inside `objects/`).

```json
{
  "objectCountDetails": {
    "testCases": 5,
    "testSets": 2,
    "requirements": 3,
    "objectLabels": 0,
    "attachments": 0,
    "testExecutions": 0,
    "testCaseLogs": 0,
    "testCaseResultOverrides": 0,
    "testSteps": 12,
    "testStepLogs": 0,
    "testSetTestCaseAssignments": 6,
    "requirementTestCaseAssignments": 4,
    "defects": 0,
    "customFieldValues": 0,
    "customFieldLabels": 0,
    "assertions": 0,
    "assertionScreenshots": 0,
    "testSetLabelFilters": 0,
    "userDefinedPrompts": 0,
    "parameters": 0,
    "testSetPackages": 0,
    "testSetTestCaseParameters": 0,
    "customFieldDefinitions": 0,
    "projectAuthorizations": 0
  },
  "project": {
    "name": "My Project",
    "description": "Project description",
    "projectPrefix": "MP"
  },
  "tmPackageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "schemaVersion": "1.0.16"
}
```

:::note
Real exports also include a `packageName` field (the suggested download filename). The importer ignores it — omit it in hand-crafted files.
:::

## objects/ — JSON file schemas

Every object file uses a **wrapper object** whose single property is a JSON array. The array contains the individual object records.

### requirements/{n}.json

```json
{
  "requirements": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "User can log in",
      "description": "The login form must accept valid credentials.",
      "foreignRef": "",
      "connectorRequirementId": "00000000-0000-0000-0000-000000000000"
    }
  ]
}
```

| Field | Type | Notes |
|-------|------|-------|
| `id` | GUID string | Unique per-requirement identifier |
| `name` | string | Required; max 255 chars |
| `description` | string | Can be empty string |
| `foreignRef` | string | External system reference; can be empty |
| `connectorRequirementId` | GUID string | Use `"00000000-0000-0000-0000-000000000000"` when not linked to a connector |

### testcases/{n}.json

```json
{
  "testCases": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "version": null,
      "name": "Verify login with valid credentials",
      "inputParams": null,
      "description": "",
      "automationId": null,
      "automationTestCaseName": null,
      "automationProjectName": null,
      "foreignRef": "",
      "connectorTestCaseId": null,
      "preCondition": null,
      "postCondition": null,
      "packageEntryPointUniqueId": null,
      "packageIdentifier": null,
      "packageEntryPointName": null,
      "feedId": null,
      "packageSourceName": null,
      "studioWebFileId": null,
      "studioWebProjectId": null
    }
  ]
}
```

| Field | Type | Notes |
|-------|------|-------|
| `id` | GUID string | Unique per test case |
| `version` | string \| null | `null` for manual test cases; `"1.0.0"` for automated test cases linked to a package version; `""` for automated test cases without a fixed version |
| `name` | string | Required; max 255 chars |
| `inputParams` | string \| null | JSON-encoded input parameters; `null` for manual |
| `description` | string | Rich text description; `""` when absent |
| `automationId` | string \| null | UiPath automation identifier; `null` for manual |
| `automationTestCaseName` | string \| null | Automation display name; `null` for manual |
| `automationProjectName` | string \| null | Automation project; `null` for manual |
| `foreignRef` | string | External system reference; `""` when absent |
| `connectorTestCaseId` | string \| null | Connector system ID; `null` when absent |
| `preCondition` | string \| null (v1.0.1+) | Pre-condition text; `null` when absent; max 8000 chars |
| `postCondition` | string \| null (v1.0.9+) | Post-condition text; `null` when absent; max 8000 chars |
| `packageEntryPointUniqueId` | string \| null (v1.0.11+) | Package entry point GUID; `null` for manual |
| `packageIdentifier` | string \| null (v1.0.11+) | Package ID; `null` for manual |
| `packageEntryPointName` | string \| null (v1.0.11+) | Entry point name; `null` for manual |
| `feedId` | string \| null (v1.0.11+) | Package feed ID; `null` for manual |
| `packageSourceName` | string \| null (v1.0.11+) | Package source name; `null` for manual |
| `studioWebFileId` | string \| null (v1.0.14+) | Studio Web file ID; `null` for manual |
| `studioWebProjectId` | string \| null (v1.0.14+) | Studio Web project ID; `null` for manual |

### teststeps/{n}.json

```json
{
  "testSteps": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440010",
      "testCaseId": "550e8400-e29b-41d4-a716-446655440001",
      "orderNo": 0,
      "actionType": null,
      "description": "Navigate to the login page",
      "expectedResult": "Login page is displayed",
      "clipboardData": ""
    }
  ]
}
```

| Field | Type | Notes |
|-------|------|-------|
| `id` | GUID string | Unique per step |
| `testCaseId` | GUID string | Must reference an existing test case `id` |
| `orderNo` | integer | **0-based** display order (first step = `0`) |
| `actionType` | string \| null | `null` for manual steps |
| `description` | string | Step action text |
| `expectedResult` | string | Expected outcome; `""` when absent |
| `clipboardData` | string | Clipboard/screenshot data; max 8000 chars; usually `""` |

### testsets/{n}.json

```json
{
  "testSets": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440020",
      "version": null,
      "name": "Regression Suite",
      "description": "",
      "source": "TestManager",
      "externalTestSetId": null,
      "sourceDetails": null,
      "folderKey": null,
      "folderName": ""
    }
  ]
}
```

| Field | Type | Notes |
|-------|------|-------|
| `id` | GUID string | Unique per test set |
| `version` | string \| null | `null` for Test Manager–managed test sets |
| `name` | string | Required; max 255 chars |
| `description` | string | `""` when absent |
| `source` | string | `"TestManager"` (default) or `"Orchestrator"` — **this is a string, not an integer** |
| `externalTestSetId` | string \| null | External ID when `source` is `"Orchestrator"`; `null` otherwise |
| `sourceDetails` | string \| null | Additional source metadata; `null` when absent |
| `folderKey` | string \| null (v1.0.11+) | UiPath Orchestrator folder key; `null` if not linked |
| `folderName` | string (v1.0.11+) | Folder display name; `""` if not linked; not stored in DB, informational only |

### testsettestcaseassignments/{n}.json

```json
{
  "testSetTestCaseAssignments": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440030",
      "testSetId": "550e8400-e29b-41d4-a716-446655440020",
      "testCaseId": "550e8400-e29b-41d4-a716-446655440001",
      "assigneeEmail": null
    }
  ]
}
```

| Field | Type | Notes |
|-------|------|-------|
| `id` | GUID string (v1.0.14+) | Assignment identifier |
| `testSetId` | GUID string | References a test set `id` |
| `testCaseId` | GUID string | References a test case `id` |
| `assigneeEmail` | string \| null (v1.0.15+) | Assignee email; `null` if unassigned |

### requirementtestcaseassignments/{n}.json

```json
{
  "requirementTestCaseAssignments": [
    {
      "requirementId": "550e8400-e29b-41d4-a716-446655440000",
      "testCaseId": "550e8400-e29b-41d4-a716-446655440001"
    }
  ]
}
```

:::warning
No `id` field. Unlike most other object types, requirement–test case assignments have only `requirementId` and `testCaseId`.
:::

### objectlabels/objectlabels-{objecttype}-{n}.json

:::note
**File naming:** Real exports use a type-qualified name such as `objectlabels-testcase-1.json`, `objectlabels-testset-1.json`, `objectlabels-requirement-1.json`, etc. The importer reads all files in the `objectlabels/` folder, so the qualifier is not strictly required when hand-crafting files, but it is recommended to follow the convention.
:::

```json
{
  "objectLabels": [
    {
      "objectId": "550e8400-e29b-41d4-a716-446655440001",
      "name": "regression",
      "description": "",
      "labelType": 0,
      "objectType": "TestCase"
    }
  ]
}
```

| Field | Type | Notes |
|-------|------|-------|
| `objectId` | GUID string | ID of the labelled object |
| `name` | string | Label text |
| `description` | string | Label description; `""` when absent |
| `labelType` | integer | `1` = system label (e.g. `"manual"`, `"automated"`); `0` = user-defined label |
| `objectType` | string | `"TestCase"`, `"TestSet"`, `"Requirement"`, `"TestExecution"`, `"TestCaseLog"` |

### customfieldvalues/{n}.json

```json
{
  "customFieldValues": [
    {
      "objectId": "550e8400-e29b-41d4-a716-446655440001",
      "objectType": "TestCase",
      "fieldName": "Priority",
      "fieldValue": "High"
    }
  ]
}
```

### projectsettings/projectsettings.json

Introduced in schema version **`1.0.16`**. Only present when "Export project configuration" is enabled.

```json
{
  "projectTimeZone": "Europe/Vienna",
  "folderKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "governanceEnabled": true,
  "governedByDefault": false,
  "signatories": [
    {
      "signatoryId": "550e8400-e29b-41d4-a716-446655440099",
      "signatoryType": 0,
      "signatoryLastKnownDetails": "john.doe@company.com"
    }
  ]
}
```

:::note
Unlike all other object files, `projectsettings.json` is a **direct object** (not a wrapper array). The file contains a single flat JSON object.
:::

| Field | Type | Notes |
|-------|------|-------|
| `projectTimeZone` | string \| null | IANA timezone string (e.g., `"Europe/Vienna"`) |
| `folderKey` | GUID \| null | Linked Orchestrator folder |
| `governanceEnabled` | boolean \| null | Whether governance is enabled for the project |
| `governedByDefault` | boolean \| null | Whether new test cases are governed by default |
| `signatories` | array \| null | List of approvers/signatories |
| `signatories[].signatoryId` | GUID | User or group ID |
| `signatories[].signatoryType` | integer | `0` = User, `1` = UserGroup |
| `signatories[].signatoryLastKnownDetails` | string | Display name or email at time of export |

:::note
`lastDisabledAt` (the governance disable timestamp) is intentionally excluded from the export — it is an internal timestamp that is not meaningful for a re-imported project.
:::

## Rules and constraints

1. **All IDs must be unique, valid GUIDs.** Do not reuse IDs across objects of the same or different type.

2. **Cross-references must resolve.** A `testCaseId` in a test step or assignment must refer to a test case `id` present in the same package.

3. **Wrapper objects are required.** Even when the collection has one item, it must still be wrapped: `{ "testCases": [ {...} ] }`, never a bare array or object.

4. **`null` vs `""` conventions:** Use `null` for optional fields that are genuinely absent (e.g. `automationId`, `inputParams`, `preCondition`, `connectorTestCaseId`, all `package*` fields on manual test cases). Use `""` only for fields that are always present as strings but happen to be empty (e.g. `foreignRef`, `description`, `clipboardData`). Mixing these up — particularly using `""` where `null` is expected — can cause silent import failures.

5. **`objectCountDetails` in the manifest should match actual counts** of objects present, but the importer does not strictly validate counts — mismatches only affect the pre-import summary display.

6. **UTF-8 without BOM** is mandatory for all files.

## Minimal valid TMH file (hand-crafted)

The smallest valid `.tmh` file that Test Manager will accept contains just a `manifest.json`:

```json
{
  "objectCountDetails": { ... },
  "project": { "name": "Test Project", "description": "", "projectPrefix": "TP" },
  "tmPackageId": "<new-guid>",
  "schemaVersion": "1.0.16"
}
```

All `objects/` subdirectories are optional. The importer silently skips any object type not present in the archive.
