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

# Quickstart

> Run the server, the demo service, and the relay daemon on one machine, then read a trace back over the /v1 API.

By the end of this page a Vexlo server on `127.0.0.1:8080` holds two traces from the demo
service, and you have read one of them back over the `/v1` API down to its captured values.
It takes four terminals: the server, the demo service, the relay daemon, and `curl`. Every
process runs on its defaults; the only flags are `-no-auth` on the server, plus `-destination`
and `-token` on the relay daemon.

## Before you start

* A built `vexlo` binary. From a checkout of `vexlo`, `make setup` then `make build` writes
  `bin/vexlo`; the build needs Go 1.27, Node.js 22.12 or later, and a C toolchain (see
  [Install and run the server](https://docs.vexlo.ai/server/install.md)). The commands below call the binary as
  `vexlo`, so put that `bin/` directory on your `PATH`.
* A checkout of `vexlo-demo` with `make setup` run once, and the `vexlo-sdk-python` checkout
  beside it under the same parent directory. `make setup` creates `.venv` with
  `python3 -m venv`, so the `python3` on your `PATH` has to be Python 3.14 or later, and it
  installs the SDK from `../vexlo-sdk-python`.
* `curl` and `jq`.
* No `~/.vexlo` directory yet, and ports 8080 and 9101 free. The server creates its server
  storage directory at `~/.vexlo/data`, the SDK writes to the spool directory `~/.vexlo`, and
  the relay daemon reads that same spool directory and binds `127.0.0.1:9101`.

**1. Start the Vexlo server**

```bash Terminal 1 theme={null}
vexlo serve -no-auth
```

`-no-auth` selects no-auth mode: no API tokens, a loopback bind, and everything scoped to the
team `default`. The server binds `127.0.0.1:8080`, applies its database migrations on startup,
and creates `~/.vexlo/data` on first run; there is no separate setup step. It logs one line per
request, so keep this terminal visible.

Confirm it answers from terminal 4:

```bash Terminal 4 theme={null}
curl -s http://127.0.0.1:8080/v1/traces | jq .
```

```json theme={null}
{
  "items": [],
  "limit": 50,
  "next_cursor": null
}
```

Nothing has been ingested yet.

**2. Run the demo service**

```bash Terminal 2 theme={null}
cd vexlo-demo
.venv/bin/python -m delivery assign o-5007
.venv/bin/python -m delivery assign o-5003
```

```json theme={null}
{"command": "assign", "order_id": "o-5007", "dasher_id": "d-12", "fee_cents": 787, "eta_minutes": 12, "outcome": "success", "reason": null}
{"command": "assign", "order_id": "o-5003", "dasher_id": null, "fee_cents": null, "eta_minutes": null, "outcome": "rejected", "reason": "dasher selection failed in zone 'SF-94110': no dasher is working this zone"}
```

Each run is one process that dispatches one order and prints one JSON object. `o-5007` is
assigned to dasher `d-12`. `o-5003` is rejected because nobody is working its zone; a rejection
is an outcome, not a command-line failure, and both runs exit 0.

The demo marks the functions it wants recorded: `@vexlo.trace` on `delivery.api.assign_dasher`,
which opens the trace, and `@vexlo.step` on the functions it calls below, the `DasherSelection`
methods among them. Only a marked function becomes a step.

It also calls `vexlo.init(service="delivery-service", tag_keys=["order_id"])` once at startup,
so every trace it records carries an `order_id` tag; step 4 uses that tag to find a trace. With
`VEXLO_SPOOL_DIR` unset, the SDK writes its records under `~/.vexlo`:

```bash Terminal 2 theme={null}
ls ~/.vexlo
```

```text theme={null}
data
spool
writers
```

`data` is the server storage directory from step 1; `spool` and `writers` belong to the SDK.
Nothing has reached the server: the SDK ends at a local write and never opens a network
connection. Getting records to the server is the relay daemon's job.

**3. Start the relay daemon**

```bash Terminal 3 theme={null}
vexlo relay run -destination http://127.0.0.1:8080 -token anything
```

The daemon refuses to start without `-destination` and `-token`, whichever mode the server
runs in. Under no-auth mode the server never reads the token, so any value works. The daemon
polls `~/.vexlo` every 250 ms, batches what it finds, and posts each batch to the server.
Terminal 1 logs the batch as a `POST /v1/records`:

```text theme={null}
... 2 lines omitted
time=2026-10-19T01:12:21.131-07:00 level=INFO msg="api: request" method=POST path=/v1/records status=204 duration_ms=23 request_id=7b1975fa8df2ce89
... 12 lines omitted
```

Above it in that log sit the server's startup line and step 1's `GET /v1/traces`. Below it come
a line for each request you make from here on, and the daemon's heartbeats: every 30 seconds
the daemon sends one `POST /v1/heartbeat` for each instrumented process it finds in the spool
directory, so two arrive together here, one per demo run.

**4. List the traces**

```bash Terminal 4 theme={null}
curl -s http://127.0.0.1:8080/v1/traces | jq .
```

```json theme={null}
{
  "items": [
    {
      "trace_id": "80c7f1ea16ecf04865ee4b82ab10be77",
      "service": "delivery-service",
      "team": "default",
      "instance_id": "845fac75004c716fc6027d391b20ce13",
      "tags": {
        "order_id": "o-5007"
      },
      "root_function": "delivery.api.assign_dasher",
      "root_status": "ok",
      "completeness": "complete",
      "started_at": "2026-10-19T08:12:20.768Z",
      "duration_ms": 2,
      "step_count": 36,
      "error_step_count": 0,
      "records_lost": 0,
      "records_shed": 0,
      "truncated": false,
      "instance_mismatch": false
    },
    {
      "trace_id": "2e14346b48ab74e574ba338be1273a6a",
      "service": "delivery-service",
      "team": "default",
      "instance_id": "4db1b78e6b9a7b17a9f32a0b610916cd",
      "tags": {
        "order_id": "o-5003"
      },
      "root_function": "delivery.api.assign_dasher",
      "root_status": "ok",
      "completeness": "complete",
      "started_at": "2026-10-19T08:12:20.832Z",
      "duration_ms": 0,
      "step_count": 10,
      "error_step_count": 1,
      "records_lost": 0,
      "records_shed": 0,
      "truncated": false,
      "instance_mismatch": false
    }
  ],
  "limit": 50,
  "next_cursor": null
}
```

Two traces, both `root_status: ok` and `completeness: complete`: every record arrived, and the
root step returned normally in both runs. The `o-5003` trace still carries
`error_step_count: 1`, because the rejection was raised inside the tree and caught before the
root step returned.

The demo never prints a trace id. Read it off the list by the `order_id` tag:

```bash Terminal 4 theme={null}
curl -s http://127.0.0.1:8080/v1/traces | jq -r '.items[] | select(.tags.order_id == "o-5003") | .trace_id'
```

```text theme={null}
2e14346b48ab74e574ba338be1273a6a
```

The SDK assigns a fresh id to every trace, so yours differs. Substitute it in the commands that
follow.

**5. Read the trace**

```bash Terminal 4 theme={null}
curl -s http://127.0.0.1:8080/v1/traces/2e14346b48ab74e574ba338be1273a6a | jq .
```

```json theme={null}
{
  "trace_id": "2e14346b48ab74e574ba338be1273a6a",
  "service": "delivery-service",
  "team": "default",
  "instance_id": "4db1b78e6b9a7b17a9f32a0b610916cd",
  "sdk_version": "0.1.0",
  "tags": {
    "order_id": "o-5003"
  },
  "started_at": "2026-10-19T08:12:20.832Z",
  "first_seen_at": "2026-10-19T08:12:21.110Z",
  "last_seen_at": "2026-10-19T08:12:21.110Z",
  "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
    }
  },
  "steps": [
    {
      "path": "",
      "parent_path": null,
      "depth": 0,
      "name": "delivery.api.assign_dasher",
      "parent_name": null,
      "attempt_grouping": "single",
      "attempts": [
        {
          "attempt": 0,
          "started_us": 0,
          "duration_us": 843,
          "status": "ok",
          "slots": {
            "input": {
              "state": "stored",
              "bytes_count": 21,
              "truncated": false,
              "encoding": "json",
              "preview": "{\"order_id\":\"o-5003\"}"
            },
            "output": {
              "state": "stored",
              "bytes_count": 165,
              "truncated": false,
              "encoding": "json",
              "preview": "{\"captured_via\":\"adapter\",\"order_id\":\"o-5003\",\"outcome\":\"rejecte"
            }
          }
        }
      ]
    },
... 128 lines omitted
    {
      "path": "AABA",
      "parent_path": "",
      "depth": 1,
      "name": "delivery.assignment.DasherSelection.pick",
      "parent_name": "delivery.api.assign_dasher",
      "attempt_grouping": "single",
      "attempts": [
        {
          "attempt": 0,
          "started_us": 649,
          "duration_us": 138,
          "status": "error",
          "error_class": "delivery.errors.NoEligibleDasherError",
          "slots": {
            "error": {
              "state": "stored",
              "bytes_count": 195,
              "truncated": false,
              "encoding": "json",
              "preview": "{\"message\":\"dasher selection failed in zone 'SF-94110': no dashe"
            },
            "input": {
              "state": "stored",
              "bytes_count": 19,
              "truncated": false,
              "encoding": "json",
              "preview": "{\"zone\":\"SF-94110\"}"
            }
          }
        }
      ]
    },
... 96 lines omitted
    {
      "path": "AABAAAI",
      "parent_path": "AABA",
      "depth": 2,
      "name": "delivery.assignment.DasherSelection.in_zone",
      "parent_name": "delivery.assignment.DasherSelection.pick",
      "attempt_grouping": "single",
      "attempts": [
        {
          "attempt": 0,
          "started_us": 764,
          "duration_us": 6,
          "status": "ok",
          "slots": {
            "input": {
              "state": "pending",
              "bytes_count": 19,
              "truncated": false,
              "encoding": "json",
              "preview": "{\"zone\":\"SF-94110\"}"
            },
            "output": {
              "state": "pending",
              "bytes_count": 2,
              "truncated": false,
              "encoding": "json",
              "preview": "[]"
            }
          }
        }
      ]
    }
  ],
  "limit": 500,
  "next_cursor": null
}
```

`steps` holds one entry per position in the call tree where a marked function ran. `path` is a
step's identity, an opaque string that is `""` for the root step; `parent_path` and `depth`
rebuild the tree. Each step carries its attempts, and a step that ran once has one,
`attempt: 0`. One response carries at most 500 steps; `next_cursor: null` at the end means all
10 of this trace's steps arrived in this one.

`delivery.assignment.DasherSelection.pick` is the step that raised: `status: error`,
`error_class: delivery.errors.NoEligibleDasherError`, and an `error` slot beside its `input`.
Two steps sit beneath it. The `candidates` step at `AABAAAA`, inside the omitted lines with its
own `in_zone` and `rank` children, returned `[]`, so `pick` had nobody to pick. `pick` then
called `in_zone` directly, the step at `AABAAAI`, to choose between its two reasons: no dasher
working the zone at all, or dashers who all missed the demo's 4.2 rating floor. `in_zone`
returned `[]` too, so the reason is the one step 2 printed, "no dasher is working this zone".

This response is the trace's shape. Each slot carries a handle in place of the value: a `state`
and, because bytes were held, `bytes_count`, `truncated` (whether the value was cut at the
16 KiB slot bound), `encoding`, and a `preview` of the first 64 code points. A value as short as
`{"order_id":"o-5003"}` fits inside its preview; the root step's 165-byte `output` is cut there.
Read the states: the root step's slots and `pick`'s slots are `stored`, while every other
step's slots are `pending`. Step 6 returns to that.

**6. Fetch the values**

`/v1/traces/{id}/values` returns the captured bytes for the steps you name by `path`. The root
step's path is the empty string, so `?path=` with nothing after it names the root step:

```bash Terminal 4 theme={null}
curl -s 'http://127.0.0.1:8080/v1/traces/2e14346b48ab74e574ba338be1273a6a/values?path=' | jq .
```

```json 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": []
}
```

`value` is the captured bytes as a JSON string and `encoding` says how to decode them. The
root step's `output` is the value `assign_dasher` returned; the line the demo printed in step 2
was built from it, which is why the two share their `outcome` and `reason`.

Now the `in_zone` step at `AABAAAI`, whose slots read `pending` in step 5:

```bash Terminal 4 theme={null}
curl -s 'http://127.0.0.1:8080/v1/traces/2e14346b48ab74e574ba338be1273a6a/values?path=AABAAAI' | jq .
```

```json theme={null}
{
  "values": [
    {
      "path": "AABAAAI",
      "slot": "input",
      "state": "pending",
      "encoding": "json",
      "value": "{\"zone\":\"SF-94110\"}",
      "bytes_count": 19,
      "truncated": false
    },
    {
      "path": "AABAAAI",
      "slot": "output",
      "state": "pending",
      "encoding": "json",
      "value": "[]",
      "bytes_count": 2,
      "truncated": false
    }
  ],
  "unknown_paths": []
}
```

The value is present and the state is still `pending`. A non-root attempt that completes with
`status: ok` ships its slots as `pending`, because at that moment the trace has not closed and
whether its values will be kept is not yet known. The API does not promote a `pending` handle
to `stored` when its value arrives. To learn whether a value came back, check for the `value`
key, not the state.

**7. Check the daemon and stop**

```bash Terminal 4 theme={null}
vexlo relay status
```

```text theme={null}
spool: 333.4KiB / 2.0GiB (0.0%)

writer 1792397540-28048-95205310
  service: delivery-service  instance: 845fac75004c716fc6027d391b20ce13
  state: active
  backlog: 0 records, 0B
  records sent: 77
  dropped:
    none

writer 1792397540-28049-d7062255
  service: delivery-service  instance: 4db1b78e6b9a7b17a9f32a0b610916cd
  state: active
  backlog: 0 records, 0B
  records sent: 24
  dropped:
    none
```

One writer per demo process, each with an empty backlog and its sent records counted.
`relay status` reads the spool directory itself, so it works whether or not the daemon is
running.

Stop the daemon and the server with Ctrl-C in terminals 3 and 1. Each logs one line and exits:

```text theme={null}
... 1 line omitted
time=2026-10-19T01:13:21.490-07:00 level=INFO msg="relay run: shutting down"
```

```text theme={null}
... 14 lines omitted
time=2026-10-19T01:13:21.499-07:00 level=INFO msg="serve: shutting down"
```

## Where next

* Everything here ran in no-auth mode. Outside it the relay daemon sends an ingest API token,
  and a reader presents a read token or reaches the server through a proxy that injects the
  header-trust mode headers. [Authentication and tokens](https://docs.vexlo.ai/server/authentication.md) covers minting
  tokens and both read paths.
* An AI agent reads this same server through the MCP server.
  [Install and configure](https://docs.vexlo.ai/mcp/install.md) covers connecting one.
* What a trace is and the words for its parts: [The execution graph](https://docs.vexlo.ai/concepts/execution-graph.md).
* To record your own service: [Mark functions](https://docs.vexlo.ai/sdk/mark-functions.md).


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