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

# Build from scratch

> Author your own agent with the SDK — its spec, instructions, tools, and handler — then create it from the console, the API, or code.

The Komodor Agentic Operation Platform (KAOP) lets you author your own agent and keep the code
yours. You write three things — a spec that declares the agent's identity, a handler that does the
work, and whatever tool access that handler needs — and KAOP supplies everything around them:
registration, work delivery, evidence, secrets, and cost accounting. This page walks the whole path,
from installing the SDK to the surfaces you can create an agent from.

## What you actually write

| Piece            | What it is                                                                                  | Where it lives                   |
| ---------------- | ------------------------------------------------------------------------------------------- | -------------------------------- |
| **Agent spec**   | The declared identity: id, name, description, labels, input schema, default input, triggers | `agent-spec.yaml`                |
| **Instructions** | The agent's context or system prompt                                                        | `agent.md`, loaded automatically |
| **Handler**      | An async function that turns a run's input into the run's output                            | your worker module               |
| **Tools**        | Framework-native tools, plus connected integrations and MCP Gateway tools                   | your handler and the console     |
| **Skills**       | Procedures the agent follows, as Markdown files                                             | a `skills/` directory            |

Everything else — heartbeating, claiming runs, streaming evidence, resolving credentials — is the
SDK's job.

## Install the SDK

Agents are built with the `komodor-agentops` Python SDK, which requires Python 3.11 or later. The
runtime is framework-agnostic: your handler can be a plain function or a full agent loop, and
adapters for the common frameworks ship as extras.

```bash theme={null}
pip install komodor-agentops                 # core runtime
pip install "komodor-agentops[claude-code]"  # Claude Agent SDK adapter
pip install "komodor-agentops[langchain]"    # LangChain adapter
pip install "komodor-agentops[adk]"          # Google ADK adapter
pip install "komodor-agentops[agno]"         # Agno adapter
pip install "komodor-agentops[all]"          # every adapter
```

<Note>
  A Go SDK is also available if you would rather write the worker in Go. See
  [Go SDK](/developer-tools/go-sdk).
</Note>

## Lay the project out

Keep the agent's identity and knowledge as files next to the code. The SDK reads the whole directory
in one call, so there is nothing to register by hand.

```text theme={null}
my_agent/
  agent-spec.yaml     # identity, labels, input schema, triggers
  agent.md            # instructions / agent context (optional)
  worker.py           # handler + entrypoint
  skills/
    triage.md         # skills, published on registration (optional)
```

### The agent spec

`agent-spec.yaml` is the source of truth for what the agent *is*. It ships inside the image, so the
control plane learns the agent's shape from the worker itself rather than from a form somebody filled
in once.

```yaml theme={null}
schema_version: 1
agent_id: incident-summarizer
name: Incident Summarizer
description: Summarizes an incident into severity, likely root cause, and next steps.
owner: platform-team
repo: https://github.com/my-org/my-agents
source_path: agents/incident_summarizer
labels:
  category: incident-response
default_input:
  prompt: Summarize the latest incident.
triggers:
  - id: nightly
    type: schedule
    name: Nightly summary
    cron: "0 6 * * *"
```

| Field                            | Required | What it does                                                                                                                                          |
| -------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema_version`                 | Yes      | The spec format version                                                                                                                               |
| `agent_id`                       | Yes      | The stable identifier. Re-registering the same id updates the same agent                                                                              |
| `name` · `description`           | Yes      | What the console shows in Fleet and in pickers                                                                                                        |
| `owner` · `repo` · `source_path` | Yes      | Provenance — who maintains the agent and where its source lives                                                                                       |
| `labels`                         | No       | Metadata you can filter and group the fleet by                                                                                                        |
| `input_schema`                   | No       | A JSON Schema for structured input. Declaring one makes the **Run agent** dialog open in JSON mode and adds a **View input schema** view on the agent |
| `default_input`                  | No       | Sample values used to prefill that dialog                                                                                                             |
| `triggers`                       | No       | Cron schedules the agent declares for itself, registered when it connects. See [Triggers & schedules](/manage-your-agents/build/triggers-schedules)   |
| `model`                          | No       | A last-resort default model, overridden by whatever the deployment specifies                                                                          |

<Note>
  The instructions themselves do not go in the spec. Put them in `agent.md` next to it —
  `AgentSpec.from_dir` picks that file up automatically, and the console shows the same content on the
  agent's **Agent instructions** step.
</Note>

## Write the handler

A worker is a spec, an async handler, and a run call.

```python theme={null}
from pathlib import Path
from typing import Any

from komodor_agentops import AgentOpsWorker, AgentSpec, Run

AGENT_SPEC = AgentSpec.from_dir(Path(__file__).parent)

async def handler(run: Run) -> dict[str, Any]:
    # A run's input is a plain dict. `prompt` carries free text; anything your
    # input_schema declares arrives alongside it.
    prompt = run.input.get("prompt", "")

    summary = await summarize_incident(prompt)

    return {
        # Structured channel — machine-readable fields.
        "severity": summary.severity,
        "root_cause": summary.root_cause,
        # Descriptive channel — the reserved `text` key holds human-readable Markdown.
        "text": summary.markdown,
    }

def main() -> None:
    AgentOpsWorker(agent=AGENT_SPEC, on_run=handler).run()

if __name__ == "__main__":
    main()
```

`.run()` sets up logging, pulls the credentials bound to the agent, opens the connection to the
control plane, and starts heartbeating. If you need to embed the worker in an existing service,
`await AgentOpsWorker(agent=AGENT_SPEC, on_run=handler).serve()` gives you the same runtime without
taking over the process.

<Note>
  The worker binds no port and serves nothing. It dials out and receives its work over that one
  connection, so there is no inbound firewall rule to open and nothing to route traffic to.
</Note>

Raising from the handler fails the run and reports the exception as the error. There is deliberately
no way to return a failed result. A cancellation from the control plane arrives as a cancelled
`await` inside your handler — let it propagate rather than swallowing it.

### What else arrives on the run

Beyond `run.input`, the handler receives context it would otherwise have to fetch:

| On `run`             | What it is                                                                                                    |
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
| `run.run_id`         | The control plane's id for this run — what its evidence and artifacts are filed under                         |
| `run.secrets`        | The credentials delivered with this run, already applied to the environment and registered for output masking |
| `run.snapshot`       | Session state a previous run of this agent left behind, or `None` on a fresh run                              |
| `run.mcp_group_name` | The integration group bound to this agent, if any                                                             |

Three things are reported separately rather than returned: `run.set_snapshot({...})` leaves state for
the next run, `run.set_diagnostics({...})` records run mechanics such as turn counts and stop
reasons, and `record_usage({...})` reports token usage so the run's cost is attributed correctly.

### Two output channels

A result can carry both structured fields and descriptive text, and both live in the single dict
your handler returns:

* **Structured output** — your own top-level keys. This is what a script, an MCP client, or a later
  workflow step reads to make a decision.
* **Descriptive output** — the reserved `text` key, holding Markdown for a person. The console,
  Slack, and history render it when someone opens the run.

An automation-only agent can return structured fields alone. An agent whose result a human reads
should provide both, and they should agree — do not bury machine values inside prose, and do not
invent a second human-readable field.

## Give it tools

Tools reach your agent from three directions, and they compose:

<Tabs>
  <Tab title="Framework-native">
    Whatever your framework already offers — Claude Agent SDK tools, LangChain tools, ADK tools.
    These live inside your handler and the adapters record their calls as evidence automatically.
  </Tab>

  <Tab title="Connected systems">
    A [built-in integration](/manage-your-agents/build/built-in-integrations) or a tool you
    registered through the [MCP Gateway](/manage-your-agents/build/mcp-gateway). These are configured
    in the console rather than in code, which is what lets you change what an agent may reach without
    a redeploy.
  </Tab>

  <Tab title="Platform tools">
    KAOP exposes its own tool surface to workers as an internal MCP server named
    `agentops_internal` — including `search_knowledge`, which queries the
    [knowledge base](/manage-your-agents/build/knowledge-base). A worker opts in rather than getting
    it automatically.
  </Tab>
</Tabs>

Remember the boundary: there are exactly two ways for an agent to reach an external system — the
built-in integration catalog, or the MCP Gateway. See
[Integrations overview](/manage-your-agents/build/integrations-overview).

## Emit evidence

Your agent can stream the same evidence the Komodor-built agents do, so a run you authored is just
as auditable as one you installed.

| What              | How                                                                                                                   |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| Trace spans       | Wrap a function with `@observe(name="...", as_type="tool")` to record a nested span                                   |
| Logs              | `await run.log("Fetching logs for pod api-7d9f")`                                                                     |
| Progress messages | `await run.message("Correlating with recent deploys")`                                                                |
| Tool calls        | `async with run.tool("kubectl_get", {"kind": "pod"}) as call:` — this records the call; your framework still makes it |
| Artifacts         | `await run.upload_artifact(name=..., content=..., mime_type=...)` attaches a file or report to the run                |

If you use a framework adapter, most of this is automatic — model calls, tool calls, and token usage
are recorded without extra code. See
[Runs & evidence](/manage-your-agents/run/runs-evidence).

## Use secrets, do not bake them in

Store API keys as [credentials](/manage-your-agents/build/credentials-secrets) and bind them to the
agent. The SDK then delivers them:

* **Per run** — bound credentials arrive with the run. Call `apply_secret_to_env("ANTHROPIC_API_KEY")`
  or `get_secret(...)` inside the handler.
* **At boot** — for tokens the worker needs before any run, nothing is required: it pulls the
  on-demand credentials bound to it at start-up.

Values are delivered in memory and never written into the run's evidence.

## Test it locally

The SDK installs an `agentops-run-worker` command that calls your handler once, with no control
plane involved.

```bash theme={null}
agentops-run-worker \
  --handler my_agent.worker:handler \
  --input-json '{"prompt": "Summarize incident 42"}' \
  --env-file .env \
  --output output.json \
  --artifact-dir artifacts/
```

It loads your env files, exposes detected secrets the way a real run does, builds a `Run` from the
flags, writes the returned output to `--output`, and drops anything the handler uploaded into
`--artifact-dir`. The same flow is available in code as `run_worker_once` and
`run_worker_once_sync`, which makes it straightforward to assert on your handler in tests.

## Make it available in chat

Chat is opt-in per agent, advertised on its agent card:

```python theme={null}
AGENT_SPEC = AgentSpec.from_dir(
    Path(__file__).parent,
    agent_card={"capabilities": {"chat": True, "streaming": True}},
)
```

See [Chat & history](/manage-your-agents/run/chat-history).

## The create surfaces

Writing the code is one half; the agent also has to exist in the control plane. Two surfaces do
that, and they produce the same agent.

<Tabs>
  <Tab title="Console">
    **Create an agent** opens on a starting-point picker with three choices — **Start from catalog**,
    **Build from scratch**, and **Import an existing agent**. Choose *Build from scratch* and the
    wizard walks you through **Agent card**, **Where it runs**, **Agent instructions**, **Model**,
    **MCP tools**, and **Triggers**, then an **Activate** step that registers the agent, mints a
    worker token — shown once — and hands you what your worker needs to start. Your progress is saved
    as a draft as you go, so you can leave and come back.
  </Tab>

  <Tab title="API">
    `POST /api/v1/agents` creates an agent, and `POST /api/v1/agents/worker-token` mints a worker
    token for one. Use these when agent creation is part of your own provisioning. See
    [APIs](/developer-tools/apis).
  </Tab>
</Tabs>

Either way the agent starts life as a **draft** and goes live on its worker's first heartbeat. There
is no separate activation step.

### Importing an agent you already run

If the agent already exists on your own infrastructure, the wizard's **Import an existing agent**
path connects it rather than deploying anything. Pick the framework it is built with — Google ADK,
LangChain, Claude Agent SDK, Agno, or something else — and the wizard generates the wrapper snippet,
three environment variables, and a one-time worker token.

An imported agent keeps its instructions, model, tools, and skills **in your code**, where the
console does not change them. What the import collects is the parts KAOP owns: its identity and
labels, its triggers, and its roles and bound secrets.

<Note>
  Re-registering an agent name that already exists reuses the same agent rather than creating a second
  one — it rotates the token and redeploys. A workspace can hold up to 60 active agents; archiving one
  frees a slot.
</Note>

## Bring the framework you already use

The runtime does not care how your handler reaches its conclusion, and the adapters make that
concrete: whichever framework you install, the same worker runtime, evidence trail, and output
contract sit underneath.

| Framework            | What the adapter gives you                                                          |
| -------------------- | ----------------------------------------------------------------------------------- |
| **Claude Agent SDK** | An instrumented query call — tool use and cost are recorded from its hooks          |
| **LangChain**        | A callback handler that records model and tool calls from a chain or agent executor |
| **Google ADK**       | Instrumentation for a Runner, with tool calls and sub-agents arriving as spans      |
| **Agno**             | A run handler wrapping a single agent or a team                                     |
| Anything else        | The bare worker API — a plain async function, wired up however you like             |

<Tip>
  The clearest reference for what a well-formed agent looks like is a Komodor-built one. Deploy a
  catalog investigator, run it, and read the run's evidence trail — that is the shape your own agent's
  runs should have. See [Use specialized agents](/manage-your-agents/build/use-specialized-agents).
</Tip>

## Patterns worth copying

* **Return both channels.** Structured fields for automation, `text` for the person reading it.
* **Keep identity in the spec.** `agent-spec.yaml` next to the code means the agent's shape is
  versioned with the agent.
* **Keep procedures in files.** A documented process belongs in `skills/`, not in the handler.
* **Let configuration be configuration.** What starts the agent and what it may reach are set in the
  console, not compiled in.

## Next steps

<CardGroup cols={2}>
  <Card title="Deploy an agent" href="/manage-your-agents/build/deploy-an-agent">
    Turn the worker into a running process and watch it register.
  </Card>

  <Card title="Skills" href="/manage-your-agents/build/skills">
    Package the procedures your agent should follow.
  </Card>

  <Card title="Credentials & secrets" href="/manage-your-agents/build/credentials-secrets">
    Store and bind the secrets the handler resolves.
  </Card>

  <Card title="Python SDK" href="/developer-tools/python-sdk">
    The full SDK surface.
  </Card>
</CardGroup>


## Related topics

- [Credentials & secrets](/manage-your-agents/build/credentials-secrets.md)
- [Providers](/manage-your-agents/build/providers.md)
- [Skills](/manage-your-agents/build/skills.md)
- [Use cases](/manage-your-agents/build/use-cases.md)
- [Knowledge base](/manage-your-agents/build/knowledge-base.md)
