Logs, jobs and results
Reading the log, declaring long-running work, and storing results.
Reading the log#
| View | Returns | Cost | Use it for |
|---|---|---|---|
await log.duckdb_fetch(sql, params) | list[tuple] | lowest | Aggregations, joins, window functions — the default. |
await log.pandas() · .polars() | DataFrame | medium | Library algorithms that expect one. |
await log.pm4py() | pm4py event log | highest | Only when an algorithm insists on it. |
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
| Relation | Contents |
|---|---|
events | The event table with the committed filter applied — what the user sees. |
events_src | The unfiltered table, for comparisons against the whole log. |
cases | Per-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#
@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: eagerin the manifest also runs it after every import. - Only job-backed handlers are tracked. A handler without
@jobis 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_idwhen run on demand: make it idempotent and rebuild fromctx.event_logalone. 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#
| Output | Where |
|---|---|
| Structured result (KPIs, rankings, summaries) | ctx.cache as JSON — readable by routes, datasets, and MCP tools. |
| Tabular result | ctx.cache as a DataFrame (stored as Parquet). |
| Large binary or model artifact | ctx.cache as bytes; ctx.workdir only for scratch. |
| Transient per-request computation | Nothing — compute and return. |
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.