Skip to main content
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). 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

Terminal 1
-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:
Terminal 4
Nothing has been ingested yet.
2

Run the demo service

Terminal 2
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:
Terminal 2
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

Terminal 3
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:
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

Terminal 4
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:
Terminal 4
The SDK assigns a fresh id to every trace, so yours differs. Substitute it in the commands that follow.
5

Read the trace

Terminal 4
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:
Terminal 4
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:
Terminal 4
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

Terminal 4
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:

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 covers minting tokens and both read paths.
  • An AI agent reads this same server through the MCP server. Install and configure covers connecting one.
  • What a trace is and the words for its parts: The execution graph.
  • To record your own service: Mark functions.