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

# Initialise and record a first trace

> Call init() once at startup, mark two functions, and read the records the SDK wrote to the spool directory.

Three things put a first trace on disk: one `vexlo.init()` call at process startup, one
function marked `@vexlo.trace`, and one it calls marked `@vexlo.step`. This page runs exactly
that and reads back what the SDK wrote. No server and no relay daemon are involved; the output
is the spool directory.

## Call `init()` once, at startup

`vexlo.init(service=...)` configures the SDK for the whole process. `service` is the one
required argument: 1 to 128 printable ASCII characters, the name every trace from this process
carries and the leading key of the function index. Call it where the process starts, never
inside a request handler: every call replaces the configuration wholesale, mints a new
`instance_id`, and restarts the background writer.

The demo service does it at the top of its `main`:

```python vexlo-demo/src/delivery/cli.py theme={null}
def main(argv: Sequence[str]) -> int:
    """Print one JSON object per run, either for the given order ids or for every scenario.
    ... 4 lines omitted
    Vexlo is configured here, once, at the process's one real startup point -- never inside a
    request handler. This only ever writes to local disk under ``VEXLO_SPOOL_DIR``; a separate
    ``vexlo relay`` daemon, not this process, decides whether and where anything is sent onward.
    """
    vexlo.init(service="delivery-service", tag_keys=["order_id"])
    ... 22 lines omitted
```

`init()` does two things beyond storing the configuration:

* It starts one daemon thread named `vexlo-writer`. Marked functions append records to an
  in-process outbox; this thread drains the outbox to the spool directory, flushing and
  `fsync`ing at least once a second, or sooner once 512 records or 1 MiB are waiting.
* It registers an `atexit` hook, once per process, that drains whatever is still buffered for
  at most 2 seconds when the interpreter exits.

Neither happens when the SDK is disabled (`enabled=False` or `VEXLO_ENABLED=0`), and `init()`
never opens a network connection: the SDK writes files, and only the relay daemon sends them
anywhere. Bad configuration raises `ConfigurationError` whatever else is set; catch
`vexlo.VexloError`. Applying a marker to a `classmethod`, `property`, `cached_property`, or
`partialmethod` raises too, where the decorator runs. Every other public call raises only under
`init(strict=True)`. The
[configuration reference](https://docs.vexlo.ai/sdk/configuration.md) lists every argument, environment variable, and
error message.

## Mark two functions and run

Save this as `first_trace.py`:

```python first_trace.py theme={null}
import vexlo

vexlo.init(service="delivery-service")


@vexlo.step
def find_order(order_id):
    return {"order_id": order_id, "zone": "downtown"}


@vexlo.trace
def assign_dasher(order_id):
    order = find_order(order_id)
    return {"order_id": order["order_id"], "zone": order["zone"], "dasher_id": "d-01"}


print(assign_dasher("o-5001"))
vexlo.flush()
```

`@vexlo.trace` on `assign_dasher` opens a trace when the function is entered with none active.
`@vexlo.step` on `find_order` records one step inside it. The `vexlo.flush()` at the end is
there so the script can be read back immediately; a long-running process does not need it,
because the writer thread flushes on its own and the `atexit` hook drains at exit.

Run it with the spool directory pointed at a directory of your choice:

```bash theme={null}
VEXLO_SPOOL_DIR=vexlo-spool python first_trace.py
```

```text theme={null}
{'order_id': 'o-5001', 'zone': 'downtown', 'dasher_id': 'd-01'}
```

The function returned what it always returns. Marking a function changes nothing about its
arguments, return value, or exceptions.

## What landed on disk

The spool directory now holds two files, every directory at mode `0700` and every file at
`0600`:

```text theme={null}
drwx------ vexlo-spool
drwx------ vexlo-spool/spool
drwx------ vexlo-spool/spool/2026-10-20T01
-rw------- vexlo-spool/spool/2026-10-20T01/1792461462-42922-ed6249fd.jsonl
drwx------ vexlo-spool/writers
-rw------- vexlo-spool/writers/1792461462-42922-ed6249fd.identity.json
```

The layout is `<spool_dir>/spool/<UTC hour>/<writer-id>.jsonl` plus
`<spool_dir>/writers/<writer-id>.identity.json`:

* The hour directory is the UTC wall-clock hour of the write, `YYYY-MM-DDTHH`. The `.jsonl`
  file inside it is the segment file, the one file records are appended to. The writer opens a
  new segment file when the hour advances and never goes back to an earlier one.
* The writer id is `<unix-seconds>-<pid>-<8 hex chars>`, minted once per process at its
  first write (a forked child mints its own). One process appends to one file per hour, so
  concurrent processes on a host never share a segment.
* The identity file names the service and the process's `instance_id`, written once and
  never rewritten. Here it holds
  `{"service": "delivery-service", "instance_id": "0ee85f26d79092faaf0d9e1d3b058057"}`.

Nothing is created until the first record is written: a process whose marked functions never
run inside a trace leaves no spool directory behind. Without `VEXLO_SPOOL_DIR` or the
`spool_dir` argument the SDK uses `~/.vexlo`, the same default the relay daemon reads from.

## The records

The segment file holds one JSON record per line, seven for this run. Pretty-printed here so
the fields are readable; on disk each record is a single line.

```json theme={null}
{
  "kind": "trace-open",
  "format_version": 2,
  "trace_id": "10db0d0b777500e3f04fe91aa5f0939c",
  "service": "delivery-service",
  "instance_id": "0ee85f26d79092faaf0d9e1d3b058057",
  "sdk_version": "0.1.0",
  "started_at": "2026-10-20T01:57:42.217Z",
  "tags": {}
}
{
  "kind": "step-attempt",
  "trace_id": "10db0d0b777500e3f04fe91aa5f0939c",
  "path": "0000",
  "name": "__main__.find_order",
  "attempt": 0,
  "started_us": 51,
  "duration_us": 12,
  "status": "ok",
  "declared_retryable": false,
  "continues_retry": false,
  "slots": {
    "input": {
      "state": "pending",
      "content_id": "2bc6cbcdd520baa89a88af87774ec52c94283bcf655ff3aabb97042192df0227",
      "bytes_count": 21,
      "truncated": false,
      "redacted": false,
      "encoding": "json",
      "preview": "{\"order_id\":\"o-5001\"}"
    },
    "output": {
      "state": "pending",
      "content_id": "c786956aae49dbb4b1f33698ebe3a5bf6dbff0c6c2f4c0566f725b131c185cc6",
      "bytes_count": 39,
      "truncated": false,
      "redacted": false,
      "encoding": "json",
      "preview": "{\"order_id\":\"o-5001\",\"zone\":\"downtown\"}"
    }
  },
  "parent_name": "__main__.assign_dasher"
}
{
  "kind": "step-attempt",
  "trace_id": "10db0d0b777500e3f04fe91aa5f0939c",
  "path": "",
  "name": "__main__.assign_dasher",
  "attempt": 0,
  "started_us": 0,
  "duration_us": 108,
  "status": "ok",
  "declared_retryable": false,
  "continues_retry": false,
  "slots": {
    "input": {
      "state": "stored",
      "content_id": "2bc6cbcdd520baa89a88af87774ec52c94283bcf655ff3aabb97042192df0227",
      "bytes_count": 21,
      "truncated": false,
      "redacted": false,
      "encoding": "json",
      "preview": "{\"order_id\":\"o-5001\"}"
    },
    "output": {
      "state": "stored",
      "content_id": "91cf9ca4cadfc2fa7ae49741c0c8f9f21c92b2f1ab065d76018074fc022b4a86",
      "bytes_count": 58,
      "truncated": false,
      "redacted": false,
      "encoding": "json",
      "preview": "{\"dasher_id\":\"d-01\",\"order_id\":\"o-5001\",\"zone\":\"downtown\"}"
    }
  }
}
{
  "kind": "value",
  "trace_id": "10db0d0b777500e3f04fe91aa5f0939c",
  "content_id": "2bc6cbcdd520baa89a88af87774ec52c94283bcf655ff3aabb97042192df0227",
  "encoding": "json",
  "bytes_count": 21,
  "truncated": false,
  "value": "{\"order_id\":\"o-5001\"}"
}
{
  "kind": "value",
  "trace_id": "10db0d0b777500e3f04fe91aa5f0939c",
  "content_id": "91cf9ca4cadfc2fa7ae49741c0c8f9f21c92b2f1ab065d76018074fc022b4a86",
  "encoding": "json",
  "bytes_count": 58,
  "truncated": false,
  "value": "{\"dasher_id\":\"d-01\",\"order_id\":\"o-5001\",\"zone\":\"downtown\"}"
}
{
  "kind": "value",
  "trace_id": "10db0d0b777500e3f04fe91aa5f0939c",
  "content_id": "c786956aae49dbb4b1f33698ebe3a5bf6dbff0c6c2f4c0566f725b131c185cc6",
  "encoding": "json",
  "bytes_count": 39,
  "truncated": false,
  "value": "{\"order_id\":\"o-5001\",\"zone\":\"downtown\"}"
}
{
  "kind": "trace-close",
  "trace_id": "10db0d0b777500e3f04fe91aa5f0939c",
  "records_lost": 0,
  "truncated": false
}
```

Reading the seven records in order:

1. `trace-open` ships when `assign_dasher` is entered, before its body runs. `tags` is empty
   because no `tag_keys` were declared.
2. The `step-attempt` for `find_order` ships when it returns, which is before its caller
   does, so a child's completion record always precedes its parent's. Its `path` is `0000`, the
   first child of the root; its `parent_name` is the root's name. Both slots are `pending`: the
   trace's sampling decision is not known until the root returns.
3. The `step-attempt` for `assign_dasher` has `path: ""`, the root, and no `parent_name`. Its
   slots are `stored`: the root returning is the moment the trace's fate is decided.
4. `started_us` is a step's offset from the start of the trace, in microseconds: `0` for the
   root by construction, 51 for `find_order`. `duration_us` is the marked function's own wall
   time, so the root's covers its child's.
5. Three `value` records carry the bytes behind the handles, one per distinct `content_id`.
   `find_order`'s input and `assign_dasher`'s input are the same bytes, so they share one
   record.
6. `trace-close` ends the trace: no records were lost and the 10,000-step cap was not hit.

Every `step-attempt` carries handles, not bytes; the `value` records carry the bytes. That
split is what lets a reader fetch the shape of a trace without its values.
[What gets captured](https://docs.vexlo.ai/sdk/what-gets-captured.md) covers the slots and their states.

## The fifth record kind

A trace that finishes quickly, like this one, uses four of the five record kinds. The fifth,
`step-open`, reports an attempt that was still running. The SDK holds one for every attempt from
the moment the marked function is entered and writes it on whichever of three triggers comes
first: the attempt has been open 1 second, its trace's `trace-close` is written, or the process
makes its 2-second shutdown flush. A held record is discarded unwritten when that attempt's own
`step-attempt` is written, so a call that returns promptly produces exactly one record and this
run produced no `step-open` at all.

A `step-open` carries what a marked function knows on entry and nothing more: `trace_id`, `path`,
`name`, `parent_name`, `attempt`, `started_us`, and `declared_retryable`. It has no
`duration_us`, no `status`, and no `slots`, because each of those is a fact about how the attempt
ended. One attempt produces at most one `step-open`, and "this attempt has not finished" is not
a field on the wire: it is what a reader derives from an open record with no completion beside
it. The `format_version: 2` on the `trace-open` record above is the stamp for the five-kind
format; version 1 was the four-kind format and carries no `step-open` at all.

One case ships nothing. A `step-open` is never written for an attempt whose path is not yet
final, and one kind of attempt has no final path on entry: an undeclared call that matches its
parent's last failed child on every retry condition but the exception it has not raised yet. Its
path waits on that exception, and an open record at a path no completion ever lands on would
show a reader a step that never ran there. The exclusion covers everything entered beneath such
an attempt too, since a path is its parent's path plus an ordinal.
`@vexlo.step(retries=True)` is the way out: a declared step's path is final on entry, so it and
its descendants report normally. A hang inside an undeclared retry candidate is the one hang
this record does not surface.

[Context across threads](https://docs.vexlo.ai/sdk/context-across-threads.md#finish-the-work-before-the-root-returns)
shows a run that produces one.

## From here

The SDK's job ends at the segment file. The relay daemon on the same host tails the spool
directory, batches the records, and sends them to the Vexlo server; the SDK holds no server
address and no token. To see this trace on the `/v1` API, run the daemon and the server as the
[quickstart](https://docs.vexlo.ai/quickstart.md) does, or read [the relay daemon page](https://docs.vexlo.ai/server/relay-daemon.md).

Next: [mark functions](https://docs.vexlo.ai/sdk/mark-functions.md) covers the two markers in full.


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