> ## 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.

# Agents cookbook

> Register an Agent, give it tools, enable tracing, record sessions, and attach file artifacts.

Use this cookbook to register an Agent, describe what it can do, and preserve
the evidence of what it did. Atlan catalogs the Agent and its activity. It does
not start or execute the run.

<Note>
  Run Agent workloads in your configured provider or coding harness. Keep
  runtime credentials and execution controls in that environment.
</Note>

<Tip>
  **If you can change the agent's code, do not build session records by hand.**
  The [tracing SDKs](/tools/sdk/overview) report sessions, spans, cost, and scores as
  the run happens, in Python and TypeScript. For a coding agent, the
  [Claude Code trace plugin](/plugins/references/claude-code) captures sessions with no
  code change at all. Hand-built session records are for the case those cannot
  cover — a runtime you do not control, or a run reconstructed after the fact.
</Tip>

Every example below assumes `ATLAN_GATEWAY_URL` and `ATLAN_TOKEN` in your
environment, and a workspace you can write to. Read
[authentication](/api/how-tos/authentication) first.

## 1. Register an Agent

`name` and `workspace_id` are the only required fields. Everything else
describes the Agent so a reader — human or agent — can tell what it is for.

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/agent/v1/agents" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support-rover",
    "display_name": "Support Rover",
    "workspace_id": "workspace_01example",
    "instructions": "Triage inbound support tickets and draft a first reply.",
    "model_id": "claude-sonnet-5",
    "max_step_count": 12
  }'
```

Or with the SDK, which is the shorter path if you are already in Python or
TypeScript — see [Install](/tools/sdk/how-tos/install):

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

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

  agent = client.agents.create({
          "name": "support-rover",
          "display_name": "Support Rover",
          "workspace_id": "workspace_01example",
          "instructions": "Triage inbound support tickets and draft a first reply.",
          "model_id": "claude-sonnet-5",
          "max_step_count": 12,
      }
  )
  ```

  ```typescript TypeScript theme={null}
  import { AtlanClient } from "@atlanai/sdk";

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

  const agent = await client.agents.create({
      name: "support-rover",
      displayName: "Support Rover",
      workspaceId: "workspace_01example",
      instructions: "Triage inbound support tickets and draft a first reply.",
      modelId: "claude-sonnet-5",
      maxStepCount: 12,
    },
  });
  ```
</CodeGroup>

The response carries the Agent's `id`. Keep it in your deployment record rather
than looking the Agent up by name later.

| Field | Use it for |
| - | - |
| `name` | Stable machine name. Required. |
| `workspace_id` | Destination workspace. Required. |
| `display_name` | Human label shown in the product. |
| `instructions` | The system prompt or operating brief. |
| `model_id` | Default model the runtime uses. |
| `max_step_count` | Step ceiling the runtime should enforce. |
| `agent_provider_id` | Link to a registered provider, see below. |
| `agent_framework_id` | Link to a registered framework. |
| `owner_id`, `project_id`, `repo_id` | Ownership and where the source lives. |

To change any of it later, `PATCH /agent/v1/agents/{agent_id}`. To retire an
Agent, `POST /agent/v1/agents/{agent_id}/archive` — there is no destructive
delete on the public plane.

For a Claude workflow, see
[Claude Managed Agents](/clients/claude/how-tos/claude-managed-agents).

## 2. Give the Agent its tools

`tools` declares what the Agent can call. Each entry is one of two shapes:

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/agent/v1/agents" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "release-notes-writer",
    "workspace_id": "workspace_01example",
    "tools": [
      { "kind": "builtin", "name": "web_search" },
      { "kind": "mcp", "server": "atlan", "tool": "registry__search" }
    ]
  }'
```

With the SDK, on an Agent that already exists — `agent_patch_agent` replaces
the whole `tools` list, so send every tool the Agent should keep:

<CodeGroup>
  ```python Python theme={null}
  agent = client.agents.update("agent_01example",
      {
          "tools": [
              {"kind": "builtin", "name": "web_search"},
              {"kind": "mcp", "server": "atlan", "tool": "registry__search"},
          ]
      },
  )
  ```

  ```typescript TypeScript theme={null}
  const agent = await client.agents.update({
    agentId: "agent_01example",
    {
      tools: [
        { kind: "builtin", name: "web_search" },
        { kind: "mcp", server: "atlan", tool: "registry__search" },
      ],
    },
  });
  ```
</CodeGroup>

| Kind | Required fields | Means |
| - | - | - |
| `builtin` | `name` | A capability the runtime provides itself. |
| `mcp` | `server`, `tool` | A specific tool on a registered MCP server. |

Find the `server` and `tool` values with
[`registry__search`](/tools/mcp/registry/references/registry-search) or
`GET /tools/mcp/v1/servers/{server_id}/tools`. See the
[MCP overview](/tools/mcp/overview) for the catalog.

<Note>
  `tools` is not how Skills attach to an Agent — see
  [Skills and Agents](#skills-and-agents) below.
</Note>

## 3. Register a provider

A provider records where the Agent runs. Required: `name`, `workspace_id`, and
`provider_type`, which is `local` or `custom`.

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/agent/v1/providers" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "internal-runner",
    "workspace_id": "workspace_01example",
    "provider_type": "custom",
    "base_url": "https://runner.example.internal",
    "docs_url": "https://docs.example.internal/runner"
  }'
```

Or with the SDK:

<CodeGroup>
  ```python Python theme={null}
  provider = client.agent_providers.create({
          "name": "internal-runner",
          "workspace_id": "workspace_01example",
          "provider_type": "custom",
          "base_url": "https://runner.example.internal",
      }
  )

  agent = client.agents.update("agent_01example",
      {"agent_provider_id": provider.id},
  )
  ```

  ```typescript TypeScript theme={null}
  const provider = await client.agentProviders.create({
      name: "internal-runner",
      workspaceId: "workspace_01example",
      providerType: "custom",
      baseUrl: "https://runner.example.internal",
    },
  });

  const agent = await client.agents.update({
    agentId: "agent_01example",
    { agentProviderId: provider.id },
  });
  ```
</CodeGroup>

Pass the returned id as `agent_provider_id` when you create or patch the Agent.
Keep provider credentials out of source code, URLs, browser storage, and
committed configuration — the provider record is a catalog entry, not a
credential store.

Environments and frameworks work the same way, at
`POST /agent/v1/environments` and `POST /agent/v1/agent-frameworks`.

## 4. Enable tracing

This is what makes the Agent record useful rather than decorative.

<CardGroup cols={3}>
  <Card title="Your own agent" icon="code" href="/tools/sdk/tutorials/quickstart">
    Use the tracing SDK. Set `service_name` to the Agent's `name` so its runs
    attach to this record.
  </Card>

  <Card title="A coding agent" icon="plug" href="/plugins/references/claude-code">
    Install the Claude Code trace plugin. No code change; sessions and tool
    calls are captured locally and exported.
  </Card>

  <Card title="Anything OTel" icon="share-nodes" href="/registry/traces/how-tos/send-and-verify">
    Point an existing OTLP exporter at `/otel/v1/traces` with your API key and
    workspace.
  </Card>
</CardGroup>

With the tracing submodule, the same `service_name` you gave the Agent ties its
runs to this record. Install it as described in [Install](/tools/sdk/how-tos/install) —
`atlanai[tracing]` in Python, the `./tracing` subpath plus its
OpenTelemetry peers in TypeScript.

<CodeGroup>
  ```python Python theme={null}
  import atlanai.tracing as atlan

  tracer = atlan.init(service_name="support-rover")

  visitor = tracer.identify(
      identity_source="slack",
      external_id="U0123ABC",
      traits={"name": "Jordan Lee"},
  )

  with atlan.propagate_attributes("ticket-4821",
      visitor_id=visitor.id if visitor else None,
  ):
      with tracer.start_as_current_span("handle-ticket", as_type="task") as root:
          with tracer.start_as_current_span("chat", as_type="llm") as generation:
              generation.update(
                  model="claude-sonnet-5",
                  provider="anthropic",
                  usage={"input_tokens": 1200, "output_tokens": 340},
                  cost={"input": 0.0036, "output": 0.0051},
              )
          root.score_trace("resolved", value=True, data_type="BOOLEAN")

  tracer.flush()
  ```

  ```typescript TypeScript theme={null}
  import * as atlan from "@atlanai/tools/sdk/how-tos/tracing";

  const tracer = atlan.init({ serviceName: "support-rover" });

  const visitor = await tracer.identify({
    identitySource: "slack",
    externalId: "U0123ABC",
    traits: { name: "Jordan Lee" },
  });

  await atlan.propagateAttributes(
    { sessionId: "ticket-4821", visitorId: visitor?.id },
    async () => {
      await tracer.startAsCurrentSpan("handle-ticket", { asType: "task" }, async (root) => {
        await tracer.startAsCurrentSpan("chat", { asType: "llm" }, async (generation) => {
          generation.update({
            model: "claude-sonnet-5",
            provider: "anthropic",
            usage: { input_tokens: 1200, output_tokens: 340 },
            cost: { input: 0.0036, output: 0.0051 },
          });
        });
        root.scoreTrace("resolved", true, { dataType: "BOOLEAN" });
      });
    },
  );

  await tracer.flush();
  ```
</CodeGroup>

Tracing uses its own credentials — `ATLAN_API_KEY` and `ATLAN_WORKSPACE_ID` —
and does not reuse the management bearer token. A process doing both configures
both.

Verify by reading the Agent's traces back rather than trusting the export:

```bash theme={null}
curl -sS --fail-with-body \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  "${ATLAN_GATEWAY_URL}/agent/v1/agents/agent_01example/traces?limit=5"
```

Then drill into one trace and its spans with
`GET /agent/v1/agents/{agent_id}/traces/{trace_id}` and
`.../traces/{trace_id}/spans`.

## 5. Record a completed run by hand

Only for a runtime you cannot instrument. Create the session with the
**original** execution timestamps, not the time you uploaded it.

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/agent/v1/sessions" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "rover-run-4821",
    "workspace_id": "workspace_01example",
    "subject_kind": "agent",
    "subject_id": "agent_01example",
    "session_status": "completed",
    "title": "Triage ticket 4821",
    "external_session_id": "run-4821",
    "model_id": "claude-sonnet-5",
    "stop_reason": "end_turn",
    "source_created_at": "2026-09-01T09:14:02Z"
  }'
```

| Field | Required | Values |
| - | - | - |
| `subject_kind` | yes | `agent` or `harness` |
| `subject_id` | yes | The Agent or harness id the run belongs to |
| `session_status` | yes | `running`, `completed`, or `failed` |
| `stop_reason` | no | Why the run ended |
| `error` | no | Failure detail when `session_status` is `failed` |
| `usage` | no | Token and cost accounting for the run |
| `external_session_id` | no | Your runtime's own id, for correlation |
| `visitor_external_id`, `visitor_name`, `visitor_email`, `visitor_type` | no | Who the run served, without calling the SDK |

Then append the transcript. `sequence_number` fixes the order, so send it
explicitly rather than relying on insertion order:

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/agent/v1/sessions/session_01example/messages" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "rover-run-4821-msg-1",
    "workspace_id": "workspace_01example",
    "sequence_number": 1,
    "message_role": "user",
    "content": "Customer cannot log in after the password reset."
  }'
```

The same two steps with the SDK:

<CodeGroup>
  ```python Python theme={null}
  session = client.sessions.create_record({
          "name": "rover-run-4821",
          "workspace_id": "workspace_01example",
          "subject_kind": "agent",
          "subject_id": "agent_01example",
          "session_status": "completed",
          "title": "Triage ticket 4821",
          "external_session_id": "run-4821",
          "model_id": "claude-sonnet-5",
          "stop_reason": "end_turn",
          "source_created_at": "2026-09-01T09:14:02Z",
      }
  )

  client.sessions.messages.create(
      session_id=session.id,
      {
          "name": "rover-run-4821-msg-1",
          "workspace_id": "workspace_01example",
          "sequence_number": 1,
          "message_role": "user",
          "content": "Customer cannot log in after the password reset.",
      },
  )

  transcript = client.sessions.messages.list(session_id=session.id)
  ```

  ```typescript TypeScript theme={null}
  const session = await client.sessions.createRecord({
      name: "rover-run-4821",
      workspaceId: "workspace_01example",
      subjectKind: "agent",
      subjectId: "agent_01example",
      sessionStatus: "completed",
      title: "Triage ticket 4821",
      externalSessionId: "run-4821",
      modelId: "claude-sonnet-5",
      stopReason: "end_turn",
      sourceCreatedAt: "2026-09-01T09:14:02Z",
    },
  });

  await client.sessions.messages.create({
    sessionId: session.id,
    {
      name: "rover-run-4821-msg-1",
      workspaceId: "workspace_01example",
      sequenceNumber: 1,
      messageRole: "user",
      content: "Customer cannot log in after the password reset.",
    },
  });

  const transcript = await client.sessions.messages.list({
    sessionId: session.id,
  });
  ```
</CodeGroup>

`message_role` is `system`, `user`, `assistant`, or `tool`. For a tool call,
use `message_role: "tool"` with `tool_name` and `tool_call_id` so the call
lines up with the assistant message that requested it.

<Warning>
  Session and message creates are **not** retried automatically, and a retried
  create can duplicate a record. `sequence_number` does not deduplicate — it
  only orders. Retry deliberately and read the transcript back before appending
  more.
</Warning>

For provider-run Agents, inspect provider-native activity through
`GET /agent/v1/sessions/{session_id}/events`. Use session messages for the
transcript records your harness uploads.

## 6. Attach a file artifact

Uploading is a two-step ticket flow: ask for a ticket, then send the bytes.

```bash theme={null}
curl -sS --fail-with-body \
  -X POST "${ATLAN_GATEWAY_URL}/file/v1/files/uploads" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"size_bytes": 20480}'
```

The `201` returns an upload ticket:

| Field | Meaning |
| - | - |
| `upload_key` | Identifies this upload |
| `upload_url` | Where to send the bytes |
| `method` | The HTTP method to use |
| `is_presigned` | `true` means `upload_url` is a direct storage URL, not the Gateway |
| `max_bytes` | Hard ceiling; a larger body is rejected `413` |
| `expires_in_seconds` | Ticket lifetime |

**Check `is_presigned` before sending.** When it is `true`, send the bytes to
`upload_url` as given and do **not** attach your Atlan token — that URL carries
its own authorization. When it is `false`, send them to the Gateway:

```bash theme={null}
curl -sS --fail-with-body \
  -X PUT "${ATLAN_GATEWAY_URL}/file/v1/files/uploads/upload_01example" \
  -H "Authorization: Bearer ${ATLAN_TOKEN}" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @./run-4821-transcript.json
```

With the SDK both steps are wrapped, so you do not have to branch on
`is_presigned` yourself:

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

  payload = Path("./run-4821-transcript.json").read_bytes()

  ticket = client.files.uploads.create({"size_bytes": len(payload)}
  )
  client.files.uploads.ingest(upload_key=ticket.upload_key, payload)
  ```

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

  const payload = await readFile("./run-4821-transcript.json");

  const ticket = await client.files.uploads.create({ sizeBytes: payload.byteLength },
  });
  await client.files.uploads.ingest({
    uploadKey: ticket.uploadKey,
    body: payload,
  });
  ```
</CodeGroup>

A successful upload returns `204` with no body. Confirm it by listing files:

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

`GET /file/v1/files/{file_id}/content` returns the current bytes, and
`GET /file/v1/files/{file_id}/versions/{version_ordinal}/content` a pinned
version. Both return the stored media type, not JSON — check the content type
before parsing, or you will write a corrupt file.

From the CLI, `atlanai file upload` and `atlanai file download` do this in one
command — see the [files guide](/tools/cli/how-tos/files).

## 7. Find and inspect Agents

Search and aggregation are `POST` because the query goes in the body:

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

With the SDK:

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

  everything = client.agents.list(limit=20)
  one = client.agents.get("agent_01example")
  traces = client.agents.traces.list("agent_01example", limit=10)
  ```

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

  const everything = await client.agents.list({ limit: 20 });
  const one = await client.agents.get({ agentId: "agent_01example" });
  const traces = await client.agents.traces.list({
    agentId: "agent_01example",
    limit: 10,
  });
  ```
</CodeGroup>

| Want | Use | SDK |
| - | - | - |
| One Agent by id | `GET /agent/v1/agents/{agent_id}` | `client.agents.get(agent_id)` |
| Every Agent in scope | `GET /agent/v1/agents` | `client.agents.list()` |
| Ranked matches | `POST /agent/v1/agents/search` | `client.agents.search(body)` |
| Counts grouped by a field | `POST /agent/v1/agents/aggregate` | `client.agents.aggregate(body)` |
| Its sessions | `GET /agent/v1/agents/{agent_id}/sessions` | `client.agents.sessions.list(agent_id)` |
| Its traces | `GET /agent/v1/agents/{agent_id}/traces` | `client.agents.traces.list(agent_id)` |
| One session's transcript | `GET /agent/v1/sessions/{session_id}/messages` | `client.sessions.messages.list(session_id)` |
| A live session's events | `GET /agent/v1/sessions/{session_id}/events/stream` | `client.sessions.stream_events(session_id)` |

All 79 agent operations are in the
[agent operations reference](/tools/sdk/references/operations/agents).

An agent doing this itself should use MCP rather than the API — see
[`registry__search`](/tools/mcp/registry/references/registry-search) and
[`registry__get_artifact`](/tools/mcp/registry/references/registry-get-artifact).

## Skills and Agents

Skills and Agents are related through the Registry's artifact graph, and that
graph is **readable but not publicly writable today**.

You can read the edges around any artifact:

* [`registry__get_related_artifacts`](/tools/mcp/registry/references/registry-get-related-artifacts) returns
  the exact incoming and outgoing edges for one artifact, each with its
  relationship type, direction, status, and the artifact at the other end.
* [`registry__search`](/tools/mcp/registry/references/registry-search) with `strategy: "group"`
  returns a ranked graph neighbourhood from a query.
* [`registry__get_artifact`](/tools/mcp/registry/references/registry-get-artifact) includes
  relationship context for a known artifact.

There is currently **no public endpoint or MCP tool that creates a
relationship**, so an Agent cannot be linked to a Skill through the public
surface. Two things that look like they would, and do not:

* `tools` on an Agent takes builtin and MCP tool references only, not Skill ids.
* `atlanai skill link` associates a **local directory** with an existing
  server-side Skill so later pushes append versions to it. It does not link a
  Skill to an Agent.

If you need that link, raise it with your Atlan contact rather than
constructing an edge another way. To hand a reviewed Skill to an agent runtime
in the meantime, see
[Discover and use a skill](/registry/skills/how-tos/discover-and-use) and
[the Skills cookbook](/cookbooks/how-tos/skills).

## Verify the result

A `2xx` confirms Atlan accepted the record. It does not confirm the record is
right. Before you rely on it:

1. Read the Agent back and confirm the workspace is the one you intended — not
   another your token can also reach.
2. Read the session back and compare its timestamps and `session_status`
   against the external runtime.
3. Confirm message `sequence_number`s are contiguous and the transcript reads
   in order.
4. For traces, confirm they appear under the Agent, not just that the export
   returned success.

## Related

<CardGroup cols={2}>
  <Card title="Agents in the product" icon="robot" href="/registry/agents/index">
    Lifecycle, sessions, and trace inspection.
  </Card>

  <Card title="Tracing SDK" icon="code" href="/tools/sdk/overview">
    Report runs as they happen instead of reconstructing them.
  </Card>

  <Card title="Errors and retries" icon="triangle-exclamation" href="/platform/references/errors-and-retries">
    Recover from a failed write without duplicating it.
  </Card>

  <Card title="Skills cookbook" icon="files" href="/cookbooks/how-tos/skills">
    Publish, retrieve, and inspect reusable Skills.
  </Card>
</CardGroup>
