Skip to content

Publishing a skill

POST /skills/active-version

Makes new content a project skill's active version immediately, skipping the review workflow but never the authority check. On-prem only.

It mirrors POST /prompts/active-prompt-version exactly — same concurrency guard, same flows, same review handshake. This page covers the skill-specific parts; the shared behaviour is documented once, on the prompt page.

The key goes in the header, and only in the header

Every read endpoint accepts ?api_key= as well. This one does not. Carrying that habit over returns:

json
{ "detail": "API key required" }

…with status 401, which reads like a bad key rather than a misplaced one.

Request

bash
curl -s -X POST \
  "$IM_URL/skills/active-version?project_id=$PID&skill_name=test" \
  -H "X-API-Key: $IM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "frontmatter": { "name": "test", "description": "What this skill is for" },
    "body": "# Guidance\n\nDo the thing.",
    "message": "tighten the wording",
    "expected_active_sha": "a28b804cda57"
  }'
Query parameterRequiredNotes
project_idyes
skill_nameyesexact and case-sensitive; names are unique per project

Body

frontmatterobjectrequired

The skill's YAML frontmatter as an object. name and description are the meaningful fields; the server normalises the rest, adding compatibility and metadata if you omit them.

Because the server normalises before comparing, resubmitting content you just fetched is correctly detected as a no-op rather than published as a new version.

bodystringrequired

The Markdown body — everything below the frontmatter in SKILL.md.

messagestringrequired

Why this version exists. At least one character — an empty message is a 422.

expected_active_shastringrequired

The sha you read before editing. Required unless force is true; omitting both is a 422:

json
{ "detail": "expected_active_sha is required: send the sha you read before editing, or force=true to overwrite the current version deliberately." }

If the active version has moved since you read it, you get a 409 and nothing is written. See Safe concurrent writes.

forceboolean

Overwrite whatever is active without checking. Defaults to false. Use it for a first publish, or when overwriting deliberately.

acknowledge_reviewstring

A review_id from a previous 422, meaning "publish despite these findings". An id, not a boolean — see the review handshake.

Separate from force, which is about concurrency, not review.

Note there is no environments field. Prompts are environment-gated; skills are not.

Response

201 Created:

json
{
  "skill_id": "a0132e30-e7c4-4b78-8719-d8e20cbe44bb",
  "skill_name": "test",
  "sha": "7f2b19d4e0c8",
  "version_label": "v1.1",
  "status": "active",
  "published_via": "api",
  "flow_mode": "ai_assisted",
  "review_status": "clean",
  "unchanged": false
}

The sha is the new version's — not the expected_active_sha you sent. Keep it: it is the expected_active_sha for your next publish.

unchangedboolean

false on a real publish. When the content matches what is already active, the response is 200 rather than 201, with unchanged: true and the existing sha — no new version, no event.

Check the status code, not just the body, if you are counting publishes.

flow_modestring

The flow that was in force. Echoed back so a client can log what actually governed the publish rather than what it assumed.

review_statusstring | null

Only meaningful on an ai_assisted project:

  • clean — the reviewer ran and had nothing to say
  • acknowledged — findings existed and you published anyway with acknowledge_review
  • nullno reviewer on this project, i.e. any other flow

The null is the point: it separates "the reviewer was happy" from "there was no reviewer", which a boolean could not.

Authorization

Three things must hold:

  1. The key's scope is write. A read key is a 403, and says so.
  2. The key's owner is a member of the project.
  3. On a review_required project only, the owner also holds the approver tag.

A non-member gets 404 Not found, not 403

Failing (2) is indistinguishable from "there is no such skill" — both return a bare 404 Not found. That is deliberate: a 403 here would confirm that a named skill exists in a project you cannot reach.

So a 404 on publish has three possible causes: the skill name is wrong, the skill is not project-tier, or the key's owner is not on the project. Check membership before assuming a typo.

The scope opens the door; the tag decides who walks through. A publish can start failing with nothing changed on your side, because the owner lost the tag or left the project — treat the key's owner as part of your configuration. See Authentication.

Project-tier skills only

This endpoint publishes project-scoped skills. Organisation-tier and global-tier skills are produced by promotion, not by publishing.

So a skill_name that resolves for the project but only from a higher tier returns 404 Not found — the lookup is scope = project and nothing else. Verified against a project inheriting sonar-clean-code from its organisation: publishing that name is a 404, even for a write key held by a project admin.

To change what such a project uses, create a project-tier skill of the same name; it will shadow the inherited one. See Resolution & pins.

The security scan

Every other route that creates a skill version runs a mandatory security scan with no override. This path skips it by default.

That was an explicit product decision, recorded as an accepted risk: it is the one route by which unscanned skill content can reach production. It sits behind configuration rather than code, so reversing it is a deployment change:

API_PUBLISH_SCAN_SKILLS=true

With the scan enabled, blocked content returns 422:

json
{ "detail": { "code": "skill_scan_blocked",
              "message": "Skill version blocked by its security scan",
              "scan": { "verdict": "critical", "findings": [] } } }

Not the same thing as the AI reviewer, and not overridable

The AI reviewer is advisory-but-blocking and clears with acknowledge_review. The security scan is neither: acknowledge_review does not apply to it. If it blocks, read scan.findings and change the content.

Errors

StatusCodeMeaning
401key missing, invalid, revoked, or sent as ?api_key=
403read-scoped key, or missing the approver tag on a review_required project
404no project-tier skill by that name, or the owner is not a project member — deliberately indistinguishable
409active_version_changedexpected_active_sha no longer matches; body carries current_active_sha
409llm_not_configuredai_assisted project with no AI provider configured for the organisation
413content_too_largebody carries limit_bytes and content_bytes
422expected_active_sha missing without force, or an empty message
422review_suggestions_openthe AI reviewer has findings; body carries review_id and suggestions
422skill_scan_blockedsecurity scan blocked the content. Not overridable
503review_unavailablethe reviewer could not run. Nothing published — fail closed, retry

Full list with the retry policy: Errors.

Next

Copyright © 2026 elsai foundry.