Prerequisites
Before you export logs, make sure you have:- A Writer API key. See instructions in the API keys guide.
Call the export endpoint
Request logs for one organization. URL:GET https://api.writer.com/api/observability/v1/organization/{organizationId}/logs/export
Omit cursor and start_date to start at the current time. Only newly settled rows stream forward.
Authenticate requests
Send the Writer API key on every request.organizationId returns 403 Forbidden. The gateway or API-key verifier can also reject access; body export still requires an authorized organization-scoped API key.
Choose a start point
You choose where the stream begins. After the first response, page with the cursor alone. The cursor already encodes your position.Start from the current time
Call the endpoint withorder=tail and neither cursor nor start_date. The stream starts at the current time, with no backfill.
Backfill from a date
Passstart_date with order=tail to request retained records from that ingestion time, then continue polling after the backlog drains. start_date accepts an ISO-8601 timestamp, epoch seconds, or epoch milliseconds. For example, start_date=2026-07-01T00:00:00Z. Available history depends on source telemetry and completed backfills; this does not guarantee full historical coverage.
Stay inside the retention window
Astart_date older than the retention window (90 days by default) returns 400 Validation Error. The detail field includes the window length and the earliest allowed ISO date. The API returns that error instead of truncating the backfill.
Page with the cursor alone
After you storex-next-cursor, send it as the cursor query parameter and omit start_date. Sending start_date together with cursor returns 400 because the cursor already encodes the position.
Set query parameters
These parameters control paging, the response shape, and filters. Filters apply in tail mode and keep(inserted_at, log_id) order across pages. Call filters qualify agent runs through matching LLM calls.
For
order=browse, provide both start_date and end_date, or omit both. Providing only one date returns 400 Validation Error.
For order=browse, continue with both cursor_started_at_ms and cursor_log_id. Read them from pagination.next_cursor with format=native, or from x-next-cursor-started-at-ms and x-next-cursor-log-id with format=otlp. The opaque cursor is tail-only, and the two-part browse parameters are rejected in tail mode.
Read the response
Tail responses put the resume token and the caught-up signal in headers. The body shape depends onformat.
Read response headers
Use these headers to resume and decide when the settled backlog is drained. Nativepagination.has_more matches x-has-more. In tail mode, native pagination.next_cursor is null; read the resume token from x-next-cursor.
Tail responses omit total_count, total_count_is_capped, and total-count headers. Browse-only bounded counts appear in those native fields, or in x-total-count and x-total-count-is-capped for OTLP. These counts are not tail catch-up signals.
Read a native body
Withformat=native, the body is a JSON object containing heterogeneous rows. result[].inserted_at_ms and result[].log_id are the two components of the tail ordering axis. Upsert by log_id, retaining the revision with the newest inserted_at_ms. This example shows selected fields from an LLM call and an agent run:
Distinguish calls and runs
Userecord_type to distinguish llm_call from agent_run. The fields describe different units of work:
Run rows describe terminal execution legs with a qualifying LLM call. Still-running and tool-only legs are excluded. Runs have no inference model, token, or body values.
Interpret run duration
A run’sduration_ms is lifecycle timing for that execution leg. It is not:
- A sum of call latency
- A session total
- Exact user-visible latency
null; do not infer a duration or replace it with zero. Parent, child, and resumed legs remain separate records. There is no session-total record.
Correlate records
Correlate records usingagent_run_id, root_agent_run_id, request_id, and session_id where present. A root ID does not guarantee a user turn or identify a subagent role. Uncorrelated calls remain exportable.
Late lifecycle events or call correlation can revise the same run identity. Accept later revisions rather than permanently discarding known IDs. See Apply delivery guarantees.
Handle unavailable bodies
When you requestinclude_bodies=true, an unavailable call body is null with a body_unavailable_reason. The reason codes distinguish:
- Capture:
not_captured - Processing or retrieval:
pending,missing,failed - Authorization:
ekm_denied,unauthorized - Retention or deletion:
expired,deleted - Other unavailability:
unavailable
body: null without hydration or an unavailable-body reason.
Read an OTLP body
Withformat=otlp (the default), the body is an OTLP/JSON payload. Choose the body shape with signal:
Pagination stays in the response headers. In tail mode, use
x-next-cursor, x-has-more, and x-tail-watermark.
Agent runs become writer.agent.run spans with kind INTERNAL, or log records with event name writer.agent.run.terminal.
Spans end at the terminal event time and start at that time minus the run duration. Unknown duration produces a point span without writer.agent.run.duration_ms; that does not mean the run took zero time.
Run spans and logs expose stable writer.record.id and correlation attributes. They do not include inference model, token, or body attributes.
Store and resend the cursor
The tail cursor is a versioned, HMAC-signed token. Treat it as an opaque string. Store it verbatim and send it back unchanged on thecursor parameter.
Apply delivery guarantees
Tail mode is an ordered revision feed. The settlement delay does not make a record immutable, and a cursor identifies a position rather than a fixed snapshot.- Ordering. Rows return in
(inserted_at ASC, log_id ASC)order. Replaying a cursor returns no row that ordered before that cursor. - Settled window. A page contains only rows whose
inserted_atis at or beforex-tail-watermark. The watermark is now minus a 5-minute settle lag. A row with an older event time and a newerinserted_atarrives when the watermark passes thatinserted_at. Settlement is not record finality. - Revision-aware upserts. Retries, overlapping workers, and later revisions can deliver the same
log_idagain. Upsert onlog_id, retaining the newestinserted_at_ms, so an older replay cannot overwrite a newer revision. Never permanently discard an ID because you have already seen it. - Replay. Replaying a cursor can observe newer rows at or after that position. After the stream is caught up, polling again can return newly settled rows. Missing source telemetry is not reconstructed, and the feed does not promise lossless real-time delivery.
Stay within limits
Each organization has a fixed window of 120 requests per 60 seconds by default. A request over that limit returns429 Rate Limited with a retry-after header. The value is the whole seconds until the window resets, with a minimum of one second. The limit is checked before authentication. Wait for retry-after, then retry.
limit is clamped to 1000. A larger value is reduced to 1000. When include_bodies=true, set limit to 50 or less. The default of 100 is above that maximum and returns 400 Validation Error.
Handle errors
Service error bodies use the Problem+JSON shape, withcode and detail. Gateway authentication errors can use a different envelope: an unauthenticated public request can return 401 with generic JSON rather than the service Problem+JSON shape. Handle the HTTP status even when the body does not contain a service error code.
Poll for new logs
Poll with the returnedx-next-cursor to pick up newly settled rows and revisions:
- Drain and pause: Drain settled pages until
x-has-moreisfalse, then pause before polling again. - Upsert revisions: Upsert by
log_id, retaining the newestinserted_at_ms. A primary key alone does not prevent older retries from overwriting newer revisions. - Persist cursor: Save the cursor only after the complete page is successfully processed. Restart from that saved cursor. These examples keep the cursor in memory only.
- Handle rate limits: On
429, wait forretry-afterbefore retrying. The 30-second pause in these examples applies after the stream is caught up.
Connect Airbyte, Fivetran, or Singer
The cursor and caught-up signal are header strings, so an incremental connector can store them as a bookmark. Useformat=native and read x-next-cursor from the header. Emit every revision and configure the destination to upsert on log_id, retaining the newest inserted_at_ms. Save the bookmark only after the destination processes the page.
Configure Airbyte
Airbyte’s cursor pagination can read a response-header token and inject it into the next request. This handles pages within a sync, not the persisted bookmark needed to resume this feed across scheduled syncs. Use these mappings for pagination within a sync:
For cross-sync resumption, implement a custom incremental source with the Airbyte Python CDK. The pagination mappings alone are not a complete connector. The source must:
- Restore the saved opaque
x-next-cursorfrom stream state and send it ascursoron the first request of each sync. Omitstart_datewhen resuming. Without saved state, choose a start point as described in Choose a start point. - Emit every row in
result, including revisions of knownlog_idvalues, before emitting a state message for that page’sx-next-cursor. Do not advance state after a partially processed page. - Continue while
x-has-moreistrue. When it isfalse, retain the returnedx-next-cursorfor the next scheduled sync, even if the final page is empty. - Use Airbyte’s state protocol: a checkpoint becomes safe to resume from only after the destination acknowledges that all preceding records are committed. Emitting records or state from the source alone is not an acknowledgment.
inserted_at_ms for it. Airbyte’s incremental sync configuration is not a replacement for restoring this bookmark. Resumable full refresh is not cross-sync incremental state either: its state is deleted at the start of a new sync job.
Set the record primary key to log_id and separately configure revision-aware destination updates that retain the newest inserted_at_ms. The primary key alone does not enforce revision ordering. On 429, honor retry-after before retrying.
Configure Fivetran or Singer
Singer taps and Fivetran custom connectors use the same header bookmark.- Singer. Persist
x-next-cursoras the stream replication bookmark inSTATEafter the target processes the page. On the next run, send that value ascursor. Emit each revision withlog_idas the record key and retain the newestinserted_at_msin the target. - Fivetran. Loop while
x-has-moreistrue, emit every revision withlog_idas the primary key, and retain the newestinserted_at_msat the destination. Storex-next-cursorin connector state only after processing the page.
Review default limits
These operational defaults apply to the export stream. To request different values, contact your Customer Success team.Next steps
View logs in AI Studio, or inspect a single agent.- Event logs: View, filter, and download request logs in AI Studio
- Agent observability: Track session logs and performance metrics for individual agents