Graine AI

Execution Logs

Every turn of a finished call — what the agent heard, how long it thought, when the caller heard a reply — plus the webhooks it sent, the prompt it ran on, and the raw numbers behind all of it.

A call that went wrong is rarely wrong everywhere. It is one turn where the transcriber took two and a half seconds to settle, or one where the model thought for 1.8 s, or one where the reply never reached the caller at all. The execution log is that call broken into turns, so the question stops being "why was this call slow?" and becomes "which turn, and which stage of it".

Every finished call has one. Phone calls and web calls produce the same log from the same measurements.

Where to find it

RouteUse it when
/history → open a call → Open full logsYou started from the call
/history/{execution_id}You have the execution id
/agent-executions/{agent_id}/{execution_id}/logsYou are debugging one agent and want a URL that names it

All three render the same view. The agent-scoped URL exists because it can be pasted into a ticket and read by a colleague who then knows which agent it is without opening it.

Turn timing

One row per turn, three stages per row.

ColumnWhat it measures
HeardFrom the caller starting to speak to the transcriber settling on the final text
ThoughtFrom the text being handed to the model to its first token
SpokeFrom the model's first token to the first audio the caller actually hears
Reply tookThe three added together — what the person on the phone waited through

A dash is not a zero. It means that stage produced no measurement for that turn, and the row says which one — "no listening timing", "no speaking timing". A turn with no spoke timing is a synthesizer that never reported, which is a different investigation from an ASR that never finalised. Reply took is blank whenever any stage is missing: a partial sum reported as a whole would be smaller than the truth, and that is the one direction a latency figure must never be wrong in.

The first row is often Greeting rather than a number. The synthesizer numbers the welcome message -1 because it precedes the conversation; it has a speaking time and no listening or thinking time, because nobody had said anything yet.

A reply spoken as several sentences is still one turn. Spoke is the first sentence — the moment sound arrives — and the turn's total speaking time is the sum of them.

The log

Everything that happened, in the order it happened, with a wall clock against each entry and three facts about which part of the platform produced it:

2026-09-10 00:15:31.402
यस, कंटिन्यू प्लीज।
request · transcriber · deepgram · turn 1 · 842 ms

~2026-09-10 00:15:32.184

response · llm · gpt-4o · turn 1 · 782 ms

~2026-09-10 00:15:32.197
Okay... what'd you like to change?
response · synthesizer · cartesia · turn 1 · 13 ms

A ~ before a timestamp means it was derived, not recorded. Only the transcriber writes a wall clock; the model and the synthesizer record durations. The rest of the turn is reconstructed from that anchor plus what each stage took. Shown identically to a recorded time, someone would correlate it against a carrier log and reach the wrong conclusion — so it is marked.

Entries expand where there is more to see:

  • What it heard on the way — the drafts the transcriber produced before settling. This is the evidence for a misheard turn, and every view that shows only the final transcript throws it away.
  • Conversation the model had in front of it — the transcript as it stood when that turn was decided.

The log is reconstructed, not replayed. Nothing writes a row when the model is called. Logging every request in full would mean writing the entire system prompt to the database on every turn — the same tens of kilobytes repeated per exchange, carrying whatever that prompt says about the person being called. The timings, the transcripts and the discarded drafts answer "why was this slow?" and "what did it mishear?" without keeping a copy of the prompt beside every customer's phone number.

Webhooks

Every webhook the call sent and whether it landed — event type, endpoint, attempt count, result, and the failure reason where there is one. A customer whose CRM never received a call asks exactly one question, and this is the answer to it.

An empty tab means no endpoint was subscribed, not that delivery failed. Subscriptions live in Integrations.

Instructions

The system prompt this call ran on, with a badge saying which one you are looking at:

  • exactly what was sent — the prompt was snapshotted when the call ended.
  • the agent's current prompt — it may have changed since — no snapshot exists for this call, so the agent was read as it stands now. An agent edited since the call would otherwise present a prompt that was never sent, and look authoritative doing it.

Raw data

The numbers exactly as each service recorded them, before any of the above is computed from them. When a reading looks wrong, this is the next step, and getting to it should not require a support ticket.

Downloading

  • Download logs — a .log file: the header, the full event stream, the webhook table and the prompt, as text a person can paste into a thread.
  • JSON — everything the API returned, including fields the page does not draw.

Both are named execution-{execution_id} because that is what a ticket is filed under.

The API

GET /api/v1/calls/{execution_id}/timeline
Authorization: Bearer <token>

Separate from GET /api/v1/calls/{execution_id} because it is large — the prompt alone is tens of kilobytes — and nothing wants it until someone opens a call and asks why it went the way it did.

{
  "execution_id": "6bd756e8-38b3-4514-a7eb-5b1dee9093cb",
  "agent_id": "fab5eff5-...",
  "started_at": "2026-09-10T00:15:30.918Z",
  "status": "completed",
  "measured": true,
  "not_measured_reason": null,
  "call": { "phone_number": "+919876543210", "duration_seconds": 51, "hangup_by": "Caller" },
  "services": {
    "transcriber": { "provider": "deepgram", "model": "nova-2" },
    "llm":         { "provider": "azure",    "model": "gpt-4o" },
    "synthesizer": { "provider": "cartesia", "model": "sonic" }
  },
  "turn_timing": {
    "summary": { "turns": 10, "measured_turns": 3,
                 "median_round_trip_ms": 1637, "slowest_round_trip_ms": 2241,
                 "slowest_turn": 2 },
    "turns": [
      { "index": -1, "greeting": true,
        "spoke": { "first_audio_ms": 115, "complete_ms": 240, "segments": 1 },
        "round_trip_ms": null, "round_trip_missing": ["heard", "thought"] },
      { "index": 1,
        "heard":   { "settled_ms": 842, "text": "यस, कंटिन्यू प्लीज।", "forced": null },
        "thought": { "first_token_ms": 782, "complete_ms": 961 },
        "spoke":   { "first_audio_ms": 13, "complete_ms": 657, "segments": 3 },
        "round_trip_ms": 1637, "round_trip_missing": null }
    ]
  },
  "events": [
    { "turn": 1, "at_ms": 1757463331402, "actor": "caller", "action": "said",
      "text": "यस, कंटिन्यू प्लीज।", "took_ms": 842 },
    { "turn": 1, "at_ms": 1757463332184, "at_ms_derived": true,
      "actor": "agent", "action": "decided", "text": null, "took_ms": 782 }
  ],
  "webhooks": [
    { "event_type": "call_progress", "callback_url": "https://…",
      "status": "delivered", "attempt_count": 1 }
  ],
  "system_prompt": "…",
  "prompt_source": "call_snapshot",
  "raw": { "transcriber_latencies": { "…": "…" } }
}

When there is nothing to show

measured is false and not_measured_reason says why. The three cases mean genuinely different things and should not be reported to a user as one:

not_measured_reasonMeaning
in_progressThe call has not finished writing. Detail appears when it ends.
never_recordedThe call predates this telemetry. A gap in history — nobody should go looking.
no_turns_completedThe call connected and no turn completed. Nobody spoke before it ended, which is a real finding about the call.

Where the numbers come from

The engine measures each turn as it runs and reports the timings when the call ends. Web calls report straight through; phone calls carry the same measurements through the telephony gateway, which folds every report it receives so a later, thinner one can add to the record and never take anything out of it.

The three services keep separate books and do not agree on how to number a turn — turn_id in the transcriber, sequence_id in the model. The timeline reconciles them by id where the ids line up and by position where they do not, which is why a turn number here is the turn of the conversation rather than any one service's counter.

On this page