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

ChannelBest For
TelegramQuick notifications, mobile alerts, simple text
ReportsInteractive 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 RoutinesReports. 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.
MethodDescription
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 RoutinesReportsClean.