Skip to main content
Using an agent? Install the Latitude skills and let it handle the setup below. latitude-setup instruments your app or agent harness, verifies traces arrive, creates a temporary account if you don’t have one yet (no signup), and ends by building your first Artifact.

Overview

Cloudflare Think uses the Vercel AI SDK internally. Think owns the streamText call, so add Latitude telemetry in beforeTurn() instead of at a model call site. No Latitude account yet? Your agent can create a temporary one and do this whole setup with the latitude-setup skill, no signup. Start with the basic setup. Add context only if you need traces to include user, session, tags, or metadata from your app.

Requirements

  • A Latitude API key, or none yet: your agent can create a temporary account with the latitude-setup skill, no signup
  • A Latitude project slug
  • A Cloudflare Workers project using @cloudflare/think
  • The nodejs_compat compatibility flag enabled in your Worker
  • LATITUDE_API_KEY and LATITUDE_PROJECT_SLUG configured as Worker secrets or variables

Basic Telemetry

This records Think model calls, tools, latency, tokens, prompts, and responses.
1

Install

2

Initialize Latitude once

If your Worker exposes secrets through process.env, this is all you need. Most Workers receive secrets through env bindings instead. In that case, keep one Latitude instance per Worker isolate:
Then use getLatitude(this.env).getTracer("cloudflare-think") in beforeTurn() and getLatitude(this.env).flush() in onChatResponse() / onChatError(). Do not create new Latitude() inside beforeTurn().

Add App Context

If your app uses Think’s default WebSocket chat entrypoint, pass the same context you would normally pass to capture() through useAgentChat({ body }).
Then read that context in beforeTurn() and pass it to getTracer():
This is the recommended setup for most Think apps.

Codemode Internal Tools

Think’s codemode execute tool appears as a normal AI SDK tool span. Tools called from inside codemode run outside the active AI SDK tool-call context, so wrap both the internal tool set and the outer execute tool with createCodemodeTelemetry(). The helper records each internal codemode tool as an ai.toolCall <name> child span under execute, stamps the same Latitude context as the turn, records AI SDK and GenAI tool attributes, and marks failed tools with exception details. When concurrent execute calls cannot be correlated after crossing the codemode sandbox boundary, ambiguous internal spans are omitted instead of attaching them to the wrong trace.
This produces one trace waterfall with the codemode internals nested under the outer tool call:
By default, tool inputs and outputs are captured as span attributes. Disable capture or redact values if codemode tools handle sensitive data:

Multiple Agents Across Durable Objects

A second agent in its own Durable Object gets a fresh isolate: no shared memory, no ambient OpenTelemetry context. Its spans start a trace of their own, so the planner or researcher you delegated to lands in a separate trace from the turn that called it. Hand the active span over with the call. injectTraceContext() serializes it into a carrier, and the callee reinstalls it with withTraceContext().
1

Send the context with the call

Call injectTraceContext() from inside the tool’s execute. It anchors on whichever span is active, so the second agent parents on that specific tool call.
2

Join the trace on the other side

withTraceContext() runs the callee’s work inside the caller’s trace. It also hands you the extracted context, so a framework that wants a tracer instead of an ambient context can get one from remote.getTracer().
Both agents now report one trace, and Latitude renders the second agent as a subagent of the tool call that invoked it:
The carrier keys are HTTP header names, so the same object works over fetch and over a service binding:
On the receiving side, withTraceContext() reads a Request or a Headers directly:
The carrier moves the trace and the Latitude context: session id, user id, user email, tags, and metadata. Two things stay behind. A capture() name is not carried, because it labels one capture root and would relabel every span the callee emits. The SDK’s default project is not carried either, since each side applies its own — pass project in the context explicitly when the two agents are configured for different Latitude projects, or their spans will split into two projects and the trace will break in half.
The carrier overrides, it does not replace. What it holds wins over whatever the callee already has; what it leaves out the callee keeps. So an absent or malformed carrier is not an error and not a reset either — the callee behaves exactly as it did before you added propagation, starting its own trace in a plain handler and staying inside an enclosing capture() where you have one.

Flushing Before Eviction

A Durable Object is evicted whenever it goes idle, and nothing runs first. Spans still sitting in the batch processor at that moment are lost: the batch timer is not a scheduler the Workers runtime keeps alive on your behalf. The fix is to flush at the end of every unit of work the object performs — a turn, an RPC method, a fetch, an alarm. createDurableObjectTelemetry() makes that cheap enough to do every time.
  • run(work) runs a unit of work and flushes once it settles, whether it resolved or threw.
  • flush() exports everything buffered so far. Callers arriving together share one export, and a caller arriving mid-export gets a later one rather than the in-flight one, which would miss the spans it is waiting on.
  • flushSoon() does not wait. It registers the export with ctx.waitUntil() so the runtime keeps the object alive for it.
A failed export goes to onError (console.error by default) instead of failing the work being traced.
Keep batching on. disableBatch: true sends one HTTP request per span and nothing waits for them, which is worse on Workers, not better. Batching plus one flush per unit of work is a single request per turn.

Programmatic Turns

If your app starts Think turns with your own runTurn() call, wrap that call with capture().
Keep getTracer() in beforeTurn():
Do not use capture.start() / scope.end() on Cloudflare Workers. Use capture() as a callback wrapper instead.

If You Cannot Wrap The Turn

If you cannot wrap the turn, pass context directly to the tracer from beforeTurn(). The context can come from agent state, Durable Object storage, auth state, or any place your agent can read during the turn.
This records the model and tool spans with the right context. It does not add a separate parent cloudflare-think-turn span.

Runnable Example

The Latitude repository includes a runnable Think example at examples/cloudflare-think-app. It includes an execute codemode tool backed by several demo tools, a second Planner agent in its own Durable Object reached over RPC, a small QA page, and a local verifier that checks model spans, codemode tool spans, userId, sessionId, and that the planner’s turn joins the orchestrator’s trace.

Seeing Your Traces

Once connected, traces appear automatically in Latitude:
  1. Open your project in the Latitude dashboard
  2. Send a message to your Think agent
  3. The turn appears with model calls, tool calls, messages, latency, token usage, and errors

See what was captured

Once a real run has landed, your agent builds your first Artifact: a single HTML page, in the Latitude look, with everything the telemetry captured from that session: model calls, tool calls, tokens, cost, timing, and the conversation as the model saw it. It is the fastest way to check the integration end to end and to see what Latitude will have to work with. The latitude-setup skill does this as its last step from its bundled first-artifact.html template, filling the page with the values the latitude CLI returns for the trace, and adds a Claim your workspace button when the account is temporary. If you set things up by hand, the same template and instructions live in the skills repo. Prompt, if you need to ask for it: