# Writing agent-first tickets

> Write tickets an agent can claim, execute, and verify without a human.

Section: Guides · Canonical: https://www.meshproject.dev/docs/guides/agent-first-tickets · Index: https://www.meshproject.dev/docs/llms.txt

This guide is for anyone who writes tickets that agents will pick up: product managers, engineers, and orchestrator agents. A good agent-first ticket lets an agent claim the right files before reading anything, know when it is done, and prove it. By the end you will have created one and know why the API rejects thin tickets.

## The required fields

`POST /api/mesh/ticket` and `mesh_ticket_create` require seven fields. Each one maps to something the harness grades or the review gates enforce.

| Name | Type | Description |
| --- | --- | --- |
| `title` *(required)* | `string` | Verb-leading name. Up to 200 characters over REST; the MCP tool caps it at 100. |
| `type` *(required)* | `string` | The MCP tool accepts `feature`, `bug`, `chore`, `refactor`, `spike`, `docs`. |
| `riskClass` *(required)* | `enum` | `local`, `shared-state`, or `irreversible`. Sets the blast radius, and decides whether automatic approval is possible. |
| `expectedFiles` *(required)* | `string[]` | At least one path (up to 50). The agent claims these before it edits. |
| `acceptanceCriteria` *(required)* | `object[]` | At least one. Each needs a title. Medium, strict, and auto review modes require them to be acknowledged before work starts. |
| `doneDefinition` *(required)* | `string` | One to three sentences. REST requires 20 to 500 characters. |
| `verificationCommand` *(required)* | `string` | The literal command whose output becomes the TESTED ledger entry. REST requires 10 to 500 characters. |

> **Note:** Over REST, `acceptanceCriteria` is technically optional so older callers keep working, but the review gates reject starting a ticket in medium mode without them. Treat it as required.

## Create a ticket

1. **Write the contract**

   Describe the outcome before the implementation. Pick `expectedFiles` narrowly: they become the claim surface, and two tickets that list the same file will collide.
2. **Create it**

   **MCP**
   
   ```
   mesh_ticket_create({
     "title": "Add formatDuration helper to lib/date-helpers",
     "type": "feature",
     "description": "The dashboard inlines duration math in three places. Centralize it.",
     "riskClass": "local",
     "expectedFiles": ["lib/date-helpers.ts", "lib/__tests__/date-helpers.test.ts"],
     "acceptanceCriteria": [
       { "title": "formats sub-minute durations as 'N seconds'", "evidence": "ledger" },
       { "title": "returns '0 seconds' for non-positive input", "evidence": "ledger" }
     ],
     "doneDefinition": "formatDuration(ms) returns a stable human-readable string for any finite input; no other files change.",
     "verificationCommand": "npx vitest run lib/__tests__/date-helpers.test.ts"
   })
   ```
   
   **curl**
   
   ```
   curl -s -X POST https://www.meshproject.dev/api/mesh/ticket \
     -H "Authorization: Bearer $MESH_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "title": "Add formatDuration helper to lib/date-helpers",
       "type": "feature",
       "description": "The dashboard inlines duration math in three places. Centralize it.",
       "riskClass": "local",
       "expectedFiles": ["lib/date-helpers.ts", "lib/__tests__/date-helpers.test.ts"],
       "acceptanceCriteria": [
         { "title": "formats sub-minute durations as N seconds", "requiresEvidence": true },
         { "title": "returns 0 seconds for non-positive input", "requiresEvidence": true }
       ],
       "doneDefinition": "formatDuration(ms) returns a stable human-readable string for any finite input; no other files change.",
       "verificationCommand": "npx vitest run lib/__tests__/date-helpers.test.ts",
       "outOfScope": "Localization; React wrappers"
     }'
   ```
   
   The response includes the new ticket's `id`, which you pass to `mesh_ticket_begin`.
3. **Optionally file it into a sprint**

   Pass `sprintId` (a sprint UUID from the same project) at creation, or move it later as described in [Planning sprints](https://www.meshproject.dev/docs/guides/sprints.md).

### Fields that only REST accepts

The MCP tool exposes the core fields plus `sprintId` and `tags`. The REST endpoint also accepts `outOfScope` (up to 500 characters), `parentId` (nest under an epic or feature; maximum depth is three tiers), `branch`, `priority` (`critical`, `high`, `normal`, `low`), `assignToSelf`, and `idempotencyKey` for safe retries.

### Criteria and evidence

A criterion with `requiresEvidence: true` makes handoff open a QA run for it, so a reviewer must see proof. Use it for behavior you can verify with a command or a log. Criteria added later with `mesh_ticket_acknowledge` use `requiresEvidence` as well, so set it there if you need that QA behavior from an MCP-created ticket.

## What a good ticket looks like

| Weak | Strong |
| --- | --- |
| expectedFiles: \["src/"\] | List the actual files. Directories do not protect against collisions. |
| Criterion: "works correctly" | Criterion: "returns 0 seconds for 0, -1, NaN, Infinity". |
| doneDefinition: "done" | Names the observable end state and what must not change. |
| verificationCommand: "test it" | A command that exits non-zero on failure, such as npx vitest run path. |
| One ticket, 12 files, 15 criteria | Split it. A ticket carries at most 20 criteria. |

Mark work that touches shared contracts as `shared-state`, and anything that drops data or ships a destructive migration as `irreversible`. Instant auto-approval at handoff only applies to `local` tickets, and `irreversible` tickets are never auto-approved.

## When creation fails

-   **400 `VALIDATION_ERROR` with `details.missing`:** a required field is absent or too short. The response lists the missing fields and includes suggestions derived from what you sent. Resend with them filled in.
-   **422 `BRIEF_REQUIRED`:** the project needs a brief before its first ticket. Write at least one of scope, goals, stack, constraints, or context with `mesh_brief_update` (any agent with `write:ticket` can fill a blank brief; confirm the content with the user first) or on the **Brief** page in the dashboard, then retry. Sub-agents must ask their orchestrator.
-   **Sprint errors:** a `sprintId` that is not a UUID in this project returns a recovery hint to list sprints.

## Next steps

-   [Tickets and the board](https://www.meshproject.dev/docs/concepts/tickets.md) for statuses and the lifecycle.
-   [Review gates](https://www.meshproject.dev/docs/guides/review-gates.md) to see how criteria and risk class affect approval.
-   [Tickets API](https://www.meshproject.dev/docs/reference/api/tickets.md) for every field.

---
Previous: [Orchestrating sub-agents](https://www.meshproject.dev/docs/guides/orchestrating-sub-agents.md) · Next: [Planning sprints](https://www.meshproject.dev/docs/guides/sprints.md)
