> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vexlo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# The execution graph

> The primitive, the three words for its parts, and one real trace read down to its captured values.

An execution graph is a per-request record of actual execution: every step a function that ran,
every edge a real invocation.

Three words:

* A **trace** is the execution graph for one request in one service. Its `trace_id` is 32
  lowercase hex characters, assigned by the SDK.
* A **step** is one marked function in that trace's call tree, carrying the function's name as
  `module.qualname` and identified by its **path**: the per-level ordinals from the root.
* An **attempt** is one execution of a step. A retried step has several, indexed from 0.

Each completed attempt carries its offset from the trace's start, its duration, `ok` or `error`
with the exception class on `error`, and the values it saw, one per slot: `input`, `output`,
`error`, and `logs` where log capture is on for the service. Kept slots follow the service's
[capture policy](https://docs.vexlo.ai/server/capture-policy.md).

## Identity, and why order does not matter

A step's identity is its path, so ingest is an idempotent upsert: a duplicate record changes
nothing, and a record that arrives before its parent lands in the right place.

Shape is what ran, with its timing and status and none of the captured bytes. A marked call
appends it to the SDK's in-process outbox as each attempt completes, and the writer thread writes
the outbox to the spool directory every second, so records still there when the process is killed
never reach disk. Values leave on a different clock, when the root returns.

On age alone, the SDK never reports an open attempt before one second, and reports it by two
seconds unless the record it holds for that attempt was dropped, for example past the 4,096 open
records it holds at most. A dropped record is loss: the attempt reaches the trace only once it
completes, and the trace's close counts it in `records_lost`. That close and the process's
two-second shutdown flush report every held attempt, whatever its age. A reported attempt reads
`status: in_flight`, with no duration and no slots.

## One real trace

`assign o-5003` in the demo service was rejected: no dasher works that zone. The root caught the
error, so the trace reads `root_status: ok` with one errored step.

```json Trace 1396289cd6ab96fa9f46ba176d7e8695, steps removed theme={null}
{
  "trace_id": "1396289cd6ab96fa9f46ba176d7e8695",
  "service": "delivery-service",
  "team": "default",
  "format_version": 2,
  "instance_id": "d5b6f944aabcc0b60397c95c7e39aadd",
  "sdk_version": "0.1.0",
  "tags": {
    "order_id": "o-5003"
  },
  "started_at": "2026-11-05T05:44:34.506Z",
  "first_seen_at": "2026-11-05T05:44:34.779Z",
  "last_seen_at": "2026-11-05T05:44:34.779Z",
  "root_status": "ok",
  "completeness": "complete",
  "step_count": 10,
  "error_step_count": 1,
  "duration_ms": 0,
  "coverage": {
    "records_lost": 0,
    "records_shed": 0,
    "truncated": false,
    "instance_mismatch": false,
    "service_loss_24h": {
      "delivery-service": 0
    }
  },
  "limit": 500,
  "next_cursor": null
}
```

`duration_ms` is whole milliseconds and this request took under one, so it reads `0`.

The ten steps, indented by depth: name, path, status, duration, and the preview of the output
slot, or of the error slot where there is none. A preview is a value's first 64 code points, which
is why the root's output stops inside `rejected`.

```text The steps array, one line per step theme={null}
delivery.api.assign_dasher                          ""          ok     543us  {"captured_via":"adapter","order_id":"o-5003","outcome":"rejecte
  delivery.orders.OrderBook.fetch                   AAAA        ok     102us  {"captured_via":"adapter","order_id":"o-5003","subtotal_cents":9
    delivery.orders.OrderBook.find                  AAAAAAA     ok     10us   {"captured_via":"adapter","order_id":"o-5003","subtotal_cents":9
  delivery.dispatch.Dispatch.check_serviceable      AAAQ        ok     58us   null
    delivery.dispatch.Dispatch.serviced_zones       AAAQAAA     ok     8us    {"$type": "builtins.frozenset"}
  delivery.assignment.DasherSelection.pick          AABA        error  168us  {"message":"dasher selection failed in zone 'SF-94110': no dashe
    delivery.assignment.DasherSelection.candidates  AABAAAA     ok     81us   []
      delivery.assignment.DasherSelection.in_zone   AABAAAAAAA  ok     6us    []
      delivery.assignment.DasherSelection.rank      AABAAAAAAE  ok     7us    []
    delivery.assignment.DasherSelection.in_zone     AABAAAI     ok     6us    []
```

`DasherSelection.pick` raised `NoEligibleDasherError`, and the step that holds the reason
succeeded: `candidates` found no dashers because `in_zone` found none. `in_zone` appears twice, at
`AABAAAAAAA` and `AABAAAI`: the same function at two paths is two steps.

`null` is `check_serviceable`'s recorded output, because it returns `None`. `serviced_zones`
returned a `frozenset`, which the SDK does not encode, so the slot holds a type marker instead and
the zone names are nowhere in the trace.

The root's values, whole:

```json Values for path "" of the same trace theme={null}
{
  "values": [
    {
      "path": "",
      "slot": "input",
      "state": "stored",
      "encoding": "json",
      "value": "{\"order_id\":\"o-5003\"}",
      "bytes_count": 21,
      "truncated": false
    },
    {
      "path": "",
      "slot": "output",
      "state": "stored",
      "encoding": "json",
      "value": "{\"captured_via\":\"adapter\",\"order_id\":\"o-5003\",\"outcome\":\"rejected\",\"plan\":null,\"reason\":\"dasher selection failed in zone 'SF-94110': no dasher is working this zone\"}",
      "bytes_count": 165,
      "truncated": false
    }
  ],
  "unknown_paths": []
}
```

`reason` is the message the errored step produced, carried up by the function that caught it.
`captured_via` says an adapter turned that return value, a dataclass, into a dict.

## What the data does not claim

A trace shows the paths that ran, never the branches not taken, and only marked
functions appear in it. So the function index reports observed callers and observed callees, never
"the callers", and a cross-service request produces one trace per service, related by the
tags the services carry, such as an order id. The API names that relation `shares_tag`. A shared
tag proves neither that one called the other nor that the two are one request, since `assign`,
`quote`, and `track` on one order all carry its order id.

A trace-detail response also carries a `coverage` object, reading clean above. `records_lost`,
`records_shed`, `truncated` and `instance_mismatch` are always there, and a search result carries
them as flat scalars per item. `service_loss_24h` counts what the service lost over 24 hours, and
is there only while [`GET /v1/coverage`](https://docs.vexlo.ai/api/tags-coverage-health) still lists that service. A
trace from a since-quiet service carries the four alone; a missing key is not a zero.
`completeness: complete` turns on those four and on the arrival of the root attempt's completion
record, never on `service_loss_24h`.

## Reading it in three rungs

Three rungs, each measured on a server seeded with the spec's 17-step conformance trace, not the one
above. Find answers what exists and returns no captured value: `vexlo_search_traces` over
its default page of 10 traces, 1,473 tokens. Shape returns one trace's structure with previews and
no bytes: `vexlo_get_trace`, 3,184. Values return the bytes for the paths you name:
`vexlo_get_values` for one path's two slots, 113. The full table is on
[token costs](https://docs.vexlo.ai/mcp/token-costs.md).

[Use cases](https://docs.vexlo.ai/concepts/use-cases.md) maps six questions onto the data that answers each.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.