Workspace API
Manage Skills
Create, edit, publish, and reassign a skill with POST /workspace/skills/create, update, publish, unpublish, delete, transfer, move, and fork.
Every write below costs 0 credits, requires skills:write, and requires the Idempotency-Key header (1-200 characters). Pass workspace_id as a query parameter when authenticating with a user JWT; API keys infer it. Each route also has a batch sibling at POST /workspace/skills/<verb>/batch, accepting 1-100 items with a per-item idempotencyKey — see Batch Writes.
The Workspace API requires the Pro plan. A request from a non-Pro workspace or a non-member receives 403 forbidden.
Every write returns a content-free receipt:
{ "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010", "installationId": null, "draftVersion": 1, "publishedRevisionId": null, "lifecycle": "draft", "changed": true}Create a Skill
POST https://api.parcelengineering.com/api/v1/workspace/skills/createCreates a Personal or Workspace skill from a portable bundle. The new skill starts as a draft; publish it (below) to make it discoverable. A Workspace skill may only override an official skill. The reserved slugs create-skill and improve-skill cannot be claimed. API keys cannot create Personal skills.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
audience | personal | workspace | Yes | Personal keeps it to you; Workspace shares it. |
bundle | object | Yes | { files: [{ path, mediaType, content }] }. mediaType is text/markdown, text/plain, or application/json. |
override | object | No | { kind: "custom", skillId } or { kind: "official", officialSkillId }. Personal may use either; Workspace may use official only. |
authoringConversationId | string | No | Spec conversation this skill was authored in. |
V1 files are bounded UTF-8 Markdown, plain text, or inert JSON. Matching .md / .markdown, .txt, and .json paths are accepted. Scripts, binaries, hooks, symlinks, executable manifests, and path traversal are refused. Fenced code blocks are kept as prose; Spec reads them and never runs them.
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/create \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: create-skill-001" \ -d '{ "audience": "workspace", "bundle": { "files": [ { "path": "SKILL.md", "mediaType": "text/markdown", "content": "<omitted>" } ] } }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/create', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'create-skill-001', }, body: JSON.stringify({ audience: 'workspace', bundle: { files: [ { path: 'SKILL.md', mediaType: 'text/markdown', content: '<omitted>', }, ], }, }), });
const { data, metadata } = await response.json();Update a Skill Draft
POST https://api.parcelengineering.com/api/v1/workspace/skills/updateReplaces or restores the draft of a skill you may edit. Provide bundle to replace it, or restoreRevisionId to roll the draft back to a published revision. Update never publishes.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | The skill to edit. |
expectedDraftVersion | integer | Yes | Draft version you last read. A mismatch is 409 revision_conflict / stale_draft_version with details.currentDraftVersion. |
bundle | object | Exactly one of bundle or restoreRevisionId | Replacement portable bundle. |
restoreRevisionId | string | Exactly one of bundle or restoreRevisionId | Restore the draft from this published revision instead. |
Sending both or neither is 400 invalid_input / update_source.
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/update \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: update-skill-001" \ -d '{ "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010", "expectedDraftVersion": 1, "bundle": { "files": [ { "path": "SKILL.md", "mediaType": "text/markdown", "content": "<omitted>" } ] } }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/update', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'update-skill-001', }, body: JSON.stringify({ skillId: '8a1f3c2e-0001-4b7d-9e5a-000000000010', expectedDraftVersion: 1, bundle: { files: [ { path: 'SKILL.md', mediaType: 'text/markdown', content: '<omitted>', }, ], }, }), });
const { data, metadata } = await response.json();Publish a Skill
POST https://api.parcelengineering.com/api/v1/workspace/skills/publishPublishes the current draft as the next immutable revision, making the skill discoverable.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | The skill to publish. |
expectedDraftVersion | integer | Yes | Draft version you last read. A mismatch is 409 revision_conflict / stale_draft_version. |
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/publish \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: publish-skill-001" \ -d '{ "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010", "expectedDraftVersion": 2 }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/publish', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'publish-skill-001', }, body: JSON.stringify({ skillId: '8a1f3c2e-0001-4b7d-9e5a-000000000010', expectedDraftVersion: 2, }), });
const { data, metadata } = await response.json();Unpublish a Skill
POST https://api.parcelengineering.com/api/v1/workspace/skills/unpublishRemoves a skill from future discovery without deleting it or its revisions. A Spec run that already holds a pin on the published revision can still read those bytes for the rest of the run.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | The skill to unpublish. |
expectedVersion | integer | Yes | Identity version you last read. A mismatch is 409 revision_conflict / stale_version with details.currentVersion. |
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/unpublish \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: unpublish-skill-001" \ -d '{ "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010", "expectedVersion": 1 }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/unpublish', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'unpublish-skill-001', }, body: JSON.stringify({ skillId: '8a1f3c2e-0001-4b7d-9e5a-000000000010', expectedVersion: 1, }), });
const { data, metadata } = await response.json();Delete a Skill
POST https://api.parcelengineering.com/api/v1/workspace/skills/deleteMarks a skill deleted. Discovery stops and no bundle is returned afterwards. Deleted is terminal for every lifecycle mutation. A deleted Personal override does not hide a Workspace skill.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | The skill to delete. |
expectedVersion | integer | Yes | Identity version you last read. A mismatch is 409 revision_conflict / stale_version. |
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/delete \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: delete-skill-001" \ -d '{ "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010", "expectedVersion": 2 }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/delete', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'delete-skill-001', }, body: JSON.stringify({ skillId: '8a1f3c2e-0001-4b7d-9e5a-000000000010', expectedVersion: 2, }), });
const { data, metadata } = await response.json();Transfer a Skill
POST https://api.parcelengineering.com/api/v1/workspace/skills/transferTransfers maintainership of a Workspace skill to another current member. Owners and admins only. Creator attribution never changes. Personal skills cannot be transferred (403 forbidden / transfer_personal). Audience and override columns do not change.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | The Workspace skill to hand over. |
expectedVersion | integer | Yes | Identity version you last read. A mismatch is 409 revision_conflict / stale_version. |
newMaintainerId | string | Yes | Member who becomes the maintainer. Must be a current workspace member. |
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/transfer \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: transfer-skill-001" \ -d '{ "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010", "expectedVersion": 1, "newMaintainerId": "8a1f3c2e-0001-4b7d-9e5a-000000000014" }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/transfer', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'transfer-skill-001', }, body: JSON.stringify({ skillId: '8a1f3c2e-0001-4b7d-9e5a-000000000010', expectedVersion: 1, newMaintainerId: '8a1f3c2e-0001-4b7d-9e5a-000000000014', }), });
const { data, metadata } = await response.json();Move a Skill
POST https://api.parcelengineering.com/api/v1/workspace/skills/moveMoves a custom skill you may edit between the Personal and Workspace audiences. The mover becomes the owner of a Personal skill or the maintainer of a Workspace skill. Creator attribution never changes. Revisions, the draft and the identity version carry over; conversations already pinned to a revision do not move.
Requires the same authority as editing the draft: the owner of a Personal skill, or the maintainer, an owner or an admin of a Workspace skill. API keys may only move into Workspace (403 forbidden / personal_denied). Moving onto a name that already exists in the target audience is 409 slug_conflict / slug_taken. Moving to the audience the skill already has returns changed: false.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | The custom skill to move. |
expectedVersion | integer | Yes | Identity version you last read. A mismatch is 409 revision_conflict / stale_version. |
audience | "personal" | "workspace" | Yes | Where the skill should live. |
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/move \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: move-skill-001" \ -d '{ "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010", "expectedVersion": 1, "audience": "workspace" }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/move', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'move-skill-001', }, body: JSON.stringify({ skillId: '8a1f3c2e-0001-4b7d-9e5a-000000000010', expectedVersion: 1, audience: 'workspace', }), });
const { data, metadata } = await response.json();Fork a Skill
POST https://api.parcelengineering.com/api/v1/workspace/skills/forkForks a skill you can read into a new Personal or Workspace draft you own. A Workspace fork may only override an official skill. Run pins are not consulted; fork copies the locator you send.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
source | object | Yes | A custom or official locator. |
audience | personal | workspace | Yes | Personal keeps the fork to you; Workspace shares it. |
override | object | No | Same shapes as create. Workspace may name an official skill only. |
name | string | No | New frontmatter name for the copy, so it can live beside its source. Validated like any skill name; omit to keep the source name. |
Custom locator rules:
source fields | What is copied |
|---|---|
Neither revisionId nor view | The published tip, with no authoring session. |
revisionId set | That revision, under published-read auth. A revision belonging to another skill is 404 not_found / revision_not_found. |
view: "draft" | The draft, even when a published revision exists. Records no fork source revision. |
Both revisionId and view: "draft" | 400 invalid_input / revision_and_draft_view. |
Every path that copies draft content uses the same draft authorization as get: object auth on web, API, and MCP; a Spec run token additionally needs a matching authoring session (403 forbidden / draft_preview_denied when it does not). Official fork currently returns 404 official_source_not_populated. A visible-but-not-installable official release is 403 forbidden / official_not_installable.
Example
curl -X POST https://api.parcelengineering.com/api/v1/workspace/skills/fork \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: fork-skill-001" \ -d '{ "source": { "source": "custom", "skillId": "8a1f3c2e-0001-4b7d-9e5a-000000000010" }, "audience": "personal" }'const response = await fetch( 'https://api.parcelengineering.com/api/v1/workspace/skills/fork', { method: 'POST', headers: { 'Authorization': 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', 'Idempotency-Key': 'fork-skill-001', }, body: JSON.stringify({ source: { source: 'custom', skillId: '8a1f3c2e-0001-4b7d-9e5a-000000000010', }, audience: 'personal', }), });
const { data, metadata } = await response.json();Errors
Skills errors use a dedicated map. They are not the Data API errors page.
{ "error": { "code": "revision_conflict", "message": "skill revision_conflict", "reason": "stale_draft_version", "details": { "currentDraftVersion": 4 } }}| Service code | HTTP |
|---|---|
invalid_input | 400 |
validation, limit_exceeded | 422 |
not_found, official_source_not_populated | 404 |
forbidden, disabled | 403 |
revision_conflict, idempotency_conflict, slug_conflict, lifecycle_conflict | 409 |
official_source_unavailable | 503 |
internal_error | 500 |
details is optional and allowlisted to exactly these keys: path, currentVersion, currentDraftVersion, baseRevisionId, rootSha256, bundleSha256. Batch per-item errors carry the same four fields (code, message, reason, details). Envelope problems (empty batch, more than 100 items, malformed body) stay request-validation 400.