On this page

Logs, jobs and results

Reading the log, declaring long-running work, and storing results.

Reading the log#

ViewReturnsCostUse it for
await log.duckdb_fetch(sql, params)list[tuple]lowestAggregations, joins, window functions — the default.
await log.pandas() · .polars()DataFramemediumLibrary algorithms that expect one.
await log.pm4py()pm4py event loghighestOnly when an algorithm insists on it.
Read through the context manager
async with ctx.event_log as log:
    rows = await log.duckdb_fetch(
        "SELECT activity, count(*) FROM events GROUP BY 1 ORDER BY 2 DESC LIMIT 20"
    )
    df = await log.pandas()
    path = log.events_path       # the Parquet file itself
    filters = log.active_filter  # what the user committed
RelationContents
eventsThe event table with the committed filter applied — what the user sees.
events_srcThe unfiltered table, for comparisons against the whole log.
casesPer-case aggregates: event count, start, end, duration.

Columns are the mapped roles plus every other column of the source file, with types coerced at import. log.column_specs() and log.data_quality() let a module adapt to what is actually there instead of assuming.

For object-centric logs, ctx.object_log exposes events_pandas(), objects_pandas(), relations_pandas(), o2o_pandas(), and ocel(), backed by the relations ocel_events, ocel_objects, ocel_relations, ocel_o2o.

Precompute, jobs and progress#

The canonical precompute handler
@on_event("log.imported")
@job(progress=True, title="My Module - precompute")
async def precompute(self, ctx: ModuleContext, payload: dict) -> None:
    await ctx.progress.update(0.05, "Loading log")
    async with ctx.event_log as log:
        df = await log.pandas()
    await ctx.progress.update(0.35, "Computing")
    result = await ctx.run_in_process(self._heavy, df)
    await ctx.progress.update(0.9, "Caching")
    await ctx.cache.set("result", result)
    await ctx.progress.update(1.0, "Done")
  • Precompute is lazy. The platform runs the job the first time a log needs the module (the page, a dashboard card, an MCP read) and mounts your panel only once it has finished, so the panel can assume its cache is filled. precompute: eager in the manifest also runs it after every import.
  • Only job-backed handlers are tracked. A handler without @job is fire-and-forget: no run state, never shown, never run on demand.
  • The handler also runs after the log's data changes and again after an interrupted run is restarted, and its payload has no import_job_id when run on demand: make it idempotent and rebuild from ctx.event_log alone. Other jobs run in parallel up to the worker limit, so keep memory bounded.
  • To run after another module, subscribe to <module_id>.completed — see Module communication.

Results#

OutputWhere
Structured result (KPIs, rankings, summaries)ctx.cache as JSON — readable by routes, datasets, and MCP tools.
Tabular resultctx.cache as a DataFrame (stored as Parquet).
Large binary or model artifactctx.cache as bytes; ctx.workdir only for scratch.
Transient per-request computationNothing — compute and return.
A cache key that encodes what changes the answer
key = f"ranking:top_n={top_n}:threshold={threshold:.3f}"
if await ctx.cache.exists(key):
    return await ctx.cache.get(key)

Invalidate on the events that change the answer: a configuration write, a re-import, a completed upstream module.