Skip to content

Quick Start

Connect to a running Zelos Agent and run your first query. By the end you'll have browsed the live catalog, pulled a frame, charted it, checked an invariant, watched values stream in, and saved a snapshot to disk.

Prerequisites

  • Zelos App or Agent running (default: localhost:2300)
  • Python 3.10+
  • pip install "zelos-sdk[notebook]" — the notebook extra adds pandas and numpy, which the frame's pandas hand-off and describe() need

No live data yet?

Run the demo data generator in another terminal so the agent has signals to serve:

zelos live demo --backfill 5m --duration 30m

The examples below use paths from this generator. Substitute your own if your catalog differs.

1. Connect

from zelos_sdk import connect

agent = connect("localhost:2300")
agent

connect() checks that something answers before it returns, and the handle renders a connection card: target, health, and how many signals are live. It is also a context manager, and agent.close() is the explicit form.

Connection options

  • Bare hostconnect("myhost")http://myhost:2300
  • Host + portconnect("myhost:2400")http://myhost:2400
  • Full URLconnect("http://myhost:2300")
  • Defaultconnect() reads ZELOS_AGENT_URL, falling back to http://localhost:2300

Catch zelos_sdk.errors.AgentError (or a subclass like AgentUnavailable) for connection failures.

For a richer snapshot, agent.info() returns health, log and config directories, memory, and settings. Anything that can't be loaded reads None and is named in info.failures.

2. Browse Signals

Every signal has a path of the form source/message.name, a data type, and usually a unit. The catalog renders as a listing:

catalog = agent.signals()
catalog

Narrow it with a glob, or look one up directly:

catalog.match("bus0/BMS_message/cells.*")     # a smaller SignalCatalog
catalog.search("cell")                        # loose, scored substring search
catalog["bus0/BMS_message/status.pack_current"]
"bus0/BMS_message/cells.cell_0" in catalog

lookback= bounds how recently a signal must have produced a sample to count as live; it defaults to 60 seconds.

3. Query Time-Series

Pull a window of samples into a SignalFrame. Pass one path, a list, or a glob — they all expand against the live catalog.

frame = agent.query("bus0/BMS_message/cells.*", start="-3m")
frame

The frame tells you what you got: rows, time span, and one line per column with its unit, type, and non-null count. start is required; end defaults to now. Both read the time grammar — a datetime, "now", ISO 8601, or a signed offset like "-3m".

Every column is a SignalSeries that carries its unit through arithmetic:

series = frame["bus0/BMS_message/cells.cell_0"]
series.min()          # NamedScalar: prints "3.628 V"
frame.describe()      # count, mean, std and quartiles per column, with units

Two messages, one frame

A frame is one row per timestamp, with nulls where a signal had no sample. Combining columns from different messages? Call frame.ffill().dropna() first — see how a frame joins signals.

Downsampling

downsample=N returns about N points per signal, shaped for charting a long window. Omit it for full resolution; max_rows=N caps the row count instead.

4. Chart It

frame.short_names().plot()

plot() emits Vega-Lite: the Zelos App renders it inline, an HTML export renders it offline, and nothing has to be added to your dependencies.

5. Latest Values

A single literal path returns a LatestValue carrying the value, its unit, its enum label if it has one, and its timestamp:

agent.latest("bus0/BMS_message/status.pack_current")
pack_current = 5.427 A @ 2026-09-04 17:07:51.920 (bus0/BMS_message/status, localhost)

A pattern or a sequence returns a Snapshot — a read-only mapping that renders as a table:

agent.latest("bus0/BMS_message/cells.*")
Snapshot @ 2026-09-04 17:07:51.920 · 8 signals
  bus0/BMS_message/cells.cell_0  3.643 V
  bus0/BMS_message/cells.cell_1  3.644 V
  bus0/BMS_message/cells.cell_2  3.646 V
  bus0/BMS_message/cells.cell_3  2.914 V
  bus0/BMS_message/cells.cell_4  3.656 V
  bus0/BMS_message/cells.cell_5  3.665 V
  bus0/BMS_message/cells.cell_6  3.673 V
  bus0/BMS_message/cells.cell_7  3.679 V

To read at a specific instant, use agent.at(paths, cursor), which reads each signal at or before that time.

6. Check an Invariant

A check turns the series you just queried into a rule, and reports its result with the evidence behind it:

agent.check.that(series, ">", 3.0)
# 2026-09-04 17:06:58.855  cells.cell_0 (3.628 V) > 3 V  always  PASSED

A series operand carries its own window, so the check covers exactly what you queried. Use a signal handle and last= when you want a rule with its own window:

agent.check.that(agent.signal("bus0/BMS_message/status.pack_current"), "<", 40.0, last="3m")

See Checks for every operator, and Notebooks as tests for what a failing check does to a run.

7. Watch Live Changes

agent.watch() polls and yields a Snapshot per tick. Bound it with until=, which reads the duration grammar:

for tick in agent.watch(["bus1/inverter_status.mode"], interval=1.0, until="5s"):
    value = tick["bus1/inverter_status.mode"]
    print(f"{value.time:%H:%M:%S}  {value.value}")

Polling can miss a transition between ticks. agent.window(path, start="-1m", duration="1m") cannot: it returns the opening state plus every change in the interval.

Keeping a watch alive

on_error="skip" keeps the loop running through transient errors, up to max_consecutive_errors consecutive failures.

8. Save and Re-Open

agent.export() writes a .trz file from live data. The file lives on the agent's host.

result = agent.export("/tmp/run.trz", start="-1m", overwrite=True)
result

Re-open it and run the same calls against the frozen copy:

with result.open() as trace:
    trace.query("bus0/BMS_message/cells.*", start="start", end="+30s")

On a file, time is measured from the file: "-60s" is the last minute of the recording and "start" / "+30s" its first thirty seconds. agent.traces({...}) opens several recordings as labeled runs on a shared clock — see Open Trace Files.

9. Inspect Actions

Actions are typed operations the agent (or an extension) exposes. Each one has a JSON Schema for its parameters.

for action in agent.actions.list():
    print(action.name)

schema = agent.actions.schema("action/name")
print(schema.action_schema)        # JSON Schema dict
print(schema.ui_schema)            # rendering hints
print(schema.default_timeout_ms)

Execute by name with a params dict:

result = agent.actions.execute("action/name", {"param": "value"}, timeout=5.0)
print("status:", result.status, "ok:", result.ok)

result.ok is True for "pass" and "done". See Run Actions for concrete extension examples.

What's Next

  • Query Live Data — the full surface of query(), latest(), at(), window(), watch(), and series math
  • Open Trace FilesTrace and TraceSet for offline analysis
  • Checks — every operator, temporal word, and tolerance
  • Notebooks — put all of this in a markdown file the agent runs
  • Run Actions — schemas, validation, dynamic choices, error handling
  • Streaming SDK — produce data the agent can serve