On this page

Jobs, modules and isolation

The job queue, the event bus, and the module system, for module authors and operators.

The job model#

queuedrunningcompleteddependents released
runningfailedthe error is kept on the row
runningcancelledcooperative, then SIGKILL for isolated workers
queuedpausedthe queue was paused; running jobs continue
Job lifecycle. Every job ends in a terminal state. A failed or cancelled upstream skips its dependents rather than blocking them.
AspectBehaviour
PersistenceThe job row and its payload survive restarts; a boot-time pass reconciles logs left mid-import.
ProgressA fraction or a count pair, persisted every 1000 events, plus a stage label.
TimeoutsJOB_EXECUTION_TIMEOUT_SECONDS (default 1800) force-stops a job and its offload children.
RetryThe stored payload replays the same work.

The queue is an in-process asyncio queue sized by WORKER_CONCURRENCY (default 2, admin-changeable live). Heavy CPU work moves to a process pool through ctx.run_in_process(...), bounded by MODULE_PROCESS_POOL_SIZE and, per user, by MAX_OFFLOADS_PER_USER.

The event bus#

In-process pub/sub, with a bounded queue per subscriber: when one fills, the oldest entry is dropped — acceptable for a progress tick, not for a terminal event.

TopicEmitted when
job.queued · job.started · job.progress · job.completed · job.failed · job.cancelledJob lifecycle
job.plan · job.snapshot · job.queue.paused · job.queue.resumedPlan published, client resync, queue control
log.imported · ocel.imported · log.readyImport normalised (also re-sent when a committed filter changes the data) · the log is open
module_run.updatedA module's compute state for a log changed
<module_id>.completedA module's precompute succeeded for a log
module.log.* · module.installedModule log line · install finished

GET/api/v1/events?topic=… is the platform stream, GET/api/v1/jobs/{id}/stream the per-job one; both are SSE with a Bearer header, and the client reconnects with backoff.

The module system#

StageBehaviour
DiscoveryScans modules/*/manifest.yaml one level deep, plus installed packages exposing the mate.modules entry point.
ValidationParses manifests and builds the dependency graph; a cycle or a missing hard dependency logs a named error (modules.discovery.dependency_cycle, …requirement_missing) and drops that module plus everything depending on it — the rest still boots.
MaterialisationCreates or reuses modules/<folder>/.venv (hashed dependency block) and bundles the frontend into .dist/.
MountRoutes under /api/v1/modules/{id}/*, event handlers on the bus, job handlers on the queue, capabilities in the registry.
GatingPer log: log model, required columns, min_events/min_cases, required modules, and the user's enable switch.
Hot reloadDev only: changed files reload in place; a changed dependency block triggers uv sync for that module.

Precompute is declared by stacking @on_event("log.imported") and @job on one handler, and it is lazy: a log is ready once parsed, and a module computes for it only when something asks — opening the module page, a dashboard card, or an MCP read — or at import for a manifest that says precompute: eager (an admin can override that per module). One row per (log, module) in log_module_runs records the state (queued, running, ready, failed, cancelled); idle and stale are derived by comparing the row with the log's data revision and the module's version, so a data change never has to touch a row. Requests are deduplicated per (log, module).

A committed filter, a reimport or a cell edit bumps the data revision and re-runs only the modules already computed on the log, with their result cache emptied first (cell edits wait for a pause). Ordering is pull-based: asking for a module first runs what it depends on — the modules it chains off via <module_id>.completed, and its hard requirements.modules — and parks it until they finish. A module whose upstream fails or is cancelled is failed with that reason, never left waiting. A run lost to an API restart is resubmitted at boot.

Install is a job: an uploaded archive (POST/api/v1/modules/install) lands in data/uploaded_modules/{id}/, the manifest is validated, dependencies materialised, the frontend bundled, and the module mounted — a failure rolls back. Git-URL and registry installs are specified in the design docs but not implemented; discovery separately picks up installed packages that expose the mate.modules entry point. Ownership is reference-counted per user, and shared artifacts are deleted at zero owners.

Isolation modes#

ModeDeclared byRuns inUse it when
In-process, threadisolation: in_process, execution: thread (default)The API process, thread poolAlmost everything.
In-process, killable workerexecution: workerA throwaway child of the APILong native calls that ignore cooperative cancellation.
Subprocessisolation: subprocessA long-lived worker on the module's own interpreterA different Python version, or a native-library conflict.
Foreign runtimeruntime: { kind: jvm }A worker started from the module's own fat jarJava methods.

In-process modules run on the platform's interpreter (currently 3.12) and are ABI-locked to it: requires-python is a validation gate there, and an interpreter selector for subprocess modules. Each module's .venv is private; inherit names libraries the platform already ships (pandas, numpy, pm4py, duckdb) — an in-process module resolves them from the platform's interpreter, while a subprocess module installs them into its own environment, because no interpreter is shared across the process boundary.

The subprocess bridge is a Unix socket with newline-delimited JSON: a 30-second handshake, ctx.* calls proxied back to the host, DataFrames handed over as Parquet, a three-second soft-cancel window followed by SIGKILL, and automatic respawn with exponential backoff (capped at 30 s, five consecutive attempts, ladder reset after 60 s of stable uptime). The JVM runtime speaks the same protocol, so the SDK surface (eventLog(), cache(), bus(), progress(), …) is equivalent.

Sidecars are optional compose profiles the operator starts; the module only connects to a configured address, probes health, wipes its scratch space before and after a run, and keeps results in ctx.cache.

MATE AI internals#

Prompt assembly per call: base system prompt → navigation note → optionally the process list → optionally activity and variant aggregates (≤40 activities, top 15 variants; skipped for OCEL) → cached module outputs within a character budget → the user's own prompt. Raw rows are never part of any of it: the module context built for an AI call has its event-log accessors replaced by objects that raise on access — the same mechanism the MCP server uses.

Guidance is cached per module and log together with a hash of the payload it was generated from, so it invalidates itself when the underlying result changes.

Observability#

EndpointReturns
GET/healthLiveness for containers and the deploy script.
GET/api/v1/system/storageDisk usage by category.
GET/api/v1/system/resourcesLive CPU and memory (admin).
GET/api/v1/system/jobs · PUTWorker concurrency; resize it live (admin).
GET/api/v1/system/diagnosticsThe copy-paste diagnostics blob.
GET/api/v1/system/mcp-metricsPrometheus text for MCP calls, latency, and rate limits.

Logs are structured, namespaced per module, and mirrored into a bounded in-memory ring buffer that the admin Jobs page tails. Container logs are capped by the compose log driver so a noisy module cannot fill the disk.