---
title: "Create skill"
method: POST
path: "/v1/skills"
tags: ["skills"]
---

# Create skill

`POST /v1/skills`

## Request

- Base URL: `https://api.patricia.app` (the document declares no server; this is the origin it was published from)
- URL: `https://api.patricia.app/v1/skills`
- Auth: HTTP bearer

Create a new skill directly. Requires skills:install, not skills:create: the body accepts a caller-chosen status_code, and an active skill is presented workspace-wide on the dashboard, so a caller who holds only skills:create no longer reaches this route at all. That caller's own no-approval lane is /authored-skills/instructions, which is personal until an admin shares it.

## Headers

- `X-Tenant-Id` string, nullable

## Request body

- ModelsBrainSkillCreateValidator: POST body. ``tenant_id`` and ``created_by`` are server-assigned. No ``created_by``: it is the provenance label, and a body that could set it could stamp a workspace's own skill ``patricia`` and wear the platform's badge. ``extra="forbid"`` turns such a body into a 422 rather than ignoring the field, so a caller who thinks they are choosing the provenance is told they are not. The repository assigns ``custom``. The 422 is a DELIBERATE contract break, chosen against the accept-and-discard bargain PUT/PATCH make. Those verbs round-trip GET bodies of existing rows, so silence there keeps working clients working. Create is where a forged label would be minted, and the published create contract never honoured the field for anyone: every user-principal request 500'd from the day the route shipped, and an API-key caller's body value was silently overwritten by the factory's injection. Refusing loudly beats continuing either of those.
  - `name` string, required
  - `description` string, nullable
  - `content` string, required
  - `category` string, nullable
  - `version` integer
  - `status_code` 'proposed' | 'active' | 'retired'
  - `eval_score` number, nullable

## Response `201`

The created skill.

- ModelsBrainSkillFullValidator: Response shape for GET / POST / PUT / PATCH.
  - `id` string, uuid, required
  - `tenant_id` string, uuid, nullable
  - `name` string, required
  - `description` string, nullable
  - `content` string, required
  - `category` string, nullable
  - `version` integer, required
  - `status_code` 'proposed' | 'active' | 'retired', required
  - `eval_score` number, nullable
  - `created_by` string, required
  - `hit_count` integer, required
  - `last_used_at` string, date-time, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `400`: Bad request
- `401`: Unauthorized
- `403`: Forbidden. Inspect response body `code` for the specific reason (for example: PERMISSION_DENIED, EMAIL_NOT_VERIFIED, TENANT_ACCESS_REQUIRED, ROLE_UNRESOLVED)
- `409`: Conflict
- `422`: Validation error

---

[API](https://skmtc.dev/patricia/apis/patricia-api.md) · [All operations](https://skmtc.dev/patricia/apis/patricia-api/llms.txt) · [OpenAPI document](https://skmtc.dev/patricia/apis/patricia-api/revisions/0dfb44495ea1?raw)
