> ## Documentation Index
> Fetch the complete documentation index at: https://platform.atlan.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> To act on Atlan objects, use the Atlan MCP server at https://api.atlan.com/mcp or the atlanai CLI; `atlanai --map json` prints its command map. Run a read-only identity check before any write.
> The docs MCP server at /mcp searches these docs only. It cannot read or change Atlan objects.
> SDK packages: Python `atlanai` (PyPI) and TypeScript `@atlanai/sdk` (npm). Show Python first, then TypeScript.

# Skills cookbook

> Publish, retrieve, version, and inspect reusable Skills.

Use this cookbook to publish a reusable Skill, retrieve a known version, and
inspect how it is used. It covers direct API publishing and supported
repository automation.

## Sync a repository from GitHub

Use Atlan SkillSync when Git should remain the source of truth for several
skills. The GitHub Action posts a read-only impact report on pull requests, then
publishes approved additions and updates from a protected branch.

The current flow requires destination workspace IDs in `.atlan/package.json`
and a repository secret named `ATLANAI_TOKEN`. The preflight workflow receives
neither that secret nor checked-out pull request code.

<Card title="Set up GitHub SkillSync" icon="code-branch" href="/registry/skills/how-tos/sync-from-github">
  Add the manifest, repository secret, pull request preflight, and protected-branch publish workflow.
</Card>

## Publish a Skill

<Note>
  **Preview.** Start with a disposable workspace and an approved canonical
  sample bundle. This direct API recipe uses the current multipart contract; it
  does not define a new Skill package format.
</Note>

<Tip>
  **The API is not the shortest path here.** `atlanai skill publish` does the
  bundling, the multipart request, and the version check in one command — see
  [Publish skills with the CLI](/registry/skills/how-tos/publish-with-cli). To publish on
  merge, use the [SkillSync Action](/registry/skills/how-tos/sync-from-github). Use this
  cookbook when you are building the request yourself because there is no CLI
  on the host, the language is unsupported, or the CLI does not expose the
  operation.
</Tip>

## Publish a Skill

Prepare a ZIP bundle with `SKILL.md` at its root and only the supporting files
the Skill needs. The `metadata` multipart part carries the Skill name, optional
description, and target workspace. The `body` part carries the ZIP.

<Note>
  **`SKILL.md` opens with YAML front matter carrying `name` and
  `description`.** These describe the Skill to the runtime that loads it, and
  are required in addition to the `metadata` part, which addresses the Skill in
  the Registry. A bundle without front matter returns
  `422 validation_failed`.
</Note>

```markdown SKILL.md theme={null}
---
name: release-notes
description: Turn merged changes into release notes.
---

# Release notes

1. Collect the merged pull requests since the last tag.
2. Group them by change type.
```

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/skill/v1/skills" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -F 'metadata={"name":"disposable-example","description":"A disposable documentation check","workspace_id":"workspace_01example"};type=application/json' \
  -F '@./disposable-example.zip;type=application/zip'
```

Or with the SDK — see [Install](/tools/sdk/how-tos/install):

<CodeGroup>
  ```python Python theme={null}
  import json
  from pathlib import Path
  from atlanai import AtlanClient

  client = AtlanClient(
      "https://api.atlan.com",
      bearer_token="...",
      workspace="workspace_01example",
  )

  skill = client.skills.create(
      # A multipart part, so this is a JSON *string* — not a dict.
      metadata=json.dumps(
          {
              "name": "disposable-example",
              "description": "A disposable documentation check",
              "workspace_id": "workspace_01example",
          }
      ),
      Path("./disposable-example.zip").read_bytes(),
  )
  print(skill.id)
  ```

  ```typescript TypeScript theme={null}
  import { readFile } from "node:fs/promises";
  import { AtlanClient } from "@atlanai/sdk";

  const client = new AtlanClient({
    gatewayOrigin: "https://api.atlan.com",
    bearerToken: "...",
    workspace: "workspace_01example",
  });

  const skill = await client.skills.create({
    // A multipart part, so this is a JSON string — not an object.
    metadata: JSON.stringify({
      name: "disposable-example",
      description: "A disposable documentation check",
      workspace_id: "workspace_01example",
    }),
    body: new Blob([await readFile("./disposable-example.zip")]),
  });
  console.log(skill.id);
  ```
</CodeGroup>

<Note>
  `metadata` and `items` are multipart parts rather than a request body, so
  pass them as JSON strings in both languages. Their keys stay `snake_case`,
  since the string is JSON you build yourself rather than an object the client
  serialises for you.
</Note>

Do not include tokens, customer data, or private source material in the
bundle. A Skill with the same name in the same workspace receives a new version
when its content changes; identical content does not create a duplicate.

The response identifies the Skill and its current version. Preserve those
values in your deployment record rather than finding the Skill again by name.

## Bulk-create Skills

Use the bulk endpoint when an application needs to create a one-time batch of
new skill bundles. It accepts at most 100 items. Each item in the `items` JSON
array supplies the same metadata used for one Skill and a `body_part` name. The
multipart request includes a ZIP body under each of those names.

<Note>
  **Preview.** A bulk response is `207 Multi-Status`, even when some items were
  created. Read every returned item instead of treating the HTTP status alone as
  a successful import.
</Note>

```bash theme={null}
'[
  {
    "metadata": {
      "name": "incident-brief",
      "description": "Create an incident brief from a reviewed timeline.",
      "workspace_id": "workspace_01example"
    },
    "body_part": "incident-brief"
  },
  {
    "metadata": {
      "name": "release-notes",
      "description": "Turn merged changes into release notes.",
      "workspace_id": "workspace_01example"
    },
    "body_part": "release-notes"
  }
]'

curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/skill/v1/skills/bulk" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -F "${items};type=application/json" \
  -F 'incident-brief=@./incident-brief.zip;type=application/zip' \
  -F 'release-notes=@./release-notes.zip;type=application/zip'
```

With the SDK, `items` is the same JSON string, and each `body_part` name
becomes its own multipart part:

<CodeGroup>
  ```python Python theme={null}
  import json
  from pathlib import Path

  result = client.skills.bulk_create(
      json.dumps(
          [
              {
                  "metadata": {
                      "name": "incident-brief",
                      "description": "Create an incident brief from a reviewed timeline.",
                      "workspace_id": "workspace_01example",
                  },
                  "body_part": "incident-brief",
              },
              {
                  "metadata": {
                      "name": "release-notes",
                      "description": "Turn merged changes into release notes.",
                      "workspace_id": "workspace_01example",
                  },
                  "body_part": "release-notes",
              },
          ]
      ),
  )

  for index, item in enumerate(result.items):
      print(index, item.status)
  ```

  ```typescript TypeScript theme={null}
  const result = await client.skills.bulkCreate({
    items: JSON.stringify([
      {
        metadata: {
          name: "incident-brief",
          description: "Create an incident brief from a reviewed timeline.",
          workspace_id: "workspace_01example",
        },
        body_part: "incident-brief",
      },
      {
        metadata: {
          name: "release-notes",
          description: "Turn merged changes into release notes.",
          workspace_id: "workspace_01example",
        },
        body_part: "release-notes",
      },
    ]),
  });

  result.items.forEach((item, index) => console.log(index, item.status));
  ```
</CodeGroup>

The response `items` array follows the request order. A `201` item includes the
created skill. A rejected item includes its own status and problem document;
successful items are not rolled back. For a duplicate-content rejection, the
problem can include `existing_artifact_id`, which identifies the skill that
already holds that content.

Use the returned IDs to inspect every created skill and record failed item
indexes for a corrected retry. Do not resubmit a whole batch until you know
which items were created.

## Find a Skill

Search takes the query in the body, so it is a `POST`:

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/skill/v1/skills/search" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"query": "release notes", "workspace_id": "workspace_01example", "limit": 20}'
```

With the SDK:

<CodeGroup>
  ```python Python theme={null}
  matches = client.skills.search({
          "query": "release notes",
          "workspace_id": "workspace_01example",
          "limit": 20,
      }
  )

  everything = client.skills.list(limit=20)
  one = client.skills.get_enriched("skill_01example")
  client.skills.update("skill_01example",
      {"description": "Turn merged changes into release notes."},
  )
  ```

  ```typescript TypeScript theme={null}
  const matches = await client.skills.search({
    query: "release notes",
    workspaceId: "workspace_01example",
    limit: 20,
  });

  const everything = await client.skills.list({ limit: 20 });
  const one = await client.skills.getEnriched("skill_01example");
  await client.skills.update("skill_01example", {
    description: "Turn merged changes into release notes.",
  });
  ```
</CodeGroup>

| Want | Use | SDK |
| - | - | - |
| Every Skill in scope | `GET /skill/v1/skills` | `client.skills.list()` |
| One Skill by id | `GET /skill/v1/skills/{skill_id}` | `client.skills.get_enriched(skill_id)` |
| Ranked matches for a query | `POST /skill/v1/skills/search` | `client.skills.search(body)` |
| Counts grouped by a field | `POST /skill/v1/skills/aggregate` | `client.skills.aggregate(body)` |
| Rename or re-describe | `PATCH /skill/v1/skills/{skill_id}` | `client.skills.update(skill_id, body)` |
| Retire it | `POST /skill/v1/skills/{skill_id}/archive` | `client.skills.archive(skill_id)` |

All 17 skill operations are in the
[skill operations reference](/tools/sdk/references/operations/skills).

From the CLI, `atlanai skill list` and `atlanai search` cover the same ground in
one command — see the [skills guide](/tools/cli/how-tos/skills). For an agent doing this
itself, use [`registry__search`](/tools/mcp/registry/references/registry-search).

## Retrieve a version

Use the current Skill when you need the latest maintained package. Use a pinned
version when an environment needs reproducible content. Verify that the
selected version contains the files your consuming environment expects.

Read a specific version's metadata before downloading it:

```bash theme={null}
curl -sS --fail-with-body \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  "${ATLAN_GATEWAY_URL}/skill/v1/skills/skill_01example/versions/1"
```

```bash theme={null}
curl -sS --fail-with-body \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -o ./disposable-example.zip \
  "${ATLAN_GATEWAY_URL}/skill/v1/skills/skill_01example/versions/1/bundle"
```

The bundle response is `application/zip`, not JSON. Compare the downloaded
package with the source bundle before a consuming environment uses it.

With the SDK, both bundle operations return raw `bytes`, so write them straight
to a file or read them in memory:

<CodeGroup>
  ```python Python theme={null}
  import io
  import zipfile

  version = client.skills.versions.get_enriched("skill_01example", version_ordinal=1
  )

  archive = client.skills.versions.get_bundle("skill_01example", version_ordinal=1
  )
  print(zipfile.ZipFile(io.BytesIO(archive)).namelist())

  # The current bundle rather than a pinned version.
  current = client.skills.get_bundle("skill_01example")
  ```

  ```typescript TypeScript theme={null}
  const version = await client.skills.versions.getEnriched({
    skillId: "skill_01example",
    versionOrdinal: 1,
  });

  const archive = await client.skills.versions.getBundle({
    skillId: "skill_01example",
    versionOrdinal: 1,
  });

  const current = await client.skills.getBundle({ skillId: "skill_01example" });
  ```
</CodeGroup>

<Note>
  `skill_get_version_enriched` returns the version's metadata and file
  manifest; the bundle operations return the archive itself. Check the content
  type before writing the bytes to a file — a problem response would otherwise
  be saved as a corrupt archive.
</Note>

From JavaScript, read the response as bytes and assert the content type before
writing the file — a JSON problem body would otherwise be saved as a corrupt
archive:

```js theme={null}
import { writeFile } from "node:fs/promises";

const response = await fetch(
  new URL(
    "/skill/v1/skills/skill_01example/versions/1/bundle",
    process.env.ATLAN_GATEWAY_URL,
  ),
  {
    headers: { Authorization: `Bearer ${process.env.ATLAN_TOKEN}` },
    signal: AbortSignal.timeout(10_000),
  },
);

if (!response.ok) {
  const problem = await response.json().catch(() => undefined);
  throw new Error(problem?.detail ?? `Atlan API returned ${response.status}`);
}
if (!response.headers.get("content-type")?.includes("application/zip")) {
  throw new TypeError("Expected an application/zip response");
}

await writeFile("./disposable-example.zip", Buffer.from(await response.arrayBuffer()));
```

## Publish a content change

Upload an updated ZIP to publish updated Skill content. Use the
version-specific read or bundle actions in the **Skill** API category to verify
that a consumer will receive the intended version. Use the same category when
only a name or description needs to change.

## Inspect usage

After publication, find out whether the Skill is actually being used and how
well it performs.

```bash theme={null}
curl -sS --fail-with-body \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  "${ATLAN_GATEWAY_URL}/skill/v1/skills/skill_01example/traces?limit=10"
```

Aggregate rather than paging when you want totals over a window. The query goes
in the body:

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/skill/v1/skills/skill_01example/traces/stats/query" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{}'
```

With the SDK. The stats request needs an explicit window and at least one named
query, and each query's `measure` comes from a fixed set:

<CodeGroup>
  ```python Python theme={null}
  import time

  runs = client.skills.traces.list("skill_01example", limit=10)

  now = int(time.time())
  stats = client.skills.traces.query_stats("skill_01example",
      {
          "request_type": "scalar",
          "queries": [{"name": "runs", "measure": "traces"}],
          "start_time_unix_seconds": now - 7 * 86_400,
          "end_time_unix_seconds": now,
      },
  )
  for result in stats.data.results:
      print(result.query_name, result.measure, result.value)

  insights = client.skills.list_insights("skill_01example")
  schemas = client.insights.schemas()
  ```

  ```typescript TypeScript theme={null}
  const runs = await client.skills.traces.list({
    skillId: "skill_01example",
    limit: 10,
  });

  const now = Math.floor(Date.now() / 1000);
  const stats = await client.skills.traces.queryStats({
    skillId: "skill_01example",
    {
      requestType: "scalar",
      queries: [{ name: "runs", measure: "traces" }],
      startTimeUnixSeconds: now - 7 * 86_400,
      endTimeUnixSeconds: now,
    },
  });

  const insights = await client.skills.listInsights({ skillId: "skill_01example" });
  const schemas = await client.insights.schemas();
  ```
</CodeGroup>

`request_type` is `scalar`, `table`, or `time_series`. `measure` is one of
`sessions`, `traces`, `spans`, `skill_invocations`, `tool_calls`, `tokens`,
`active_users`, `error_rate`, `success_rate`, `tokens_per_trace`, `cost`,
`latency_p50`, `latency_p90`, or `latency_p99`.

| Want | Use | SDK |
| - | - | - |
| Recent runs that used the Skill | `GET /skill/v1/skills/{skill_id}/traces` | `client.skills.traces.list(skill_id)` |
| One of those traces | `GET /skill/v1/skills/{skill_id}/traces/{trace_id}` | `client.skills.traces.get(skill_id, trace_id)` |
| Its spans | `.../traces/{trace_id}/spans` | `client.skills.traces.list_spans(skill_id, trace_id)` |
| Totals over a window | `POST /skill/v1/skills/{skill_id}/traces/stats/query` | `client.skills.traces.query_stats(skill_id, body)` |
| Computed insights | `GET /skill/v1/skills/{skill_id}/insights` | `client.skills.list_insights(skill_id)` |
| What insights exist | `GET /skill/v1/insights/schemas` | `client.insights.schemas()` |

Traces only appear here if the consuming runtime reports them. If a Skill you
know is in use shows nothing, the gap is instrumentation, not the Skill — see
[the tracing SDKs](/tools/sdk/overview) or the
[Claude Code trace plugin](/plugins/references/claude-code).

From the CLI, `atlanai skill runs` and `atlanai trace` cover the same ground —
see the [traces guide](/tools/cli/how-tos/traces).

## Related

<CardGroup cols={2}>
  <Card title="Publish with the CLI" icon="terminal" href="/registry/skills/how-tos/publish-with-cli">
    One command instead of a multipart request.
  </Card>

  <Card title="Publish from GitHub" icon="github" href="/registry/skills/how-tos/sync-from-github">
    Publish on merge from a protected branch.
  </Card>

  <Card title="Discover and use a skill" icon="magnifying-glass" href="/registry/skills/how-tos/discover-and-use">
    Reviewing a Skill before use.
  </Card>

  <Card title="Agents cookbook" icon="robot" href="/cookbooks/how-tos/agents">
    Register an Agent and record its runs.
  </Card>
</CardGroup>
