Routines can generate HTML reports with interactive Plotly charts and markdown content. Reports are viewable in the Condor web dashboard, providing a richer experience than Telegram messages for data visualization.
Why Reports?
| Channel | Best For |
|---|
| Telegram | Quick notifications, mobile alerts, simple text |
| Reports | Interactive charts, detailed analysis, scheduled outputs |
When you schedule a routine to run every hour, you may not want 24 Telegram messages per day. Instead, generate reports and check them in the dashboard when convenient.
Viewing Reports
Reports appear in the Condor web dashboard under Routines → Reports. Open one in the full-screen viewer and use the left/right arrow keys to step through a routine’s report history.
Each report includes:
- Title and creation time
- Source routine name and tags
- Interactive HTML content (zoomable charts, expandable sections)
Condor keeps a rolling buffer of the most recent reports (default 100, set by the CONDOR_MAX_REPORTS environment variable). Older reports are cleaned up automatically.
Creating Reports
1. Import ReportBuilder
from condor.reports import ReportBuilder
2. Generate Your Data
Create figures and tables as you normally would:
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(x=dates, y=prices, name="Price"))
fig.update_layout(title="Market Analysis")
3. Build the Report
ReportBuilder takes a title, and its methods are chainable. Tag the report with its source, add sections, then await save():
builder = ReportBuilder("Analysis Summary")
builder.source("routine", "my_routine").tags(["analysis"])
builder.markdown("## Analysis Summary\n\nKey findings from today's scan.")
builder.plotly(fig)
builder.table([{"symbol": "SOL", "change": "+4.2%"}])
await builder.save()
Complete Example
# routines/price_snapshot.py
from pydantic import BaseModel, Field
from telegram.ext import ContextTypes
import plotly.graph_objects as go
from config_manager import get_client
from condor.reports import ReportBuilder
class Config(BaseModel):
"""Snapshot prices for a set of pairs and save a report."""
exchange: str = Field(default="binance_perpetual", description="Exchange to query")
pairs: list[str] = Field(
default=["BTC-USDT", "ETH-USDT", "SOL-USDT"],
description="Trading pairs to snapshot",
)
async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str:
chat_id = context._chat_id if hasattr(context, "_chat_id") else None
client = await get_client(chat_id, context=context)
if not client:
return "No server available"
prices = await client.market_data.get_prices(config.exchange, config.pairs)
rows = [{"pair": pair, "price": price} for pair, price in prices.items()]
fig = go.Figure(go.Bar(x=[r["pair"] for r in rows], y=[r["price"] for r in rows]))
fig.update_layout(title=f"Prices on {config.exchange}")
builder = ReportBuilder(f"Price Snapshot — {config.exchange}")
builder.source("routine", "price_snapshot").tags(["prices"])
builder.markdown(f"## Prices on {config.exchange}")
builder.plotly(fig)
builder.table(rows)
await builder.save()
# Return text for Telegram
return f"Saved price snapshot for {len(rows)} pairs"
ReportBuilder Methods
All methods return the builder, so they can be chained.
| Method | Description |
|---|
source(source_type, source_name) | Tag where the report came from (e.g. "routine", "price_snapshot") |
tags(tags) | Attach a list of string tags |
kpi(label, value, delta=None, trend="neutral") | Add a KPI tile |
markdown(text) | Add markdown-formatted text |
plotly(fig) | Add a Plotly figure (interactive in HTML) |
table(rows, columns=None) | Add a table from a list of dicts |
await save(report_id=None) | Write the report to disk; pass an existing id to update in place |
Use Cases
Scheduled Market Analysis
Run a routine every hour to analyze market conditions:
/routines → technical_analysis → Schedule → Every 1 hour
Instead of 24 Telegram messages, check the reports dashboard for the latest analysis with interactive charts.
Agent Decision Logging
When an agent runs a routine for analysis, the report captures what the agent “saw” when making decisions:
builder = ReportBuilder("Agent Analysis")
builder.source("routine", "bb_trader_analysis").tags(["agent"])
builder.markdown(f"## Agent Analysis\n\nCurrent price: ${price}\n\nSignal: {signal}")
builder.plotly(indicator_chart)
await builder.save()
This helps you understand and debug agent behavior by reviewing the data it analyzed.
Comparing Tokens
Build research routines that compare multiple assets:
builder = ReportBuilder("Solana DEX Token Comparison")
builder.source("routine", "dex_comparison").tags(["solana", "dex"])
builder.markdown("## Solana DEX Token Comparison")
builder.plotly(market_cap_chart)
builder.plotly(fee_revenue_chart)
builder.table(comparison_rows)
await builder.save()
Report Storage
Reports are written as HTML files to the reports/ directory in your Condor install (~/condor/reports/), with an index in reports_index.json. The web dashboard reads from this directory.
ls ~/condor/reports/
# 20260115_143000_price-snapshot_a1b2c3.html
# 20260115_150000_market-scanner_d4e5f6.html
To clean up reports manually:
rm ~/condor/reports/*.html
Or use the dashboard’s cleanup function under Routines → Reports → Clean.