Tool
watch
Run a program and turn what it does into tables you can query. watch collects the program’s traces, logs and metrics, and samples its CPU and memory, so you can ask SQL questions about a run instead of reading its output.
| If you | Use |
|---|---|
| Want to profile a program, or find why it is slow, failing or hanging | watch |
| Want to query a run’s telemetry once it is captured | query over the watch schema |
| Want to find the code behind a slow operation | explore |
| Want a step-by-step profiling workflow | Profiling with watch |
What gets captured
watch starts the program with its OpenTelemetry settings pointed at the RepoQL host. Anything the program exports arrives as spans, logs and metrics. RepoQL also samples the CPU and memory of the program and every process it starts.
Running under watch doesn’t add tracing to a program. Spans and logs appear only if the program has an OpenTelemetry SDK and exports them. A program without one still gets the CPU and memory samples. A very short program can exit before its first metrics are exported, so let it run for a few seconds.
The program inherits your environment. You can add variables, for example to switch on an application’s own telemetry. RepoQL’s exporter settings, such as endpoints and protocols, always win, so capture can’t be turned off by accident. Your own service name is kept.
Keep to one run
Every run shares the watch schema. Each has its own run ID. Pass that ID to the query macros, such as watch.summary('<run_id>'), so another run’s data doesn’t mix into your measurement.
Through MCP, watch returns as soon as the program has started, so you can query while it runs. There is no MCP call to stop it: stop the program itself. In the terminal, rql watch stays attached, prints the program’s output and returns when it exits.
Example: watch a short script
Terminal
rql watch -- python3 -c "import time; t=time.time(); n=0
while time.time()-t<6: n+=1
print(n)"
Output
[rql watch] run: 233ae9daecd04ba2978685f439f0595f
[rql watch] pid: 69245
[rql watch] schema: watch
[rql watch] endpoint: http://127.0.0.1:59279/
[rql watch] telemetry_schema: watch
Wait for this app to exit: rql monitor --target 233ae9daecd04ba2978685f439f0595f --until exited --timeout 1h
Exit 0 means the app exited successfully; failure exits 1. Works after the app has already exited.
Observe its output: rql monitor --target 233ae9daecd04ba2978685f439f0595f --since 5m --jsonl
[rql watch] query telemetry:
SELECT * FROM watch.summary('233ae9daecd04ba2978685f439f0595f'); -- payloads arriving? exited? slowest spans
SELECT * FROM watch.errors('233ae9daecd04ba2978685f439f0595f'); -- exceptions with innermost frames
SELECT severity_text, body_text FROM watch.otel_log WHERE run_id = '233ae9daecd04ba2978685f439f0595f' ORDER BY observed_time_unix_nano DESC LIMIT 25;
SELECT * FROM watch.surface; -- everything queryable
39342610
Terminal
rql query "SELECT * FROM watch.summary('233ae9daecd04ba2978685f439f0595f')"
Output
run_id executable started_at status duration capture spans logs metrics errors hints
233ae9daecd04ba2978685f439f0595f python3 2026-09-29 06:38:15.331327 exited 0 · last event 5s ago 7s 7 payloads (metrics=7), 8061 bytes, 06:38:16-06:38:22 none none 8 metrics, mem peak 8683520.0B, cpu total 4.701s none available_metrics(), surface
Terminal
rql query "SELECT name, type, unit, point_count, query_hint FROM watch.available_metrics() ORDER BY name"
Output
name type unit point_count query_hint
process.cpu.time sum s 12 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'process.cpu.time'
process.memory.usage gauge By 6 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'process.memory.usage'
process.memory.virtual gauge By 6 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'process.memory.virtual'
process.thread.count gauge {thread} 6 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'process.thread.count'
repoql.watch.process.cpu.time sum s 6 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'repoql.watch.process.cpu.time'
repoql.watch.process.tree.memory.usage gauge By 7 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'repoql.watch.process.tree.memory.usage'
repoql.watch.process.tree.memory.virtual gauge By 7 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'repoql.watch.process.tree.memory.virtual'
repoql.watch.process.tree.process.count gauge {process} 7 SELECT * FROM watch.otel_metric_point WHERE metric_name = 'repoql.watch.process.tree.process.count'
- A run ID and starter queries.
rql watchprints them before the program’s own output, which comes last. - A summary of the run.
watch.summary()shows the exit status, how much was captured, and what each signal holds. This script has no OpenTelemetry SDK, so spans and logs arenoneand every metric is one RepoQL sampled. - Metrics you can drill into.
watch.available_metrics()lists each metric with a ready-to-runquery_hint. Therepoql.watch.process.tree.*metrics cover the program and every process it started.
Empty isn’t failure. No spans can mean the program has no tracing, sends its telemetry somewhere else, or hasn’t exported a batch yet. Check the capture column, or the raw watch.payload table, before you conclude anything.
Where to go next in the data
SELECT * FROM watch.surface lists everything you can query, with an example call for each. The ones you’ll use most:
| Query | Answers |
|---|---|
watch.summary() | Each run’s status, capture counts, signals and next queries. |
watch.errors() | Failures grouped by fingerprint, with first and last seen times. |
watch.span_stats() | Operations ranked by volume, errors, latency and total time. |
watch.trace_tree('<trace_id>') | One trace as a tree of spans with timings. |
watch.available_metrics() | Metric names, units and a query for each. |
watch.watch_run | Every run: its process, arguments, exit code, and the OpenTelemetry settings it received. |
Telemetry is stored on your machine, under the host repository’s .repoql/cache/otel/. Runs that finished more than six hours ago can be cleaned up, so keep any numbers you need. This is separate from RepoQL’s own usage telemetry.
Parameters
| MCP · CLI | Meaning |
|---|---|
executable · first argument | Required. The program to run: a name on your PATH or a file path. |
arguments · the arguments after the program | Arguments passed to the program exactly as given, without a shell. In the terminal, put -- before the program so its flags aren’t read as rql’s. |
workingDirectory · --cwd | The program’s working directory. Defaults to the directory you run from. |
environment · --env | Extra variables as key=value;key2=value2. %VAR% expands; escape a literal ;, = or \ with a backslash. |
CLI only: --listen | Kept for compatibility. Only its default is accepted. |
CLI only: rql watch env | Prints the OpenTelemetry variables for a program you start yourself, such as one run by an orchestrator or Docker. It starts nothing. |
Further reference
For an Aspire app, Observe, fix and optimise with Aspire sends every service’s telemetry to the same watch tables.
In the installed manual: help:///tools/watch/watch.md, help:///tools/watch/watching-claude.md, help:///commands/watch.md and help:///patterns/profiling.md.