Skip to navigation

Reporting telemetry from an external agent

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, the 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.
  • 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.

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

FrameworkWhat to installWhat to enable
Flue@flue/runtime, @runtypelabs/flue-otel, @opentelemetry/api, @opentelemetry/sdk-trace-node, @opentelemetry/exporter-trace-otlp-httpinstrument(createRuntypeFlueInstrumentation())
Vercel AI SDK@opentelemetry/sdk-node, @opentelemetry/exporter-trace-otlp-protoexperimental_telemetry: { isEnabled: true, functionId: 'my-agent' }
LangGraphopeninference-instrumentation-langchain, opentelemetry-exporter-otlp-proto-httpLangChainInstrumentor().instrument(tracer_provider=provider)
OpenAI Agents SDKopeninference-instrumentation-openai-agents, opentelemetry-exporter-otlp-proto-httpOpenAIAgentsInstrumentor().instrument(tracer_provider=provider)
Mastra@mastra/core, @mastra/observability, @mastra/otel-exporter, @opentelemetry/exporter-trace-otlp-protoAn 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
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).

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
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
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
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
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
hermes plugins install runtypelabs/hermes-runtype-otel --enable

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

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 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
pi install npm:@runtypelabs/pi-otel

Set these variables in the shell that starts Pi:

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

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, 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
{
"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
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. 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
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 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:

SpanAttributeEffect on the run
Agent invocationgen_ai.operation.name = invoke_agentOpens and closes the run. Its end time completes the run.
Agent invocationgen_ai.agent.nameThe agent name shown on the run.
Agent invocationgen_ai.conversation.idGroups runs from one conversation.
Model callgen_ai.operation.name = chat, generate_content, or text_completionRecords one model call.
Model callgen_ai.request.model, gen_ai.response.modelThe model. Runtype prices the response model when it’s set.
Model callgen_ai.usage.input_tokens, gen_ai.usage.output_tokensToken counts and the cost estimate.
Model callgen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokensPrompt-cache token counts.
Tool callgen_ai.operation.name = execute_toolRecords one tool call.
Tool callgen_ai.tool.name, gen_ai.tool.call.idThe 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
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.

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
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
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
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
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:

AttributeResult
gen_ai.input.messagesThe run’s transcript.
gen_ai.system_instructionsThe transcript’s system turn.
gen_ai.output.messagesThe run’s final output.
gen_ai.tool.call.argumentsA tool call’s input parameters.
gen_ai.tool.call.resultA 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.

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.

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:

LimitValueResponse when exceededWhat to change
Request body, after decompression8 MiB413Lower OTEL_BSP_MAX_EXPORT_BATCH_SIZE.
Spans in one request10,000413Lower OTEL_BSP_MAX_EXPORT_BATCH_SIZE.
LogRecord entries in one request10,000413Lower OTEL_BLRP_MAX_EXPORT_BATCH_SIZE.
Distinct traces in one request100413Lower OTEL_BSP_MAX_EXPORT_BATCH_SIZE or OTEL_BSP_SCHEDULE_DELAY.
Distinct agents in one logs request100413Split the export by service.
Spans in one trace, across all exports10,000 spans or 4 MiB413 (trace_too_large)Export each invocation as a separate trace.
Time to send a trace’s spans7 days after Runtype accepts the trace’s first export409 (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:

StatusRejection codeCauseFix
400missing_agent_attributionNo resource attribute, header, or span attribute names an agent.Set x-runtype-agent-id or runtype.agent.id.
400ambiguous_agent_attributionSpans in one trace name different agents.See Run several agents in one process.
400NoneA span has a missing or zero ID, or the payload is malformed.Fix the exporter payload. Runtype writes nothing from that export.
401NoneThe API key is invalid.Check the key in OTEL_EXPORTER_OTLP_HEADERS.
403NoneThe credential is not an API key, or the key has neither TELEMETRY:WRITE nor AGENTS:WRITE.Use a telemetry API key.
404unreachable_agentThe agent doesn’t exist, or the key can’t use it.Check the agent ID and the key’s organization.
409execution_conflictThe run ID already belongs to another agent or another account.Use a new trace ID.
409trace_expired, trace_deletedThe trace’s 7-day window closed, or its run was deleted.Start a new trace. Retrying does not recreate the run.
413trace_too_large, or noneA limit in Limits was exceeded.Send smaller batches.
415NoneThe content type or content encoding is not supported.Send protobuf or JSON, with gzip, deflate, or no compression.
429NoneYour account’s ingest capacity is used up.Retry after the Retry-After interval.
503NoneRuntype 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
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.

The following table describes the status values:

StatusMeaningWhat to do
idleNo accepted records remain from the last 30 days.Check that the exporter is running and points at Runtype.
observingSome accepted records are still being delivered.Wait, then check again.
degradedSome records expired, were suppressed, conflicted, or failed to deliver.Check the errors list, and add traceId to inspect one trace.
queryableEvery 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
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:

FieldRequiredDescription
executionIdYesThe run’s ID, up to 255 characters.
agentIdYesThe receiving agent’s ID.
agentNameNoThe agent name shown on the run.
inputMessagesNoUp to 500 { "role", "content" } objects that form the run’s transcript.
eventsYes1 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.

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

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: