> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Reporting telemetry from an external agent

> Send traces from an agent that runs outside Runtype and view its runs in the dashboard

Use this guide to send telemetry from an agent that runs outside Runtype. Runtype
reads traces from Flue, the Vercel AI SDK, LangGraph, the OpenAI Agents SDK,
Mastra, OpenClaw, Hermes, [Pi](#pi-adapter), the
[Cloudflare Agents SDK](#cloudflare-agents-sdk), and your own agent loop. If your agent emits OpenTelemetry data, configure the
exporter without a Runtype SDK.

Runtype shows each imported run on the receiving agent's **Runs** view. Your
agent keeps using its existing model provider and credentials.
Runtype does not run the agent or bill the imported run as a Runtype run.

## Before you begin

You need the following:

* A Runtype agent that receives the runs. Runtype files every run under an
  agent, so it rejects a trace that names no agent.
* An API key with the `TELEMETRY:WRITE` scope. Keys with `AGENTS:WRITE` also
  work. See [Create a telemetry API key](#create-a-telemetry-api-key).
* If you send OTLP, configure a trace exporter. The OpenClaw, Hermes, and Pi
  plugins include one. Other frameworks need an OpenTelemetry SDK with an
  OTLP/HTTP trace exporter. See
  [Configure the OTLP/HTTP exporter](#configure-the-otlphttp-exporter).

The **Connect an agent** page in the dashboard can set up the first two for
you. To use it, follow these steps:

1. Open the **Connect an agent** page. Inside a product, click **Connect an
   agent** in the product's dock or on an empty product's architecture, or
   click **New > Connect an agent**. The agent joins the product as a capability when its
   first run arrives. Outside a product, the **Agents** page links to the page
   in a workspace with no agents, and the command palette lists it as
   **Connect an agent**.
2. Select your framework.
3. Under the framework chips, select the agent that receives the runs, or
   create one from the same list. In a workspace with no agents, the page
   creates the agent for you.
4. Under **What you want**, select **Only see its runs**. To also let Runtype
   call the agent over A2A, keep **Let Runtype call it**, which includes its
   runs. OpenClaw, Hermes, and Pi support only **Only see its runs**. For the call
   setup, see
   [Connect an agent from the dashboard](/developer-guides/guides/bring-your-own-agent#connect-an-agent-from-the-dashboard).
5. Under **Who sets it up**, select **Your coding agent** or **Yourself**.

With **Your coding agent**, copy the setup prompt and paste it into a coding
agent, such as Claude Code, Cursor, or Codex, in your project. The prompt
carries the install command, environment variables, and framework-specific code
or CLI commands. It contains no key. Instead, it tells the coding agent to
request a test key that can only send telemetry, with a command similar to the
following:

**`Request a telemetry key`**

```bash title="Request a telemetry key"
runtype keys request --name "Support agent telemetry" --scopes TELEMETRY:WRITE --env test --reason "Send Support agent traces to Runtype" --inline
```

You approve the request in the browser. The coding agent then uses the printed
key wherever the prompt shows the `rt_YOUR_TELEMETRY_KEY` placeholder.
`runtype keys` is an alias of `runtype api-keys`.

With **Yourself**, follow the numbered steps. Flue, the AI SDK, LangGraph, and
the OpenAI Agents SDK have four: **Install**, **Add your key**, **Instrument**,
and **Import it first**. Mastra has the first three. For Cloudflare Agents, **Add your key** configures a Workers
Observability destination, and the last step is **Name the destination**. A
step is marked done after you copy its block. If no run arrives, expand **Not
seeing runs?** for the common causes.

## Create a telemetry API key

In a workspace with no agents, **Connect an agent** creates a
`TELEMETRY:WRITE` test key for you. A test key stays revealable, so a later
visit to the page fills the blocks again. Store the key as `RUNTYPE_API_KEY`
for OpenClaw, Pi, and the exporter-based recipes. Hermes reads the same key from
`RUNTYPE_OTEL_API_KEY`.

In a workspace that already has agents, select the receiving agent, select
**Yourself**, and then click **Create a test key** in the **Add your key**
step. The page shows **Test key created** with the masked key and fills the key
into the environment block.

To create a production key, click **Create a live key**. A production key is
shown once, in a dialog, and you can't reveal it afterwards. The page doesn't
fill it into the blocks, so paste it wherever `rt_YOUR_TELEMETRY_KEY` appears.

You can also create the key on the dashboard **API Keys** page. Select the
**Telemetry Ingest** permission group, which grants the `TELEMETRY:WRITE`
scope.

We recommend a dedicated key, because the key can appear in collector
configuration, container environments, and other shared locations. Ingest only
appends data, so the key needs no read or management permissions. An API key
can't create another API key. Create the telemetry key in the dashboard, or
request one with `runtype keys request` and approve the request in the browser.

## Configure the OTLP/HTTP exporter

For a framework that uses an OpenTelemetry SDK, set one base endpoint and
identify the Runtype agent that receives the runs:

**`Environment variables`**

```bash title="Environment variables"
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.runtype.com/v1/otel
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer rt_YOUR_API_KEY,x-runtype-agent-id=agent_YOUR_AGENT_ID
```

Replace the following:

* `rt_YOUR_API_KEY`: the telemetry API key.
* `agent_YOUR_AGENT_ID`: the ID of the Runtype agent that receives the runs.

If you used **Connect an agent** with a test key, its **.env** block already
contains both values.

OpenTelemetry appends `/v1/traces`, `/v1/metrics`, and `/v1/logs` to the base
endpoint for each signal, so one endpoint configures all three signals.

Runtype accepts OTLP over HTTP, not OTLP over gRPC. If your SDK defaults to
gRPC, set the HTTP protocol:

**`Use when the SDK defaults to gRPC`**

```bash title="Use when the SDK defaults to gRPC"
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
```

Restart the agent. The next exported run appears in the agent's **Runs** view.

## Configure the exporter in code

If your application builds the OpenTelemetry pipeline in code, configure the
trace exporter with the same endpoint.

For a Node.js application, use the following TypeScript configuration:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'https://api.runtype.com/v1/otel/v1/traces',
    headers: {
      authorization: `Bearer ${process.env.RUNTYPE_API_KEY}`,
      'x-runtype-agent-id': 'agent_YOUR_AGENT_ID',
    },
  }),
})
sdk.start()

// A batch processor holds spans for about 5 seconds, so a run that exits first sends none.
process.once('beforeExit', () => void sdk.shutdown().catch(console.error))
```

Replace `agent_YOUR_AGENT_ID` with the ID of the Runtype agent that receives
the runs.

For a Python application, install `opentelemetry-exporter-otlp-proto-http` and
configure the HTTP trace exporter as follows:

**`Python SDK`**

```python title="Python SDK"
import os

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

exporter = OTLPSpanExporter(
  endpoint="https://api.runtype.com/v1/otel/v1/traces",
  headers={
    "authorization": f"Bearer {os.environ['RUNTYPE_API_KEY']}",
    "x-runtype-agent-id": "agent_YOUR_AGENT_ID",
  },
)
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
```

Replace `agent_YOUR_AGENT_ID` with the ID of the Runtype agent that receives
the runs. The Python gRPC exporter is the default, so select the HTTP exporter
or set `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`.

## Verify that runs arrive

To check the endpoint, the API key, and the content type, send an empty
OTLP/JSON export:

**`cURL smoke test`**

```bash title="cURL smoke test"
curl -X POST https://api.runtype.com/v1/otel/v1/traces \
  -H "Authorization: Bearer $RUNTYPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"resourceSpans": []}'
```

Set `RUNTYPE_API_KEY` to the telemetry API key before you run the command. The
endpoint returns `HTTP 200` with `{}` when it accepts the empty export.

While **Connect an agent** waits for the first run, the dock shows **Waiting
for the first trace from AGENT\_NAME.**, where `AGENT_NAME` is the receiving
agent's name. The progress panel beside the steps counts the completed steps,
for example **2 of 4 complete**.

After the first run arrives, the page shows **AGENT\_NAME is connected**. If you
selected **Let Runtype call it**, the page shows it after you also connect the
agent. Click **Open run** to open the run. Inside a product, click **Go to the
product canvas** to return to the product. Under **Next, if you want**, the
page offers **Let Runtype call it** when you chose only to see runs, and
**Switch to a live key**. After the agent joins a product, the page also offers
**Add a web chat surface**. Click **Copy prompt** to copy a prompt that asks your
coding agent to add a web chat surface to the product.

If a run arrives but its spans don't identify the framework that you selected,
the **Framework** step shows **A trace arrived, but nothing confirms it came
from FRAMEWORK. Still watching.** If the spans identify a different framework,
it shows **Received a FRAMEWORK run; still waiting for FRAMEWORK.** instead,
naming both frameworks. Check that the instrumentation or native plugin loads
before the agent runs.

To confirm from the API, list the agent's imported runs with a key that has
`AGENTS:READ`. A `TELEMETRY:WRITE` key can't read runs.

**`List imported runs`**

```bash title="List imported runs"
curl "https://api.runtype.com/v1/runs?agentId=agent_YOUR_AGENT_ID&source=ingested" \
  -H "Authorization: Bearer rt_YOUR_READ_API_KEY"
```

Replace the following:

* `agent_YOUR_AGENT_ID`: the ID of the receiving agent.
* `rt_YOUR_READ_API_KEY`: an API key with `AGENTS:READ`.

## Send traces from a supported framework

Each framework in this table needs an OpenTelemetry provider and exporter
behind its instrumentation. Instrumentation alone emits spans that nothing
sends, and the run never appears. OpenClaw, Hermes, and Pi use native plugins
instead.

| Framework         | What to install                                                                                                                             | What to enable                                                         |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Flue              | `@flue/runtime`, `@runtypelabs/flue-otel`, `@opentelemetry/api`, `@opentelemetry/sdk-trace-node`, `@opentelemetry/exporter-trace-otlp-http` | `instrument(createRuntypeFlueInstrumentation())`                       |
| Vercel AI SDK     | `@opentelemetry/sdk-node`, `@opentelemetry/exporter-trace-otlp-proto`                                                                       | `experimental_telemetry: { isEnabled: true, functionId: 'my-agent' }`  |
| LangGraph         | `openinference-instrumentation-langchain`, `opentelemetry-exporter-otlp-proto-http`                                                         | `LangChainInstrumentor().instrument(tracer_provider=provider)`         |
| OpenAI Agents SDK | `openinference-instrumentation-openai-agents`, `opentelemetry-exporter-otlp-proto-http`                                                     | `OpenAIAgentsInstrumentor().instrument(tracer_provider=provider)`      |
| Mastra            | `@mastra/core`, `@mastra/observability`, `@mastra/otel-exporter`, `@opentelemetry/exporter-trace-otlp-proto`                                | An `OtelExporter` on the `Mastra` constructor's `observability` config |

When you select **Yourself**, **Connect an agent** shows this setup for each
framework as copyable blocks, pointed at the API host your account uses. The
instrumentation file is `instrumentation.ts` for Flue and the Vercel AI SDK, `instrumentation.py` for
LangGraph and the OpenAI Agents SDK, and `src/mastra/index.ts` for Mastra. The
**Your entry point** block imports that file before the agent runs. The Vercel
AI SDK also shows **Your generateText call**, because it turns telemetry on per
call. Mastra shows **Your agent run** instead of an entry point block.

OpenClaw, Hermes, and Pi use native plugins. Each has three setup blocks:
**Terminal**, then an environment block, then a CLI block. The environment and
CLI blocks are **Gateway environment** and **OpenClaw CLI** for OpenClaw,
**Hermes environment** and **Hermes CLI** for Hermes, and **Shell environment**
and **Pi CLI** for Pi.

The **Cloudflare Agents** chip sets up the Workers Observability path that the
Cloudflare Agents SDK section describes. Its blocks are **Terminal**,
**Cloudflare destination**, `src/traced-ai.ts`, **Your onChatMessage**, and
`wrangler.jsonc`. The **Any OTel source** chip has no
install block. It shows the environment block and the attributes that Runtype
reads.

Mastra's exporter reads neither `OTEL_EXPORTER_OTLP_ENDPOINT` nor
`OTEL_EXPORTER_OTLP_HEADERS`, and it needs the full trace path. Pass the
endpoint and headers to `OtelExporter`:

**`Mastra`**

```typescript title="Mastra"
new OtelExporter({
  provider: {
    custom: {
      endpoint: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
      headers: {
        authorization: `Bearer ${process.env.RUNTYPE_API_KEY}`,
        'x-runtype-agent-id': process.env.RUNTYPE_AGENT_ID,
      },
      protocol: 'http/protobuf',
    },
  },
})
```

`OtelExporter` loads the exporter package that its `protocol` names, and it
treats that package as optional, so `npm i @mastra/otel-exporter` does not
install it. Install `@opentelemetry/exporter-trace-otlp-proto` alongside it for
`http/protobuf`. Without it, the exporter logs one console error and sends
nothing.

An agent authored in Runtype and self-hosted with `@runtypelabs/runtime` needs
none of this. Set `RUNTYPE_API_KEY` on the deployment and the runtime exports
the same spans itself, with conversation content on by default. See
[Self-hosting (BYOC)](/developer-guides/guides/self-hosting).

The exporter-based frameworks report the model, token usage, cost, tool calls,
transcript, and final output when their instrumentation sends those values.
Flue also reports the iteration count, stop reason, and approval decisions.
The OpenClaw and Hermes adapters, and the Pi coding agent extension, send
content only when you enable capture.

### Flush before the process exits

A batch span processor holds spans for about 5 seconds, and it exports child
spans before their parents. A process that exits inside that window loses the
closing `invoke_agent` span. Without that span, the run shows as still running.
A short-lived run, such as a CLI invocation or a serverless handler, hits this
every time.

Shut the pipeline down before the process ends. In an `instrumentation.ts` file
that owns the SDK or the provider, register the flush beside it:

**`Flush an SDK or provider on exit`**

```typescript title="Flush an SDK or provider on exit"
process.once('beforeExit', () => void sdk.shutdown().catch(console.error))
```

For Mastra, call `mastra.shutdown()`, which flushes the batch processor. Put
it in a `finally` block, because a run that threw is the one whose closing span
matters most. In a serverless handler, call `await provider.forceFlush()`
before you return. The Python SDK's `TracerProvider` registers its own exit
handler, so the LangGraph and OpenAI Agents SDK setups need no extra step.

### OpenClaw adapter

The OpenClaw plugin uses Gateway hooks to export a trace for each observed
agent turn. It reports an `invoke_agent` span, an attempt-level `chat` span,
and `execute_tool` spans for observed completed tools. The `chat` span does
not describe each physical provider request. You don't need a separate
OpenTelemetry provider.

Review the plugin and its requested capabilities before granting conversation
access. Install and enable `@runtypelabs/openclaw-adapter`:

**`OpenClaw CLI`**

```bash title="OpenClaw CLI"
openclaw plugins install @runtypelabs/openclaw-adapter
openclaw plugins enable runtype
openclaw config set plugins.entries.runtype.hooks.allowConversationAccess true --strict-json
```

The `allowConversationAccess` grant lets the plugin receive model and
conversation hooks.

Inspect the loaded plugin without `--json`: JSON output can expose environment
values.

Set these variables on the OpenClaw Gateway process:

**`Gateway environment`**

```bash title="Gateway environment"
RUNTYPE_AGENT_ID=agent_YOUR_AGENT_ID
RUNTYPE_API_KEY=rt_YOUR_TELEMETRY_KEY
RUNTYPE_CAPTURE_CONTENT=true
```

Replace `agent_YOUR_AGENT_ID` with the receiving agent's ID and
`rt_YOUR_TELEMETRY_KEY` with a `TELEMETRY:WRITE` key. The adapter posts to
`https://api.runtype.com/v1/otel/v1/traces` by default. For another Runtype
environment, set `RUNTYPE_OTLP_URL` to its full OTLP trace URL.

Restart the Gateway to load the plugin and inspect its runtime hooks:

**`Check OpenClaw plugin`**

```bash title="Check OpenClaw plugin"
openclaw gateway restart
openclaw plugins inspect runtype --runtime
```

Content capture is off by default. Set `RUNTYPE_CAPTURE_CONTENT=true` to send
bounded prompt, answer, and tool content for transcripts and eval case capture.
Omit that line from the example for a metadata-only trace. The plugin redacts
configured secret values, but that filter cannot identify every secret in an
agent's content. If OpenClaw supplies no content for a turn, the run has no
transcript to capture as an eval case.

Send a normal OpenClaw message after restarting the Gateway, then check the
receiving agent's **Runs** view. OpenClaw may emit `llm_output` after
`agent_end`. The adapter waits up to one second for pending model or tool
output before exporting the available facts. Telemetry failures don't fail the
agent turn, and the plugin has no durable retry queue.

### Hermes adapter

The Hermes plugin uses Hermes observer hooks. It sends one OTLP trace per
observed agent turn, with an `invoke_agent` span, model activity, and completed
tool calls. You don't need a separate OpenTelemetry provider.

Install and enable the plugin from its public repository. It uses only the
Python standard library, so it needs no `pip` install.

**`Hermes CLI`**

```bash title="Hermes CLI"
hermes plugins install runtypelabs/hermes-runtype-otel --enable
```

Set these variables in the Hermes gateway or CLI process environment:

**`Hermes environment`**

```bash title="Hermes environment"
RUNTYPE_AGENT_ID=agent_YOUR_AGENT_ID
RUNTYPE_OTEL_API_KEY=rt_YOUR_TELEMETRY_KEY
RUNTYPE_OTEL_CAPTURE_CONTENT=true
```

Replace `agent_YOUR_AGENT_ID` with the receiving agent's ID and
`rt_YOUR_TELEMETRY_KEY` with a `TELEMETRY:WRITE` key. The adapter posts to
`https://api.runtype.com/v1/otel/v1/traces` by default. For another Runtype
environment, set `RUNTYPE_OTEL_ENDPOINT` to its full OTLP trace URL.

Content capture is off by default. Set `RUNTYPE_OTEL_CAPTURE_CONTENT=true`
to send bounded prompt, answer, and tool content for transcripts and eval case
capture. Omit that line from the example for a metadata-only trace. The plugin
redacts configured API keys and common key patterns, but that filter cannot
identify every secret in an agent's content.

Restart a running gateway with `hermes gateway restart`, or run one CLI turn
with `hermes chat --oneshot -q "Reply with one sentence"`. A successfully
exported turn appears in the receiving agent's **Runs** view.

Hermes can return a provider error before it emits the terminal
`on_session_end` hook. The plugin cannot treat `api_request_error` as terminal
because Hermes may retry or switch providers. A long-running gateway therefore
doesn't export that trace immediately. Turns that reach `on_session_end`
report an observed failure or interruption as an error.

### Pi adapter

`@runtypelabs/pi-otel` reports [Pi](https://pi.dev) agent runs. It has one
binding for each Pi runtime:

* `@runtypelabs/pi-otel/coding-agent` is an extension for the Pi coding agent.
  The **Pi** chip on **Connect an agent** sets it up.
* `@runtypelabs/pi-otel/durable` observes a pi-durable harness, including one
  hosted by the Cloudflare Agents SDK's `PiHarness`.

Both bindings send OTLP/JSON to Runtype directly, so you don't need a separate
OpenTelemetry provider. Each Pi run becomes one Runtype run, with an
`invoke_agent` span, one `chat` span for each model response, and one
`execute_tool` span for each tool result. Runtype prices the run from the
model name. Upstream, pi-durable is experimental and `PiHarness` is in beta.
The package supports pi-durable and the Pi coding agent `1.0.x` only.

#### Pi coding agent

Install the package with Pi's package manager:

**`Pi CLI`**

```bash title="Pi CLI"
pi install npm:@runtypelabs/pi-otel
```

Set these variables in the shell that starts Pi:

**`Shell environment`**

```bash title="Shell environment"
export RUNTYPE_AGENT_ID=agent_YOUR_AGENT_ID
export RUNTYPE_API_KEY=rt_YOUR_TELEMETRY_KEY
export RUNTYPE_CAPTURE_CONTENT=true
```

Replace `agent_YOUR_AGENT_ID` with the receiving agent's ID and
`rt_YOUR_TELEMETRY_KEY` with a `TELEMETRY:WRITE` key. The extension posts to
`https://api.runtype.com/v1/otel/v1/traces` by default. For another Runtype
environment, set `RUNTYPE_OTLP_URL` to its full OTLP trace URL.

Content capture is off by default, because a coding session carries source
code. Set `RUNTYPE_CAPTURE_CONTENT=true` to send prompts, answers, and tool
content for transcripts and eval case capture. Omit that line for a
metadata-only trace.

Start `pi` in the same shell and send a prompt. A run is one prompt until Pi
settles, so retries and compaction stay in the same run. The run appears in
the receiving agent's **Runs** view when Pi finishes answering. Without a key
or an agent ID, the extension does nothing. Set `RUNTYPE_DEBUG=true` to print
the reason to stderr.

#### pi-durable and `PiHarness`

Install the package next to pi-durable:

**`Terminal`**

```bash title="Terminal"
npm install @runtypelabs/pi-otel
```

Call `observePiDurable` right after you open the harness, and await
`recover` before any work resumes. With `PiHarness`, do both in the harness
factory, before you return the harness:

**`PiHarness factory`**

```ts title="PiHarness factory"
import { observePiDurable } from '@runtypelabs/pi-otel/durable'

harness: async ({ storage, context }) => {
  const pi = await Harness.open(storage, { models, registry }, context)
  this.telemetry = observePiDurable(pi, {
    agentId: this.env.RUNTYPE_AGENT_ID,
    apiKey: this.env.RUNTYPE_API_KEY,
    harnessId: this.ctx.id.toString(),
  })
  await this.telemetry.recover(context)
  return pi
}
```

`harnessId` must be stable for the harness's storage. pi-durable numbers
conversations per storage, so it keeps runs from different Durable Objects
apart. `recover()` picks up a run that was in flight when the Durable Object was
evicted, so it continues in the same Runtype run. Await it before any work
resumes. Do not attach through `this.harness.pi()`. It resolves only after
`PiHarness` has resumed Pi's work, so the observer misses work that resumes
after an eviction. Each input submission becomes one run. A submission that ends `unanswered` is a
failed run.

The observer sends each model and tool span as Pi commits it, and the
`invoke_agent` span when the submission settles. In a Worker, keep the request
open for delivery with `ctx.waitUntil(this.telemetry.flush())` after each
prompt. In Node, `await observer.flush()` before the process exits.

The durable binding sends content by default. Pass `content: false` for a
metadata-only trace, which also turns off eval capture. To drop one kind of
content, set that kind to `false`, for example
`content: { systemInstructions: false }`. A `redact` function can rewrite or
drop each value before it is sent. For every option, see the
[package README](https://www.npmjs.com/package/@runtypelabs/pi-otel).

Report each Pi run through one producer only. Two producers for the same run
send two `invoke_agent` spans, and Runtype counts its tokens and cost twice.

### Cloudflare Agents SDK adapter

The Cloudflare Agents SDK adapter, `@runtypelabs/cloudflare-agents`, is a beta
integration published to npm. The **Cloudflare Agents** chip on
**Connect an agent** sets up the
[native Workers Observability path](#cloudflare-agents-sdk), not this adapter.
Use the adapter when you need durable delivery of each user request from your
own Worker.
The adapter is validated against `agents@0.22.0`,
`@cloudflare/ai-chat@0.11.0`, and `ai@6.0.277`.

The adapter records each user request through a companion Durable Object and
sends OTLP/HTTP to Runtype directly. It does not need a Cloudflare trace
exporter. If you use Cloudflare tracing for Workers diagnostics, keep it on.
Those traces still appear in **Logs**, but they don't complete an adapter run.

The adapter needs a second Durable Object class beside your `AIChatAgent`, with
its own SQLite migration and these Worker-only bindings:

* `RUNTYPE_AGENT_ID`: the Runtype agent that owns these imported runs.
* `RUNTYPE_API_KEY`: a `TELEMETRY:WRITE` key stored with `wrangler secret put`.
* `RUNTYPE_BASE_URL`: `https://api.runtype.com`.

Keep all three bindings server-side. Don't send the API key to the browser,
place it in a WebSocket frame, or put it in a client bundle. Export the
companion class from the Worker module and add its binding and SQLite migration
alongside your Agent class:

**`wrangler.jsonc`**

```jsonc title="wrangler.jsonc"
{
  "durable_objects": {
    "bindings": [{ "name": "AdapterTelemetryOutbox", "class_name": "RuntypeOtlpOutbox" }],
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["RuntypeOtlpOutbox"] }],
}
```

Use a stable application submission ID for one user request, a separate
conversation ID for attribution, and the Agents SDK request ID as the exchange
ID. Generate a new submission ID for an explicit rerun. Keep the same one for
transport retries and reconnects. Don't derive any of these IDs from a
Cloudflare trace, a message's position in a conversation, or the latest message
body.

The following sample wraps a `streamText` call in an `AIChatAgent`:

**`Cloudflare Agents SDK adapter`**

```ts title="Cloudflare Agents SDK adapter"
import { AIChatAgent } from '@cloudflare/ai-chat'
import { streamText } from 'ai'
import {
  createDurableObserver,
  createRunIdentity,
  observeStreamTextOptions,
} from '@runtypelabs/cloudflare-agents'
import { RuntypeOtlpOutbox } from '@runtypelabs/cloudflare-agents/durable-object'

export { RuntypeOtlpOutbox }

interface Env {
  RUNTYPE_AGENT_ID: string
  RUNTYPE_API_KEY: string
  RUNTYPE_BASE_URL: string
  AdapterTelemetryOutbox: DurableObjectNamespace<RuntypeOtlpOutbox>
}

class SupportAgent extends AIChatAgent<Env> {
  async onChatMessage(onFinish, options) {
    const turn = {
      submissionId: readStableSubmissionId(options?.body),
      conversationId: readConversationId(options?.body),
      exchangeId: readStableExchangeId(options?.requestId),
      continuation: options?.continuation === true,
    }
    const identity = await createRunIdentity({
      agentId: this.env.RUNTYPE_AGENT_ID,
      submissionId: turn.submissionId,
      conversationId: turn.conversationId,
    })
    const outbox = this.env.AdapterTelemetryOutbox.get(
      this.env.AdapterTelemetryOutbox.idFromName(identity.traceId)
    )
    const stream = await observeStreamTextOptions(
      {
        model: myModel,
        system: 'Your application instruction.',
        messages: await toModelMessages(this.messages),
        tools: myServerTools,
        abortSignal: options?.abortSignal,
        onFinish,
      },
      {
        turn,
        content: {
          messages: true,
          instructions: true,
          tools: false,
          errors: false,
        },
        observer: createDurableObserver(outbox),
      }
    )
    return streamText(stream).toUIMessageStreamResponse()
  }
}
```

`readStableSubmissionId`, `readConversationId`, `readStableExchangeId`,
`myModel`, `toModelMessages`, and `myServerTools` belong to your application.
Validate the IDs before you call the adapter. Keep your existing
`streamText(...).toUIMessageStreamResponse()` call, because the wrapper does not
consume or replace the response stream. Keep your existing `abortSignal`,
`onFinish`, `onError`, and `prepareStep` callbacks in the options you pass. The
adapter runs its callbacks alongside them.

The adapter records no content by default. Each `content` field turns on one
kind:

* `messages`: user and assistant content.
* `instructions`: system and developer messages, including the instruction in
  effect after `prepareStep`.
* `tools`: tool arguments and results. Tool names and durations are recorded
  either way.
* `errors`: error text.

If you set `messages` and leave `instructions` unset, the adapter records
instructions too. To record messages without instructions, set `instructions`
to `false`.

To remove or replace a value before the adapter stores or sends it, pass a
`redact(kind, value)` function in `content`. Return `undefined` to omit the
value. After redaction, the adapter drops any value larger than 32 KiB. To
lower that limit, set `maxBytes` in `content`.

The adapter retries a delivery that was not acknowledged, and it completes a
run only after it observes the final result. A run with a missing final result
or a delivery gap stays incomplete instead of showing as successful. A failure
to report telemetry does not fail the response to your user.

The adapter does not support approval continuations, client-executed tools,
automatic subagent trees, linking to Cloudflare traces, or approving and
resuming from Runtype. Cloudflare traces in **Logs** can show incomplete
infrastructure activity, but they don't supply output, usage, or a final result
for an adapter run.

### Cloudflare Agents SDK

This path sends Cloudflare Agents SDK spans through Workers Observability. It
uses published packages and is separate from the
[adapter](#cloudflare-agents-sdk-adapter). A plain `AIChatAgent` that calls
`streamText` creates no GenAI spans. Wrap the AI SDK once at module scope and
call the wrapped namespace from `onChatMessage`:

**`Instrument the native AI SDK trace`**

```typescript title="Instrument the native AI SDK trace"
import * as ai from 'ai'
import { wrapAISDK } from 'agents/observability/ai'

export const tracedAI = wrapAISDK(ai, { storeMessages: true, storeTools: true })
```

Content is off unless you set `storeMessages` and `storeTools`. Without them,
the run has no transcript and you can't capture it as an eval case.

The export is a Workers Observability destination that you configure in the
Cloudflare dashboard. Set its endpoint and headers there, and name the
destination in `wrangler.jsonc`. The `OTEL_EXPORTER_*` variables do not
configure this path. Follow the
[Flue on Cloudflare Workers](/developer-guides/guides/bring-your-own-agent#flue-on-cloudflare-workers-no-code)
destination setup. It needs a Workers Paid plan, and Cloudflare bills every
sampled span.

Cloudflare can group several turns into one trace, or split one request across
several traces. Runtype creates one run per trace, so a request paused for
approval can appear as more than one run. Don't count connections, traces, or
runs as a count of user requests.

Slow and cancelled turns can emit `cloudflare.warning.type = span_not_ended`
without GenAI attributes. Runtype shows that activity in **Logs** as incomplete,
with an unknown outcome, and creates no run for it. An answer that the client
received does not fill in the missing output or usage.

Runtype reads the `gen_ai.*` attributes on these traces and uses the first chat
span's `gen_ai.input.messages` as the run's transcript. `tool_approval` spans
stay in **Logs** and don't become a second tool call. Runtype shows the
approval requests and decisions that it finds in a trace as read-only history.
It does not approve, resume, or link approvals across traces. With logging off,
the decisions remain but their content and reasons don't. Runs imported before
approval history was recorded have no approval history, which does not mean
that no approval happened.

## Instrument a custom agent loop

If you instrument your own loop, emit spans that follow the OpenTelemetry
generative AI (GenAI) semantic conventions. Runtype creates a run for a trace
that contains a GenAI operation or model telemetry. A trace without either
stays in **Logs** as a diagnostic and creates no run.

The following table lists the span attributes that Runtype reads:

| Span             | Attribute                                                                          | Effect on the run                                           |
| ---------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Agent invocation | `gen_ai.operation.name` = `invoke_agent`                                           | Opens and closes the run. Its end time completes the run.   |
| Agent invocation | `gen_ai.agent.name`                                                                | The agent name shown on the run.                            |
| Agent invocation | `gen_ai.conversation.id`                                                           | Groups runs from one conversation.                          |
| Model call       | `gen_ai.operation.name` = `chat`, `generate_content`, or `text_completion`         | Records one model call.                                     |
| Model call       | `gen_ai.request.model`, `gen_ai.response.model`                                    | The model. Runtype prices the response model when it's set. |
| Model call       | `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`                          | Token counts and the cost estimate.                         |
| Model call       | `gen_ai.usage.cache_read.input_tokens`, `gen_ai.usage.cache_creation.input_tokens` | Prompt-cache token counts.                                  |
| Tool call        | `gen_ai.operation.name` = `execute_tool`                                           | Records one tool call.                                      |
| Tool call        | `gen_ai.tool.name`, `gen_ai.tool.call.id`                                          | The tool's name and call ID.                                |

To mark a run as failed, set the `invoke_agent` span's status to `ERROR`. A span
with status `UNSET` and a recorded `exception` event also counts as failed. A
status of `OK` counts as successful.

The following sample opens an `invoke_agent` span with a child `chat` span and
a child `execute_tool` span:

**`Custom agent loop`**

```typescript title="Custom agent loop"
import { trace, SpanStatusCode } from '@opentelemetry/api'

const tracer = trace.getTracer('support-agent')

await tracer.startActiveSpan('invoke_agent support-agent', async (agentSpan) => {
  agentSpan.setAttributes({
    'gen_ai.operation.name': 'invoke_agent',
    'gen_ai.agent.name': 'support-agent',
    'gen_ai.conversation.id': conversationId,
  })
  try {
    const reply = await tracer.startActiveSpan('chat claude-sonnet-5', async (chatSpan) => {
      const response = await callModel(messages)
      chatSpan.setAttributes({
        'gen_ai.operation.name': 'chat',
        'gen_ai.request.model': 'claude-sonnet-5',
        'gen_ai.usage.input_tokens': response.usage.inputTokens,
        'gen_ai.usage.output_tokens': response.usage.outputTokens,
      })
      chatSpan.end()
      return response
    })
    await tracer.startActiveSpan('execute_tool lookup_order', async (toolSpan) => {
      toolSpan.setAttributes({
        'gen_ai.operation.name': 'execute_tool',
        'gen_ai.tool.name': 'lookup_order',
        'gen_ai.tool.call.id': reply.toolCallId,
      })
      await lookupOrder(reply.toolArguments)
      toolSpan.end()
    })
  } catch (error) {
    agentSpan.recordException(error as Error)
    agentSpan.setStatus({ code: SpanStatusCode.ERROR })
    throw error
  } finally {
    agentSpan.end()
  }
})
```

`conversationId`, `messages`, `callModel`, and `lookupOrder` belong to your
application. Use an opaque value for `gen_ai.conversation.id`, not personal
data or credentials. To add conversation content to the run, see
[Capture conversation content](#capture-conversation-content).

## Send several services through one collector

If an OpenTelemetry Collector exports several instrumented services in one
request, identify each service with a resource attribute:

**`Per-service attribution`**

```bash title="Per-service attribution"
OTEL_RESOURCE_ATTRIBUTES=runtype.agent.id=agent_YOUR_AGENT_ID
```

Replace `agent_YOUR_AGENT_ID` with the ID of the Runtype agent for that service.

For each trace, Runtype resolves the agent from the resource attribute first,
then the `x-runtype-agent-id` header, and then the `runtype.agent.id` attribute
on the `invoke_agent` span. The header is the fallback for traces that don't
set the resource attribute.

### Run several agents in one process

If one process runs several agents, set `runtype.agent.id` on the
`invoke_agent` span that opens each run:

**`Per-run attribution`**

```typescript title="Per-run attribution"
span.setAttribute('runtype.agent.id', 'agent_YOUR_AGENT_ID')
```

Replace `agent_YOUR_AGENT_ID` with the ID of the agent for that run.

Leave the `x-runtype-agent-id` header and the `runtype.agent.id` resource
attribute unset in that process. Either one takes precedence over the span
attribute, so every run goes to the agent that it names.

If two `invoke_agent` spans in one trace name different agents, and neither the
resource attribute nor the header names the agent, Runtype rejects the trace as
`ambiguous_agent_attribution`. Runtype also rejects a trace whose spans carry
different `runtype.agent.id` resource attributes. If agents delegate inside one
trace, set the agent at the resource or header level, or export each invocation
as a separate trace.

## Identify the instrumentation that produced a trace

To record which library emitted a trace and its version, set
`runtype.adapter.name` and `runtype.adapter.version`. Runtype reads them from
the resource first and from the run's `invoke_agent` span second, and records
the pair on the run.

**`Adapter provenance`**

```bash title="Adapter provenance"
OTEL_RESOURCE_ATTRIBUTES=runtype.agent.id=agent_YOUR_AGENT_ID,runtype.adapter.name=my-exporter,runtype.adapter.version=1.4.0
```

`@runtypelabs/flue-otel` sets both on every `invoke_agent` span that it opens,
so a Flue run identifies its producer without resource attributes. Your own
resource attribute takes precedence when you set both. Runtype never uses the
pair to accept, price, or attribute a run.

## Record the agent version and alias

To file a run under the agent version that produced it, set
`runtype.agent.version_id`. Set `runtype.agent.alias` to the release alias
(`live`, `staging`, and so on) that the version was resolved through. Runtype
then groups the run with that version in Runs, trends, and eval capture, the
same way it groups hosted runs.

**`Agent version and alias`**

```bash title="Agent version and alias"
OTEL_RESOURCE_ATTRIBUTES=runtype.agent.id=agent_YOUR_AGENT_ID,runtype.agent.version_id=agentver_YOUR_VERSION_ID,runtype.agent.alias=staging
```

* Runtype reads the attributes from the resource first and from the run's
  `invoke_agent` span second.
* The version must belong to the agent that the run is attributed to. Runtype
  drops a version ID that it does not find on that agent and still accepts
  the run.
* A run that names an alias records that it was resolved through an alias. A
  run with only a version ID records that it was pinned to that version.
* `runtype.agent.alias_revision` is provenance only. Runtype never groups or
  filters on it.

An agent exported with `@runtypelabs/runtime` emits these attributes
automatically from its `ExportedAgent.release`. You do not need to set them in
your host code.

## Capture conversation content

Runtype stores conversation content only when your instrumentation puts it on
the span. The Vercel AI SDK, LangGraph, the OpenAI Agents SDK, and Mastra
record content by default, so their runs carry a transcript unless you turn
recording off. To turn it off, set `recordInputs: false` and
`recordOutputs: false` on the AI SDK, or `OPENINFERENCE_HIDE_INPUTS=true` for
OpenInference. The agent's logging policy applies to **Logs**, not to the run's
transcript.

The OpenClaw, Hermes, and Pi adapters use the standard `gen_ai.*` attributes.
For OpenClaw, Hermes, and the Pi coding agent, content capture is off by
default. Set `RUNTYPE_CAPTURE_CONTENT=true` for OpenClaw and the Pi coding
agent, or `RUNTYPE_OTEL_CAPTURE_CONTENT=true` for Hermes. The Pi durable
binding sends content unless you pass `content: false`. If an adapter
receives no content, the run has no transcript to capture as an eval case.

For your own loop, set the GenAI content attributes. Runtype maps them to the
run as follows:

| Attribute                    | Result                          |
| ---------------------------- | ------------------------------- |
| `gen_ai.input.messages`      | The run's transcript.           |
| `gen_ai.system_instructions` | The transcript's `system` turn. |
| `gen_ai.output.messages`     | The run's final output.         |
| `gen_ai.tool.call.arguments` | A tool call's input parameters. |
| `gen_ai.tool.call.result`    | A tool call's result.           |

Runtype also reads these alternative attribute names:

* `flue.tool.call.arguments` and `flue.tool.call.result` from
  `@flue/opentelemetry`.
* The Vercel AI SDK's `ai.prompt.messages`, `ai.response.text`,
  `ai.toolCall.args`, and `ai.toolCall.result`.
* OpenInference's indexed attributes, such as
  `llm.input_messages.0.message.content`, and the `input.value` and
  `output.value` pair on tool spans.

If a span carries both a standard name and an alternative name, Runtype uses
the standard attribute.

> **Note**
>
> From version 0.5, `@runtypelabs/flue-otel` sends the transcript, system prompt,
> and tool arguments and results by default, so its runs can be captured as eval
> cases. Each kind can be turned off with its `content` option. See
> [Instrumenting a Flue agent](/developer-guides/guides/flue-instrumentation).

Point exactly one Flue instrumentation package at Runtype. If you export with
both `@runtypelabs/flue-otel` and `@flue/opentelemetry`, Runtype receives two
`invoke_agent` spans for one run, and the token counts and cost double.

Runtype applies these limits to content:

* It redacts credential-shaped strings before storage.
* It drops a content attribute over 256 KiB, and content past 384 KiB on one
  span.
* It drops content past 2 MiB for the whole trace. Splitting a trace across
  exporter batches does not reset this limit.

Runtype drops each affected value whole instead of truncating it.

Runtype calculates a cost estimate from the token counts and its model catalog.
Your provider charges you for the model calls, so treat the cost as an
estimate.

## Logging privacy

Runtype removes content attributes from the span attributes that it shows in
**Logs**, and stores that content on the run. When the agent's logging is on,
**Logs** also shows each model call's scrubbed input, instructions, and output
in separate fields. Runtype discards the following content attributes and stores them
nowhere: OpenInference's `retrieval.documents.*`, `llm.prompt_template.*`, and
`embedding.embeddings.*`, Mastra's `mastra.<span type>.input` and `.output`,
and the Cloudflare Agents SDK's `cloudflare.agents.metadata.*` and
`cloudflare.agents.runtime_context.*`.

Runtype also stores OTLP `LogRecord` bodies and attributes under the receiving
agent's logging policy. To keep log bodies and attributes out of storage, set
the agent's **Verbose logging** option to **Off** in its **Logging** settings
before you enable the logs signal. Runtype then keeps each record's severity and
trace ID, but not its body or attributes, and the log detail view shows
**Verbose logging off.** If the payloads can contain personal data, turn on
personally identifiable information (PII) redaction.

If secret scrubbing or your PII policy changes a conversation ID, or the ID is
longer than 256 UTF-8 bytes, **Logs** shows a stable digest of it instead.
Distinct IDs keep distinct digests. The digest is a pseudonym, not
anonymization.

To store `LogRecord` data, name the agent with `runtype.agent.id` on the logs
resource or with the `x-runtype-agent-id` header. Without either, Runtype
returns OTLP `partial_success` and discards those records. A trace ID on a
`LogRecord` links it to the trace in **Logs**, but it does not create a run.

Runtype acknowledges metrics but does not store them. To keep telemetry, send
traces or logs.

## Limits

The following table lists the limits that apply to one OTLP export and to one
trace:

| Limit                                  | Value                                                 | Response when exceeded    | What to change                                                       |
| -------------------------------------- | ----------------------------------------------------- | ------------------------- | -------------------------------------------------------------------- |
| Request body, after decompression      | 8 MiB                                                 | `413`                     | Lower `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`.                              |
| Spans in one request                   | 10,000                                                | `413`                     | Lower `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`.                              |
| `LogRecord` entries in one request     | 10,000                                                | `413`                     | Lower `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE`.                             |
| Distinct traces in one request         | 100                                                   | `413`                     | Lower `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` or `OTEL_BSP_SCHEDULE_DELAY`. |
| Distinct agents in one logs request    | 100                                                   | `413`                     | Split the export by service.                                         |
| Spans in one trace, across all exports | 10,000 spans or 4 MiB                                 | `413` (`trace_too_large`) | Export each invocation as a separate trace.                          |
| Time to send a trace's spans           | 7 days after Runtype accepts the trace's first export | `409` (`trace_expired`)   | Start a new trace for a new run.                                     |

Runtype accepts `gzip`, `deflate`, or no `Content-Encoding`. It rejects `zstd`
with `415`. It accepts `application/x-protobuf` and `application/json` bodies.

Runtype accepts or rejects each trace in an export whole. It never stores part
of a trace's spans from one request.

## Troubleshoot ingest errors

The following table lists the errors that the OTLP endpoints return:

| Status | Rejection code                   | Cause                                                                                          | Fix                                                                                    |
| ------ | -------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `400`  | `missing_agent_attribution`      | No resource attribute, header, or span attribute names an agent.                               | Set `x-runtype-agent-id` or `runtype.agent.id`.                                        |
| `400`  | `ambiguous_agent_attribution`    | Spans in one trace name different agents.                                                      | See [Run several agents in one process](#run-several-agents-in-one-process).           |
| `400`  | None                             | A span has a missing or zero ID, or the payload is malformed.                                  | Fix the exporter payload. Runtype writes nothing from that export.                     |
| `401`  | None                             | The API key is invalid.                                                                        | Check the key in `OTEL_EXPORTER_OTLP_HEADERS`.                                         |
| `403`  | None                             | The credential is not an API key, or the key has neither `TELEMETRY:WRITE` nor `AGENTS:WRITE`. | Use a telemetry API key.                                                               |
| `404`  | `unreachable_agent`              | The agent doesn't exist, or the key can't use it.                                              | Check the agent ID and the key's organization.                                         |
| `409`  | `execution_conflict`             | The run ID already belongs to another agent or another account.                                | Use a new trace ID.                                                                    |
| `409`  | `trace_expired`, `trace_deleted` | The trace's 7-day window closed, or its run was deleted.                                       | Start a new trace. Retrying does not recreate the run.                                 |
| `413`  | `trace_too_large`, or none       | A limit in [Limits](#limits) was exceeded.                                                     | Send smaller batches.                                                                  |
| `415`  | None                             | The content type or content encoding is not supported.                                         | Send protobuf or JSON, with `gzip`, `deflate`, or no compression.                      |
| `429`  | None                             | Your account's ingest capacity is used up.                                                     | Retry after the `Retry-After` interval.                                                |
| `503`  | None                             | Runtype can't accept the export or verify the key right now.                                   | Retry after the `Retry-After` interval. A `503` does not mean the export was accepted. |

Runtype checks each trace in an export separately. If some traces are accepted
and others are rejected, the endpoint returns `200` with an OTLP
`partial_success` that names the rejected traces. Your exporter does not resend
the accepted traces. The error status in the table applies only when Runtype
accepts no trace from the export. If the rejected traces have different causes,
the status is `400`.

OTLP exporters retry `429`, `502`, `503`, and `504`, but not `500`. Rejected
exports still count against your ingest capacity. Runtype removes duplicates
that retries create when it shows **Logs** and trace trees.

A `200` response means Runtype accepted the export. The data can take a short
time to appear in **Logs**. Deleting a run, or tightening its agent's logging
or PII policy, removes content that Runtype hasn't delivered yet.

### Inspect ingest health

If exported data never appears, check ingest health with a separate key that
has `ANALYTICS:READ` or `AGENTS:READ`. A `TELEMETRY:WRITE` key can't read this
endpoint.

**`Ingest health`**

```bash title="Ingest health"
curl https://api.runtype.com/v1/telemetry/ingest-health \
  -H "Authorization: Bearer rt_YOUR_READ_API_KEY"
```

Replace `rt_YOUR_READ_API_KEY` with an API key that has `ANALYTICS:READ` or
`AGENTS:READ`.

The response counts the records that Runtype accepted, is still delivering, and
has confirmed over the last 30 days, and it reports the ingest capacity that
remains. To see the counts for one trace, add `?traceId=YOUR_HEX_TRACE_ID`. The
response never contains transcripts or payloads. For every field, see the
[API Reference](/api-reference).

The following table describes the `status` values:

| Status      | Meaning                                                                  | What to do                                                       |
| ----------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `idle`      | No accepted records remain from the last 30 days.                        | Check that the exporter is running and points at Runtype.        |
| `observing` | Some accepted records are still being delivered.                         | Wait, then check again.                                          |
| `degraded`  | Some records expired, were suppressed, conflicted, or failed to deliver. | Check the `errors` list, and add `traceId` to inspect one trace. |
| `queryable` | Every accepted record was delivered.                                     | No action needed.                                                |

The status summarizes the delivery receipts that Runtype recorded. It doesn't
test whether **Logs** can return the data at that moment.

## Use compatibility ingest without OpenTelemetry

To send execution events without OpenTelemetry, post a batch to the
compatibility endpoint:

**`Direct ingest`**

```bash title="Direct ingest"
curl -X POST https://api.runtype.com/v1/executions/ingest \
  -H "Authorization: Bearer rt_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "executionId": "YOUR_EXECUTION_ID",
    "agentId": "agent_YOUR_AGENT_ID",
    "events": [
      {
        "type": "execution_start",
        "executionId": "YOUR_EXECUTION_ID",
        "seq": 0,
        "kind": "agent",
        "startedAt": "2026-01-01T00:00:00Z"
      },
      {
        "type": "execution_complete",
        "executionId": "YOUR_EXECUTION_ID",
        "seq": 1,
        "kind": "agent",
        "success": true
      }
    ]
  }'
```

Replace the following:

* `rt_YOUR_API_KEY`: the telemetry API key.
* `agent_YOUR_AGENT_ID`: the ID of the target Runtype agent.
* `YOUR_EXECUTION_ID`: a unique ID for the run. It can't start with `otel-`.

The following table describes the request body:

| Field           | Required | Description                                                                               |
| --------------- | -------- | ----------------------------------------------------------------------------------------- |
| `executionId`   | Yes      | The run's ID, up to 255 characters.                                                       |
| `agentId`       | Yes      | The receiving agent's ID.                                                                 |
| `agentName`     | No       | The agent name shown on the run.                                                          |
| `inputMessages` | No       | Up to 500 `{ "role", "content" }` objects that form the run's transcript.                 |
| `events`        | Yes      | 1 to 500 events. Each event has a `type`, the `executionId`, and a `seq` of 0 or greater. |

To send more than 500 events, send several batches with the same `executionId`.
A later batch updates the run. For example, it can complete a run or a tool
call that an earlier batch opened. For the event schema, see the
[API Reference](/api-reference).

The endpoint returns `202` with a body similar to the following:

**`202 response`**

```json title="202 response"
{
  "accepted": 2,
  "executionId": "YOUR_EXECUTION_ID",
  "execution": "created",
  "toolCalls": 0,
  "status": "completed"
}
```

The endpoint uses the same key scopes and returns the same `400`, `403`, `404`,
`409`, and `413` errors as the OTLP endpoints. An `executionId` that starts with
`otel-` returns `409`.

Runs from this endpoint appear in the agent's **Runs** view and eval capture,
with a transcript when you send one. They also appear in **Logs** with an agent
span and one span per tool call. Use OTLP/HTTP when your producer already emits
OpenTelemetry, or when you need a span per model call or OTLP `LogRecord` data.
Use compatibility ingest when a JSON batch is easier to produce than a trace.

## Next steps

Continue with one of these guides:

* [Bring your own agent](/developer-guides/guides/bring-your-own-agent): compare the ways to connect an external agent.
* [Improving agents from production](/developer-guides/guides/improving-agents-from-production): turn imported runs into eval cases.
* [Working with logs](/user-guide/logs/working-with-logs): inspect the spans and log records that your agent sends.
* [Instrumenting a Flue agent](/developer-guides/guides/flue-instrumentation): use the first-party Flue instrumentation.
* [Authentication](/developer-guides/getting-started/authentication): review API key scopes.
* [Self-hosting (BYOC)](/developer-guides/guides/self-hosting): run an exported flow or agent on your own infrastructure.
* [Working with runs](/user-guide/logs/working-with-runs): find runs and read the run detail page.